fix(AppLifecycle): coalesce multi-file activation redirections into single event#6276
fix(AppLifecycle): coalesce multi-file activation redirections into single event#6276
Conversation
Port agents and skills from PowerToys for automated PR and issue workflows, generalized for any repository: Agents (8): ReviewPR, FixPR, TriagePR, ReviewIssue, PlanIssue, FixIssue, IssueToPR, ReviewTheReview Skills (10): pr-review (13-dimension analysis), pr-fix, pr-rework (iterative review-fix-build loops), pr-triage, issue-review, issue-fix, issue-review-review, issue-to-pr-cycle, continuous-issue-triage, parallel-job-orchestrator Key generalizations: - Auto-detect owner/repo via gh CLI (Get-RepoSlug helper) - Build commands use existing BuildAll.ps1 - Worktree scripts use existing worktree-manager skill - Review dimension 10 rewritten for C++/WinRT + WIL patterns - All 13 review dimensions updated with WinAppSDK-specific checks - No hardcoded repo references; fully portable Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bump AppLifecycleContract from v2 to v3 and expose a public constructor on AppActivationArguments(ExtendedActivationKind, IInspectable) so callers can create instances directly for activation redirection scenarios without relying on internal factory methods. Fixes #6075
…ingle event When a user opens multiple files at once, each file triggers a separate process that redirects to the main single-instanced app. Previously, each redirection fired a separate Activated event, causing the app to receive N individual activations instead of one event with all N files. This change: - Adds a multi-file constructor to FileActivatedEventArgs that accepts a pre-built IVector<IStorageItem> for efficient in-process merging. - Rewrites AppInstance::ProcessRedirectionRequests() to collect all pending file activation redirections, wait a brief coalescing window (150ms) for late-arriving redirections, and fire a single merged Activated event containing all files. - Non-file activation types continue to fire immediately as before. - All coalesced redirection cleanup events are properly signaled so redirecting processes can exit normally. Fixes #5066
|
Please don't add a forced delay for file activation. The issue can be worked around (at least in packaged apps) by using the <Extensions>
<uap3:Extension Category="windows.fileTypeAssociation">
<uap3:FileTypeAssociation Name="my-extensions" MultiSelectModel="Player">
..This means the app is only launched once (with multiple files provided to the file activation args). If you are going to add a delay, please make this optional as start-up speed is very important to me. |
|
Could single instance apps receive multiple files before? Wondering if this is a breaking change (and if so, 2.0 is around the corner...) |
| activity.DequeueRedirectionRequest(id); | ||
| wil::unique_cotaskmem_string idString; | ||
| THROW_IF_FAILED(StringFromCLSID(id, &idString)); | ||
| GUID id; |
There was a problem hiding this comment.
NIT: GUID id{};
ES.23: Prefer the {}-initializer syntax
If it was just a dumb struct then routine not to initialize at declaration but it's got objects e.g. std::wstring so CoalescedRequestInfo cri; memset(&cri, 0, sizeof(cri)); would be BadMojo(TM). So better to make all attributes initialized at constructor rather than just some
| }; | ||
|
|
||
| std::vector<CoalescedRequestInfo> fileRequestInfos; | ||
| auto coalescedFiles = winrt::single_threaded_vector<winrt::Windows::Storage::IStorageItem>(); |
There was a problem hiding this comment.
NIT: auto coalescedFiles{ ... };
ES.23: Prefer the {}-initializer syntax
same comment applies throughout
| // File activations are collected for coalescing; other types fire immediately. | ||
| auto dequeueAndProcess = [&]() | ||
| { | ||
| GUID id; |
There was a problem hiding this comment.
GUID id{};
always initialize variables
same comment applies throughout
| // ensures we capture all files from a single user action. | ||
| if (!fileRequestInfos.empty()) | ||
| { | ||
| constexpr DWORD c_fileCoalescingWindowMs = 150; |
| if (!fileRequestInfos.empty()) | ||
| { | ||
| constexpr DWORD c_fileCoalescingWindowMs = 150; | ||
| if (WaitForSingleObject(m_innerActivated.get(), c_fileCoalescingWindowMs) == WAIT_OBJECT_0) |
There was a problem hiding this comment.
Errors are silently ignored
Add logging if a errors occur
|
|
||
| // Signal cleanup events for all coalesced file activation requests so that the | ||
| // redirecting processes can exit. | ||
| for (const auto& req : fileRequestInfos) |
There was a problem hiding this comment.
Please don't abbreviate, it makes code harder to understand and maintain
SUGESTION: for (const auto& fileRequestInfo : fileRequestInfos)
| { | ||
| if (verb.empty()) | ||
| { | ||
| throw std::invalid_argument("verb"); |
There was a problem hiding this comment.
Why throw? WIL THROW*() is used almost everywhere
And being a WinRT object throwing a C++ object will lose its data and other information except its equivalent HRESULT. If you e.g. replace this with
THROW_HR_MSG(E_INVALIDARG, "verb");
the WinRT caller receives the same HRESULT (E_INVALIDARG) but WIL logged a message first so the caller has that context available if running under a debugger, or ETW events are being captured, or telemetry captured and recorded this error.
same comment applies throughout
| @@ -0,0 +1,201 @@ | |||
| Apache License | |||
There was a problem hiding this comment.
Apache License? Is that a problem for us?
Check with WinAppSDK core folks (@Scottj1s @alexlamtest etc)
Summary
Fixes #5066 — App activation fires multiple times when launching multiple files
When a user selects N files and opens them with a WinAppSDK single-instanced app, N processes launch. N-1 processes redirect to the main instance via
RedirectActivationToAsync(). Previously, the main instance fired N separateActivatedevents — each containing 1 file — instead of 1 event containing all N files.Root Cause
ProcessRedirectionRequests()inAppInstance.cpphad a simple while-loop that dequeued each redirection request and firedm_activatedEventindependently for each one.Fix
dev/AppLifecycle/FileActivatedEventArgs.hFileActivatedEventArgs(verb, IVector<IStorageItem>)for coalesced multi-file activationsdev/AppLifecycle/AppInstance.cppRewrote
ProcessRedirectionRequests()to:coalescedFilesvectorFileActivatedEventArgsActivatedevent with all filesNon-file activations continue to fire immediately, preserving backward compatibility.
Testing
WindowsAppRuntime_DLL.vcxprojbuilds cleanly (x64 Release)Activatedevent with all files in theFilescollectionNotes
.github/infrastructure files unrelated to this fix