Skip to content

fix(launch): route WindowsApps UWP reparse-point stubs through ShellExecute (closes #30) - #31

Open
cioinside wants to merge 1 commit into
mainfrom
fix/launch-windows-uwp-shellexecute
Open

fix(launch): route WindowsApps UWP reparse-point stubs through ShellExecute (closes #30)#31
cioinside wants to merge 1 commit into
mainfrom
fix/launch-windows-uwp-shellexecute

Conversation

@cioinside

Copy link
Copy Markdown
Owner

Summary

Fixes da_launch's silent failure mode for WindowsApps Store-app reparse-point executables (mspaint, msedge, etc.). Before this PR, calling da_launch({ argv: ["mspaint"] }) returned { pid, killed: false } but the spawned process exited within ~1 second because CreateProcessW does not establish the AppX activation context. There was no diagnostic surface — no crash, no Application Error event, no WER report, no crash dump.

After this PR, WindowsApps paths are detected post-resolution and dispatched via ShellExecuteExW (through the open package, which uses cmd /c start "" /b <target> <args...> with windowsVerbatimArguments). The UWP app now launches normally and stays alive.

Detection — isWindowsAppsPath(p)

Case-insensitive substring check on the normalized path, looking for \windowsapps\ as a directory component (not arbitrary substring — guards against mywindowsappsbackup). No-op on non-Windows. After resolveProgram() so PATH/PATHEXT matching still wins first.

Dispatch — launchViaShellExecute(resolvedPath, args)

Routes through the open package (already in dependencies for launchUrl). On Windows the package shells out to cmd /s /c start "" /b <target> <args...> with windowsVerbatimArguments — the only documented way to invoke ShellExecuteExW from Node.js without a native binding.

Returns a fire-and-forget SpawnHandle shaped like launchUrl (pid: null, killed: false, exited: Promise.resolve(0), kill() is a no-op). Rationale: open() returns the transient cmd.exe child process, which exits within milliseconds and is NOT the spawned UWP app. Exposing that PID would be misleading. Callers that need to terminate the spawned UWP app should use taskkill /im <name>.exe via a separate da_launch, or da_window_list + a window-close gesture.

Behaviour change summary

argv Before After
["mspaint"] spawn -> reparse stub -> exit <1s, silent failure ShellExecute -> AppX activation -> mspaint stays alive
["cmd", "/c", "start", "mspaint.exe"] spawn -> cmd -> ShellExecute -> mspaint alive (workaround) unchanged
["notepad.exe"], ["C:\\Windows\\System32\\notepad.exe"] spawn -> notepad alive unchanged (regression-tested)
["echo", "hello"] on POSIX spawn -> echo runs unchanged
URL args (http://...) launchUrl route unchanged

Tests added (test/unit/launch.test.ts)

9 new test cases:

isWindowsAppsPath:

  • non-Windows returns false regardless of input
  • system WindowsApps path detection (e.g. C:\Program Files\WindowsApps\Microsoft.Paint_...\mspaint.exe)
  • user-profile reparse stub detection (e.g. %USERPROFILE%\AppData\Local\Microsoft\WindowsApps\mspaint.exe)
  • non-WindowsApps binaries (notepad.exe, git.exe, etc.) return false
  • case-insensitive on directory component
  • forward-slash separators accepted
  • substring windowsapps in a file name (not a directory) returns false (regression guard)
  • empty input returns false

launchProgram UWP dispatch:

  • end-to-end: launchProgram(['mspaint']) returns pid: null and the spawned mspaint.exe process is observable via Get-CimInstance Win32_Process — gates on accessSync(F_OK) (which succeeds on reparse stubs where existsSync/statSync report EACCES), skips on macOS/Linux or hosts without mspaint installed
  • regression: launchProgram(['C:\\Windows\\System32\\notepad.exe']) still returns a numeric pid (i.e. still uses spawn, not the new ShellExecute path)

Why the workaround in the issue still works (and is preserved)

The PR does not remove the existing child_process.spawn path. Callers that already work around via argv: ["cmd", "/c", "start", "mspaint.exe"] continue to use spawncmd.exe itself is not under WindowsApps, so the new isWindowsAppsPath check returns false and the legacy code path is taken. The new ShellExecute route is purely additive.

Reproduction (from issue #30)

> da_launch({ argv: ["mspaint"], timeoutMs: 15000 })
{ "pid": 64440, "killed": false }                          # BEFORE: pid back, but process gone in <1s
{ "pid": null,   "killed": false }                         # AFTER:  pid:null (fire-and-forget), process alive

> Get-CimInstance Win32_Process -Filter "Name = 'mspaint.exe'"
# BEFORE: empty (process died)
# AFTER:  returns the AppX-Activated Microsoft.Paint process

5 control methods that already worked (Start-Process, cmd /c start mspaint.exe, cmd /c start ms-paint:, explorer.exe ms-paint:, shell:AppsFolder\Microsoft.Paint_...!App) all go through ShellExecuteExW — confirming the diagnosis.

Checklist

  • npm run typecheck clean
  • New isWindowsAppsPath unit tests pass on Windows
  • End-to-end mspaint-launch test passes on Windows (asserts process stays alive)
  • Notepad regression test passes (asserts spawn path still used for non-UWP)
  • Existing pre-existing failures (Unix echo/sleep/POSIX paths on Windows hosts) unchanged — unrelated to this fix

Closes #30

…xecute

da_launch's `child_process.spawn` -> `CreateProcessW` path on Windows
silently fails for Microsoft Store apps (mspaint, msedge, etc.) because
the AppX activation context is not established. The new process exits
cleanly within ~1s of the tool call returning, with no diagnostic
surface (no crash, no Application Error, no WER, no prefetch).

Detection post-resolution via `isWindowsAppsPath`, dispatch via the
already-installed `open` package (which uses `cmd /c start` +
`ShellExecuteExW` under the hood). Returns a fire-and-forget handle
shaped like `launchUrl` (pid:null) because the spawned UWP process is
owned by the AppX runtime and not trackable via the ChildProcess API.

Reproduction and full analysis in issue #30.

Closes #30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(launch): da_launch fails for WindowsApps UWP reparse-point executables (e.g. mspaint) - needs ShellExecute fallback

1 participant