Repository navigation
fix(update): name a witnessless update marker and its recovery on refusal #8541
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f55a926
013ebae
24dfd23
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,7 +50,7 @@ service. It does not continue extracting into a deleted stage after recovery sta | |
|
|
||
| If installer exit cannot be confirmed, the update fails and keeps the staging folder and | ||
| its ownership marker. Confirm that no installer or OpenCodex process runs from that folder | ||
| before removing it. A retained folder does not prevent another update attempt. | ||
| before removing it. A retained folder does not prevent another update attempt; a leftover update marker can, see [If every update and sync says update-in-flight](#if-every-update-and-sync-says-update-in-flight). | ||
|
|
||
| Check your registry connection if the install repeatedly exceeds the budget, then retry | ||
| `ocx update`. Increasing the budget is not needed to clean up a timed-out install safely. | ||
|
|
@@ -98,6 +98,43 @@ ownership marker. Delete a leftover only after confirming that no OpenCodex proc | |
| If a folder will not delete, a process is still running from it. Signing out or restarting Windows | ||
| releases the lock. | ||
|
|
||
| ## If every update and sync says update-in-flight | ||
|
|
||
| If every `ocx update` and `ocx sync` fails with `update-in-flight`, even after waiting, restarting | ||
| the service or rebooting, an interrupted update may have left its marker behind | ||
| ([#8524](https://github.com/lidge-jun/opencodex/issues/8524)). The marker is | ||
| `update-orchestrated.json` in the OpenCodex home (`%USERPROFILE%\.opencodex` on Windows, | ||
| `~/.opencodex` elsewhere). | ||
|
|
||
| Current releases record the Windows installer's supervisor in the marker. The marker then clears | ||
| by itself once the 15-minute fence has passed and that supervisor and the launcher are gone. | ||
| Releases up to 2.100.x did not record the supervisor. A marker they left after an interrupted | ||
| update (a closed console, Ctrl+C, Task Manager, sleep or power loss) cannot be checked, so it never | ||
| clears by itself. When that is the cause, `ocx update` and `ocx sync` in a terminal print the | ||
| marker's path and the rename command after `update-in-flight`. The dashboard, Desktop and Remote | ||
| Link only report `update-in-flight`, so run `ocx update` in a terminal to see the guidance. | ||
|
|
||
| Before you rename the marker, make sure no update is running: | ||
|
|
||
| 1. No `ocx update` or dashboard update is in progress in another window. | ||
| 2. No `npm`, `pnpm` or `bun` global install is running. On Windows, check Task Manager for | ||
| `node.exe` running npm and for a `powershell.exe` whose command line contains `npm-install-job.ps1`. | ||
| 3. Then rename the marker (renaming keeps a copy) and run `ocx update` again: | ||
|
|
||
| ```bat | ||
| ren "%USERPROFILE%\.opencodex\update-orchestrated.json" update-orchestrated.json.stale | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Identify this command as Command Prompt syntax. PowerShell does not expand As per coding guidelines, “Keep commands, paths, configuration keys, defaults, branch names, and URLs synchronized with the repository.”
|
||
| ``` | ||
|
|
||
| ```bash | ||
| mv -n ~/.opencodex/update-orchestrated.json ~/.opencodex/update-orchestrated.json.stale | ||
| ``` | ||
|
|
||
| Use the exact path the error prints if `OPENCODEX_HOME` points somewhere else. If that path contains | ||
| characters a shell could expand or interpret, or on Windows anything other than plain ASCII, the error names the file | ||
| without a command; rename it by hand. A marker that is | ||
| still inside its fence, or whose recorded process is still running, does not print this guidance. | ||
| In that case, wait or let the running update finish. | ||
|
|
||
| ## If the update reports an UNSUPERVISED proxy | ||
|
|
||
| When the background service cannot be refreshed after an update, the updater first asks launchd | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -81,23 +81,56 @@ export function parentHoldsSyncMutex(home, claimedPid = process.env.OCX_SYNC_MUT | |
| } | ||
|
|
||
| /** Shared wire format for launcher and Bun sync admission refusals. */ | ||
| export function refuseSync(reason, args) { | ||
| export function refuseSync(reason, args, hint = []) { | ||
| const exit = reason === "sync-in-progress" ? 1 : 7; | ||
| console.error(reason); | ||
| // Recovery guidance follows the reason line on stderr; stdout and the JSON document never change. | ||
| for (const line of hint) console.error(line); | ||
| if (args.includes("--json")) console.log(JSON.stringify({ schema: "ocx.sync.fanout/1", home: { state: "skipped", exit }, fanout: { state: "not-applicable", reason }, children: [] })); | ||
| return exit; | ||
| } | ||
|
|
||
| export function acquireSyncGuard(home, args) { | ||
| const lease = tryAcquireUpdateSyncMutex(home, "sync"); | ||
| if (lease.kind !== "acquired") return { kind: "refused", exit: refuseSync(lease.holder === "sync" ? "sync-in-progress" : "update-in-flight", args) }; | ||
| if (inspectUpdateMarker(home) !== "free") { | ||
| const detail = {}; | ||
| if (inspectUpdateMarker(home, { detail }) !== "free") { | ||
| lease.release(); | ||
| return { kind: "refused", exit: refuseSync("update-in-flight", args) }; | ||
| return { kind: "refused", exit: refuseSync("update-in-flight", args, detail.witnessless ? staleUpdateMarkerHint(home) : []) }; | ||
| } | ||
| return lease; | ||
| } | ||
|
|
||
| /** | ||
| * Recovery guidance for a marker that records an interrupted installer with no witness (#8524). | ||
| * OpenCodex 2.100.x and earlier never recorded the Windows installer supervisor, so such a marker | ||
| * never clears by itself. Formatting only: the witnessless fact comes from the refusing inspection. | ||
| */ | ||
| export function staleUpdateMarkerHint(home, platform = process.platform) { | ||
| const path = join(home, "update-orchestrated.json"); | ||
| const windows = platform === "win32"; | ||
| // A copy-paste command must not expand anything from the path. POSIX single quotes expand | ||
| // nothing. cmd.exe and PowerShell disagree on quoting (PowerShell also closes a string on | ||
| // typographic quotes and globs [ ]), so Windows gets a command only for plain printable ASCII | ||
| // without $ ` % " ! [ ]. A path with a control character gets no command on any platform. | ||
| const controlFree = !/[\x00-\x1f\x7f-\x9f]/.test(path); | ||
| const rename = !controlFree ? null : windows | ||
| ? (/^[\x20-\x7e]+$/.test(path) && !/[$`%"![\]]/.test(path) ? `ren "${path}" update-orchestrated.json.stale` : null) | ||
| : `mv -n ${posixQuote(path)} ${posixQuote(path + ".stale")}`; | ||
| const shown = path.replace(/[\x00-\x1f\x7f-\x9f]/g, "?"); | ||
| return [ | ||
| `The update marker ${shown} records an interrupted update whose installer can no longer be checked`, | ||
| windows | ||
| ? "(OpenCodex 2.100.x and earlier did not record the Windows installer), so it will not clear by itself." | ||
| : "(its installer was never recorded), so it will not clear by itself.", | ||
| "If no update is running (no ocx update, no npm, pnpm or bun global install, no OpenCodex installer", | ||
| "PowerShell process running npm-install-job.ps1), rename the marker and run ocx update again:", | ||
| rename === null ? " rename that file by hand, for example to update-orchestrated.json.stale" : ` ${rename}`, | ||
| ]; | ||
| } | ||
|
|
||
| function posixQuote(value) { return `'${value.replaceAll("'", "'\\''")}'`; } | ||
|
|
||
| function inspectOwnershipLease(home) { | ||
| try { | ||
| const statePaths = serviceStateFilesFor(home, isTestHomeGuardArmed() ? home : join(homedir(), ".opencodex")); | ||
|
|
@@ -110,7 +143,7 @@ function inspectOwnershipLease(home) { | |
| } catch { return "unknown"; } | ||
| } | ||
|
|
||
| export function inspectUpdateMarker(home, { now = Date.now, platform = process.platform, signal } = {}) { | ||
| export function inspectUpdateMarker(home, { now = Date.now, platform = process.platform, signal, detail } = {}) { | ||
| const probe = signal ?? (id => process.kill(id, 0)); | ||
| const windows = platform === "win32"; | ||
| const path = join(home, "update-orchestrated.json"); | ||
|
|
@@ -158,7 +191,14 @@ export function inspectUpdateMarker(home, { now = Date.now, platform = process.p | |
| // An unconfirmed installer is free only once its process-tree witness is gone: the POSIX group, | ||
| // or on Windows the supervisor whose exit closes the kill-on-close job. | ||
| const witness = windows ? marker.installerPid : marker.installerPgid; | ||
| return unknown || (marker.terminationUnconfirmed && witness === undefined) ? "unknown" : "free"; | ||
| if (unknown) return "unknown"; | ||
| if (marker.terminationUnconfirmed && witness === undefined) { | ||
| // Unknown only because nothing witnesses the installer: the fence has passed and every | ||
| // recorded process is gone. The refusal can then name the marker and its manual recovery. | ||
| if (detail) detail.witnessless = true; | ||
|
Comment on lines
+184
to
+187
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Check the launcher before reporting a witnessless marker. For a terminal Probe the launcher before setting this detail, and retain the unknown result when its liveness cannot be determined.
|
||
| return "unknown"; | ||
| } | ||
| return "free"; | ||
| } catch (error) { return !found && error?.code === "ENOENT" ? "free" : "unknown"; } | ||
| finally { if (fd !== undefined) closeSync(fd); } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Limit the hint claim to qualifying markers.
An older marker can cause refusal while its fence is active, a recorded process is alive, or a probe is inconclusive. Those paths do not set
witnesslessand do not print recovery guidance. State the qualifying conditions here and indocs-site/src/content/docs/troubleshooting/update-failed.mdLines 111–115 and its Turkish translation Lines 87–92.As per coding guidelines, “Document current shipped or intentionally pending behavior.”
Source: Coding guidelines