@opengsd/gsd-pi 1.10.0 → 1.11.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/README.md +10 -9
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto-model-selection.js +1 -0
- package/dist/resources/extensions/gsd/auto-start.js +1 -1
- package/dist/resources/extensions/gsd/auto-verification.js +25 -12
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +4 -3
- package/dist/resources/extensions/gsd/db/engine.js +8 -2
- package/dist/resources/extensions/gsd/db-canonical-foundation-schema.js +120 -0
- package/dist/resources/extensions/gsd/db-migration-steps.js +4 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +12 -3
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +16 -4
- package/dist/resources/extensions/gsd/model-cost-table.js +6 -3
- package/dist/resources/extensions/gsd/model-router.js +10 -5
- package/dist/resources/extensions/gsd/planning-subagent-registry.js +12 -4
- package/dist/resources/extensions/gsd/preferences-models.js +50 -10
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +41 -6
- package/dist/resources/extensions/gsd/preferences.js +7 -0
- package/dist/resources/extensions/gsd/templates/PREFERENCES.md +3 -0
- package/dist/resources/extensions/gsd/tools/plan-slice.js +5 -1
- package/dist/resources/extensions/gsd/verification-evidence.js +5 -3
- package/dist/resources/extensions/ollama/ollama-chat-provider.js +2 -2
- package/dist/resources/extensions/subagent/index.js +1 -1
- package/dist/startup-model-validation.d.ts +1 -1
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
- package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
- package/integrations/hermes/pyproject.toml +1 -1
- package/package.json +7 -6
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/dist/rpc.d.ts +1 -1
- package/packages/contracts/dist/rpc.d.ts.map +1 -1
- package/packages/contracts/dist/rpc.js +1 -1
- package/packages/contracts/dist/rpc.js.map +1 -1
- package/packages/contracts/dist/rpc.test.js +1 -1
- package/packages/contracts/dist/rpc.test.js.map +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/cli/args.js +2 -2
- package/packages/gsd-agent-modes/dist/cli/args.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/gsd-cloud/README.md +12 -2
- package/packages/gsd-cloud/dist/background-cli.test.js +157 -80
- package/packages/gsd-cloud/dist/background-cli.test.js.map +1 -1
- package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cli.js +36 -12
- package/packages/gsd-cloud/dist/cli.js.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.d.ts +6 -1
- package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.js +106 -24
- package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.test.js +224 -59
- package/packages/gsd-cloud/dist/cloud-runtime.test.js.map +1 -1
- package/packages/gsd-cloud/dist/runtime-artifacts.d.ts +3 -0
- package/packages/gsd-cloud/dist/runtime-artifacts.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-artifacts.js +36 -0
- package/packages/gsd-cloud/dist/runtime-artifacts.js.map +1 -0
- package/packages/gsd-cloud/dist/runtime-process.d.ts +5 -0
- package/packages/gsd-cloud/dist/runtime-process.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/runtime-process.js +429 -61
- package/packages/gsd-cloud/dist/runtime-process.js.map +1 -1
- package/packages/gsd-cloud/dist/runtime-process.test.js +493 -47
- package/packages/gsd-cloud/dist/runtime-process.test.js.map +1 -1
- package/packages/gsd-cloud/dist/runtime-telemetry.d.ts +117 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.js +291 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.js.map +1 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.test.d.ts +2 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.test.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.test.js +434 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.test.js.map +1 -0
- package/packages/gsd-cloud/package.json +2 -2
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/types.d.ts +2 -2
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +113 -2
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +84 -9
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/models.js +18 -6
- package/packages/pi-ai/dist/models.js.map +1 -1
- package/packages/pi-ai/dist/providers/amazon-bedrock.js +3 -2
- package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
- package/packages/pi-ai/dist/providers/google.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.js +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.d.ts +1 -1
- package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.js +1 -1
- package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +9 -1
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -3
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/theme/theme.d.ts +1 -1
- package/packages/pi-coding-agent/dist/theme/theme.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/theme/theme.js +1 -0
- package/packages/pi-coding-agent/dist/theme/theme.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/dist/theme/theme.d.ts +1 -1
- package/pkg/dist/theme/theme.d.ts.map +1 -1
- package/pkg/dist/theme/theme.js +1 -0
- package/pkg/dist/theme/theme.js.map +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-model-selection.ts +1 -0
- package/src/resources/extensions/gsd/auto-start.ts +1 -1
- package/src/resources/extensions/gsd/auto-verification.ts +46 -12
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +4 -3
- package/src/resources/extensions/gsd/db/engine.ts +9 -1
- package/src/resources/extensions/gsd/db-canonical-foundation-schema.ts +129 -0
- package/src/resources/extensions/gsd/db-migration-steps.ts +5 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +12 -3
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +15 -4
- package/src/resources/extensions/gsd/model-cost-table.ts +14 -3
- package/src/resources/extensions/gsd/model-router.ts +10 -5
- package/src/resources/extensions/gsd/planning-subagent-registry.ts +18 -4
- package/src/resources/extensions/gsd/preferences-models.ts +54 -10
- package/src/resources/extensions/gsd/preferences-types.ts +10 -2
- package/src/resources/extensions/gsd/preferences-validation.ts +49 -6
- package/src/resources/extensions/gsd/preferences.ts +13 -0
- package/src/resources/extensions/gsd/templates/PREFERENCES.md +3 -0
- package/src/resources/extensions/gsd/tests/db-canonical-foundation.test.ts +459 -0
- package/src/resources/extensions/gsd/tests/db-engine-migrate-guards.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +22 -7
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +24 -0
- package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +38 -1
- package/src/resources/extensions/gsd/tests/preferences.test.ts +71 -4
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/service-tier.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/thinking-level-resolution.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/workflow-authority-faults.test.ts +238 -0
- package/src/resources/extensions/gsd/tests/workflow-authority-fixture.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/workflow-authority-fixture.ts +141 -0
- package/src/resources/extensions/gsd/tests/workflow-authority-projection-conflict.test.ts +177 -0
- package/src/resources/extensions/gsd/tests/workflow-fault-harness.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/workflow-fault-harness.ts +72 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +40 -0
- package/src/resources/extensions/gsd/tools/plan-slice.ts +5 -1
- package/src/resources/extensions/gsd/verification-evidence.ts +5 -2
- package/src/resources/extensions/ollama/ollama-chat-provider.ts +2 -2
- package/src/resources/extensions/ollama/tests/ollama-chat-provider-think.test.ts +4 -0
- package/src/resources/extensions/subagent/index.ts +1 -1
- /package/dist/web/standalone/.next/static/{EAhnhYMt5UVOiG9-iPDjc → 8ub6oiEp2YnlOHQU20LJr}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{EAhnhYMt5UVOiG9-iPDjc → 8ub6oiEp2YnlOHQU20LJr}/_ssgManifest.js +0 -0
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
// Project/App: Open GSD
|
|
2
2
|
// File Purpose: Detached cloud runtime process lifecycle and status persistence.
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { chmodSync, closeSync, linkSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
6
|
+
import { dirname } from "node:path";
|
|
6
7
|
import { CLOUD_RUNTIME_INITIAL_CONNECT_WINDOW_MS } from "./cloud-runtime.js";
|
|
8
|
+
import { canonicalConfigPath, runtimeArtifactPath } from "./runtime-artifacts.js";
|
|
7
9
|
const PROCESS_STARTUP_GRACE_MS = 5_000;
|
|
8
10
|
const FORCED_STOP_TIMEOUT_MS = 5_000;
|
|
9
11
|
const STOP_GRACE_PERIOD_MS = 5_000;
|
|
10
12
|
const STOP_POLL_INTERVAL_MS = 50;
|
|
13
|
+
const MALFORMED_START_LOCK_GRACE_MS = 5_000;
|
|
11
14
|
export const BACKGROUND_RUNTIME_READY_TIMEOUT_MS = CLOUD_RUNTIME_INITIAL_CONNECT_WINDOW_MS + PROCESS_STARTUP_GRACE_MS;
|
|
15
|
+
const START_LOCK_TIMEOUT_MS = BACKGROUND_RUNTIME_READY_TIMEOUT_MS
|
|
16
|
+
+ 2 * (STOP_GRACE_PERIOD_MS + FORCED_STOP_TIMEOUT_MS)
|
|
17
|
+
+ STOP_POLL_INTERVAL_MS;
|
|
12
18
|
export async function startBackgroundRuntime(opts) {
|
|
13
|
-
await acquireRuntimeStartLock(opts.configPath);
|
|
19
|
+
const releaseStartLock = await acquireRuntimeStartLock(opts.configPath);
|
|
14
20
|
try {
|
|
15
21
|
await stopBackgroundRuntime(opts.configPath);
|
|
16
22
|
const logFile = runtimeLogPath(opts.configPath);
|
|
@@ -37,14 +43,32 @@ export async function startBackgroundRuntime(opts) {
|
|
|
37
43
|
throw new Error(`could not start the background runtime; see ${logFile}`);
|
|
38
44
|
}
|
|
39
45
|
const pid = child.pid;
|
|
40
|
-
|
|
46
|
+
let processStartIdentity = null;
|
|
47
|
+
try {
|
|
48
|
+
processStartIdentity = (opts.processIdentityReader ?? readProcessStartIdentity)(pid);
|
|
49
|
+
if (!processStartIdentity) {
|
|
50
|
+
throw new Error(`could not determine process identity for PID ${pid}`);
|
|
51
|
+
}
|
|
52
|
+
writeRuntimeStateWithIdentity(opts.configPath, pid, opts.projectDirs, processStartIdentity);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (child.connected)
|
|
56
|
+
child.disconnect();
|
|
57
|
+
if (processStartIdentity) {
|
|
58
|
+
await terminateProcess(pid, processStartIdentity);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
await terminateKnownChild(child);
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
41
65
|
try {
|
|
42
66
|
await waitUntilReady(child, opts.readyTimeoutMs ?? BACKGROUND_RUNTIME_READY_TIMEOUT_MS);
|
|
43
67
|
}
|
|
44
68
|
catch (error) {
|
|
45
69
|
if (child.connected)
|
|
46
70
|
child.disconnect();
|
|
47
|
-
await terminateProcess(pid);
|
|
71
|
+
await terminateProcess(pid, processStartIdentity);
|
|
48
72
|
removeRuntimeState(opts.configPath);
|
|
49
73
|
throw error;
|
|
50
74
|
}
|
|
@@ -59,7 +83,7 @@ export async function startBackgroundRuntime(opts) {
|
|
|
59
83
|
};
|
|
60
84
|
}
|
|
61
85
|
finally {
|
|
62
|
-
|
|
86
|
+
releaseStartLock();
|
|
63
87
|
}
|
|
64
88
|
}
|
|
65
89
|
/**
|
|
@@ -69,39 +93,55 @@ export async function startBackgroundRuntime(opts) {
|
|
|
69
93
|
* same device token.
|
|
70
94
|
*/
|
|
71
95
|
export function writeRuntimeState(configPath, pid, projects) {
|
|
96
|
+
const processStartIdentity = readProcessStartIdentity(pid);
|
|
97
|
+
if (!processStartIdentity) {
|
|
98
|
+
throw new Error(`could not determine process identity for PID ${pid}`);
|
|
99
|
+
}
|
|
100
|
+
writeRuntimeStateWithIdentity(configPath, pid, projects, processStartIdentity);
|
|
101
|
+
}
|
|
102
|
+
function writeRuntimeStateWithIdentity(configPath, pid, projects, processStartIdentity) {
|
|
72
103
|
const statePath = runtimeStatePath(configPath);
|
|
73
104
|
mkdirSync(dirname(statePath), { recursive: true });
|
|
74
|
-
writePrivateJson(statePath, {
|
|
105
|
+
writePrivateJson(statePath, {
|
|
106
|
+
pid,
|
|
107
|
+
projects,
|
|
108
|
+
process_start_identity: processStartIdentity,
|
|
109
|
+
});
|
|
75
110
|
}
|
|
76
111
|
export function clearRuntimeState(configPath) {
|
|
77
112
|
removeRuntimeState(configPath);
|
|
78
113
|
}
|
|
79
114
|
export async function stopBackgroundRuntime(configPath) {
|
|
80
|
-
const
|
|
81
|
-
if (!
|
|
115
|
+
const located = readRuntimeState(configPath);
|
|
116
|
+
if (!located)
|
|
82
117
|
return false;
|
|
83
|
-
|
|
84
|
-
|
|
118
|
+
const processStartIdentity = located.state.process_start_identity;
|
|
119
|
+
if (!processStartIdentity || !runtimeProcessMatches(located.state)) {
|
|
120
|
+
removeRuntimeStateFile(located.path);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
await terminateProcess(located.state.pid, processStartIdentity);
|
|
124
|
+
removeRuntimeStateFile(located.path);
|
|
85
125
|
return true;
|
|
86
126
|
}
|
|
87
127
|
export function backgroundRuntimeStatus(configPath) {
|
|
88
|
-
const
|
|
89
|
-
if (!
|
|
128
|
+
const located = readRuntimeState(configPath);
|
|
129
|
+
if (!located) {
|
|
90
130
|
return { running: false, pid: null, projects: [], log_file: runtimeLogPath(configPath) };
|
|
91
131
|
}
|
|
92
|
-
if (!
|
|
93
|
-
|
|
132
|
+
if (!runtimeProcessMatches(located.state)) {
|
|
133
|
+
removeRuntimeStateFile(located.path);
|
|
94
134
|
return {
|
|
95
135
|
running: false,
|
|
96
136
|
pid: null,
|
|
97
|
-
projects: state.projects,
|
|
137
|
+
projects: located.state.projects,
|
|
98
138
|
log_file: runtimeLogPath(configPath),
|
|
99
139
|
};
|
|
100
140
|
}
|
|
101
141
|
return {
|
|
102
142
|
running: true,
|
|
103
|
-
pid: state.pid,
|
|
104
|
-
projects: state.projects,
|
|
143
|
+
pid: located.state.pid,
|
|
144
|
+
projects: located.state.projects,
|
|
105
145
|
log_file: runtimeLogPath(configPath),
|
|
106
146
|
};
|
|
107
147
|
}
|
|
@@ -137,40 +177,45 @@ function waitUntilReady(child, timeoutMs) {
|
|
|
137
177
|
child.on("message", onMessage);
|
|
138
178
|
});
|
|
139
179
|
}
|
|
140
|
-
function runtimeStatePath(configPath) {
|
|
141
|
-
return
|
|
180
|
+
export function runtimeStatePath(configPath) {
|
|
181
|
+
return runtimeArtifactPath(configPath, "state");
|
|
142
182
|
}
|
|
143
|
-
function runtimeLogPath(configPath) {
|
|
144
|
-
return
|
|
183
|
+
export function runtimeLogPath(configPath) {
|
|
184
|
+
return runtimeArtifactPath(configPath, "log");
|
|
145
185
|
}
|
|
146
186
|
function runtimeStartLockPath(configPath) {
|
|
147
|
-
return
|
|
187
|
+
return runtimeArtifactPath(configPath, "start.lock");
|
|
148
188
|
}
|
|
149
|
-
async function acquireRuntimeStartLock(configPath) {
|
|
189
|
+
export async function acquireRuntimeStartLock(configPath, onRecoveryClaimed) {
|
|
150
190
|
const lockPath = runtimeStartLockPath(configPath);
|
|
151
191
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
192
|
+
const processStartIdentity = readProcessStartIdentity(process.pid);
|
|
193
|
+
if (!processStartIdentity) {
|
|
194
|
+
throw new Error(`could not determine process identity for PID ${process.pid}`);
|
|
195
|
+
}
|
|
196
|
+
const owner = {
|
|
197
|
+
pid: process.pid,
|
|
198
|
+
process_start_identity: processStartIdentity,
|
|
199
|
+
};
|
|
152
200
|
// Match worst-case `startBackgroundRuntime` hold time: stop prior runtime, wait for
|
|
153
201
|
// ready, and tear down the child if ready fails, plus one poll interval.
|
|
154
|
-
const deadline = Date.now()
|
|
155
|
-
+ BACKGROUND_RUNTIME_READY_TIMEOUT_MS
|
|
156
|
-
+ 2 * (STOP_GRACE_PERIOD_MS + FORCED_STOP_TIMEOUT_MS)
|
|
157
|
-
+ STOP_POLL_INTERVAL_MS;
|
|
202
|
+
const deadline = Date.now() + START_LOCK_TIMEOUT_MS;
|
|
158
203
|
while (Date.now() < deadline) {
|
|
159
204
|
try {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
205
|
+
createRuntimeStartLock(lockPath, owner);
|
|
206
|
+
let released = false;
|
|
207
|
+
return () => {
|
|
208
|
+
if (released)
|
|
209
|
+
return;
|
|
210
|
+
released = true;
|
|
211
|
+
releaseRuntimeStartLock(lockPath, owner);
|
|
212
|
+
};
|
|
164
213
|
}
|
|
165
214
|
catch (error) {
|
|
166
215
|
if (error.code !== "EEXIST")
|
|
167
216
|
throw error;
|
|
168
217
|
try {
|
|
169
|
-
|
|
170
|
-
const incompleteLockIsStale = ownerPid === null
|
|
171
|
-
&& Date.now() - statSync(lockPath).mtimeMs > 1_000;
|
|
172
|
-
if ((ownerPid !== null && !processIsRunning(ownerPid)) || incompleteLockIsStale) {
|
|
173
|
-
unlinkSync(lockPath);
|
|
218
|
+
if (recoverRuntimeStartLock(lockPath, configPath, onRecoveryClaimed)) {
|
|
174
219
|
continue;
|
|
175
220
|
}
|
|
176
221
|
}
|
|
@@ -182,44 +227,329 @@ async function acquireRuntimeStartLock(configPath) {
|
|
|
182
227
|
}
|
|
183
228
|
throw new Error("timed out waiting for the background runtime start lock");
|
|
184
229
|
}
|
|
185
|
-
function
|
|
186
|
-
const
|
|
187
|
-
if (readRuntimeStartLockOwner(lockPath) !== process.pid)
|
|
188
|
-
return;
|
|
230
|
+
function createRuntimeStartLock(lockPath, owner) {
|
|
231
|
+
const temporaryPath = `${lockPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
189
232
|
try {
|
|
190
|
-
|
|
233
|
+
writePrivateJson(temporaryPath, owner);
|
|
234
|
+
linkSync(temporaryPath, lockPath);
|
|
191
235
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
throw error;
|
|
236
|
+
finally {
|
|
237
|
+
removeRuntimeStateFile(temporaryPath);
|
|
195
238
|
}
|
|
196
239
|
}
|
|
240
|
+
function releaseRuntimeStartLock(lockPath, owner) {
|
|
241
|
+
if (!runtimeStartLockOwnersEqual(readRuntimeStartLockOwner(lockPath), owner))
|
|
242
|
+
return;
|
|
243
|
+
removeRuntimeStateFile(lockPath);
|
|
244
|
+
}
|
|
197
245
|
function readRuntimeStartLockOwner(lockPath) {
|
|
198
246
|
try {
|
|
199
|
-
const
|
|
200
|
-
|
|
247
|
+
const text = readFileSync(lockPath, "utf8").trim();
|
|
248
|
+
if (/^\d+$/.test(text)) {
|
|
249
|
+
const pid = Number.parseInt(text, 10);
|
|
250
|
+
return pid > 0 ? { pid } : null;
|
|
251
|
+
}
|
|
252
|
+
const value = JSON.parse(text);
|
|
253
|
+
if (!Number.isInteger(value.pid) || (value.pid ?? 0) <= 0)
|
|
254
|
+
return null;
|
|
255
|
+
if (typeof value.process_start_identity !== "string")
|
|
256
|
+
return null;
|
|
257
|
+
return { pid: value.pid, process_start_identity: value.process_start_identity };
|
|
201
258
|
}
|
|
202
259
|
catch (error) {
|
|
203
260
|
if (error.code === "ENOENT")
|
|
204
261
|
return null;
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function runtimeStartLockOwnerIsRunning(owner) {
|
|
266
|
+
if (!owner.process_start_identity)
|
|
267
|
+
return processIsRunning(owner.pid);
|
|
268
|
+
return processMatchesIdentity(owner.pid, owner.process_start_identity);
|
|
269
|
+
}
|
|
270
|
+
function runtimeStartLockCanBeRecovered(lockPath, configPath) {
|
|
271
|
+
const owner = readRuntimeStartLockOwner(lockPath);
|
|
272
|
+
if (owner?.process_start_identity)
|
|
273
|
+
return !runtimeStartLockOwnerIsRunning(owner);
|
|
274
|
+
const age = Date.now() - statSync(lockPath).mtimeMs;
|
|
275
|
+
if (owner) {
|
|
276
|
+
if (!runtimeStartLockOwnerIsRunning(owner))
|
|
277
|
+
return true;
|
|
278
|
+
return age > START_LOCK_TIMEOUT_MS
|
|
279
|
+
&& inspectProcessCommandConfig(owner.pid, configPath) === false;
|
|
280
|
+
}
|
|
281
|
+
return age > MALFORMED_START_LOCK_GRACE_MS;
|
|
282
|
+
}
|
|
283
|
+
function recoverRuntimeStartLock(lockPath, configPath, onRecoveryClaimed) {
|
|
284
|
+
const recoveryPath = `${lockPath}.recovery.${process.pid}.${randomUUID()}`;
|
|
285
|
+
try {
|
|
286
|
+
linkSync(lockPath, recoveryPath);
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
if (error.code === "ENOENT")
|
|
290
|
+
return false;
|
|
205
291
|
throw error;
|
|
206
292
|
}
|
|
293
|
+
try {
|
|
294
|
+
if (!runtimeStartLockCanBeRecovered(recoveryPath, configPath))
|
|
295
|
+
return false;
|
|
296
|
+
onRecoveryClaimed?.();
|
|
297
|
+
if (!pathsReferenceSameFile(lockPath, recoveryPath))
|
|
298
|
+
return false;
|
|
299
|
+
unlinkSync(lockPath);
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
removeRuntimeStateFile(recoveryPath);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function pathsReferenceSameFile(firstPath, secondPath) {
|
|
307
|
+
try {
|
|
308
|
+
const first = statSync(firstPath, { bigint: true });
|
|
309
|
+
const second = statSync(secondPath, { bigint: true });
|
|
310
|
+
return first.dev === second.dev && first.ino === second.ino;
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function runtimeStartLockOwnersEqual(first, second) {
|
|
317
|
+
return first?.pid === second.pid
|
|
318
|
+
&& first.process_start_identity === second.process_start_identity;
|
|
207
319
|
}
|
|
208
320
|
function readRuntimeState(configPath) {
|
|
321
|
+
const statePath = runtimeStatePath(configPath);
|
|
322
|
+
const current = readRuntimeStateFile(statePath);
|
|
323
|
+
if (current) {
|
|
324
|
+
if (typeof current.process_start_identity === "string"
|
|
325
|
+
&& statePath !== legacyRuntimeStatePath(configPath)) {
|
|
326
|
+
return { path: statePath, state: current };
|
|
327
|
+
}
|
|
328
|
+
return migrateLegacyRuntimeState(configPath, statePath, current);
|
|
329
|
+
}
|
|
330
|
+
const legacyPath = legacyRuntimeStatePath(configPath);
|
|
331
|
+
if (legacyPath === statePath)
|
|
332
|
+
return null;
|
|
333
|
+
const legacy = readRuntimeStateFile(legacyPath);
|
|
334
|
+
if (!legacy)
|
|
335
|
+
return null;
|
|
336
|
+
return migrateLegacyRuntimeState(configPath, legacyPath, legacy);
|
|
337
|
+
}
|
|
338
|
+
function readRuntimeStateFile(path) {
|
|
209
339
|
try {
|
|
210
|
-
const value = JSON.parse(readFileSync(
|
|
340
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
211
341
|
const pid = value.pid;
|
|
212
|
-
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0
|
|
342
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0
|
|
343
|
+
|| !Array.isArray(value.projects))
|
|
213
344
|
return null;
|
|
214
345
|
return {
|
|
215
346
|
pid,
|
|
216
347
|
projects: value.projects.filter((project) => typeof project === "string"),
|
|
348
|
+
process_start_identity: value.process_start_identity,
|
|
217
349
|
};
|
|
218
350
|
}
|
|
219
351
|
catch {
|
|
220
352
|
return null;
|
|
221
353
|
}
|
|
222
354
|
}
|
|
355
|
+
function migrateLegacyRuntimeState(configPath, sourcePath, state) {
|
|
356
|
+
const destinationPath = runtimeStatePath(configPath);
|
|
357
|
+
if (sourcePath === legacyRuntimeStatePath(configPath)
|
|
358
|
+
&& !processCommandMatchesConfig(state.pid, configPath))
|
|
359
|
+
return null;
|
|
360
|
+
if (typeof state.process_start_identity === "string") {
|
|
361
|
+
if (sourcePath !== destinationPath) {
|
|
362
|
+
writeRuntimeStateWithIdentity(configPath, state.pid, state.projects, state.process_start_identity);
|
|
363
|
+
removeRuntimeStateFile(sourcePath);
|
|
364
|
+
}
|
|
365
|
+
return { path: destinationPath, state };
|
|
366
|
+
}
|
|
367
|
+
const processStartIdentity = readProcessStartIdentity(state.pid);
|
|
368
|
+
if (!processStartIdentity)
|
|
369
|
+
return null;
|
|
370
|
+
writeRuntimeStateWithIdentity(configPath, state.pid, state.projects, processStartIdentity);
|
|
371
|
+
if (sourcePath !== destinationPath)
|
|
372
|
+
removeRuntimeStateFile(sourcePath);
|
|
373
|
+
return {
|
|
374
|
+
path: destinationPath,
|
|
375
|
+
state: { ...state, process_start_identity: processStartIdentity },
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function legacyRuntimeStatePath(configPath) {
|
|
379
|
+
return runtimeArtifactPath(`${dirname(canonicalConfigPath(configPath))}/daemon.yaml`, "state");
|
|
380
|
+
}
|
|
381
|
+
function runtimeProcessMatches(state) {
|
|
382
|
+
return typeof state.process_start_identity === "string"
|
|
383
|
+
&& processIsRunning(state.pid)
|
|
384
|
+
&& readProcessStartIdentity(state.pid) === state.process_start_identity;
|
|
385
|
+
}
|
|
386
|
+
function readProcessStartIdentity(pid) {
|
|
387
|
+
try {
|
|
388
|
+
if (process.platform === "linux") {
|
|
389
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
390
|
+
const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
391
|
+
return fields[19] ?? null;
|
|
392
|
+
}
|
|
393
|
+
if (process.platform === "darwin" || process.platform === "freebsd") {
|
|
394
|
+
return execFileSync("/bin/ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
395
|
+
encoding: "utf8",
|
|
396
|
+
}).trim() || null;
|
|
397
|
+
}
|
|
398
|
+
if (process.platform === "win32") {
|
|
399
|
+
return execFileSync("powershell.exe", [
|
|
400
|
+
"-NoProfile",
|
|
401
|
+
"-NonInteractive",
|
|
402
|
+
"-Command",
|
|
403
|
+
`(Get-Process -Id ${pid}).StartTime.ToUniversalTime().Ticks`,
|
|
404
|
+
], { encoding: "utf8" }).trim() || null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
function processCommandMatchesConfig(pid, configPath) {
|
|
413
|
+
return inspectProcessCommandConfig(pid, configPath) === true;
|
|
414
|
+
}
|
|
415
|
+
function inspectProcessCommandConfig(pid, configPath) {
|
|
416
|
+
const expectedConfigPath = canonicalConfigPath(configPath);
|
|
417
|
+
try {
|
|
418
|
+
if (process.platform === "linux") {
|
|
419
|
+
const args = readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0").filter(Boolean);
|
|
420
|
+
return commandArgsMatchConfig(args, expectedConfigPath);
|
|
421
|
+
}
|
|
422
|
+
if (process.platform === "darwin" || process.platform === "freebsd") {
|
|
423
|
+
const command = execFileSync("/bin/ps", ["-ww", "-o", "command=", "-p", String(pid)], {
|
|
424
|
+
encoding: "utf8",
|
|
425
|
+
}).trim();
|
|
426
|
+
return commandLineMatchesRuntimeConfig(command, expectedConfigPath, process.platform);
|
|
427
|
+
}
|
|
428
|
+
if (process.platform === "win32") {
|
|
429
|
+
const command = execFileSync("powershell.exe", [
|
|
430
|
+
"-NoProfile",
|
|
431
|
+
"-NonInteractive",
|
|
432
|
+
"-Command",
|
|
433
|
+
`(Get-CimInstance Win32_Process -Filter \"ProcessId = ${pid}\").CommandLine`,
|
|
434
|
+
], { encoding: "utf8" }).trim();
|
|
435
|
+
return commandLineMatchesRuntimeConfig(command, expectedConfigPath, process.platform);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
export function commandLineMatchesRuntimeConfig(command, configPath, platform) {
|
|
444
|
+
if (platform === "win32") {
|
|
445
|
+
return commandArgsMatchConfig(splitWindowsCommandLine(command), canonicalConfigPath(configPath));
|
|
446
|
+
}
|
|
447
|
+
return commandStringMatchesConfig(command, canonicalConfigPath(configPath));
|
|
448
|
+
}
|
|
449
|
+
function splitWindowsCommandLine(command) {
|
|
450
|
+
const args = [];
|
|
451
|
+
let index = 0;
|
|
452
|
+
while (index < command.length) {
|
|
453
|
+
while (/\s/.test(command[index] ?? ""))
|
|
454
|
+
index += 1;
|
|
455
|
+
if (index >= command.length)
|
|
456
|
+
break;
|
|
457
|
+
let argument = "";
|
|
458
|
+
let inQuotes = false;
|
|
459
|
+
while (index < command.length) {
|
|
460
|
+
if (!inQuotes && /\s/.test(command[index] ?? ""))
|
|
461
|
+
break;
|
|
462
|
+
let backslashes = 0;
|
|
463
|
+
while (command[index] === "\\") {
|
|
464
|
+
backslashes += 1;
|
|
465
|
+
index += 1;
|
|
466
|
+
}
|
|
467
|
+
if (command[index] === '"') {
|
|
468
|
+
argument += "\\".repeat(Math.floor(backslashes / 2));
|
|
469
|
+
if (backslashes % 2 === 0) {
|
|
470
|
+
inQuotes = !inQuotes;
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
argument += '"';
|
|
474
|
+
}
|
|
475
|
+
index += 1;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
argument += "\\".repeat(backslashes);
|
|
479
|
+
if (index < command.length) {
|
|
480
|
+
argument += command[index];
|
|
481
|
+
index += 1;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
args.push(argument);
|
|
485
|
+
}
|
|
486
|
+
return args;
|
|
487
|
+
}
|
|
488
|
+
function commandStringMatchesConfig(command, configPath) {
|
|
489
|
+
const runtimeCommand = findRuntimeCommand(command);
|
|
490
|
+
if (!runtimeCommand)
|
|
491
|
+
return false;
|
|
492
|
+
const executablePrefix = command.slice(0, runtimeCommand.index);
|
|
493
|
+
if (!commandPrefixHasCloudRuntimeLauncher(executablePrefix))
|
|
494
|
+
return false;
|
|
495
|
+
if (runtimeCommand.command === "login"
|
|
496
|
+
&& !/(?:^|\s)--foreground(?:\s|$)/.test(command.slice(runtimeCommand.index)))
|
|
497
|
+
return false;
|
|
498
|
+
const match = command.slice(runtimeCommand.index).match(/(?:^|\s)(?:--config|-c)(?:=|\s+)(.*?)(?=\s+-{1,2}[A-Za-z](?:[\w-]*)(?:=|\s|$)|$)/);
|
|
499
|
+
const configuredPath = match?.[1]?.trim().replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, "$1$2");
|
|
500
|
+
return configuredPath !== undefined && canonicalConfigPath(configuredPath) === configPath;
|
|
501
|
+
}
|
|
502
|
+
function commandArgsMatchConfig(args, configPath) {
|
|
503
|
+
const runtimeCommand = findRuntimeCommandInArgs(args);
|
|
504
|
+
if (!runtimeCommand)
|
|
505
|
+
return false;
|
|
506
|
+
if (!argsHaveCloudRuntimeLauncher(args.slice(0, runtimeCommand.index)))
|
|
507
|
+
return false;
|
|
508
|
+
const runtimeArgs = args.slice(runtimeCommand.index + 1);
|
|
509
|
+
if (runtimeCommand.command === "login" && !runtimeArgs.includes("--foreground"))
|
|
510
|
+
return false;
|
|
511
|
+
const configIndex = runtimeArgs.findIndex((arg) => arg === "--config" || arg === "-c");
|
|
512
|
+
const configuredPath = configIndex >= 0 ? runtimeArgs[configIndex + 1] : undefined;
|
|
513
|
+
const equalsArgument = runtimeArgs.find((arg) => arg.startsWith("--config=") || arg.startsWith("-c="))
|
|
514
|
+
?.replace(/^(?:--config|-c)=/, "");
|
|
515
|
+
return [configuredPath, equalsArgument].some((value) => value !== undefined && canonicalConfigPath(value) === configPath);
|
|
516
|
+
}
|
|
517
|
+
function argsHaveCloudRuntimeLauncher(args) {
|
|
518
|
+
if (args.length === 1)
|
|
519
|
+
return isCloudRuntimeExecutable(args[0]);
|
|
520
|
+
return args.length === 2
|
|
521
|
+
&& isNodeExecutable(args[0])
|
|
522
|
+
&& isCloudRuntimeExecutable(args[1]);
|
|
523
|
+
}
|
|
524
|
+
function commandPrefixHasCloudRuntimeLauncher(prefix) {
|
|
525
|
+
const args = splitFlattenedCommandPrefix(prefix);
|
|
526
|
+
return args !== null && argsHaveCloudRuntimeLauncher(args);
|
|
527
|
+
}
|
|
528
|
+
function splitFlattenedCommandPrefix(prefix) {
|
|
529
|
+
const args = [];
|
|
530
|
+
const token = /\s*(?:"([^"]*)"|'([^']*)'|(\S+))/gy;
|
|
531
|
+
while (token.lastIndex < prefix.length) {
|
|
532
|
+
const match = token.exec(prefix);
|
|
533
|
+
if (!match)
|
|
534
|
+
return null;
|
|
535
|
+
args.push(match[1] ?? match[2] ?? match[3]);
|
|
536
|
+
}
|
|
537
|
+
return args;
|
|
538
|
+
}
|
|
539
|
+
function findRuntimeCommand(command) {
|
|
540
|
+
const match = /(?:^|\s)(_run|connect|login)(?=\s|$)/.exec(command);
|
|
541
|
+
return match ? { command: match[1], index: match.index } : null;
|
|
542
|
+
}
|
|
543
|
+
function findRuntimeCommandInArgs(args) {
|
|
544
|
+
const index = args.findIndex((arg) => arg === "_run" || arg === "connect" || arg === "login");
|
|
545
|
+
return index < 0 ? null : { command: args[index], index };
|
|
546
|
+
}
|
|
547
|
+
function isCloudRuntimeExecutable(argument) {
|
|
548
|
+
return /(?:^|[\\/])gsd-cloud(?:\.[cm]?js)?$/.test(argument);
|
|
549
|
+
}
|
|
550
|
+
function isNodeExecutable(argument) {
|
|
551
|
+
return /(?:^|[\\/])node(?:\.exe)?$/i.test(argument);
|
|
552
|
+
}
|
|
223
553
|
function processIsRunning(pid) {
|
|
224
554
|
try {
|
|
225
555
|
process.kill(pid, 0);
|
|
@@ -229,36 +559,74 @@ function processIsRunning(pid) {
|
|
|
229
559
|
return error.code === "EPERM";
|
|
230
560
|
}
|
|
231
561
|
}
|
|
232
|
-
async function waitForProcessExit(pid, timeoutMs) {
|
|
562
|
+
async function waitForProcessExit(pid, expectedIdentity, timeoutMs) {
|
|
233
563
|
const deadline = Date.now() + timeoutMs;
|
|
234
|
-
while (
|
|
564
|
+
while (processMatchesIdentity(pid, expectedIdentity)) {
|
|
235
565
|
if (Date.now() >= deadline)
|
|
236
566
|
return false;
|
|
237
567
|
await new Promise((resolve) => setTimeout(resolve, STOP_POLL_INTERVAL_MS));
|
|
238
568
|
}
|
|
239
569
|
return true;
|
|
240
570
|
}
|
|
241
|
-
async function terminateProcess(pid) {
|
|
242
|
-
signalProcess(pid, "SIGTERM")
|
|
243
|
-
|
|
571
|
+
async function terminateProcess(pid, expectedIdentity) {
|
|
572
|
+
if (!signalProcess(pid, expectedIdentity, "SIGTERM"))
|
|
573
|
+
return;
|
|
574
|
+
if (await waitForProcessExit(pid, expectedIdentity, STOP_GRACE_PERIOD_MS))
|
|
244
575
|
return;
|
|
245
|
-
signalProcess(pid, "SIGKILL")
|
|
246
|
-
|
|
576
|
+
if (!signalProcess(pid, expectedIdentity, "SIGKILL"))
|
|
577
|
+
return;
|
|
578
|
+
if (!await waitForProcessExit(pid, expectedIdentity, FORCED_STOP_TIMEOUT_MS)) {
|
|
247
579
|
throw new Error(`background runtime PID ${pid} did not stop`);
|
|
248
580
|
}
|
|
249
581
|
}
|
|
250
|
-
function
|
|
582
|
+
async function terminateKnownChild(child) {
|
|
583
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
584
|
+
return;
|
|
585
|
+
child.kill("SIGTERM");
|
|
586
|
+
if (await waitForChildExit(child, STOP_GRACE_PERIOD_MS))
|
|
587
|
+
return;
|
|
588
|
+
child.kill("SIGKILL");
|
|
589
|
+
if (!await waitForChildExit(child, FORCED_STOP_TIMEOUT_MS)) {
|
|
590
|
+
throw new Error(`background runtime PID ${child.pid ?? "unknown"} did not stop`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function waitForChildExit(child, timeoutMs) {
|
|
594
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
595
|
+
return Promise.resolve(true);
|
|
596
|
+
return new Promise((resolve) => {
|
|
597
|
+
const onExit = () => {
|
|
598
|
+
clearTimeout(timeout);
|
|
599
|
+
resolve(true);
|
|
600
|
+
};
|
|
601
|
+
const timeout = setTimeout(() => {
|
|
602
|
+
child.removeListener("exit", onExit);
|
|
603
|
+
resolve(false);
|
|
604
|
+
}, timeoutMs);
|
|
605
|
+
child.once("exit", onExit);
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
function signalProcess(pid, expectedIdentity, signal) {
|
|
609
|
+
if (!processMatchesIdentity(pid, expectedIdentity))
|
|
610
|
+
return false;
|
|
251
611
|
try {
|
|
252
612
|
process.kill(pid, signal);
|
|
613
|
+
return true;
|
|
253
614
|
}
|
|
254
615
|
catch (error) {
|
|
255
|
-
if (error.code
|
|
256
|
-
|
|
616
|
+
if (error.code === "ESRCH")
|
|
617
|
+
return false;
|
|
618
|
+
throw error;
|
|
257
619
|
}
|
|
258
620
|
}
|
|
621
|
+
function processMatchesIdentity(pid, expectedIdentity) {
|
|
622
|
+
return processIsRunning(pid) && readProcessStartIdentity(pid) === expectedIdentity;
|
|
623
|
+
}
|
|
259
624
|
function removeRuntimeState(configPath) {
|
|
625
|
+
removeRuntimeStateFile(runtimeStatePath(configPath));
|
|
626
|
+
}
|
|
627
|
+
function removeRuntimeStateFile(path) {
|
|
260
628
|
try {
|
|
261
|
-
unlinkSync(
|
|
629
|
+
unlinkSync(path);
|
|
262
630
|
}
|
|
263
631
|
catch (error) {
|
|
264
632
|
if (error.code !== "ENOENT")
|