@bitkyc08/opencodex 2.53.0 → 2.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/{index-BlO4Yl6q.js → index-CkvITofZ.js} +2 -2
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/devin.ts +39 -7
- package/src/cli/capabilities.ts +4 -2
- package/src/cli/catalog.ts +39 -10
- package/src/cli/dispatch.ts +19 -64
- package/src/cli/doctor.ts +1 -1
- package/src/cli/internal-command.ts +44 -0
- package/src/cli/registry.ts +9 -7
- package/src/cli/restart-scope.ts +184 -0
- package/src/codex/app-server-processes.ts +20 -5
- package/src/codex/app-server-restart-service.ts +29 -0
- package/src/codex/catalog/provider-fetch.ts +13 -5
- package/src/codex/catalog/sync.ts +12 -3
- package/src/codex/desktop-app/darwin.ts +268 -0
- package/src/codex/desktop-app/handoff.ts +303 -0
- package/src/codex/desktop-app/linux.ts +388 -0
- package/src/codex/desktop-app/lock.ts +226 -0
- package/src/codex/desktop-app/types.ts +141 -0
- package/src/codex/desktop-app/windows.ts +239 -0
- package/src/codex/desktop-app-restart.ts +264 -279
- package/src/codex/inject.ts +83 -21
- package/src/codex/sync.ts +16 -22
- package/src/generated/compatibility-version.json +58 -22
- package/src/lib/codex-restart-contract.ts +31 -0
- package/src/providers/registry.ts +13 -12
- package/src/server/responses/core.ts +62 -4
- package/src/server/responses/encrypted-payload.ts +161 -0
- package/src/server/responses.ts +1 -1
- package/src/types/provider.ts +6 -5
- package/src/web-search/index.ts +14 -67
- package/src/web-search/passthrough-bridge.ts +256 -32
- package/src/web-search/sidecar-providers.ts +76 -0
|
@@ -1,52 +1,94 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Full restart of the Codex desktop app (the Electron shell),
|
|
2
|
+
* Full restart of the Codex desktop app (the Electron shell) on macOS, Linux and
|
|
3
|
+
* Windows.
|
|
3
4
|
*
|
|
4
|
-
* `--restart-codex`
|
|
5
|
-
* `codex-code-mode-host` processes
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* keeps showing the old catalog until the app itself is restarted (#2292).
|
|
5
|
+
* WHY THIS EXISTS AT ALL. `--restart-codex` used to signal only `codex app-server` /
|
|
6
|
+
* `codex-code-mode-host` processes, and on every platform that turns out not to
|
|
7
|
+
* refresh the model picker. The app-server it signals is a CHILD of the desktop app
|
|
8
|
+
* (measured: pid 16733 under pid 15901 on macOS, 3285204 under 3284901 on Linux), so
|
|
9
|
+
* the app simply respawns it while the renderer keeps the roster it built at launch.
|
|
10
|
+
* The matcher was never the problem; the only thing that reliably refreshes the
|
|
11
|
+
* picker is restarting the shell that owns it.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
* `--restart-
|
|
15
|
-
* a
|
|
16
|
-
*
|
|
13
|
+
* WHY THE CONSENT CHANGED. This capability was deliberately kept behind a separate
|
|
14
|
+
* Windows-only `--restart-desktop-app` flag, because quitting the app ends live
|
|
15
|
+
* conversations and that is a larger consent than restarting a background helper.
|
|
16
|
+
* That reasoning was sound and has been superseded by an explicit maintainer
|
|
17
|
+
* decision: `--restart-codex` now means the app is fully stopped and started again.
|
|
18
|
+
* The narrow behaviour did not disappear, it moved to `--restart-app-server-only`.
|
|
17
19
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* WHAT IS SHARED AND WHAT IS NOT. The ladder below — discover, enumerate, find
|
|
21
|
+
* shells, check self-ancestry, graceful, wait, re-verify identity, force, wait,
|
|
22
|
+
* refuse-or-relaunch — is identical on all three platforms. Only identity,
|
|
23
|
+
* discovery, membership, the two stop primitives and relaunch differ, and those live
|
|
24
|
+
* behind DesktopAppAdapter. Re-deriving the PID-reuse and fail-closed reasoning once
|
|
25
|
+
* per operating system is how two of the three end up subtly wrong.
|
|
26
|
+
*
|
|
27
|
+
* EVERYTHING FAILS CLOSED. A failed discovery, a failed enumeration, an unreadable
|
|
28
|
+
* process identity or an unreadable ancestry chain never authorises a kill and is
|
|
29
|
+
* never reported as "nothing to do". A stale picker is a much smaller problem than a
|
|
30
|
+
* wrongly killed process.
|
|
31
|
+
*
|
|
32
|
+
* Design and audit history: devlog/_plan/260913_cross_platform_desktop_app_restart/.
|
|
22
33
|
*/
|
|
23
|
-
import {
|
|
24
|
-
|
|
34
|
+
import {
|
|
35
|
+
acquireDesktopRestartLock,
|
|
36
|
+
releaseDesktopRestartLock,
|
|
37
|
+
type DesktopRestartLockIo,
|
|
38
|
+
} from "./desktop-app/lock";
|
|
39
|
+
import { rootShells, type DesktopAppAdapter, type DesktopExec, type DesktopProcess } from "./desktop-app/types";
|
|
40
|
+
import { darwinDesktopAppAdapter, darwinDefaultExec } from "./desktop-app/darwin";
|
|
41
|
+
import { linuxDesktopAppAdapter, linuxDefaultExec } from "./desktop-app/linux";
|
|
42
|
+
import { windowsDesktopAppAdapter, windowsDefaultExec } from "./desktop-app/windows";
|
|
43
|
+
|
|
44
|
+
export type { DesktopAppExecOptions } from "./desktop-app/types";
|
|
25
45
|
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
46
|
+
/** How long a graceful close is given before the forced pass. */
|
|
47
|
+
const GRACEFUL_EXIT_TIMEOUT_MS = 15_000;
|
|
48
|
+
/** How long a forced kill is given before the target counts as surviving. */
|
|
49
|
+
const FORCED_EXIT_TIMEOUT_MS = 5_000;
|
|
50
|
+
|
|
51
|
+
export interface DesktopAppRestartHandoff {
|
|
52
|
+
helperPid: number;
|
|
53
|
+
logPath: string;
|
|
30
54
|
}
|
|
31
55
|
|
|
32
56
|
export interface DesktopAppRestartIo {
|
|
33
57
|
platform?: NodeJS.Platform;
|
|
34
|
-
/**
|
|
35
|
-
|
|
58
|
+
/** Overrides the adapter chosen from `platform`. Tests drive every branch through this. */
|
|
59
|
+
adapter?: DesktopAppAdapter;
|
|
60
|
+
execFile?: DesktopExec;
|
|
36
61
|
/** Process ancestry of the current process, innermost first. Used for the self-kill guard. */
|
|
37
62
|
ancestryPids?: () => number[];
|
|
38
63
|
isAlive?: (pid: number) => boolean;
|
|
39
64
|
sleep?: (ms: number) => void;
|
|
40
65
|
now?: () => number;
|
|
66
|
+
lock?: DesktopRestartLockIo;
|
|
67
|
+
/**
|
|
68
|
+
* Hand the restart to a detached helper when this process is inside the tree.
|
|
69
|
+
* Supplied by wp5; absent here means the ladder refuses instead, which is the
|
|
70
|
+
* behaviour that shipped before the handoff existed.
|
|
71
|
+
*/
|
|
72
|
+
startHandoff?: () => DesktopAppRestartHandoff | null;
|
|
73
|
+
/**
|
|
74
|
+
* False forbids a handoff. The helper passes it so recursion is structurally
|
|
75
|
+
* impossible, and the management service passes it because it runs inside a proxy
|
|
76
|
+
* that never exits — a handoff waiting for the caller to exit would always time out
|
|
77
|
+
* after telling the operator it had been handed off.
|
|
78
|
+
*/
|
|
79
|
+
allowHandoff?: boolean;
|
|
41
80
|
}
|
|
42
81
|
|
|
43
82
|
export type DesktopAppRestartReason =
|
|
44
|
-
| "
|
|
83
|
+
| "unsupported_platform"
|
|
45
84
|
| "package_discovery_failed"
|
|
46
85
|
| "process_probe_failed"
|
|
47
86
|
| "no_targets"
|
|
48
87
|
| "self_ancestry"
|
|
49
|
-
| "
|
|
88
|
+
| "restart_in_flight"
|
|
89
|
+
| "handoff_started"
|
|
90
|
+
| "targets_survived"
|
|
91
|
+
| "relaunch_failed";
|
|
50
92
|
|
|
51
93
|
export interface DesktopAppRestartResult {
|
|
52
94
|
attempted: boolean;
|
|
@@ -54,302 +96,245 @@ export interface DesktopAppRestartResult {
|
|
|
54
96
|
surviving: number[];
|
|
55
97
|
relaunch: "started" | "skipped";
|
|
56
98
|
reason?: DesktopAppRestartReason;
|
|
99
|
+
handoff?: DesktopAppRestartHandoff;
|
|
57
100
|
}
|
|
58
101
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const PROBE_TIMEOUT_MS = 10_000;
|
|
65
|
-
|
|
66
|
-
interface DesktopPackage {
|
|
67
|
-
family: string;
|
|
68
|
-
installLocation: string;
|
|
69
|
-
aumid: string;
|
|
70
|
-
}
|
|
102
|
+
const ADAPTERS: Partial<Record<NodeJS.Platform, { adapter: DesktopAppAdapter; exec: DesktopExec }>> = {
|
|
103
|
+
darwin: { adapter: darwinDesktopAppAdapter, exec: darwinDefaultExec },
|
|
104
|
+
linux: { adapter: linuxDesktopAppAdapter, exec: linuxDefaultExec },
|
|
105
|
+
win32: { adapter: windowsDesktopAppAdapter, exec: windowsDefaultExec },
|
|
106
|
+
};
|
|
71
107
|
|
|
72
|
-
|
|
73
|
-
* Runtime discovery, never a hardcoded identifier. The beta MSIX package family
|
|
74
|
-
* changes between builds, so a literal AUMID would silently stop matching and
|
|
75
|
-
* then either do nothing or — worse — match a package we did not mean.
|
|
76
|
-
*/
|
|
77
|
-
function discoverPackage(exec: NonNullable<DesktopAppRestartIo["execFile"]>): DesktopPackage | null {
|
|
78
|
-
const script = [
|
|
79
|
-
"$ErrorActionPreference='SilentlyContinue'",
|
|
80
|
-
"Import-Module Appx -ErrorAction SilentlyContinue",
|
|
81
|
-
"$p = Get-AppxPackage -Name OpenAI.Codex",
|
|
82
|
-
"if (-not $p) { $p = Get-AppxPackage -Name OpenAI.CodexBeta }",
|
|
83
|
-
"if (-not $p -or -not $p.InstallLocation) { 'MISS' } else {",
|
|
84
|
-
" $p.PackageFamilyName; $p.InstallLocation; \"$($p.PackageFamilyName)!App\"",
|
|
85
|
-
"}",
|
|
86
|
-
].join("; ");
|
|
87
|
-
let stdout: string;
|
|
108
|
+
function defaultIsAlive(pid: number): boolean {
|
|
88
109
|
try {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
windowsHide: true,
|
|
92
|
-
});
|
|
110
|
+
process.kill(pid, 0);
|
|
111
|
+
return true;
|
|
93
112
|
} catch {
|
|
94
|
-
return
|
|
113
|
+
return false;
|
|
95
114
|
}
|
|
96
|
-
const lines = stdout.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0);
|
|
97
|
-
if (lines.length < 3 || lines[0] === "MISS") return null;
|
|
98
|
-
const [family, installLocation, aumid] = lines;
|
|
99
|
-
if (!family || !installLocation || !aumid) return null;
|
|
100
|
-
return { family, installLocation, aumid };
|
|
101
115
|
}
|
|
102
116
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
parentPid: number;
|
|
106
|
-
/** Win32_Process CreationDate. Guards against PID reuse across the wait window. */
|
|
107
|
-
createdAt: string;
|
|
117
|
+
function defaultSleep(ms: number): void {
|
|
118
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
/**
|
|
111
|
-
*
|
|
112
|
-
* location AND owned by the current user. The install location alone is not
|
|
113
|
-
* enough: an MSIX package under `WindowsApps` is shared, so on a multi-user
|
|
114
|
-
* machine another account's Codex desktop matches the same path. The app-server
|
|
115
|
-
* collector already pays for `GetOwner` for exactly this reason.
|
|
122
|
+
* True when the pid still names the process we verified.
|
|
116
123
|
*
|
|
117
|
-
*
|
|
118
|
-
* a
|
|
124
|
+
* Between listing and signalling there is a graceful-close window long enough for the
|
|
125
|
+
* OS to recycle a pid, and the next step is a hard kill. A pid alone is not an
|
|
126
|
+
* identity across that window; the start-time token is what distinguishes a process
|
|
127
|
+
* from its replacement.
|
|
119
128
|
*/
|
|
120
|
-
|
|
121
|
-
exec: NonNullable<DesktopAppRestartIo["execFile"]>,
|
|
122
|
-
installLocation: string,
|
|
123
|
-
): DesktopProcess[] | null {
|
|
124
|
-
const literal = installLocation.replace(/'/g, "''");
|
|
125
|
-
const script = [
|
|
126
|
-
"$ErrorActionPreference='SilentlyContinue'",
|
|
127
|
-
`$root = '${literal}'`,
|
|
128
|
-
"$me = ([Security.Principal.WindowsIdentity]::GetCurrent()).Name",
|
|
129
|
-
"Get-CimInstance Win32_Process -Filter \"Name='ChatGPT.exe'\" |",
|
|
130
|
-
" Where-Object { $_.ExecutablePath -and $_.ExecutablePath.StartsWith($root, 'OrdinalIgnoreCase') } |",
|
|
131
|
-
" ForEach-Object {",
|
|
132
|
-
" $o = Invoke-CimMethod -InputObject $_ -MethodName GetOwner",
|
|
133
|
-
" if ($o -and $o.ReturnValue -eq 0 -and $o.User) {",
|
|
134
|
-
" $owner = if ($o.Domain) { \"$($o.Domain)\\$($o.User)\" } else { $o.User }",
|
|
135
|
-
" if ($owner -ieq $me) {",
|
|
136
|
-
" \"$($_.ProcessId) $($_.ParentProcessId) $($_.CreationDate.ToString('o'))\"",
|
|
137
|
-
" }",
|
|
138
|
-
" }",
|
|
139
|
-
" }",
|
|
140
|
-
// Statements must be newline-separated. Joining with a space concatenates
|
|
141
|
-
// `$ErrorActionPreference='SilentlyContinue' $root = '...'` into one malformed statement,
|
|
142
|
-
// which PowerShell rejects — so the probe threw and every caller read "not running" (#2557).
|
|
143
|
-
].join("\n");
|
|
144
|
-
let stdout: string;
|
|
145
|
-
try {
|
|
146
|
-
stdout = exec(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
147
|
-
timeout: PROBE_TIMEOUT_MS,
|
|
148
|
-
windowsHide: true,
|
|
149
|
-
});
|
|
150
|
-
} catch {
|
|
151
|
-
// A probe that could not run is NOT proof the app is absent. Returning [] here made a
|
|
152
|
-
// failed enumeration indistinguishable from "no targets", so the CLI reported the app as
|
|
153
|
-
// not running and skipped a restart the user had explicitly asked for.
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
const processes: DesktopProcess[] = [];
|
|
157
|
-
for (const line of stdout.split(/\r?\n/)) {
|
|
158
|
-
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s*$/.exec(line);
|
|
159
|
-
if (!match) continue;
|
|
160
|
-
const pid = Number(match[1]);
|
|
161
|
-
const parentPid = Number(match[2]);
|
|
162
|
-
const createdAt = match[3]!;
|
|
163
|
-
if (Number.isSafeInteger(pid) && Number.isSafeInteger(parentPid)) {
|
|
164
|
-
processes.push({ pid, parentPid, createdAt });
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return processes;
|
|
168
|
-
}
|
|
129
|
+
type IdentityCheck = "same" | "gone" | "unknown";
|
|
169
130
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
*/
|
|
175
|
-
function stillSameProcess(
|
|
176
|
-
exec: NonNullable<DesktopAppRestartIo["execFile"]>,
|
|
177
|
-
installLocation: string,
|
|
131
|
+
function checkIdentity(
|
|
132
|
+
adapter: DesktopAppAdapter,
|
|
133
|
+
exec: DesktopExec,
|
|
134
|
+
install: Parameters<DesktopAppAdapter["listProcesses"]>[1],
|
|
178
135
|
target: DesktopProcess,
|
|
179
|
-
):
|
|
180
|
-
const processes =
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
136
|
+
): IdentityCheck {
|
|
137
|
+
const processes = adapter.listProcesses(exec, install);
|
|
138
|
+
// THREE outcomes, not two. Collapsing them into a boolean is what made this ladder
|
|
139
|
+
// claim a restart it never performed: a re-probe that could not RUN looked identical
|
|
140
|
+
// to a process that had exited, and the caller recorded the pid as stopped, skipped
|
|
141
|
+
// the forced pass, and relaunched into an app that was still running - reporting
|
|
142
|
+
// success the whole way. Measured on a real Windows host, where the app kept its
|
|
143
|
+
// original pid and start time through a restart that said it had stopped it.
|
|
144
|
+
if (processes === null) return "unknown";
|
|
145
|
+
const current = processes.find(entry => entry.pid === target.pid);
|
|
146
|
+
if (current === undefined) return "gone";
|
|
147
|
+
// Same pid, different start time: the pid was recycled and now belongs to somebody
|
|
148
|
+
// else. Treated as gone, because the process we meant to stop no longer exists and
|
|
149
|
+
// signalling this pid would hit an unrelated process.
|
|
150
|
+
return current.createdAt === target.createdAt ? "same" : "gone";
|
|
192
151
|
}
|
|
193
152
|
|
|
194
153
|
/**
|
|
195
|
-
*
|
|
154
|
+
* Pids of the running desktop-app tree, or null when discovery or the probe failed.
|
|
196
155
|
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* miss the exact case the guard exists for and we would terminate our own host.
|
|
200
|
-
* The chain therefore comes from CIM, with a bound so a corrupted parent cycle
|
|
201
|
-
* cannot spin.
|
|
156
|
+
* Read-only. Used by the CLI to exclude app-servers the desktop restart is about to
|
|
157
|
+
* take anyway, so an operator\u2019s in-flight turn is not interrupted twice in one command.
|
|
202
158
|
*/
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
return chain;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function defaultExecFile(file: string, args: readonly string[], options?: DesktopAppExecOptions): string {
|
|
226
|
-
return execFileSync(file, [...args], {
|
|
227
|
-
encoding: "utf-8",
|
|
228
|
-
timeout: options?.timeout ?? PROBE_TIMEOUT_MS,
|
|
229
|
-
windowsHide: options?.windowsHide ?? true,
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function defaultIsAlive(pid: number): boolean {
|
|
234
|
-
try {
|
|
235
|
-
process.kill(pid, 0);
|
|
236
|
-
return true;
|
|
237
|
-
} catch {
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function defaultSleep(ms: number): void {
|
|
243
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function waitForExit(
|
|
247
|
-
pid: number,
|
|
159
|
+
/**
|
|
160
|
+
* Poll the platform's own process list until it stops listing this process.
|
|
161
|
+
*
|
|
162
|
+
* A single post-kill enumeration is not enough. Measured on Windows: `taskkill /T /F`
|
|
163
|
+
* succeeds, the process is genuinely dead a moment later, and yet the very next
|
|
164
|
+
* `Win32_Process` query still lists it. Checking once turned that lag into a reported
|
|
165
|
+
* survivor, which blocked the relaunch and left the machine with no app at all - the
|
|
166
|
+
* failure mode is the mirror of claiming a stop that never happened, and just as bad.
|
|
167
|
+
*
|
|
168
|
+
* Liveness is polled first because it is cheap; the enumeration is what decides. A probe
|
|
169
|
+
* that cannot run keeps the loop going rather than deciding either way, and if the
|
|
170
|
+
* deadline passes without a clean "gone" the caller treats it as a survivor.
|
|
171
|
+
*/
|
|
172
|
+
function waitUntilGone(
|
|
173
|
+
adapter: DesktopAppAdapter,
|
|
174
|
+
exec: DesktopExec,
|
|
175
|
+
install: Parameters<DesktopAppAdapter["listProcesses"]>[1],
|
|
176
|
+
target: DesktopProcess,
|
|
248
177
|
timeoutMs: number,
|
|
249
178
|
isAlive: (pid: number) => boolean,
|
|
250
179
|
sleep: (ms: number) => void,
|
|
251
180
|
now: () => number,
|
|
252
181
|
): boolean {
|
|
253
182
|
const deadline = now() + timeoutMs;
|
|
254
|
-
|
|
255
|
-
if (!isAlive(pid)) return true;
|
|
183
|
+
for (;;) {
|
|
184
|
+
if (!isAlive(target.pid) && checkIdentity(adapter, exec, install, target) === "gone") return true;
|
|
185
|
+
if (now() >= deadline) break;
|
|
256
186
|
sleep(250);
|
|
257
187
|
}
|
|
258
|
-
|
|
188
|
+
// One last look after the deadline, so a process that exited during the final sleep is
|
|
189
|
+
// not reported as surviving purely because of poll timing.
|
|
190
|
+
return checkIdentity(adapter, exec, install, target) === "gone";
|
|
259
191
|
}
|
|
260
192
|
|
|
261
|
-
|
|
262
|
-
* Stop every package-tree root gracefully, force the stragglers, then relaunch
|
|
263
|
-
* through the discovered AUMID. Returns without relaunching if anything
|
|
264
|
-
* survived, because launching a second shell beside a stuck one is worse than
|
|
265
|
-
* leaving the user to restart it.
|
|
266
|
-
*/
|
|
267
|
-
export function restartCodexDesktopApp(io: DesktopAppRestartIo = {}): DesktopAppRestartResult {
|
|
193
|
+
export function listCodexDesktopAppPids(io: DesktopAppRestartIo = {}): number[] | null {
|
|
268
194
|
const platform = io.platform ?? process.platform;
|
|
195
|
+
const selected = ADAPTERS[platform];
|
|
196
|
+
const adapter = io.adapter ?? selected?.adapter;
|
|
197
|
+
if (!adapter) return null;
|
|
198
|
+
const exec = io.execFile ?? selected?.exec;
|
|
199
|
+
if (!exec) return null;
|
|
200
|
+
const install = adapter.discover(exec);
|
|
201
|
+
if (!install) return null;
|
|
202
|
+
const processes = adapter.listProcesses(exec, install);
|
|
203
|
+
return processes === null ? null : processes.map(entry => entry.pid);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function restartCodexDesktopApp(io: DesktopAppRestartIo = {}): DesktopAppRestartResult {
|
|
269
207
|
const skipped = (reason: DesktopAppRestartReason): DesktopAppRestartResult => ({
|
|
270
208
|
attempted: false, stopped: [], surviving: [], relaunch: "skipped", reason,
|
|
271
209
|
});
|
|
272
|
-
if (platform !== "win32") return skipped("windows_only");
|
|
273
210
|
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
|
|
211
|
+
const platform = io.platform ?? process.platform;
|
|
212
|
+
const selected = ADAPTERS[platform];
|
|
213
|
+
const adapter = io.adapter ?? selected?.adapter;
|
|
214
|
+
const exec = io.execFile ?? selected?.exec;
|
|
215
|
+
if (!adapter || !exec) return skipped("unsupported_platform");
|
|
277
216
|
|
|
278
|
-
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
if (
|
|
217
|
+
// Step 0. Two restarts at once are destructive rather than merely wasteful: the
|
|
218
|
+
// first quits and relaunches, the second sees the freshly started shell as a target
|
|
219
|
+
// and kills it. Own-pid reentrancy means the wp5 helper runs this same step and
|
|
220
|
+
// finds the lock its caller made out to it.
|
|
221
|
+
const acquisition = acquireDesktopRestartLock(io.lock);
|
|
222
|
+
if (!acquisition.acquired) return skipped("restart_in_flight");
|
|
284
223
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return skipped("self_ancestry");
|
|
290
|
-
}
|
|
291
|
-
const ancestry = new Set(ancestryPids);
|
|
292
|
-
if (processes.some(p => ancestry.has(p.pid))) {
|
|
293
|
-
// Terminating our own tree would kill this command mid-flight and leave the
|
|
294
|
-
// user with neither a restarted app nor an explanation.
|
|
295
|
-
return skipped("self_ancestry");
|
|
296
|
-
}
|
|
224
|
+
let handedOff = false;
|
|
225
|
+
try {
|
|
226
|
+
const install = adapter.discover(exec);
|
|
227
|
+
if (!install) return skipped("package_discovery_failed");
|
|
297
228
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
229
|
+
const processes = adapter.listProcesses(exec, install);
|
|
230
|
+
// A probe that could not run is not evidence of absence. Reporting it as no_targets
|
|
231
|
+
// told users the app was not running and silently skipped the restart they asked
|
|
232
|
+
// for (#2557).
|
|
233
|
+
if (processes === null) return skipped("process_probe_failed");
|
|
303
234
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
235
|
+
const shells = rootShells(processes, install, adapter);
|
|
236
|
+
if (shells.length === 0) return skipped("no_targets");
|
|
237
|
+
|
|
238
|
+
const ancestryPids = io.ancestryPids ? io.ancestryPids() : adapter.ancestryPids(exec);
|
|
239
|
+
// An empty chain means "could not establish that we are outside the tree", which
|
|
240
|
+
// covers both an unreadable hop and a walk that hit its bound.
|
|
241
|
+
const insideTree = ancestryPids.length === 0
|
|
242
|
+
|| processes.some(entry => ancestryPids.includes(entry.pid));
|
|
243
|
+
if (insideTree) {
|
|
244
|
+
if (io.allowHandoff === false || !io.startHandoff) return skipped("self_ancestry");
|
|
245
|
+
const handoff = io.startHandoff();
|
|
246
|
+
if (!handoff) return skipped("self_ancestry");
|
|
247
|
+
handedOff = true;
|
|
248
|
+
return {
|
|
249
|
+
attempted: false, stopped: [], surviving: [],
|
|
250
|
+
relaunch: "skipped", reason: "handoff_started", handoff,
|
|
251
|
+
};
|
|
319
252
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
253
|
+
|
|
254
|
+
// Captured while the tree is still ALIVE. On Linux the relaunch needs the
|
|
255
|
+
// graphical session variables, and after termination there is nothing to read them
|
|
256
|
+
// from. Ordering this wrongly works on macOS and Windows and produces a Linux app
|
|
257
|
+
// that cannot reach the compositor.
|
|
258
|
+
const context = adapter.captureRelaunchContext(exec, install, processes);
|
|
259
|
+
|
|
260
|
+
const isAlive = io.isAlive ?? defaultIsAlive;
|
|
261
|
+
const sleep = io.sleep ?? defaultSleep;
|
|
262
|
+
const now = io.now ?? (() => Date.now());
|
|
263
|
+
const stopped: number[] = [];
|
|
264
|
+
const surviving: number[] = [];
|
|
265
|
+
|
|
266
|
+
for (const shell of shells) {
|
|
267
|
+
const pid = shell.pid;
|
|
268
|
+
// The listing is already one probe old.
|
|
269
|
+
const before = checkIdentity(adapter, exec, install, shell);
|
|
270
|
+
if (before === "gone") {
|
|
271
|
+
stopped.push(pid);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (before === "unknown") {
|
|
275
|
+
// We could not look, so we cannot claim this exited and we must not signal a
|
|
276
|
+
// process we failed to re-verify. Reporting it as surviving is the honest answer:
|
|
277
|
+
// it blocks the relaunch, which is exactly right when the tree state is unknown.
|
|
278
|
+
surviving.push(pid);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
adapter.requestQuit(exec, install, shell);
|
|
283
|
+
} catch {
|
|
284
|
+
/* a refused graceful close still gets the forced pass below */
|
|
285
|
+
}
|
|
286
|
+
// Liveness AND enumeration have to agree before a stop is claimed. A pid-based
|
|
287
|
+
// liveness probe is a weaker instrument than the platform's own process list, and
|
|
288
|
+
// on a packaged app the two disagree in BOTH directions.
|
|
289
|
+
if (waitUntilGone(adapter, exec, install, shell, GRACEFUL_EXIT_TIMEOUT_MS, isAlive, sleep, now)) {
|
|
290
|
+
stopped.push(pid);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
// The wait window is long enough for a pid to be recycled, and the next step is a
|
|
294
|
+
// hard kill. Confirm it is still the process we verified, or leave it alone.
|
|
295
|
+
const afterGraceful = checkIdentity(adapter, exec, install, shell);
|
|
296
|
+
if (afterGraceful === "gone") {
|
|
297
|
+
stopped.push(pid);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (afterGraceful === "unknown") {
|
|
301
|
+
surviving.push(pid);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
adapter.forceStop(exec, shell);
|
|
306
|
+
} catch {
|
|
307
|
+
/* the process state decides, not the exit code */
|
|
308
|
+
}
|
|
309
|
+
// Same rule after the forced pass: only an enumeration that no longer contains this
|
|
310
|
+
// process proves it stopped. Everything else is a survivor, and a survivor blocks
|
|
311
|
+
// the relaunch rather than producing a second shell beside a live one.
|
|
312
|
+
if (waitUntilGone(adapter, exec, install, shell, FORCED_EXIT_TIMEOUT_MS, isAlive, sleep, now)) {
|
|
313
|
+
stopped.push(pid);
|
|
314
|
+
} else {
|
|
315
|
+
surviving.push(pid);
|
|
316
|
+
}
|
|
323
317
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
stopped
|
|
329
|
-
continue;
|
|
318
|
+
|
|
319
|
+
if (surviving.length > 0) {
|
|
320
|
+
// Launching a second shell beside a stuck one is worse than leaving the operator
|
|
321
|
+
// to restart it.
|
|
322
|
+
return { attempted: true, stopped, surviving, relaunch: "skipped", reason: "targets_survived" };
|
|
330
323
|
}
|
|
324
|
+
|
|
331
325
|
try {
|
|
332
|
-
exec
|
|
333
|
-
timeout: PROBE_TIMEOUT_MS, windowsHide: true,
|
|
334
|
-
});
|
|
326
|
+
adapter.relaunch(exec, install, context);
|
|
335
327
|
} catch {
|
|
336
|
-
|
|
328
|
+
// Distinct from targets_survived on purpose. Everything DID die and the relaunch
|
|
329
|
+
// is what failed; the old code reported the two as one and sent operators looking
|
|
330
|
+
// for processes that were not there.
|
|
331
|
+
return { attempted: true, stopped, surviving, relaunch: "skipped", reason: "relaunch_failed" };
|
|
337
332
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
return { attempted: true, stopped, surviving, relaunch: "skipped", reason: "targets_survived" };
|
|
333
|
+
return { attempted: true, stopped, surviving: [], relaunch: "started" };
|
|
334
|
+
} finally {
|
|
335
|
+
// On the handoff path ownership was transferred to the helper, so releasing here
|
|
336
|
+
// would drop a lock that is still protecting a restart about to happen.
|
|
337
|
+
if (!handedOff) releaseDesktopRestartLock(io.lock);
|
|
344
338
|
}
|
|
345
|
-
|
|
346
|
-
try {
|
|
347
|
-
exec(resolveTrustedWindowsPowerShellExe(), [
|
|
348
|
-
"-NoProfile", "-NonInteractive", "-Command",
|
|
349
|
-
`Start-Process 'shell:AppsFolder\\${pkg.aumid}'`,
|
|
350
|
-
], { timeout: PROBE_TIMEOUT_MS, windowsHide: true });
|
|
351
|
-
} catch {
|
|
352
|
-
return { attempted: true, stopped, surviving, relaunch: "skipped", reason: "targets_survived" };
|
|
353
|
-
}
|
|
354
|
-
return { attempted: true, stopped, surviving, relaunch: "started" };
|
|
355
339
|
}
|
|
340
|
+
|