fix(release): bound the Windows process probe so its poll deadlines hold - #3241
fix(release): bound the Windows process probe so its poll deadlines hold#3241Joob1n wants to merge 11 commits into
Conversation
The packaged-renderer smoke reserved a TCP port, released it, and handed the number to Electron, leaving a window in which anything else on the runner could bind it first — and when the poll then timed out, the log named neither the polled port nor which of the three causes applied (issue apache#3196; it has now hit the same release check twice more). The smoke now launches with --remote-debugging-port=0 and reads the port Chromium actually bound from the DevToolsActivePort file it writes into the isolated user-data directory, so the race is gone rather than reported. The timeout message classifies what remains: a missing port file means the browser never opened an endpoint; a port plus fetch errors means it bound where the poll looked and did not answer in time. Verified against a real packaged app: the smoke passes end to end with the port discovered from the file. Fixes apache#3196 Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 3 included reviews per hour; 0 remain after this review. 📝 WalkthroughProblem solvedThe PR removes the CDP port race in The verifier removes stale port data before startup. It reads and polls the port for the current process. Timeouts report the port, elapsed time, and the failure stage. They distinguish missing port files, fetch failures, unexpected HTTP responses, and missing page targets. The verifier still fails immediately when the renderer process exits before readiness. Design and scopeThe PR extends the existing packaged-app verifier path. It does not create a parallel path or change exported declarations. This is the smallest coherent solution. It removes manual port reservation and release and uses Chromium’s No code or tests can be removed without weakening behavior or regression coverage based on the available evidence. Complexity delta
Total maintenance complexity decreases. The added branches and deadlines are necessary for reliable discovery on cold runners and for actionable failures. ValidationValidation included packaged-app testing, stale-file testing with a pre-seeded Review-relevant risksThe current diff affects release-verifier internals. No apparent effect on user-visible behavior, public contracts, security, licensing, release artifacts, or governance was identified. Any material change in a protected area requires independent human review under repository policy. The person performing the merge reviews the final diff, and a maintainer makes the final determination. WalkthroughThe packaged app verifier now lets Chromium select the DevTools port, reads the assigned port from ChangesPackaged renderer verification
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: 🟡 Moderate · up to The release smoke now discovers Chromium’s self-selected CDP port, but a stalled CDP request can still exceed the intended timeout, delay cleanup, and leave the verifier process running; non-OK responses also lack status details in diagnostics. Merge should wait for bounded requests and explicit handling of this follow-up risk. Sequence Diagram(s)sequenceDiagram
participant Verifier
participant PackagedRenderer
participant Chromium
participant DevToolsEndpoint
Verifier->>PackagedRenderer: Remove stale port file and launch with --remote-debugging-port=0
PackagedRenderer->>Chromium: Start renderer
Chromium-->>Verifier: Write DevToolsActivePort
Verifier->>DevToolsEndpoint: GET /json/list
DevToolsEndpoint-->>Verifier: Return renderer targets or polling state
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
PR Summary by QodoEliminate CDP port race in packaged renderer smoke checks
AI Description
Diagram
High-Level Assessment
Files changed (1)
|
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: d614f76c-c566-4f54-b899-a5f062e7af47
📒 Files selected for processing (1)
scripts/verify-packaged-app.mjs
Included review availability: Your plan provides up to 3 included reviews per hour; 2 remain after this review.
| const response = await fetch(`http://127.0.0.1:${port}/json/list`); | ||
| if (response.ok) { | ||
| const targets = await response.json(); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- file outline ---'
ast-grep outline scripts/verify-packaged-app.mjs --view expanded
printf '%s\n' '--- relevant source ---'
sed -n '1,145p' scripts/verify-packaged-app.mjs
printf '%s\n' '--- package/runtime declarations ---'
rg -n --glob 'package.json' --glob '*.json' '"(engines|node|electron)"' . | head -80
printf '%s\n' '--- usages and tests ---'
rg -n 'findRendererTarget|DevToolsActivePort|verify-packaged-app|AbortSignal|fetch\\(' scripts test tests package.json .github 2>/dev/null | head -200Repository: maka-agent/maka-agent
Length of output: 13651
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- runtime and workflow versions ---'
node --version
rg -n -C 3 'node-version|setup-node|verify-packaged-app|smokePackagedRenderer' .github scripts package.json 2>/dev/null | head -160
printf '%s\n' '--- launch and cleanup context ---'
sed -n '257,310p' scripts/verify-packaged-app.mjs
sed -n '186,245p' scripts/verify-packaged-app.mjs
printf '%s\n' '--- standalone cancellation probe ---'
node - <<'JS'
const events = [];
const signal = AbortSignal.timeout(30);
const stalledFetch = (_url, { signal }) =>
new Promise((resolve, reject) => {
const onAbort = () => {
events.push(`aborted:${signal.reason?.name}`);
reject(signal.reason);
};
if (signal.aborted) return onAbort();
signal.addEventListener('abort', onAbort, { once: true });
});
globalThis.fetch = stalledFetch;
const started = Date.now();
try {
await fetch('http://127.0.0.1:1/json/list', { signal });
} catch (error) {
console.log(JSON.stringify({
apiAvailable: typeof AbortSignal.timeout === 'function',
elapsedMs: Date.now() - started,
errorName: error?.name,
events,
}));
}
JSRepository: maka-agent/maka-agent
Length of output: 16284
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- package engine ---'
sed -n '1,18p' package.json
printf '%s\n' '--- smoke renderer callers and working-directory setup ---'
rg -n -C 5 'smokePackagedRenderer\\(|workingDirectory:|mkdtemp|temporaryDirectory|rm\\(' scripts .github | head -240
printf '%s\n' '--- deterministic abort probe ---'
node - <<'JS'
(async () => {
const controller = new AbortController();
const keepAlive = setTimeout(() => controller.abort(new DOMException('deadline', 'TimeoutError')), 30);
const events = [];
const stalledFetch = (_url, { signal }) =>
new Promise((resolve, reject) => {
const onAbort = () => {
events.push(`fetch-aborted:${signal.reason?.name}`);
reject(signal.reason);
};
if (signal.aborted) return onAbort();
signal.addEventListener('abort', onAbort, { once: true });
});
globalThis.fetch = stalledFetch;
const started = Date.now();
try {
await fetch('http://127.0.0.1:1/json/list', { signal: controller.signal });
} catch (error) {
console.log(JSON.stringify({
apiAvailable: typeof AbortSignal.timeout === 'function',
elapsedMs: Date.now() - started,
errorName: error?.name,
events,
}));
} finally {
clearTimeout(keepAlive);
}
})();
JSRepository: maka-agent/maka-agent
Length of output: 911
Fix now: bound each CDP request by the remaining deadline.
Pass a deadline-based AbortSignal to fetch(); it also cancels response.json() body reads. Skip the 250 ms delay when the deadline expires so the timeout and finally cleanup run promptly.
Source: Path instructions
There was a problem hiding this comment.
Confirmed on exact head 090158d8d; I agree with this finding and calibrate it as P2. The outer 120-second loop does not bound an individual fetch() or response.json(). A peer that accepts the TCP connection and then never responds can therefore pin the verifier until the workflow timeout, bypassing the intended diagnostic deadline and delaying stopChild(). The smallest fix is to pass an AbortSignal based on the remaining deadline to the request/body read and add a hanging-server regression. I am not opening a duplicate inline thread.
中文说明
已在当前 head 复现并确认,校准为 P2。外层 120 秒循环并不能限制单次 fetch/response.json;如果对端建立连接后不响应,验证会一直挂到 workflow 超时,finally/stopChild 也无法及时执行。请用剩余 deadline 创建 AbortSignal,并补“连接成功但不响应”的测试。
There was a problem hiding this comment.
@Astro-Han, agreed. The finding remains valid and open.
The fix should derive an AbortSignal from the remaining findRendererTarget() deadline for both fetch() and response.json(). The regression should use a server that accepts the connection but does not send a response. It should verify that the verifier reaches its diagnostic timeout and executes cleanup without waiting for the workflow timeout.
You are interacting with an AI system.
Code Review by Qodo
1.
|
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for removing the reserve-then-release race and making Chromium's DevToolsActivePort the source of truth—the direction and scope are both good.
I’m leaving this as COMMENT for the current head because the Windows installer lifecycle check reproduces a stale-port failure already captured in the existing CodeRabbit/Qodo thread on scripts/verify-packaged-app.mjs:108. The lifecycle reuses the smoke directory, the old DevToolsActivePort remains, and port ??= permanently caches that old value while the new process listens elsewhere. The smallest fix is to remove the stale file before spawning (or otherwise require a fresh value for this launch) and add a regression with a pre-existing port file. It would also be prudent to bound each CDP fetch() attempt with an abort deadline so one half-open response cannot outlive the outer poll deadline.
Once that existing thread is addressed and the Windows lifecycle lane is green, this should be straightforward to re-review.
AI-assisted review disclosure: Codex reviewed exact head 7f61f1e and verified the blocking path against the current failing Windows CI log; no duplicate inline comment was added.
中文说明
这个改动从 Chromium 实际写出的 DevToolsActivePort 读取端口,方向和范围都是对的。但当前 Windows installer lifecycle 已真实复现旧端口文件的问题:测试复用 smoke 目录,旧文件仍存在,而 port ??= 一旦读到旧端口就不会再刷新,最终一直轮询错误端口并超时。建议在启动前删除旧文件(或明确只接受本次启动产生的新值),并补一个预置旧端口文件的回归测试。现有行内线程已经覆盖,因此不重复发 inline comment。
The first CI run of the port-file diagnostics answered the question the old log could not: the poll read port 51921 from DevToolsActivePort and fetch failed for the full 30 seconds. Chromium removes that file only on a clean exit, and the upgrade-lifecycle check reuses one user-data directory across two app versions with a kill between them — so the file belonged to the previous instance and pointed at a port nothing listened on. Deleting it before spawning means whatever appears was written by this child. Verified by pre-seeding a stale file naming a dead port: the smoke passes against a real packaged app. Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
|
The first CI run of this PR paid for itself: the package job failed again, but this time the log said |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
scripts/verify-packaged-app.mjs (1)
111-119: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRecord non-OK
/json/listresponses.When
fetch()returnsok === false, setlastErrorwith the HTTP status before retrying. Otherwise, the timeout reports only the port and does not distinguish an unhealthy endpoint from a missing port file.Source: Path instructions
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 5f514c1a-da4f-4e57-b98e-3700d365b648
📒 Files selected for processing (1)
scripts/verify-packaged-app.mjs
Included review availability: Your plan provides up to 3 included reviews per hour; 1 remains after this review.
The second diagnostics run separated another cause: the standalone smoke read the child's own port, /json/list answered throughout, and no debuggable page target appeared within the old 30-second line — a healthy app on a cold Windows runner that scans a first-run executable, consistent with the step having been observed at 74 seconds. Per this file's own rule that a wrong deadline fails a good release, target discovery and the renderer-usable loop both get 120 seconds (the child exiting still fails immediately; the workflow timeout stays the outer bound), and the timeout message now states where discovery stalled — no port file, a port that never answers, an unexpected HTTP status, and an endpoint with no page target are four different faults. Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
|
The second diagnostics run separated one more cause, again in a way the old log could not: this time the standalone smoke (fresh directory, so the port file was the child's own after |
Astro-Han
left a comment
There was a problem hiding this comment.
The stale DevToolsActivePort issue is fixed on this head: the verifier removes the old file before spawn and now waits longer for the packaged renderer. The authority remains Chromium's actual port, which is the right design.
I’m keeping COMMENT because the existing unresolved CodeRabbit thread at scripts/verify-packaged-app.mjs:117-119 still applies: neither fetch() nor response.json() is bounded by the remaining deadline. A half-open CDP endpoint can therefore outlive the 120-second outer loop and leave cleanup to the workflow timeout. Please pass an AbortSignal derived from the remaining attempt/deadline budget and cover a server that accepts but never answers.
The new Windows workflow is still queued; it must pass before this can be considered merge-ready. No duplicate inline comment was added.
AI-assisted review disclosure: Codex re-reviewed current head 6532f7a, the two intervening deltas, current Windows runs, and existing thread state.
中文说明
当前 head 已修复旧 DevToolsActivePort 文件问题,并延长 packaged renderer 等待时间,方向正确。仍需处理现有 CodeRabbit 线程:fetch() 和 response.json() 没有受剩余 deadline 的 AbortSignal 约束,半开连接仍可能让 verifier 无限等待到 workflow timeout。建议按剩余时间传入 abort signal,并补“连接建立但永不响应”的回归测试。新的 Windows workflow 仍在排队。
Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for replacing the reserve-then-release race with Chromium's own DevToolsActivePort record. The stale-file cleanup and more specific diagnostics are sound, and this release-verifier-only change does not require product UI screenshots. The PR body and all four commits have complete Claude Code disclosure/trailers.
One P2 remains in the existing CodeRabbit thread, which I confirmed and replied to rather than duplicating: an individual fetch() or body read is not bounded by the 120-second loop, so a peer that accepts the connection and never responds can hang the job until the workflow timeout. Please add a remaining-deadline AbortSignal and a hanging-server regression.
The required Release Windows check and Storybook jobs are also still running. Please resolve the P2 and let the exact-head checks complete before rereview.
Reviewed with Codex as an AI-assisted code review. I verified the exact-head diff, deadline behavior, existing threads, CI, UI scope, and commit provenance; no external model output was used.
中文说明
用 DevToolsActivePort 消除端口竞态的方向正确,stale file 清理与诊断也合理;这是 release verifier 内部改动,不需要产品截图,AI 说明和四个 commit trailer 都完整。当前仍有一个已存在的 CodeRabbit P2:单次 fetch/response.json 没有被 120 秒 deadline 约束,连接后不响应会挂到 workflow 超时。我已在原 thread 回复确认,没有重复开 inline。修复并等 required checks 完成后再 review。
|
One more data point while this waits for review: #3148's merge-CI just hit the same class again — |
# Conflicts: # scripts/verify-packaged-app.mjs
The third diagnostics run surfaced a genuine app fault — the installed candidate's Runtime Host stopped responding during startup, so the main process never served CDP — but the loop reported 355 seconds against a 120-second deadline, because a fetch against a bound-but-unresponsive endpoint hangs for undici's 300-second headers timeout. Each attempt now carries a 2-second abort, so the poll keeps its cadence and the deadline means what it says; the app's own fatal stderr still rides along in the failure message, which is how this fault was attributed at all. Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
|
The approved run's one failure is worth reading rather than retrying blindly — the new diagnostics attributed it precisely, and it is not the port plumbing this PR fixes: the upgraded 0.1.11 install's own stderr shows |
Astro-Han
left a comment
There was a problem hiding this comment.
[P2] Bound the WebSocket handshake as well as the CDP HTTP poll
The previous unbounded fetch() / response-body issue is fixed on exact head 119d9da: the per-attempt AbortSignal also aborts a stalled response.json() read.
However, evaluateInRenderer() still awaits the WebSocket open or error event before starting its timeout. A peer that accepts the TCP connection but never completes the WebSocket handshake therefore leaves the verifier in CONNECTING until the workflow-level timeout. I reproduced this locally with a TCP server that accepts the connection without completing the upgrade; neither event fired and the promise remained unsettled.
This is relevant to the current exact-head Release Windows run: all earlier package, standalone-smoke, and upgrade-lifecycle steps passed, while Verify automatic update end to end has remained in progress for over 30 minutes.
Please start the timeout before opening the WebSocket, cover both the handshake and the Runtime.evaluate response, close the socket when the deadline expires, and add a regression using a server that accepts the connection without completing the WebSocket handshake. The current required run should also reach a terminal state and its log should be inspected before rereview.
Reviewed by Astro-Han with Codex assisting as an AI-assisted code review. Astro-Han made the review decision; Codex verified the exact-head diff, reproduced the hanging handshake, and monitored the required Windows run.
中文说明
最新提交已经修复原来的 fetch/body-read 超时问题,但 WebSocket 建连阶段仍没有超时:TCP 建立后如果不完成 WebSocket 握手,验证会一直卡到 workflow timeout。本地已复现,当前 Windows exact-head run 的自动更新步骤也已长时间不结束。请让同一个 deadline 覆盖握手和 Runtime.evaluate、超时后关闭 socket,并补 hanging-handshake 回归测试。
The approved run reached the auto-update step, waited for the upgraded app to relaunch, and hung there for 59 minutes until the runner cancelled the job — past a 120-second deadline. The app had in fact relaunched (the runner terminated it as an orphan at cleanup); the `Get-CimInstance Win32_Process` probe never returned, and a poll loop only checks its deadline between probes, so an unbounded probe makes the deadline a lie. Same defect class as the CDP poll this PR already bounded, one layer down. The probe now carries a 30-second timeout — far above a normal query, far below the 60s and 120s deadlines above it — so a stuck WMI query fails naming itself instead of freezing the job. Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
|
Status on the approved run, since it reads worse than it is: the fix worked, and the run died of something else. On The job then went on to the auto-update verification, got as far as That is the same defect class this PR already fixed one layer up ( Both hangs were invisible before: the old code could only report "did not expose CDP within 30 seconds" or nothing at all. Worth stating plainly, though: I have not reproduced the WMI stall locally, so the 30-second bound is a guard derived from the observed failure, not from a root cause I can demonstrate. If the probe starts failing at 30s on healthy runners, that is a signal worth reading rather than raising the number. Could someone approve the runs for |
Bounding the probe turned a 59-minute zombie job into a 74-second failure, and the next run showed why that is not enough: the Windows process query stalls at one specific moment — while NSIS hands off and relaunches the upgraded app, with the process table in flux — so the probe timing out aborted a verification whose 120-second deadline had barely started, about a relaunch that had in fact happened. A bounded probe that gives up says nothing about the processes, so the loops now treat a probe failure as an unknown round and keep polling; their own deadline stays the authority and reports the last probe failure alongside, so a persistent stall is still visible and is not mistaken for a missing app. Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
|
The probe bound in That run reached That makes the bound alone wrong in a way worth naming: a probe that gives up says nothing about the processes, but it was aborting a verification whose own 120-second deadline had barely started — about a relaunch that had in fact happened (the earlier run's cleanup terminated the relaunched Where that leaves this PR's scope: it started as "read the CDP port Chromium bound rather than reserving one" and has become one coherent rule for the release verifier — every poll loop bounds its own attempts, and no single stalled attempt outranks the loop's deadline. Three instances now: the CDP fetch (undici's 300s default blew a 120s deadline to 355s), the process probe (unbounded, hung a job for an hour), and the loops around it. I would rather state that plainly than pretend the last two commits are about port discovery. Not verified locally: I cannot reproduce the WMI stall on macOS, so the retry behaviour is reasoned from the two observed runs, not demonstrated. If a stall ever outlasts a full 120-second window, the new message will say so, and that is a genuine finding rather than this loop's problem. Runs on |
|
Coordination note: @liugddx's #3265 fixes the same CDP fault independently, reading the port from Chromium's |
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks — reading the port back from DevToolsActivePort is the right fix, and removing the race beats reporting it. Reviewed exact head af023a7ff95d12f44496dd2c9a87c71aac1af977.
The core change is sound and is what the browser-automation tools do for exactly this reason: --remote-debugging-port=0 plus the port file is the only discovery method with no window between choosing a number and binding it. Deleting a stale port file before spawn is the necessary companion — Chromium removes it only on a clean exit, and the upgrade-lifecycle path SIGKILLs between versions in one user-data directory, so without that rm the poll would read the dead instance's port. Bounding the fetch against undici's 300-second header timeout, and bounding the PowerShell probe that runs inside deadline-bounded loops, are both real fixes to the same underlying mistake: a deadline you only check between unbounded operations is not a deadline. I also checked that rm is already imported in verify-windows-autoupdate.mjs and that both call sites there were updated to the new findRendererTarget signature.
Two P2s and a P3 inline, all about where the new bounds were and were not applied. Also worth noting: no CI checks are reported at this head, so the end-to-end macOS run in the description is currently the only evidence — and since these scripts only execute in the release workflow, that is the evidence that matters most.
Review disclosure: this review was prepared with Claude Code, which read the diff at this head, checked the import and call-site consistency described above, and enumerated the surviving timeout constants across all three scripts. Nothing here was reproduced by running the verifiers. The human contributor reviewed this before posting.
| 'powershell', | ||
| ['-NoProfile', '-NonInteractive', '-Command', script], | ||
| { | ||
| timeoutMs: 30_000, |
There was a problem hiding this comment.
[P2] A 30-second probe bound does not fit inside a 60-second wait. waitForInstalledProcessesToExit defaults to timeoutMs = 60_000, so with this bound in place a loop that hits the stall this PR is fixing gets at most two observations before its deadline, and if both stall it throws Installed Maka processes did not exit within 60000ms: <unknown> — never having read the process table once. That is a more confusing failure than the hang it replaces, because the message names a condition the code never actually observed. The comment above is right that the loop's deadline should stay the authority, but that only holds when the probe bound is small relative to the budget; here it is half of it. Make the probe bound a fraction of the caller's timeout — a few seconds, matching the 1-second poll interval — or raise the wait's default, and have the message distinguish "still running" from "never successfully probed" rather than printing <unknown> for both.
| }); | ||
|
|
||
| const target = await findRendererTarget(cdpPort, child); | ||
| const target = await findRendererTarget(userData, child); |
There was a problem hiding this comment.
[P2] Apply the new headroom to the Windows renderer waits too, since that is the path the failures came from. smokePackagedRenderer raised both its CDP discovery and its renderer-state deadline to 120 seconds, with the reasoning that a cold runner slow to serve CDP is as slow to mount React. That reasoning applies unchanged to rendererDeadline on the next line, which stays at 30 seconds, and to the deadline in the nested smokeRenderer at line 487, which also stays at 30 seconds — and both of those are in the Windows auto-update verifier, which the description names as having hit this fault twice, once in the standalone smoke and once in the upgrade-lifecycle step. As it stands the PR concludes that 30 seconds is too tight for a cold Windows runner and then leaves two Windows renderer waits at 30 seconds, so the same timeout can still fail a good build with a message that says nothing about why. Either raise them with the same justification or say why the post-upgrade relaunch is expected to be faster than a cold start; a shared constant next to findRendererTarget's deadline would keep the answer in one place.
| (target) => target.type === 'page' && target.webSocketDebuggerUrl, | ||
| ); | ||
| if (page) return page; | ||
| port ??= await readDevToolsPort(userDataDirectory); |
There was a problem hiding this comment.
[P3] Make findRendererTarget enforce the freshness it now depends on, rather than trusting each caller to have deleted the file. port ??= caches the first value read and never re-reads, so a DevToolsActivePort left by a previous instance is latched for the entire 120-second window and the run ends with port <dead> did not answer — which reads as a hung app rather than as a stale file, the precise misdiagnosis this PR exists to eliminate. All three current call sites do delete the file first, so nothing is broken today; what is fragile is that the requirement lives only in three separate comments, on an exported function whose signature gives no hint of it. The fourth caller will not know. Passing the spawn timestamp and ignoring a port file older than it, or having this function do the deletion as part of a startRendererTarget(userDataDirectory) that owns both halves, puts the invariant where it cannot be forgotten. Inference about the future caller; the caching behaviour itself is confirmed by reading the code at this head.
Astro-Han
left a comment
There was a problem hiding this comment.
Re-reviewed at exact head 5543fba031f0f1bb372e0f6305691747f18a6293. The head moved only by a merge of origin/main: all three changed files are byte-identical to the head I reviewed before (af023a7ff), so I am not re-filing the findings as new inline comments — the existing threads still apply verbatim and remain accurate at this commit.
Standing, unchanged:
- [P2]
verify-windows-installer-lifecycle.mjs— a 30-second probe bound inside a 60-secondwaitForInstalledProcessesToExitdefault gives the loop at most two observations, and two stalls producedid not exit within 60000ms: <unknown>for a condition never actually observed. - [P2]
verify-windows-autoupdate.mjs— the new 120-second headroom was applied insmokePackagedRendererbut not torendererDeadlineor the nestedsmokeRendererdeadline, both still 30 seconds, on the Windows path the description names as having hit this fault twice. - [P3]
verify-packaged-app.mjs—findRendererTargetlatches the first port it reads and never re-reads, so its freshness precondition lives only in three call-site comments rather than in the exported signature.
The core change is still right and I still like it: --remote-debugging-port=0 plus the port file is the only discovery method with no window between choosing a number and binding it, and deleting a stale port file before spawn is the necessary companion. One coordination note that has become relevant since: #3265 is fixing the same CDP-port discovery race in this same file, and its branch is carrying its own copy of a waitForDevToolsPort helper plus a 30s→90s discovery timeout. Whichever lands second will conflict here — this PR is the narrower and better-scoped of the two, so my suggestion is that #3265 rebase onto this rather than the reverse.
No CI checks are reported at this head.
Review disclosure: this review was prepared with Claude Code, which compared the three changed files between the previous head and this one by content hash and re-read the surrounding timeout constants. The human contributor reviewed this before posting.
|
Reviewed exact head The core fix is sound: using I do not think this is merge-ready yet. The existing unresolved inline threads still apply:
Also, the exact-head GitHub Actions runs currently show AI-assisted review disclosure: Codex inspected the exact-head diff, current unresolved review threads, and exact-head workflow/check state. No code or PR metadata was changed. |
…bserved Review found the probe bound was the right idea at the wrong proportion: 30 seconds inside a 60-second poll left room for two attempts, so a single stall could consume the whole window — a loop that can only try twice cannot survive a stall, which is the entire point of tolerating one. The probe is now 10 seconds and takes the budget as a parameter. The deadline messages also claimed more than they knew. Never having read the process table is a different fault from processes refusing to exit, or from an installer failing to relaunch, and only some of those are about Maka; each loop now reports which of the two it observed. Renderer readiness moves to one exported constant. The auto-update path kept two 30-second waits after the shared smoke was raised to 120 for slow cold Windows runners, which is exactly the drift a shared number prevents; the single-evaluation and taskkill timeouts stay where they are, being bounded calls rather than readiness waits. `findRendererTarget` also re-reads DevToolsActivePort each round instead of latching the first value, so a caller that leaves a predecessor's file behind converges once the child overwrites it rather than polling a dead port for the full deadline. Verified against a real packaged app with a stale file pre-seeded. Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
|
@likun666661 — all three addressed in P2 — probe budget. You are right, and it makes my own change nearly pointless as written: a 30-second probe inside a 60-second poll leaves room for two attempts, so one stall consumes the window — and a loop that can only try twice cannot survive a stall, which is the entire reason to tolerate one. The probe is now 10 seconds and takes The diagnostic is separated the way you asked, in both loops. Never having read the process table is now its own message — "Could not read the process table within 60000ms, so whether installed Maka processes exited is unknown" — distinct from having observed processes that would not exit. Only one of those two faults is about Maka, and the old wording sent the reader after the wrong one. P2 — deadline consistency. Fixed through a shared constant, as you preferred: P3 — the latch. Also right, and worth stating what the fix does and does not buy. Verified against a real packaged app, including the stale-port-file case. On CI evidence: you are right that there is none for the exact head — the runs sit in |
Astro-Han
left a comment
There was a problem hiding this comment.
Re-reviewed at 7eeb843e. All three standing findings are fixed, and the fixes are better than what I asked for. No new findings.
- Duplicated renderer budget — resolved by extracting
RENDERER_READY_TIMEOUT_MSand using it at all four wait sites. The two30_000renderer deadlines inverify-windows-autoupdate.mjsthat previously contradicted the 120s one are now the same constant, so the "cold runner is slow to serve CDP and equally slow to mount React" reasoning is stated once and applied everywhere. I checked the remaining30_000literals in that file: they are per-command and per-evaluate timeouts, not renderer readiness, so leaving them is correct. - Latched
DevToolsActivePort— resolved twice over. The poll now re-reads instead of??=-latching, so it converges once Chromium overwrites the file; and every call site removes the file before spawning (verify-packaged-app.mjs:317,verify-windows-autoupdate.mjs:242and:478). The comment is honest about the residual case it cannot detect — a stale file that is never overwritten — which is exactly the right thing to say, since removal at the call sites is what actually closes it. - Probe failure masked as a Maka failure — resolved in both loops.
observeddistinguishes "never read the process table" from "read it and the processes were still there / never came back", and the two messages now send the reader after different things. That was the substance of the finding: the old message asserted something about the installer that the run had no evidence for.
The unasked-for fourth change is the best one: dropping listInstalledProcesses's probe timeout from 30s to a configurable 10s means a stalled probe leaves room for several more attempts inside the 60s and 120s budgets, instead of a loop that can only try twice. That is the difference between tolerating a stall and merely surviving one. Worth watching in practice that 10s stays comfortably above a healthy Get-CimInstance on a loaded runner — if it does not, every probe times out and the run fails with the new "unknown" message. That message is at least honest about what happened, so I am noting it rather than filing it.
Reviewed with Claude Opus as an analysis assistant. Every disposition above was verified by reading the source at this head and diffing it against the head I previously reviewed; the Windows paths were not executed.
Not marking this approved only because my standing authorization in this review campaign is comment-only; nothing here blocks it from my side.
Astro-Han
left a comment
There was a problem hiding this comment.
Approving at 7eeb843e. The three standing findings are fixed at their mechanisms: one shared RENDERER_READY_TIMEOUT_MS replaces the contradicting deadlines, the DevToolsActivePort poll re-reads and every call site clears a stale file before spawning, and probe failures are no longer reported as installer failures. The probe-timeout change is a real improvement on top. Detail in the previous review; nothing outstanding from my side.
The rollback gate's first execution failed at 'asserting the uninstall registration was restored' with an empty registry — after the exit-102 and byte-identical tree assertions had already passed. The registry snapshot the installer hook restores was legitimately empty, and the lane's own timeline says why: 08:13:37 autoupdate step spawns its silent uninstall 08:13:56 autoupdate declares success — waitUntilMissing saw the FILES go 08:13:58 rollback step installs the candidate 08:14:2x candidate writes its registry registration 08:14:25 failpoint upgrade snapshots the registration: empty Launched without _?=, 'Uninstall Maka.exe' copies itself to %TEMP% and detaches; the copy removes $INSTDIR first and deletes the uninstall and install registry keys as its LAST action (uninstaller.nsh: RMDir /r, then shortcuts, SHChangeNotify, app-data handling, then DeleteRegKey) — on a busy runner tens of seconds after the files vanished. The candidate installed inside that window, and the stale detached uninstaller then deleted the candidate's fresh registration. The upgrade's /D= argument masks the loss everywhere else: $INSTDIR no longer depends on InstallLocation, so the backup, failpoint, restore and tree assertions all still pass and the damage only surfaces at the registration check. Fix at the completion signal: the registration's disappearance is the detached uninstaller's final destructive instruction, so waiting for it IS waiting for the uninstall to be over. A shared waitForUninstallRegistrationToClear now runs after every harness uninstall (lifecycle, autoupdate, rollback), the rollback harness additionally refuses to install until any earlier registration has cleared, and a new precondition assert fails the run by name if the candidate's registration is missing before the upgrade — so a recurrence points at the guilty side immediately instead of two steps later. No polling of process lists: the registry entry itself is the signal, which also avoids the unbounded Get-CimInstance stall reported in apache#3241. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Generated-by: Claude Fable 5
apache#3265 fixes the same reserve-then-release race by reading the port from Chromium's `DevTools listening on …` stderr line, which is fresh per launch and therefore cannot go stale — the failure mode this PR had to guard against explicitly, because the upgrade-lifecycle check reuses one user-data directory across two app versions with a kill in between. That mechanism needs no delete-before-spawn convention, so it wins on merit and the port half goes there, along with the per-attempt poll bound both PRs arrived at independently. What remains is disjoint at file level and nobody else has it: the Windows process probe that hung a job for 59 minutes past a 120-second deadline, and the poll semantics that let a stalled probe be an unknown round rather than a verdict. Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
Astro-Han
left a comment
There was a problem hiding this comment.
The central argument is right and worth saying so plainly: an unbounded probe inside a deadline-bounded poll loop makes the deadline a lie, and the comment at lines 43–47 names the exact incident that proves it. Tolerating a single stalled probe instead of failing on it is also the right call — the loop's deadline should be the authority, not one flaky WMI query. I have no objection to the shape of the fix.
What I am raising is that the change is not yet complete on its own terms, in three places, and none of them is visible from this diff alone.
Architecturally there is one thing worth deciding rather than fixing: listInstalledProcesses has two callers, and only one of them was taught to tolerate a failed probe. The rule this PR establishes — a bounded probe that fails means unknown, and only the loop's deadline decides — is stated in a comment inside the function, but enforced in the caller. That works while there are two callers and one file; it will not survive a third. Either the tolerant loop becomes the shared helper both callers use, or the rule needs to live somewhere a new caller cannot miss.
None of the findings below blocks a release. I am leaving this as COMMENT rather than approving because the checklist claims test coverage that does not exist in the tree, and that is the kind of thing a second reviewer should not have to discover.
AI disclosure: this review was produced with Claude Code (Opus 5), with a subagent covering security, correctness, integration and simplification in parallel. I independently re-derived every finding published here by reading verify-windows-installer-lifecycle.mjs, verify-windows-autoupdate.mjs and verify-packaged-app.mjs at c611cfd, and confirmed the absence of a test file at this head. The subagent additionally reports executing the injected-probe scenarios; I did not re-run those myself, so treat the specific timings as its evidence rather than mine. Per AGENTS.md this is not independent human review.
| if (Date.now() >= deadline) { | ||
| // Never having seen the process table is a different fault from seeing | ||
| // processes that would not exit, and only one of them is about Maka. | ||
| if (!observed) { |
There was a problem hiding this comment.
[P2] Key this branch on whether the LAST probe succeeded, not on whether any probe ever did. observed is sticky: once any probe returns, it stays true for the rest of the loop, while processes keeps whatever that probe saw. So the sequence "first probe returns Maka.exe (1234), that process then exits, every later probe stalls" arrives at the deadline with observed === true and a processes array captured at t=0 — and falls through to the branch below, which reports Installed Maka processes did not exit within 60000ms: Maka.exe (1234). That is a definite claim about a process that may well have exited, derived from a snapshot up to a minute stale, and the The last probe also failed footnote does not undo the sentence in front of it. It defeats the distinction this PR exists to draw — the comment on line 90 says never having seen the table is a different fault from seeing processes that would not exit, and this is a third state, "saw them once and no longer knows", currently reported as the second. Confirmed by reading the loop at this head; the subagent reports reproducing it with an injected probe that succeeds once and then fails 15 times, yielding exactly that message. The fix is also a simplification: drop observed and processes in favour of one lastObservation set on success and cleared in the catch, then branch on whether it is undefined — three state variables become two and the semantics move from "ever" to "last". Regression test: inject a probe returning one process, then failing until the deadline, and assert the message reports unknown rather than "did not exit".
| // of the job during an auto-update relaunch, so the 120s relaunch deadline | ||
| // never fired and the runner cancelled the run an hour later. | ||
| // | ||
| // Well under the 60s and 120s budgets above it, so a stalled probe leaves |
There was a problem hiding this comment.
[P2] Scope this claim, and fix or hand off the loop it does not hold for. "A stalled probe leaves room for several more attempts" is true of waitForInstalledProcessesToExit below, because you gave it a try/catch. It is not true of the other caller: scripts/verify-windows-autoupdate.mjs:405 awaits listInstalledProcesses(installDirectory) bare inside its 120s relaunch loop, with no try/catch anywhere in verifyWindowsAutoupdate, so one rejected probe propagates out and fails the whole auto-update verification on the first stall. That loop is the one the incident in lines 45–47 actually happened in. This PR still improves it — an hour-long hang becomes a 10s failure — but it converts a hang into a hard failure rather than into the tolerated retry this comment describes, and a healthy-but-slow probe that would previously have completed now fails a release verification instead. Confirmed by reading both files at this head; also confirmed that #3265 at 23e8a8bc9 does not touch lines 401–413, so neither PR currently owns it. Either wrap that probe in the same try/catch and let the 120s deadline decide, or narrow this comment to the function it describes and say who picks up the relaunch loop. Regression test: make listInstalledProcesses injectable into verifyWindowsAutoupdate, have it reject for the first N calls and then report the relaunched process, and assert the relaunch wait still succeeds.
| let processes = []; | ||
| let observed = false; | ||
| let probeError; | ||
| for (;;) { |
There was a problem hiding this comment.
[P2] Land the injected-probe tests you describe in the PR body, or correct the checklist. There is no scripts/verify-windows-installer-lifecycle.test.mjs at this head, and waitForInstalledProcessesToExit is the rare release-script function that is trivially testable without Windows — listProcesses, sleep and timeoutMs are all injectable parameters, so the transient-failure, persistent-failure and never-exits paths are a few dozen lines of plain Node with no PowerShell involved. The PR checklist states tests cover the change and fail without it; at this head nothing in the tree does either. This matters more than usual here because the whole change is about which of three failure messages a future maintainer sees at 2am, and message selection is exactly what a test pins. Confirmed by listing scripts/ at c611cfd. One practical note when you add it: scripts/*.test.mjs is not collected by npm test, so it needs registering behind an explicit entry point the way check:release and windows:inventory are, or the test check will stay blind to it.
| let probeError; | ||
| for (;;) { | ||
| try { | ||
| processes = await listProcesses(installDirectory); |
There was a problem hiding this comment.
[P3] Derive the probe's bound from the time the loop has left, rather than letting the callee's default decide. This call passes no options, so each attempt is bounded only by listInstalledProcesses's own timeoutMs = 10_000 default, and the relationship between that number and this loop's deadline exists only in the comment above. No caller violates it today, but waitForInstalledProcessesToExit(dir, { timeoutMs: 5_000 }) would run for roughly 15 seconds while claiming a 5 second bound — reintroducing the precise defect this PR is fixing, one level up. Passing { timeoutMs: Math.min(10_000, deadline - Date.now()) } makes the deadline self-enforcing and also caps the overshoot at the poll interval. Inference: no such caller exists at this head, so this is about keeping the invariant true rather than a defect you can trigger today.
| // processes that would not exit, and only one of them is about Maka. | ||
| if (!observed) { | ||
| throw new Error( | ||
| `Could not read the process table within ${timeoutMs}ms, so whether installed Maka processes exited is unknown.\nLast process probe failed: ${probeError?.message}`, |
There was a problem hiding this comment.
[P3] Truncate the probe error before interpolating it. runCommand's timeout message is ${command} ${args.join(' ')} did not finish within ${timeoutMs}ms, and the last element of args is the whole multi-line Get-CimInstance script — so probeError?.message drops roughly 800 characters of embedded PowerShell, newlines included, into the CI failure output, both here and in the The last probe also failed branch below. The point of this change is that a failure should say which kind of fault occurred; the sentence that does that ends up buried under the script that caused it. No disclosure concern — the only interpolated value is the temporary install directory. Taking error.message.split('\n')[0], or eliding over-long args inside runCommand itself, keeps the diagnosis readable. Confirmed by reading verify-packaged-app.mjs at this head.
There was a problem hiding this comment.
Thanks for following up on the Windows installer-verifier regression exposed by the merged #2650/#2658 lifecycle work. Reviewed at exact head c611cfd7b1708a2a051047676d1b726ae69ae37c.
The central fix is sound: a deadline-bounded poll cannot contain an unbounded Get-CimInstance Win32_Process call, and bounding each probe restores authority to the outer deadline. Treating a transient WMI failure as an unknown round instead of letting one probe decide the lifecycle check is also the right direction.
I read the existing reviews and exact-head inline threads before finalizing this pass. Astro-Han has already captured the remaining concerns inline, including the sticky observed state, the direct auto-update caller that does not tolerate a rejected probe, the missing committed regression tests, the probe-budget coupling, and the verbose timeout diagnostic. I agree with those threads and am not duplicating them with another set of inline comments.
The exact-head syntax check, diff check, repository lint, and workspace typecheck pass. The GitHub test check is green; the package check currently fails in the automatic-update path. I am leaving this as COMMENT rather than approval until the existing P2 threads are resolved and the required release evidence reaches an accepted terminal state.
No additional finding beyond the existing exact-head threads came out of this pass.
AI-assisted review disclosure: Codex read the exact-head diff, all existing review bodies and inline threads, checked the relevant callers, and ran the static/type checks above. The reviewer made the final scope and disposition decisions.
…ntracts Split out of apache#3265 at its reviewer's request so these fixes merge on their own evidence and that PR stands on the installer transaction alone. Contents: - Read the CDP port from the DevTools stderr announcement (waitForDevToolsPort) instead of pre-reserving one; widen renderer discovery to 90s with per-probe AbortSignal bounds and errno cause chains (four observed CI failures in this family). - waitForUsableRenderer: poll the renderer-usable state with the deadline as the sole authority — one stalled Runtime.evaluate used to fail the whole gate (run 32352924376); the WebSocket handshake now has its own bound so a port that accepts but never speaks fails the probe, not the lane. - Tolerate taskkill exit 128 when the relaunched instance already exited; the authoritative assertion remains waitForInstalledProcessesToExit. - Match the versioned uninstall DisplayName ('Maka 0.1.11'): the -eq 'Maka' filter matched nothing, deterministically, and every reader of the scan was blind. - Bound every PowerShell probe that runs under a polling deadline (the anti-pattern apache#3241 names), and let waits tolerate one failed probe: a failed enumeration is never treated as 'no processes'. - waitForUninstallRegistrationToClear: a detached uninstaller deletes its registry keys after waitUntilMissing sees the files disappear; wait for the registration to clear before the next install, with the one-registry-call residual window stated precisely. - directoryTreeManifest/diffTreeManifests shared exports for the rollback gate, now recording empty directories so their loss is visible; capture upgrade-state evidence on a relaunch version mismatch. - Commit the table-driven contract tests as scripts/verify-windows-harness.test.mjs and wire them into the CI planner test step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Generated-by: Claude Fable 5
|
Closing in favour of #3327, which covers this file's change and considerably more. The story of this PR, for anyone who lands here from a CI log: it began as a fix for the reserve-then-release CDP port race, and every run of it surfaced a deeper cause underneath — an unbounded The port half went to #3265 earlier: reading Chromium's Nothing here is abandoned; it is all in a PR that reviewers asked for and that covers more ground. Thanks @likun666661 for the review that made the remaining piece correct, and @liugddx for proposing the split before a merge conflict decided it for us. Still open and unowned from this line of work: the |
…ntracts (#3327) * test(windows): harden the release-verification harness and pin its contracts Split out of #3265 at its reviewer's request so these fixes merge on their own evidence and that PR stands on the installer transaction alone. Contents: - Read the CDP port from the DevTools stderr announcement (waitForDevToolsPort) instead of pre-reserving one; widen renderer discovery to 90s with per-probe AbortSignal bounds and errno cause chains (four observed CI failures in this family). - waitForUsableRenderer: poll the renderer-usable state with the deadline as the sole authority — one stalled Runtime.evaluate used to fail the whole gate (run 32352924376); the WebSocket handshake now has its own bound so a port that accepts but never speaks fails the probe, not the lane. - Tolerate taskkill exit 128 when the relaunched instance already exited; the authoritative assertion remains waitForInstalledProcessesToExit. - Match the versioned uninstall DisplayName ('Maka 0.1.11'): the -eq 'Maka' filter matched nothing, deterministically, and every reader of the scan was blind. - Bound every PowerShell probe that runs under a polling deadline (the anti-pattern #3241 names), and let waits tolerate one failed probe: a failed enumeration is never treated as 'no processes'. - waitForUninstallRegistrationToClear: a detached uninstaller deletes its registry keys after waitUntilMissing sees the files disappear; wait for the registration to clear before the next install, with the one-registry-call residual window stated precisely. - directoryTreeManifest/diffTreeManifests shared exports for the rollback gate, now recording empty directories so their loss is visible; capture upgrade-state evidence on a relaunch version mismatch. - Commit the table-driven contract tests as scripts/verify-windows-harness.test.mjs and wire them into the CI planner test step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Generated-by: Claude Fable 5 * test(windows): collect the upgraded-smoke child's stderr persistently The upgraded-app smoke pipes stderr but only waitForDevToolsPort's temporary listener ever read it: once removed, the paused stream lets Chromium's --enable-logging=stderr output fill the pipe and block the child, and the evidence the pipe exists to preserve is lost. Attach the same persistent collector every sibling smoke uses and append its tail to renderer-readiness failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Generated-by: Claude Fable 5 * test(autoupdate): tolerate a timed-out taskkill when stopping the relaunch Run 32378497920: taskkill /T /F on the force-run instance exceeded its 30s bound on a wedged runner and failed the gate, even though the authoritative assertion - waitForInstalledProcessesToExit, which fails with the live process list if anything from the install tree still runs - was one line below. Treat a kill that overran its bound like exit 128: the kill is the mechanism, the exit wait is the assertion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Generated-by: Claude Fable 5 * test(windows): share one probe-tolerant policy for appearance and termination Review round on #3327 named the two loops that still predated the policy the rest of the harness already follows. The relaunch wait awaited listInstalledProcesses directly inside its 120-second loop, so one transient WMI failure rejected the gate even though the upgraded app may already have been running (run 32340493254). It is now waitForInstalledProcessAppearance in the lifecycle module: the mirror of the exit wait - probes are tolerated, the deadline is the authority, and the last probe error is evidence only when no later enumeration succeeds. Covered by a failure-then-success regression. The cleanup path still ran its own strict taskkill loop, so the same stale-PID exit 128 the main path tolerates (run 32378497920) could skip the authoritative exit wait and the uninstall/registration barrier, leaking a registration onto the runner. Both paths now share terminateInstalledProcesses: tolerant kill (exit 128 and an overrun bound are mechanism failures), then the exit wait as the assertion. Covered by mechanism-tolerance and rethrow tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Generated-by: Claude Fable 5 * fix(windows): bound verifier probes and cleanup * fix(windows): retry installed version probes * fix(windows): prove exit after every taskkill failure --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
The Windows upgrade-lifecycle check hung a CI job for 59 minutes past a 120-second deadline, and the runner cancelled it. The app it was waiting for had in fact relaunched — the runner terminated
Maka-0.1.12-win-x64as an orphan during cleanup — so the deadline should have fired and did not.listInstalledProcessesrunsGet-CimInstance Win32_ProcessthroughrunCommandwith notimeoutMs, and a poll loop only checks its deadline between probes. One stalled probe therefore makes the deadline unenforceable. The stall is not random: it happens while NSIS hands off and relaunches the upgraded app, with the process table in flux, and it reproduced on two consecutive runs at the same step.Two changes, from one rule — a poll loop bounds its own attempts, and no single stalled attempt outranks the loop's deadline:
timeoutMs(10 seconds by default), well under the 60-second and 120-second budgets above it. An earlier revision used 30 seconds; @likun666661 pointed out that leaves room for two attempts inside a 60-second poll, so one stall consumes the whole window — and a loop that can only try twice cannot survive a stall, which is the entire point of tolerating one.waitForInstalledProcessesToExittreats a failed probe as an unknown round and keeps polling. Its deadline stays the authority, and the message now distinguishes the two faults: never having read the process table is reported as unknown, separately from having observed processes that would not exit. Only one of those is about Maka.Scope
This PR originally also replaced the reserve-then-release CDP port discovery. That half now belongs to #3265, which reads the port from Chromium's
DevTools listening on …stderr line — fresh per launch, so it cannot go stale, which is the failure this branch had to guard against explicitly (the upgrade-lifecycle check reuses one user-data directory across two app versions with a kill in between). It wins on merit, and @liugddx and I split at file level so neither of us pays another flake-priced CI cycle. Both PRs independently arrived at the same per-attempt poll bound; that goes with the port half.What is left touches only
scripts/verify-windows-installer-lifecycle.mjs, which #3265 does not touch.Verification
Measured on CI, across three revisions of this branch, at the same step:
Locally, the loop was driven through all three of its paths with an injected probe: transient stalls followed by a clean exit return successfully after retrying; probes that never succeed report
Could not read the process table within …, so whether installed Maka processes exited is unknown; observed processes that do not exit report themselves by name and pid.lint,format:check— pass.Known unrelated failure on this lane
The auto-update step can still fail after this fix, at the next stall:
taskkill /PID … did not finish within 30000mswhile stopping the relaunched instance. Same family — Windows process operations stalling around the NSIS relaunch — but it lives inverify-windows-autoupdate.mjs, which is #3265's half under the split. Data handed over there rather than patched here.Separately, #3279 tracks an intermittent product-side fault on the same path: the upgraded install's Runtime Host stops responding during startup, which this lane surfaces as a renderer that never appears.
Breaking change
None. Release verifier internals only.
AI use
Select exactly one:
Tool(s) and scope: Claude Code — drafted the change and ran the verification above. Reviewed and submitted by the contributor of record.
Generated-by: Claude Codeis on the commits.Checklist
Does this PR entail a change in behavior?