@bitkyc08/opencodex 2.7.43 → 2.8.2-preview.20260731
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/bin/ocx.mjs +34 -8
- package/gui/dist/assets/index-BHsKRFh9.css +1 -0
- package/gui/dist/assets/index-GC0Vlu1Z.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +42 -7
- package/src/adapters/cursor/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/kiro.ts +15 -1
- package/src/adapters/openai-chat.ts +55 -4
- package/src/claude/alias.ts +94 -14
- package/src/claude/outbound.ts +6 -3
- package/src/cli/catalog-prewarm.ts +24 -0
- package/src/cli/claude-desktop.ts +2 -2
- package/src/cli/claude.ts +32 -7
- package/src/cli/doctor.ts +48 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/init.ts +129 -102
- package/src/cli/interactive-confirm.ts +5 -1
- package/src/cli/star-prompt.ts +26 -4
- package/src/cli/v2.ts +10 -1
- package/src/codex/account-store.ts +2 -0
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/parsing.ts +26 -1
- package/src/codex/catalog/provider-fetch.ts +240 -82
- package/src/codex/catalog/sync.ts +27 -5
- package/src/codex/catalog.ts +3 -3
- package/src/codex/features.ts +524 -5
- package/src/codex/quota.ts +77 -2
- package/src/codex/runtime.ts +10 -1
- package/src/config.ts +8 -0
- package/src/generated/jawcode-model-metadata.ts +12 -12
- package/src/github/star-state.ts +191 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +6 -20
- package/src/lib/destination-policy.ts +21 -3
- package/src/lib/provider-outbound.ts +8 -2
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/win-exec.ts +12 -2
- package/src/lib/winsw.ts +6 -0
- package/src/oauth/index.ts +29 -5
- package/src/oauth/key-providers.ts +21 -2
- package/src/oauth/kiro-credentials.ts +129 -9
- package/src/oauth/kiro.ts +15 -3
- package/src/oauth/login-cli.ts +1 -1
- package/src/oauth/store.ts +2 -0
- package/src/providers/derive.ts +2 -2
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/registry.ts +114 -0
- package/src/router.ts +5 -3
- package/src/server/auth-cors.ts +4 -2
- package/src/server/index.ts +3 -3
- package/src/server/live.ts +75 -25
- package/src/server/management/agent-settings-routes.ts +82 -8
- package/src/server/management/config-routes.ts +24 -7
- package/src/server/management/context.ts +11 -1
- package/src/server/management/model-routes.ts +61 -14
- package/src/server/management/provider-routes.ts +44 -9
- package/src/server/management/shared.ts +18 -5
- package/src/server/management/sidebar-routes.ts +39 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/proxy-liveness.ts +9 -2
- package/src/server/responses/core.ts +31 -20
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/startup-action-control.ts +30 -14
- package/src/service.ts +395 -31
- package/src/storage/policy-job.ts +26 -5
- package/src/storage/restore-job.ts +16 -5
- package/src/storage/worker-lifecycle.ts +81 -0
- package/src/tray/windows.ts +86 -13
- package/src/types.ts +16 -0
- package/src/update/badge.ts +72 -0
- package/src/update/job.ts +8 -4
- package/src/usage/expected-prices.ts +6 -5
- package/src/usage/log.ts +8 -0
- package/src/web-search/loop.ts +57 -16
- package/gui/dist/assets/index-Czw-jpTU.css +0 -1
- package/gui/dist/assets/index-cmds12BG.js +0 -67
package/src/service.ts
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* restore it via the command.
|
|
7
7
|
*/
|
|
8
8
|
import { execFileSync, execSync } from "node:child_process";
|
|
9
|
+
import { findLiveProxy } from "./server/proxy-liveness";
|
|
9
10
|
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
12
|
import { dirname, join, resolve } from "node:path";
|
|
12
|
-
import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort } from "./config";
|
|
13
|
+
import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort, verifyPidIdentity } from "./config";
|
|
13
14
|
import { loadConfig } from "./config";
|
|
14
15
|
import { restoreNativeCodex } from "./codex/inject";
|
|
15
16
|
import { stripGrokConfig } from "./grok/inject";
|
|
@@ -35,6 +36,7 @@ import { defaultWinswEntry, installWinswService, startWinswService, stopWinswSer
|
|
|
35
36
|
import { hardenSecretDir, hardenSecretPath } from "./lib/windows-secret-acl";
|
|
36
37
|
import { windowsEnvIndirectBatchPathList, windowsEnvIndirectBatchValue } from "./lib/win-paths";
|
|
37
38
|
import { recordOwnedConfigPath } from "./lib/config-ownership";
|
|
39
|
+
import { maybeShowStarPrompt } from "./cli/star-prompt";
|
|
38
40
|
|
|
39
41
|
const LABEL = "com.opencodex.proxy";
|
|
40
42
|
const TASK = "opencodex-proxy";
|
|
@@ -354,8 +356,41 @@ function sh(cmd: string): string {
|
|
|
354
356
|
return execSync(cmd, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
355
357
|
}
|
|
356
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Decode schtasks stdout. `/query /xml` emits UTF-16LE (often with BOM) because the
|
|
361
|
+
* registered task document is UTF-16; reading that as UTF-8 makes every health check
|
|
362
|
+
* fail ("registration present but unhealthy") and rolls back a successful elevated create.
|
|
363
|
+
*/
|
|
364
|
+
export function decodeSchtasksOutput(buffer: Buffer): string {
|
|
365
|
+
if (buffer.length === 0) return "";
|
|
366
|
+
const bomUtf16Le = buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe;
|
|
367
|
+
const bomUtf16Be = buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff;
|
|
368
|
+
const looksUtf16Le = buffer.length >= 4
|
|
369
|
+
&& buffer[1] === 0x00
|
|
370
|
+
&& buffer[3] === 0x00
|
|
371
|
+
&& buffer[0] !== 0x00;
|
|
372
|
+
if (bomUtf16Le || looksUtf16Le) {
|
|
373
|
+
return buffer.toString("utf16le").replace(/^\uFEFF/, "").trim();
|
|
374
|
+
}
|
|
375
|
+
if (bomUtf16Be) {
|
|
376
|
+
// Swap pairs then decode as utf16le.
|
|
377
|
+
const swapped = Buffer.alloc(buffer.length - 2);
|
|
378
|
+
for (let i = 2; i + 1 < buffer.length; i += 2) {
|
|
379
|
+
swapped[i - 2] = buffer[i + 1]!;
|
|
380
|
+
swapped[i - 1] = buffer[i]!;
|
|
381
|
+
}
|
|
382
|
+
return swapped.toString("utf16le").trim();
|
|
383
|
+
}
|
|
384
|
+
return buffer.toString("utf8").replace(/^\uFEFF/, "").trim();
|
|
385
|
+
}
|
|
386
|
+
|
|
357
387
|
function runFile(file: string, args: string[]): string {
|
|
358
|
-
|
|
388
|
+
const buffer = execFileSync(file, args, {
|
|
389
|
+
encoding: "buffer",
|
|
390
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
391
|
+
windowsHide: true,
|
|
392
|
+
}) as Buffer;
|
|
393
|
+
return decodeSchtasksOutput(buffer);
|
|
359
394
|
}
|
|
360
395
|
|
|
361
396
|
function windowsSchtasks(): string {
|
|
@@ -393,6 +428,70 @@ export type WindowsSchedulerTaskProbe =
|
|
|
393
428
|
| { status: "absent" }
|
|
394
429
|
| { status: "unknown"; detail: string };
|
|
395
430
|
|
|
431
|
+
export type WindowsSchedulerProxyProbe =
|
|
432
|
+
| { status: "running"; port: number }
|
|
433
|
+
| { status: "not-running" }
|
|
434
|
+
| { status: "unknown" };
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Render Task Scheduler status without exposing localized `schtasks` table output.
|
|
438
|
+
* The task probe answers installation state; the identity-checked health probe answers
|
|
439
|
+
* runtime state. Keep probe details out of this user-facing line because they can contain
|
|
440
|
+
* incorrectly decoded, locale-specific command output.
|
|
441
|
+
*/
|
|
442
|
+
export function formatWindowsSchedulerServiceStatus(
|
|
443
|
+
task: WindowsSchedulerTaskProbe,
|
|
444
|
+
proxy: WindowsSchedulerProxyProbe,
|
|
445
|
+
): string {
|
|
446
|
+
if (task.status === "present") {
|
|
447
|
+
if (proxy.status === "running") {
|
|
448
|
+
return `✅ service installed (Task Scheduler); OpenCodex proxy running on port ${proxy.port}.`;
|
|
449
|
+
}
|
|
450
|
+
if (proxy.status === "not-running") {
|
|
451
|
+
return "⚠️ service installed (Task Scheduler); OpenCodex proxy not running.";
|
|
452
|
+
}
|
|
453
|
+
return "⚠️ service installed (Task Scheduler); OpenCodex proxy status unknown.";
|
|
454
|
+
}
|
|
455
|
+
if (task.status === "absent") {
|
|
456
|
+
if (proxy.status === "running") {
|
|
457
|
+
return `❌ service not installed (Task Scheduler); OpenCodex proxy is running independently on port ${proxy.port}.`;
|
|
458
|
+
}
|
|
459
|
+
if (proxy.status === "unknown") {
|
|
460
|
+
return "❌ service not installed (Task Scheduler); OpenCodex proxy status unknown.";
|
|
461
|
+
}
|
|
462
|
+
return "❌ service not installed (Task Scheduler).";
|
|
463
|
+
}
|
|
464
|
+
if (proxy.status === "running") {
|
|
465
|
+
return `⚠️ Task Scheduler registration unknown; OpenCodex proxy running on port ${proxy.port}.`;
|
|
466
|
+
}
|
|
467
|
+
if (proxy.status === "not-running") {
|
|
468
|
+
return "⚠️ service status unknown (Task Scheduler query failed); OpenCodex proxy not running.";
|
|
469
|
+
}
|
|
470
|
+
return "⚠️ service status unknown (Task Scheduler and proxy checks failed).";
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export async function inspectWindowsSchedulerServiceStatus(io: {
|
|
474
|
+
probeTask?: () => WindowsSchedulerTaskProbe;
|
|
475
|
+
findProxy?: () => Promise<{ port: number } | null>;
|
|
476
|
+
} = {}): Promise<string> {
|
|
477
|
+
let task: WindowsSchedulerTaskProbe;
|
|
478
|
+
try {
|
|
479
|
+
task = (io.probeTask ?? probeWindowsSchedulerTask)();
|
|
480
|
+
} catch (error) {
|
|
481
|
+
task = { status: "unknown", detail: schtasksErrorDetail(error) };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
let proxy: WindowsSchedulerProxyProbe;
|
|
485
|
+
try {
|
|
486
|
+
const live = await (io.findProxy ?? findLiveProxy)();
|
|
487
|
+
proxy = live ? { status: "running", port: live.port } : { status: "not-running" };
|
|
488
|
+
} catch {
|
|
489
|
+
proxy = { status: "unknown" };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return formatWindowsSchedulerServiceStatus(task, proxy);
|
|
493
|
+
}
|
|
494
|
+
|
|
396
495
|
function schtasksErrorDetail(error: unknown): string {
|
|
397
496
|
return error instanceof Error ? error.message : String(error);
|
|
398
497
|
}
|
|
@@ -486,7 +585,9 @@ export function evaluateWindowsSchedulerInstallVerification(inputs: {
|
|
|
486
585
|
: !assetsHealthy
|
|
487
586
|
? "Required scheduler service assets are missing."
|
|
488
587
|
: !registrationHealthy
|
|
489
|
-
?
|
|
588
|
+
? (inputs.xml.trim()
|
|
589
|
+
? "Task Scheduler registration is present but unhealthy."
|
|
590
|
+
: "Task Scheduler task is present but its XML could not be read.")
|
|
490
591
|
: nativeStatusUnknown
|
|
491
592
|
? "The Task Scheduler task was created, but OpenCodex could not verify that the native WinSW service is absent."
|
|
492
593
|
: "ok";
|
|
@@ -505,9 +606,18 @@ export function evaluateWindowsSchedulerInstallVerification(inputs: {
|
|
|
505
606
|
/** Conflict-free postcondition check for an elevated scheduler install. */
|
|
506
607
|
export function verifyWindowsSchedulerInstall(taskName = TASK): WindowsSchedulerInstallVerification {
|
|
507
608
|
const taskInstalled = windowsSchedulerTaskInstalled(taskName);
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
609
|
+
let xml = "";
|
|
610
|
+
if (taskInstalled) {
|
|
611
|
+
try { xml = querySchtasks(["/query", "/tn", taskName, "/xml"]); } catch { xml = ""; }
|
|
612
|
+
}
|
|
613
|
+
// After elevated create, non-elevated `/query /xml` can fail or return empty while the
|
|
614
|
+
// task is still listed. Fall back to the on-disk document we registered.
|
|
615
|
+
if (taskInstalled && !xml.trim()) {
|
|
616
|
+
const diskPath = windowsTaskXmlPath();
|
|
617
|
+
if (existsSync(diskPath)) {
|
|
618
|
+
try { xml = decodeSchtasksOutput(readFileSync(diskPath)); } catch { /* keep empty */ }
|
|
619
|
+
}
|
|
620
|
+
}
|
|
511
621
|
return evaluateWindowsSchedulerInstallVerification({
|
|
512
622
|
taskInstalled,
|
|
513
623
|
xml,
|
|
@@ -895,6 +1005,22 @@ function taskXmlString(value: string): string {
|
|
|
895
1005
|
.replace(/'/g, "'");
|
|
896
1006
|
}
|
|
897
1007
|
|
|
1008
|
+
/**
|
|
1009
|
+
* RunLevel check. Schema default is LeastPrivilege (omitted on export). Elevated
|
|
1010
|
+
* `schtasks /create` often rewrites the registered task to HighestAvailable even when
|
|
1011
|
+
* the source XML asked for LeastPrivilege — still InteractiveToken / same user.
|
|
1012
|
+
* Keep accepting HighestAvailable here: rejecting it would false-fail healthy elevated
|
|
1013
|
+
* installs, and windowsTaskRegistrationHealthy tests encode that contract.
|
|
1014
|
+
*/
|
|
1015
|
+
function taskXmlRunLevelAcceptable(principal: string): boolean {
|
|
1016
|
+
if (taskXmlHasPrefixedTag(principal, "RunLevel")) return false;
|
|
1017
|
+
const count = taskXmlElementCount(principal, "RunLevel");
|
|
1018
|
+
if (count === 0) return true;
|
|
1019
|
+
if (count > 1) return false;
|
|
1020
|
+
const value = new RegExp(`<RunLevel(?:\\s[^>]*?)?>\\s*([^<]*?)\\s*<\\/RunLevel>`, "i").exec(principal)?.[1]?.trim().toLowerCase();
|
|
1021
|
+
return value === "leastprivilege" || value === "highestavailable";
|
|
1022
|
+
}
|
|
1023
|
+
|
|
898
1024
|
export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServiceListenPort()): string {
|
|
899
1025
|
const { bun, cli } = entry;
|
|
900
1026
|
const bunRuntime = durableBunRuntime();
|
|
@@ -1076,7 +1202,7 @@ function taskXmlDecodedValueEquals(xml: string, tag: string, expected: string):
|
|
|
1076
1202
|
// `[^<]*` refuses nested markup, so a decoy inside a child element cannot match.
|
|
1077
1203
|
const value = new RegExp(`<${tag}(?:\\s[^>]*?)?>([^<]*)<\\/${tag}>`, "i").exec(xml)?.[1];
|
|
1078
1204
|
if (value === undefined) return false;
|
|
1079
|
-
return taskXmlDecodeEntities(value).trim() === expected.trim();
|
|
1205
|
+
return taskXmlDecodeEntities(value).trim().toLowerCase() === expected.trim().toLowerCase();
|
|
1080
1206
|
}
|
|
1081
1207
|
|
|
1082
1208
|
function taskXmlOptionalValueEquals(xml: string, tag: string, expected: string): boolean {
|
|
@@ -1112,13 +1238,14 @@ export function windowsTaskRegistrationHealthy(
|
|
|
1112
1238
|
return taskXmlElementCount(triggers, "LogonTrigger") > 0
|
|
1113
1239
|
&& taskXmlOptionalValueEquals(trigger, "Enabled", "true")
|
|
1114
1240
|
&& /<LogonType>\s*InteractiveToken\s*<\/LogonType>/i.test(principal)
|
|
1115
|
-
&&
|
|
1241
|
+
&& taskXmlRunLevelAcceptable(principal)
|
|
1116
1242
|
&& taskXmlOptionalValueEquals(settings, "Enabled", "true")
|
|
1117
1243
|
&& /<MultipleInstancesPolicy>\s*IgnoreNew\s*<\/MultipleInstancesPolicy>/i.test(settings)
|
|
1118
1244
|
&& /<ExecutionTimeLimit>\s*PT0S\s*<\/ExecutionTimeLimit>/i.test(settings)
|
|
1119
1245
|
// Compare decoded VALUES, not encodings: Task Scheduler canonicalizes the
|
|
1120
1246
|
// quotes we wrote as `"` back to literal `"` on export, so an escaped
|
|
1121
1247
|
// needle never matched and a healthy task read as permanently stale (#608).
|
|
1248
|
+
// Case-insensitive: elevated `schtasks /create` may rewrite System32 casing.
|
|
1122
1249
|
&& taskXmlDecodedValueEquals(action, "Command", wscript)
|
|
1123
1250
|
&& taskXmlDecodedValueEquals(action, "Arguments", `/b /nologo "${launcher}"`);
|
|
1124
1251
|
}
|
|
@@ -1191,10 +1318,23 @@ function writeServiceAssetWithRetry(path: string, content: string, encoding: "ut
|
|
|
1191
1318
|
}
|
|
1192
1319
|
}
|
|
1193
1320
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1321
|
+
/**
|
|
1322
|
+
* Rewrite on-disk scheduler assets (script/VBS/XML) without re-registering the task.
|
|
1323
|
+
* Used by fresh install (before schtasks /create) and by repair (no elevation).
|
|
1324
|
+
*/
|
|
1325
|
+
function writeWindowsSchedulerAssets(): void {
|
|
1196
1326
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
1197
1327
|
writeServiceApiTokenFile();
|
|
1328
|
+
const script = windowsServiceScriptPath();
|
|
1329
|
+
writeServiceAssetWithRetry(script, buildWindowsServiceScript(), "utf8");
|
|
1330
|
+
// UTF-16LE + BOM: a BOM-less UTF-8 VBS mis-decodes non-ASCII (e.g. Korean) profile
|
|
1331
|
+
// paths on some WSH/codepage combinations — same contract as the task XML below.
|
|
1332
|
+
writeServiceAssetWithRetry(windowsLauncherVbsPath(), `\uFEFF${buildWindowsLauncherVbs(script)}`, "utf16le");
|
|
1333
|
+
writeServiceAssetWithRetry(windowsTaskXmlPath(), `\uFEFF${buildWindowsTaskXml(script)}`, "utf16le");
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function installWindows(): void {
|
|
1337
|
+
recordOwnedConfigPath(getConfigDir(), serviceStatePath());
|
|
1198
1338
|
// Transactional backend switch: installing the scheduler backend removes a native
|
|
1199
1339
|
// service first — two live managers would both respawn the proxy (conflict).
|
|
1200
1340
|
if (statusWinswRaw() !== "nonexistent") {
|
|
@@ -1211,23 +1351,115 @@ function installWindows(): void {
|
|
|
1211
1351
|
// End a running task BEFORE rewriting the assets it is executing — cmd.exe reading the
|
|
1212
1352
|
// script mid-rewrite runs a torn batch file, and its open handle can fail the write.
|
|
1213
1353
|
try { stopWindows(); } catch { /* not running */ }
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
// UTF-16LE + BOM: a BOM-less UTF-8 VBS mis-decodes non-ASCII (e.g. Korean) profile
|
|
1217
|
-
// paths on some WSH/codepage combinations — same contract as the task XML below.
|
|
1218
|
-
writeServiceAssetWithRetry(windowsLauncherVbsPath(), `\uFEFF${buildWindowsLauncherVbs(script)}`, "utf16le");
|
|
1219
|
-
writeServiceAssetWithRetry(windowsTaskXmlPath(), `\uFEFF${buildWindowsTaskXml(script)}`, "utf16le");
|
|
1220
|
-
schtasks(buildWindowsSchtasksCreateArgs(script));
|
|
1354
|
+
writeWindowsSchedulerAssets();
|
|
1355
|
+
schtasks(buildWindowsSchtasksCreateArgs(windowsServiceScriptPath()));
|
|
1221
1356
|
schtasks(["/run", "/tn", TASK]);
|
|
1222
1357
|
writeServiceInstallState("scheduler");
|
|
1223
1358
|
}
|
|
1224
1359
|
|
|
1360
|
+
export interface RepairServiceDeps {
|
|
1361
|
+
diagnose?: () => ServiceDiagnostic;
|
|
1362
|
+
assertEnv?: () => void;
|
|
1363
|
+
assertAuth?: () => void;
|
|
1364
|
+
writeSchedulerAssets?: () => void;
|
|
1365
|
+
stopScheduler?: () => void;
|
|
1366
|
+
startScheduler?: () => void;
|
|
1367
|
+
writeSchedulerState?: () => void;
|
|
1368
|
+
writeNativeState?: () => void;
|
|
1369
|
+
repairNative?: () => void | Promise<void>;
|
|
1370
|
+
repairLaunchd?: () => void;
|
|
1371
|
+
repairSystemd?: () => void;
|
|
1372
|
+
/** Test seam — defaults to process.platform so Linux CI cannot hit real installSystemd. */
|
|
1373
|
+
platform?: NodeJS.Platform;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Repair an already-installed background service without Task Scheduler re-registration.
|
|
1378
|
+
*
|
|
1379
|
+
* Windows scheduler: rewrite assets + stop/start — no `schtasks /create`, no UAC.
|
|
1380
|
+
* Windows native: WinSW asset rewrite + restart (skips `install /p` when present).
|
|
1381
|
+
* macOS/Linux: re-run the user-level install/reload path.
|
|
1382
|
+
*/
|
|
1383
|
+
export async function repairService(deps: RepairServiceDeps = {}): Promise<void> {
|
|
1384
|
+
const diagnose = deps.diagnose ?? diagnoseService;
|
|
1385
|
+
const platform = deps.platform ?? process.platform;
|
|
1386
|
+
const diag = diagnose();
|
|
1387
|
+
if (!diag.supported) {
|
|
1388
|
+
throw new Error(`Background service is unsupported (${diag.summary}).`);
|
|
1389
|
+
}
|
|
1390
|
+
if (diag.conflict) {
|
|
1391
|
+
throw new Error(
|
|
1392
|
+
"Cannot repair while Task Scheduler and native WinSW are both present. "
|
|
1393
|
+
+ "Run 'ocx service uninstall' then reinstall one backend with 'ocx service install'.",
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
if (!diag.installed) {
|
|
1397
|
+
throw new Error("Background service is not installed. Run 'ocx service install' first.");
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
(deps.assertEnv ?? assertServiceEnvironmentMatchesInstall)();
|
|
1401
|
+
(deps.assertAuth ?? assertServiceAuthEnvironment)();
|
|
1402
|
+
|
|
1403
|
+
if (platform === "win32") {
|
|
1404
|
+
if (diag.backend === "native") {
|
|
1405
|
+
await (deps.repairNative ?? (() => installWinswService(defaultWinswEntry(import.meta.dir))))();
|
|
1406
|
+
(deps.writeNativeState ?? (() => writeServiceInstallState("native")))();
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
try { (deps.stopScheduler ?? stopWindows)(); } catch { /* not running */ }
|
|
1410
|
+
(deps.writeSchedulerAssets ?? writeWindowsSchedulerAssets)();
|
|
1411
|
+
(deps.startScheduler ?? startWindows)();
|
|
1412
|
+
(deps.writeSchedulerState ?? (() => writeServiceInstallState("scheduler")))();
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
if (platform === "darwin") {
|
|
1416
|
+
(deps.repairLaunchd ?? installLaunchd)();
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
if (platform === "linux") {
|
|
1420
|
+
(deps.repairSystemd ?? installSystemd)();
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
throw new Error(`Background service repair is unsupported on ${platform}.`);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1225
1426
|
/**
|
|
1226
1427
|
* Opt-in native backend (`ocx service install --native`). Transactional: removes the
|
|
1227
1428
|
* scheduler backend first; on failure the machine is left with NO service (explicitly
|
|
1228
1429
|
* reported) — never a silent fallback to the scheduler.
|
|
1229
1430
|
*/
|
|
1431
|
+
/** Refuse WinSW when the interactive user is a Microsoft account (SCM cannot authenticate it). */
|
|
1432
|
+
export function assertWindowsNativeServiceAccountSupported(): void {
|
|
1433
|
+
if (process.platform !== "win32") return;
|
|
1434
|
+
const source = readWindowsPrincipalSource();
|
|
1435
|
+
if (source?.toLowerCase() === "microsoftaccount") {
|
|
1436
|
+
throw new Error(
|
|
1437
|
+
"The native (WinSW) service backend cannot run under a Microsoft-account Windows login. "
|
|
1438
|
+
+ "Keep the Task Scheduler backend (`ocx service install`) or sign in with a local/domain account before `ocx service install --native`.",
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function readWindowsPrincipalSource(): string | null {
|
|
1444
|
+
if (process.platform !== "win32") return null;
|
|
1445
|
+
const ps = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
1446
|
+
if (!existsSync(ps)) return null;
|
|
1447
|
+
try {
|
|
1448
|
+
const out = execFileSync(ps, [
|
|
1449
|
+
"-NoLogo",
|
|
1450
|
+
"-NoProfile",
|
|
1451
|
+
"-NonInteractive",
|
|
1452
|
+
"-Command",
|
|
1453
|
+
"(Get-LocalUser -Name $env:USERNAME -ErrorAction SilentlyContinue).PrincipalSource",
|
|
1454
|
+
], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }).trim();
|
|
1455
|
+
return out || null;
|
|
1456
|
+
} catch {
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1230
1461
|
async function installWindowsNative(): Promise<void> {
|
|
1462
|
+
assertWindowsNativeServiceAccountSupported();
|
|
1231
1463
|
recordOwnedConfigPath(getConfigDir(), serviceStatePath());
|
|
1232
1464
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
1233
1465
|
writeServiceApiTokenFile();
|
|
@@ -1262,11 +1494,49 @@ async function installWindowsNative(): Promise<void> {
|
|
|
1262
1494
|
writeServiceInstallState("native");
|
|
1263
1495
|
}
|
|
1264
1496
|
function startWindows(): void { schtasks(["/run", "/tn", TASK]); }
|
|
1265
|
-
|
|
1497
|
+
|
|
1498
|
+
export function isWindowsSchedulerEndBenign(error: unknown): boolean {
|
|
1499
|
+
const detail = schtasksErrorDetail(error).toLowerCase();
|
|
1500
|
+
return detail.includes("no running instance")
|
|
1501
|
+
|| detail.includes("not currently running")
|
|
1502
|
+
|| detail.includes("0x41330");
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* End the scheduler task. "Already stopped" is success; other `/end` failures are
|
|
1507
|
+
* swallowed so callers can still run tracked-proxy + live-proxy cleanup.
|
|
1508
|
+
*
|
|
1509
|
+
* Do not key a restart-window wait on `/end` failure: the #764 case is an `/end`
|
|
1510
|
+
* that *succeeds* while the wrapper survives and respawns. That verification lives
|
|
1511
|
+
* on the stop-verification path (poll across the restart window), not here.
|
|
1512
|
+
*/
|
|
1513
|
+
export function stopWindows(): void {
|
|
1514
|
+
try {
|
|
1515
|
+
schtasks(["/end", "/tn", TASK]);
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
if (isWindowsSchedulerEndBenign(error)) return;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1266
1520
|
function statusWindows(): string { try { return schtasks(["/query", "/tn", TASK]); } catch { return ""; } }
|
|
1267
1521
|
function statusWindowsXml(): string { try { return schtasks(["/query", "/tn", TASK, "/xml"]); } catch { return ""; } }
|
|
1268
1522
|
function uninstallWindows(): void {
|
|
1269
|
-
|
|
1523
|
+
const probe = probeWindowsSchedulerTask(TASK);
|
|
1524
|
+
if (probe.status === "present") {
|
|
1525
|
+
try {
|
|
1526
|
+
schtasks(["/delete", "/tn", TASK, "/f"]);
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
throw new Error(`Failed to delete Task Scheduler task ${TASK}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1529
|
+
}
|
|
1530
|
+
const afterDelete = probeWindowsSchedulerTask(TASK);
|
|
1531
|
+
if (afterDelete.status === "present") {
|
|
1532
|
+
throw new Error(`Task Scheduler task ${TASK} is still present after delete — refusing to remove service assets. Retry from an elevated shell.`);
|
|
1533
|
+
}
|
|
1534
|
+
if (afterDelete.status === "unknown") {
|
|
1535
|
+
throw new Error(`Task Scheduler task ${TASK} presence could not be verified after delete — refusing to remove service assets.`);
|
|
1536
|
+
}
|
|
1537
|
+
} else if (probe.status === "unknown") {
|
|
1538
|
+
throw new Error(`Task Scheduler task ${TASK} presence could not be verified — refusing to remove service assets.`);
|
|
1539
|
+
}
|
|
1270
1540
|
if (existsSync(windowsServiceScriptPath())) unlinkSync(windowsServiceScriptPath());
|
|
1271
1541
|
if (existsSync(windowsLauncherVbsPath())) unlinkSync(windowsLauncherVbsPath());
|
|
1272
1542
|
if (existsSync(windowsTaskXmlPath())) unlinkSync(windowsTaskXmlPath());
|
|
@@ -1425,18 +1695,77 @@ function platformOps(backend: ServiceBackend = "scheduler"): ServiceOps | null {
|
|
|
1425
1695
|
|
|
1426
1696
|
type TrackedProxyCleanupResult = "none" | "stale" | "stopped";
|
|
1427
1697
|
|
|
1698
|
+
function verifiedKillTarget(pid: number | null | undefined): number | null {
|
|
1699
|
+
if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0) return null;
|
|
1700
|
+
const verified = verifyPidIdentity(pid);
|
|
1701
|
+
return verified === pid ? verified : null;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/**
|
|
1705
|
+
* Whether a proxy is still answering after the service manager claimed to stop it.
|
|
1706
|
+
*
|
|
1707
|
+
* `ops.stop()` reports the outcome of the STOP COMMAND, not of the process. A Windows scheduler
|
|
1708
|
+
* task whose wrapper survives `schtasks /end` respawns its child a few seconds later, so a stop
|
|
1709
|
+
* that returned success can still leave a live proxy — and `ocx service stop` then restored
|
|
1710
|
+
* native Codex on top of a running one (#764). The tracked-pid cleanup does not catch it either:
|
|
1711
|
+
* the respawned child writes a different pid, or none this process knows about.
|
|
1712
|
+
*
|
|
1713
|
+
* Probed rather than assumed, and bounded. The respawn risk is specific to a supervisor that can
|
|
1714
|
+
* restart its child — the Windows scheduler wrapper — so only that case pays the restart window.
|
|
1715
|
+
* Everywhere else a single probe answers the question, because nothing is going to bring the
|
|
1716
|
+
* proxy back after `launchctl unload` or `systemctl stop`. Making every platform wait 7s on a
|
|
1717
|
+
* stop that already succeeded would trade one bug for a worse everyday one.
|
|
1718
|
+
*/
|
|
1719
|
+
export async function proxyStillLiveAfterStop(deps: {
|
|
1720
|
+
findProxy?: () => Promise<{ port: number } | null>;
|
|
1721
|
+
sleep?: (ms: number) => Promise<void>;
|
|
1722
|
+
now?: () => number;
|
|
1723
|
+
/** Whether the stopped supervisor can respawn its child; only then is polling worth the wait. */
|
|
1724
|
+
canRespawn?: boolean;
|
|
1725
|
+
} = {}): Promise<{ port: number } | null> {
|
|
1726
|
+
const findProxy = deps.findProxy ?? findLiveProxy;
|
|
1727
|
+
const sleep = deps.sleep ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)));
|
|
1728
|
+
const now = deps.now ?? Date.now;
|
|
1729
|
+
const canRespawn = deps.canRespawn ?? process.platform === "win32";
|
|
1730
|
+
const deadline = now() + (canRespawn ? 7000 : 0);
|
|
1731
|
+
for (;;) {
|
|
1732
|
+
try {
|
|
1733
|
+
const live = await findProxy();
|
|
1734
|
+
if (live) return live;
|
|
1735
|
+
} catch {
|
|
1736
|
+
// A probe failure is not proof the proxy is gone; keep polling until the deadline.
|
|
1737
|
+
}
|
|
1738
|
+
if (now() >= deadline) return null;
|
|
1739
|
+
await sleep(1000);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1428
1743
|
async function stopTrackedProxyIfRunning(): Promise<TrackedProxyCleanupResult> {
|
|
1744
|
+
let stopped = false;
|
|
1429
1745
|
const pid = readPid();
|
|
1430
|
-
|
|
1431
|
-
if (
|
|
1746
|
+
const trackedKillPid = verifiedKillTarget(pid);
|
|
1747
|
+
if (trackedKillPid !== null && isProcessAlive(trackedKillPid)) {
|
|
1748
|
+
await stopProxy(trackedKillPid);
|
|
1749
|
+
removePid(trackedKillPid);
|
|
1750
|
+
removeRuntimePort(trackedKillPid);
|
|
1751
|
+
stopped = true;
|
|
1752
|
+
} else if (pid) {
|
|
1432
1753
|
removePid(pid);
|
|
1433
1754
|
removeRuntimePort(pid);
|
|
1434
|
-
return "stale";
|
|
1435
1755
|
}
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1756
|
+
// Orphan recovery: the pid file can be missing/stale while the service wrapper keeps
|
|
1757
|
+
// a live proxy running — mirror `ocx stop`'s identity-checked findLiveProxy fallback.
|
|
1758
|
+
const live = await findLiveProxy({ timeoutMs: 1500 });
|
|
1759
|
+
const liveKillPid = verifiedKillTarget(live?.pid);
|
|
1760
|
+
if (liveKillPid !== null) {
|
|
1761
|
+
await stopProxy(liveKillPid);
|
|
1762
|
+
removePid(liveKillPid);
|
|
1763
|
+
removeRuntimePort(liveKillPid);
|
|
1764
|
+
stopped = true;
|
|
1765
|
+
}
|
|
1766
|
+
if (stopped) return "stopped";
|
|
1767
|
+
if (pid) return "stale";
|
|
1768
|
+
return "none";
|
|
1440
1769
|
}
|
|
1441
1770
|
|
|
1442
1771
|
async function stopTrackedProxyForServiceCommand(): Promise<TrackedProxyCleanupResult> {
|
|
@@ -1711,6 +2040,13 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
1711
2040
|
console.error("--native (WinSW) is Windows-only.");
|
|
1712
2041
|
process.exit(1);
|
|
1713
2042
|
}
|
|
2043
|
+
if (command === "repair") {
|
|
2044
|
+
assertServiceEnvironmentMatchesInstall();
|
|
2045
|
+
assertServiceAuthEnvironment();
|
|
2046
|
+
await repairService();
|
|
2047
|
+
console.log("✅ opencodex background service repaired (assets refreshed, no Task Scheduler re-registration).");
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
1714
2050
|
// Non-install subcommands follow the backend recorded at install time (state v2).
|
|
1715
2051
|
const backend: ServiceBackend = parsed.backend ?? (process.platform === "win32" ? readServiceBackend() : "scheduler");
|
|
1716
2052
|
const ops = platformOps(backend);
|
|
@@ -1727,18 +2063,39 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
1727
2063
|
? "✅ opencodex native service installed + started (windowless, starts at boot, auto-restarts on crash)."
|
|
1728
2064
|
: "✅ opencodex service installed + started (auto-starts on login, auto-restarts on crash).");
|
|
1729
2065
|
if (process.platform === "linux") console.log(" For auto-start on boot: loginctl enable-linger $USER");
|
|
2066
|
+
// Service users never reach the `ocx start` prompt: the proxy they run is the
|
|
2067
|
+
// supervised child, which always carries OCX_SERVICE=1. This command, though, is
|
|
2068
|
+
// hand-typed in a real terminal, so it is the one interactive moment they get.
|
|
2069
|
+
// Same one-time marker and same guards (TTY, gh auth, agent deferral) apply.
|
|
2070
|
+
await maybeShowStarPrompt();
|
|
1730
2071
|
break;
|
|
1731
2072
|
case "start":
|
|
1732
2073
|
ops.start();
|
|
1733
2074
|
console.log("✅ service started.");
|
|
1734
2075
|
break;
|
|
1735
|
-
case "stop":
|
|
2076
|
+
case "stop": {
|
|
1736
2077
|
assertServiceEnvironmentMatchesInstall();
|
|
1737
2078
|
// Only stop what is actually installed. The unguarded call ran a real `launchctl unload`
|
|
1738
2079
|
// (and its Windows/Linux twins) even with nothing installed.
|
|
1739
|
-
if (ops.status() !== null || isServiceInstalled())
|
|
2080
|
+
if (ops.status() !== null || isServiceInstalled()) {
|
|
2081
|
+
ops.stop();
|
|
2082
|
+
}
|
|
1740
2083
|
await stopTrackedProxyForServiceCommand();
|
|
1741
2084
|
{
|
|
2085
|
+
// Verify rather than trust the stop command: a surviving wrapper respawns its child
|
|
2086
|
+
// seconds later, and restoring native Codex on top of a live proxy is the failure #764
|
|
2087
|
+
// reports as "stop reports success without stopping the proxy".
|
|
2088
|
+
const survivor = await proxyStillLiveAfterStop();
|
|
2089
|
+
if (survivor) {
|
|
2090
|
+
console.error(
|
|
2091
|
+
`❌ service stop did not take effect: a proxy is still listening on port ${survivor.port}.`
|
|
2092
|
+
+ "\nNative Codex was NOT restored, because doing so while the proxy is running leaves"
|
|
2093
|
+
+ " both pointing at each other. Check for a second service backend (`ocx service status`)"
|
|
2094
|
+
+ " or a manually started proxy, then re-run `ocx service stop`.",
|
|
2095
|
+
);
|
|
2096
|
+
process.exitCode = 1;
|
|
2097
|
+
break;
|
|
2098
|
+
}
|
|
1742
2099
|
const restore = restoreNativeCodex();
|
|
1743
2100
|
if (restore.success) console.log("✅ service stopped + native Codex restored.");
|
|
1744
2101
|
else console.error(`⚠️ service stopped, but native Codex restore FAILED: ${restore.message}\nRun \`ocx restore\` (or check $CODEX_HOME/config.toml) before using native Codex.`);
|
|
@@ -1749,9 +2106,14 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
1749
2106
|
else if (!grok.ok) console.error(`⚠️ ${grok.message}`);
|
|
1750
2107
|
}
|
|
1751
2108
|
break;
|
|
2109
|
+
}
|
|
1752
2110
|
case "status": {
|
|
1753
|
-
|
|
1754
|
-
|
|
2111
|
+
if (process.platform === "win32" && backend === "scheduler") {
|
|
2112
|
+
console.log(await inspectWindowsSchedulerServiceStatus());
|
|
2113
|
+
} else {
|
|
2114
|
+
const s = ops.status();
|
|
2115
|
+
console.log(s ? `✅ running:\n${s}` : "❌ service not installed/running.");
|
|
2116
|
+
}
|
|
1755
2117
|
console.log(`Diagnostics: ${serviceDiagnosticsSummary()}`);
|
|
1756
2118
|
break;
|
|
1757
2119
|
}
|
|
@@ -1783,9 +2145,11 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
1783
2145
|
console.log("✅ service uninstalled.");
|
|
1784
2146
|
break;
|
|
1785
2147
|
default:
|
|
1786
|
-
console.error("Usage: ocx service [install|start|stop|status|uninstall|remove] [--native|--scheduler]");
|
|
2148
|
+
console.error("Usage: ocx service [install|repair|start|stop|status|uninstall|remove] [--native|--scheduler]");
|
|
1787
2149
|
console.error(" With no subcommand, installs/updates and starts the background service.");
|
|
2150
|
+
console.error(" repair: refresh assets and restart an already-installed service (no admin re-prompt).");
|
|
1788
2151
|
console.error(" --native (Windows only): register a real SCM service via WinSW instead of Task Scheduler.");
|
|
1789
2152
|
process.exit(1);
|
|
1790
2153
|
}
|
|
1791
2154
|
}
|
|
2155
|
+
|
|
@@ -18,6 +18,11 @@ import {
|
|
|
18
18
|
type PolicyRunResult,
|
|
19
19
|
type PolicySkipReason,
|
|
20
20
|
} from "./policy";
|
|
21
|
+
import {
|
|
22
|
+
drainStorageWorkers,
|
|
23
|
+
registerStorageWorker,
|
|
24
|
+
terminateStorageWorker,
|
|
25
|
+
} from "./worker-lifecycle";
|
|
21
26
|
|
|
22
27
|
export type PolicyJobStatus = "idle" | "running";
|
|
23
28
|
|
|
@@ -127,7 +132,7 @@ function disownActiveRun(): void {
|
|
|
127
132
|
export function resetStorageCleanupPolicyJobForTests(): void {
|
|
128
133
|
disownActiveRun();
|
|
129
134
|
if (activeWorker) {
|
|
130
|
-
|
|
135
|
+
void terminateStorageWorker(activeWorker);
|
|
131
136
|
activeWorker = null;
|
|
132
137
|
}
|
|
133
138
|
inflight = null;
|
|
@@ -136,11 +141,24 @@ export function resetStorageCleanupPolicyJobForTests(): void {
|
|
|
136
141
|
state = { status: "idle" };
|
|
137
142
|
}
|
|
138
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Await-able sibling of the reset above, for test teardown.
|
|
146
|
+
*
|
|
147
|
+
* `bun test --isolate` reclaims a file's realm at the file boundary. A storage
|
|
148
|
+
* worker still exiting at that moment trips a Bun-internal assertion on Windows
|
|
149
|
+
* and takes the whole run down, so a suite that spawns workers must be able to
|
|
150
|
+
* wait for them rather than fire-and-forget.
|
|
151
|
+
*/
|
|
152
|
+
export async function resetStorageCleanupPolicyJobForTestsAsync(): Promise<void> {
|
|
153
|
+
resetStorageCleanupPolicyJobForTests();
|
|
154
|
+
await drainStorageWorkers();
|
|
155
|
+
}
|
|
156
|
+
|
|
139
157
|
/** Terminate an in-flight worker during process shutdown. */
|
|
140
158
|
export function abortStorageCleanupPolicyJob(): void {
|
|
141
159
|
disownActiveRun();
|
|
142
160
|
if (activeWorker) {
|
|
143
|
-
|
|
161
|
+
void terminateStorageWorker(activeWorker);
|
|
144
162
|
activeWorker = null;
|
|
145
163
|
}
|
|
146
164
|
releaseHeldMutationSlot();
|
|
@@ -218,13 +236,14 @@ function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number }): Prom
|
|
|
218
236
|
const requestId = crypto.randomUUID();
|
|
219
237
|
let settled = false;
|
|
220
238
|
const worker = new Worker(new URL("./policy-worker.ts", import.meta.url).href);
|
|
239
|
+
registerStorageWorker(worker);
|
|
221
240
|
activeWorker = worker;
|
|
222
241
|
|
|
223
242
|
const timer = setTimeout(() => {
|
|
224
243
|
if (settled) return;
|
|
225
244
|
settled = true;
|
|
226
245
|
cancelActiveRun = null;
|
|
227
|
-
|
|
246
|
+
void terminateStorageWorker(worker);
|
|
228
247
|
if (activeWorker === worker) activeWorker = null;
|
|
229
248
|
reject(new Error("storage_cleanup_worker_timeout"));
|
|
230
249
|
}, WORKER_TIMEOUT_MS);
|
|
@@ -235,8 +254,10 @@ function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number }): Prom
|
|
|
235
254
|
cancelActiveRun = null;
|
|
236
255
|
clearTimeout(timer);
|
|
237
256
|
if (activeWorker === worker) activeWorker = null;
|
|
238
|
-
|
|
239
|
-
|
|
257
|
+
// Settle the caller only after the thread is actually gone, so a suite
|
|
258
|
+
// that awaits its request cannot reach the next test file with a worker
|
|
259
|
+
// still exiting behind it.
|
|
260
|
+
void terminateStorageWorker(worker).then(fn, fn);
|
|
240
261
|
};
|
|
241
262
|
|
|
242
263
|
cancelActiveRun = () => {
|