fix(windows): spawn .cmd shims through the interpreter and report owned exit codes - #5
Merged
Merged
Conversation
…ed exit codes
Two Windows defects blocked the local project workflow:
`localapp init` installed dependencies with spawn("npm", …). Node refuses to
spawn a .cmd shim without a shell, so it failed with ENOENT and then rolled the
project back, leaving the "run npm install yourself" hint impossible to follow.
Route the bare package-manager name through the command interpreter, which is
absolute and resolves the shim through PATHEXT.
`--job-owner` waited for its root and returned Ok(()) without reading its exit
code, so every owned command looked successful. Read it with GetExitCodeProcess
and make it the wrapper's own exit status; a failing project script is no longer
reported as a passing one.
Verified on Windows 11 26200: `localapp init` now installs 570 packages instead
of failing, and the wrapper reports 7, 0 and 3 for children that exit 7, 0 and 3.
`localapp dev` is still blocked and is not touched here: its script runner hands
a bare `npm.cmd` to the owned-process wrapper, which only accepts an absolute
executable. Routing it through cmd.exe from this layer does not work — the
wrapper re-quotes each argv element and the interpreter then starts an
interactive session instead of running the command — so that fix belongs in
`job_owner`, which should wrap a .cmd/.bat target itself.
Three defects, verified on one machine by running the whole flow:
`localapp init` wrote a project whose own test suite could never pass on
Windows. The template's failures were the two assertions that compare
multi-line source text: a Windows checkout and the Windows template sync carry
CRLF, so `toContain("...\n...")` never matched and `localapp check` failed at
the tests phase — which also blocked `localapp dev`, since it runs the same
check. Normalize line endings in the template test's reader.
`localapp dev` handed the owned-process wrapper a bare `npm.cmd`. The wrapper
only accepts an absolute executable path and cannot load a script image, so the
project scripts never started. Resolve the shim through PATH and PATHEXT on the
CLI side — an extensionless file of the same name must not win: npm ships one,
and returning it made the interpreter fail with exit 1 — and wrap a `.cmd`/
`.bat` target in the command interpreter inside `job_owner`, the only layer that
controls the quoting cmd.exe parses.
Installing an application into the local Server failed on Windows with
`APP_INSTALL_FAILED: EPERM fsync`. The retained-package step opened the copy
read-only and called `handle.sync()`; FlushFileBuffers needs a write-capable
handle there. Use the file's own `syncFile` helper, which already owns that
rule, instead of the inline open/sync/close.
Verified with the packaged build on Windows 11 26200:
localapp init app1 -> 570 packages installed
localapp check -> success, all 7 phases passed
localapp dev -> App URL served (HTTP 200, Vite dev shell) and the
embedded Server answered /health, with no shim
The new cases built a fake PATH with the host path delimiter and a native temp directory, so they passed on Windows and failed on the Linux runner: the resolver splits with the win32 delimiter and the injected win32 paths do not exist there. Take the existence check as an injected seam instead, so the PATHEXT ordering rule (npm's extensionless POSIX script must not win over its .cmd shim) is asserted the same way on every platform.
Patodo
added a commit
that referenced
this pull request
Sep 18, 2026
Ships the Windows local-flow fixes from #5: init installs dependencies, `localapp dev` starts the project scripts through the interpreter, installing an application into the local Server no longer fails on a read-only fsync, and the template's own tests stop being line-ending sensitive. Version-bound test assertions move to 0.2.7 and the reviewed public source baseline digests are re-recorded for the two files this touches. The flagged content in both files is unchanged: the same three canonical package markers and the same apiKey fixture as before.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
让 Windows 上的本地开发流程真正可用。三条缺陷都是我在同一台机器上跑完整流程时定位并验证的。
①
localapp init装不上依赖Node 在 Windows 上不能 spawn
.cmd垫片(CVE-2024-27980 之后必须经 shell),PATH 上只有npm.cmd/npm.ps1,于是 ENOENT →dependency_install_failed,并且失败后把刚建的项目回滚删除。新增
src/process/command-invocation.ts:裸命令名交给命令解释器(解释器是绝对路径,并通过 PATHEXT 解析垫片);带路径的调用保持不变。②
localapp dev起不来项目脚本dev的 runner 把裸npm.cmd交给受控包装层,而包装层只接受绝对可执行路径,脚本永远启动不了。npm,先试裸名就会拿到它,解释器执行失败返回 1——所以必须先按 PATHEXT 解析(测试里加了无扩展名诱饵钉住)。job_owner遇到.cmd/.bat目标时自己起命令解释器并构造命令行。这是唯一能控制 cmd 解析的那一行引号的层——从 TS 侧传/d /s /c给包装层不行,因为它会对每个 argv 元素重新加引号,解释器随后起的是交互式会话。③ 装应用时服务端
EPERM fsyncapp-installer.ts保留包那一步用只读句柄打开副本后调handle.sync();Windows 的FlushFileBuffers需要可写句柄,于是每次都APP_INSTALL_FAILED: EPERM fsync。同文件里早已有syncFile()承担这条规则("r+"+ 容忍纯访问类失败),改用它。(定位过程:
fs.fsyncSync打点抓不到,换成连FileHandle.prototype.sync一起打点才确认——这也说明61f5c8a那批加固漏掉的正是这种 FileHandle 形态。)④ 模板自带测试在 Windows 上必然失败
localapp check在任何新脚手架工程上都是红的,因为模板测试有两条断言直接比较多行源码文本,而 Windows 检出与 Windows 侧的模板同步都是 CRLF,toContain("...\n...")永远不匹配。这独立地挡住了dev(dev会跑同一套检查)。在测试的读取函数里统一换行。端到端验证(Windows 11 26200,用打包产物,未加任何垫片)
测试
tests/command-invocation.test.ts:6 例(含无扩展名诱饵、PATHEXT 顺序、路径不被改写)native-adapter.node-test.mjs:源码断言钉住退出码传播与.cmd/.bat包装tsc --noEmit通过;test:public-source通过未包含
localapp server restart会报ipc_response_invalid但实际重启成功——原因是控制处理器期限 5s 短于 daemon 自身的 readiness 预算 15s,连接在写响应前被销毁。与本次改动无关,需要单独修。