@jmoyers/harness 0.1.8 → 0.1.10
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 +33 -155
- package/package.json +5 -1
- package/packages/harness-ai/src/anthropic-client.ts +99 -0
- package/packages/harness-ai/src/anthropic-protocol.ts +581 -0
- package/packages/harness-ai/src/anthropic-provider.ts +82 -0
- package/packages/harness-ai/src/async-iterable-stream.ts +65 -0
- package/packages/harness-ai/src/index.ts +36 -0
- package/packages/harness-ai/src/json-parse.ts +66 -0
- package/packages/harness-ai/src/sse.ts +80 -0
- package/packages/harness-ai/src/stream-object.ts +96 -0
- package/packages/harness-ai/src/stream-text.ts +1340 -0
- package/packages/harness-ai/src/types.ts +330 -0
- package/packages/harness-ai/src/ui-stream.ts +217 -0
- package/scripts/codex-live-mux-runtime.ts +123 -7
- package/scripts/control-plane-daemon.ts +20 -3
- package/scripts/harness.ts +566 -133
- package/src/cli/gateway-record.ts +16 -1
- package/src/control-plane/agent-realtime-api.ts +4 -0
- package/src/control-plane/prompt/agent-prompt-extractor.ts +191 -0
- package/src/control-plane/prompt/extractors/claude-prompt-extractor.ts +53 -0
- package/src/control-plane/prompt/extractors/codex-prompt-extractor.ts +50 -0
- package/src/control-plane/prompt/extractors/cursor-prompt-extractor.ts +56 -0
- package/src/control-plane/prompt/session-prompt-engine.ts +69 -0
- package/src/control-plane/prompt/thread-title-namer.ts +290 -0
- package/src/control-plane/stream-command-parser.ts +12 -0
- package/src/control-plane/stream-protocol.ts +109 -0
- package/src/control-plane/stream-server-command.ts +14 -0
- package/src/control-plane/stream-server-session-runtime.ts +12 -0
- package/src/control-plane/stream-server.ts +485 -19
- package/src/mux/input-shortcuts.ts +9 -0
- package/src/mux/live-mux/critique-review.ts +5 -1
- package/src/mux/live-mux/git-parsing.ts +24 -0
- package/src/mux/live-mux/global-shortcut-handlers.ts +8 -0
- package/src/mux/render-frame.ts +1 -1
- package/src/pty/pty_host.ts +46 -1
- package/src/services/control-plane.ts +22 -0
- package/src/services/runtime-control-actions.ts +69 -0
- package/src/services/runtime-navigation-input.ts +4 -0
- package/src/services/runtime-rail-input.ts +4 -0
- package/src/services/runtime-workspace-actions.ts +5 -0
- package/src/ui/global-shortcut-input.ts +2 -0
package/scripts/harness.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
normalizeGatewayHost,
|
|
27
27
|
normalizeGatewayPort,
|
|
28
28
|
normalizeGatewayStateDbPath,
|
|
29
|
+
resolveGatewayLockPath,
|
|
29
30
|
parseGatewayRecordText,
|
|
30
31
|
resolveGatewayLogPath,
|
|
31
32
|
resolveGatewayRecordPath,
|
|
@@ -78,6 +79,9 @@ const DEFAULT_GATEWAY_START_RETRY_WINDOW_MS = 6000;
|
|
|
78
79
|
const DEFAULT_GATEWAY_START_RETRY_DELAY_MS = 40;
|
|
79
80
|
const DEFAULT_GATEWAY_STOP_TIMEOUT_MS = 5000;
|
|
80
81
|
const DEFAULT_GATEWAY_STOP_POLL_MS = 50;
|
|
82
|
+
const DEFAULT_GATEWAY_LOCK_TIMEOUT_MS = 7000;
|
|
83
|
+
const DEFAULT_GATEWAY_LOCK_POLL_MS = 40;
|
|
84
|
+
const GATEWAY_LOCK_VERSION = 1;
|
|
81
85
|
const DEFAULT_PROFILE_ROOT_PATH = 'profiles';
|
|
82
86
|
const DEFAULT_SESSION_ROOT_PATH = 'sessions';
|
|
83
87
|
const PROFILE_STATE_FILE_NAME = 'active-profile.json';
|
|
@@ -176,6 +180,7 @@ interface RuntimeInspectOptions {
|
|
|
176
180
|
interface SessionPaths {
|
|
177
181
|
recordPath: string;
|
|
178
182
|
logPath: string;
|
|
183
|
+
lockPath: string;
|
|
179
184
|
defaultStateDbPath: string;
|
|
180
185
|
profileDir: string;
|
|
181
186
|
profileStatePath: string;
|
|
@@ -222,6 +227,33 @@ interface OrphanProcessCleanupResult {
|
|
|
222
227
|
errorMessage: string | null;
|
|
223
228
|
}
|
|
224
229
|
|
|
230
|
+
interface GatewayProcessIdentity {
|
|
231
|
+
pid: number;
|
|
232
|
+
startedAt: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface GatewayControlLockRecord {
|
|
236
|
+
version: number;
|
|
237
|
+
owner: GatewayProcessIdentity;
|
|
238
|
+
acquiredAt: string;
|
|
239
|
+
workspaceRoot: string;
|
|
240
|
+
token: string;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
interface GatewayControlLockHandle {
|
|
244
|
+
lockPath: string;
|
|
245
|
+
record: GatewayControlLockRecord;
|
|
246
|
+
release: () => void;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
interface ParsedGatewayDaemonEntry {
|
|
250
|
+
pid: number;
|
|
251
|
+
host: string;
|
|
252
|
+
port: number;
|
|
253
|
+
authToken: string | null;
|
|
254
|
+
stateDbPath: string;
|
|
255
|
+
}
|
|
256
|
+
|
|
225
257
|
interface ActiveProfileState {
|
|
226
258
|
version: number;
|
|
227
259
|
mode: typeof PROFILE_LIVE_INSPECT_MODE;
|
|
@@ -348,6 +380,7 @@ function resolveSessionPaths(
|
|
|
348
380
|
return {
|
|
349
381
|
recordPath: resolveGatewayRecordPath(invocationDirectory, process.env),
|
|
350
382
|
logPath: resolveGatewayLogPath(invocationDirectory, process.env),
|
|
383
|
+
lockPath: resolveGatewayLockPath(invocationDirectory, process.env),
|
|
351
384
|
defaultStateDbPath: resolveHarnessRuntimePath(
|
|
352
385
|
invocationDirectory,
|
|
353
386
|
DEFAULT_GATEWAY_DB_PATH,
|
|
@@ -365,6 +398,7 @@ function resolveSessionPaths(
|
|
|
365
398
|
return {
|
|
366
399
|
recordPath: resolve(sessionRoot, 'gateway.json'),
|
|
367
400
|
logPath: resolve(sessionRoot, 'gateway.log'),
|
|
401
|
+
lockPath: resolve(sessionRoot, 'gateway.lock'),
|
|
368
402
|
defaultStateDbPath: resolve(sessionRoot, 'control-plane.sqlite'),
|
|
369
403
|
profileDir: resolve(workspaceDirectory, DEFAULT_PROFILE_ROOT_PATH, sessionName),
|
|
370
404
|
profileStatePath: resolve(sessionRoot, PROFILE_STATE_FILE_NAME),
|
|
@@ -939,6 +973,210 @@ function removeGatewayRecord(recordPath: string): void {
|
|
|
939
973
|
}
|
|
940
974
|
}
|
|
941
975
|
|
|
976
|
+
function readProcessStartedAt(pid: number): string | null {
|
|
977
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
try {
|
|
981
|
+
const output = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
|
|
982
|
+
encoding: 'utf8',
|
|
983
|
+
}).trim();
|
|
984
|
+
return output.length > 0 ? output : null;
|
|
985
|
+
} catch {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function resolveCurrentProcessIdentity(): GatewayProcessIdentity {
|
|
991
|
+
const startedAt = readProcessStartedAt(process.pid);
|
|
992
|
+
if (startedAt === null) {
|
|
993
|
+
throw new Error(
|
|
994
|
+
`failed to resolve current process start timestamp for pid=${String(process.pid)}`,
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
return {
|
|
998
|
+
pid: process.pid,
|
|
999
|
+
startedAt,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function parseGatewayControlLockText(text: string): GatewayControlLockRecord | null {
|
|
1004
|
+
let parsed: unknown;
|
|
1005
|
+
try {
|
|
1006
|
+
parsed = JSON.parse(text);
|
|
1007
|
+
} catch {
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
const candidate = parsed as Record<string, unknown>;
|
|
1014
|
+
if (candidate['version'] !== GATEWAY_LOCK_VERSION) {
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
if (typeof candidate['acquiredAt'] !== 'string' || candidate['acquiredAt'].trim().length === 0) {
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
if (
|
|
1021
|
+
typeof candidate['workspaceRoot'] !== 'string' ||
|
|
1022
|
+
candidate['workspaceRoot'].trim().length === 0
|
|
1023
|
+
) {
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
if (typeof candidate['token'] !== 'string' || candidate['token'].trim().length === 0) {
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
const owner = candidate['owner'];
|
|
1030
|
+
if (typeof owner !== 'object' || owner === null || Array.isArray(owner)) {
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
const ownerRecord = owner as Record<string, unknown>;
|
|
1034
|
+
const pid = ownerRecord['pid'];
|
|
1035
|
+
const startedAt = ownerRecord['startedAt'];
|
|
1036
|
+
if (!Number.isInteger(pid) || (pid as number) <= 0) {
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
if (typeof startedAt !== 'string' || startedAt.trim().length === 0) {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
return {
|
|
1043
|
+
version: GATEWAY_LOCK_VERSION,
|
|
1044
|
+
owner: {
|
|
1045
|
+
pid: pid as number,
|
|
1046
|
+
startedAt,
|
|
1047
|
+
},
|
|
1048
|
+
acquiredAt: candidate['acquiredAt'] as string,
|
|
1049
|
+
workspaceRoot: candidate['workspaceRoot'] as string,
|
|
1050
|
+
token: candidate['token'] as string,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function readGatewayControlLock(lockPath: string): GatewayControlLockRecord | null {
|
|
1055
|
+
if (!existsSync(lockPath)) {
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
try {
|
|
1059
|
+
return parseGatewayControlLockText(readFileSync(lockPath, 'utf8'));
|
|
1060
|
+
} catch {
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function removeGatewayControlLock(lockPath: string): void {
|
|
1066
|
+
try {
|
|
1067
|
+
unlinkSync(lockPath);
|
|
1068
|
+
} catch (error: unknown) {
|
|
1069
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
1070
|
+
if (code !== 'ENOENT') {
|
|
1071
|
+
throw error;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function isGatewayControlLockOwnerAlive(record: GatewayControlLockRecord): boolean {
|
|
1077
|
+
if (!isPidRunning(record.owner.pid)) {
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
const startedAt = readProcessStartedAt(record.owner.pid);
|
|
1081
|
+
if (startedAt === null) {
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
return startedAt === record.owner.startedAt;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function createGatewayControlLockHandle(
|
|
1088
|
+
lockPath: string,
|
|
1089
|
+
record: GatewayControlLockRecord,
|
|
1090
|
+
): GatewayControlLockHandle {
|
|
1091
|
+
return {
|
|
1092
|
+
lockPath,
|
|
1093
|
+
record,
|
|
1094
|
+
release: () => {
|
|
1095
|
+
const current = readGatewayControlLock(lockPath);
|
|
1096
|
+
if (current === null) {
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (
|
|
1100
|
+
current.token !== record.token ||
|
|
1101
|
+
current.owner.pid !== record.owner.pid ||
|
|
1102
|
+
current.owner.startedAt !== record.owner.startedAt
|
|
1103
|
+
) {
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
removeGatewayControlLock(lockPath);
|
|
1107
|
+
},
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
async function acquireGatewayControlLock(
|
|
1112
|
+
lockPath: string,
|
|
1113
|
+
workspaceRoot: string,
|
|
1114
|
+
timeoutMs = DEFAULT_GATEWAY_LOCK_TIMEOUT_MS,
|
|
1115
|
+
): Promise<GatewayControlLockHandle> {
|
|
1116
|
+
const owner = resolveCurrentProcessIdentity();
|
|
1117
|
+
const deadlineMs = Date.now() + timeoutMs;
|
|
1118
|
+
const candidate: GatewayControlLockRecord = {
|
|
1119
|
+
version: GATEWAY_LOCK_VERSION,
|
|
1120
|
+
owner,
|
|
1121
|
+
acquiredAt: new Date().toISOString(),
|
|
1122
|
+
workspaceRoot,
|
|
1123
|
+
token: randomUUID(),
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
while (true) {
|
|
1127
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
1128
|
+
try {
|
|
1129
|
+
const fd = openSync(lockPath, 'wx');
|
|
1130
|
+
try {
|
|
1131
|
+
writeFileSync(fd, `${JSON.stringify(candidate, null, 2)}\n`, 'utf8');
|
|
1132
|
+
} finally {
|
|
1133
|
+
closeSync(fd);
|
|
1134
|
+
}
|
|
1135
|
+
return createGatewayControlLockHandle(lockPath, candidate);
|
|
1136
|
+
} catch (error: unknown) {
|
|
1137
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
1138
|
+
if (code !== 'EEXIST') {
|
|
1139
|
+
throw error;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const existing = readGatewayControlLock(lockPath);
|
|
1144
|
+
if (existing === null) {
|
|
1145
|
+
removeGatewayControlLock(lockPath);
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
if (existing.owner.pid === owner.pid && existing.owner.startedAt === owner.startedAt) {
|
|
1150
|
+
return createGatewayControlLockHandle(lockPath, existing);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
if (!isGatewayControlLockOwnerAlive(existing)) {
|
|
1154
|
+
removeGatewayControlLock(lockPath);
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
if (Date.now() >= deadlineMs) {
|
|
1159
|
+
throw new Error(
|
|
1160
|
+
`timed out waiting for gateway control lock: lockPath=${lockPath} ownerPid=${String(existing.owner.pid)} acquiredAt=${existing.acquiredAt}`,
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
await delay(DEFAULT_GATEWAY_LOCK_POLL_MS);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
async function withGatewayControlLock<T>(
|
|
1168
|
+
lockPath: string,
|
|
1169
|
+
workspaceRoot: string,
|
|
1170
|
+
operation: () => Promise<T>,
|
|
1171
|
+
): Promise<T> {
|
|
1172
|
+
const handle = await acquireGatewayControlLock(lockPath, workspaceRoot);
|
|
1173
|
+
try {
|
|
1174
|
+
return await operation();
|
|
1175
|
+
} finally {
|
|
1176
|
+
handle.release();
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
942
1180
|
function parseActiveProfileState(raw: unknown): ActiveProfileState | null {
|
|
943
1181
|
if (typeof raw !== 'object' || raw === null) {
|
|
944
1182
|
return null;
|
|
@@ -1176,6 +1414,72 @@ function readProcessTable(): readonly ProcessTableEntry[] {
|
|
|
1176
1414
|
return entries;
|
|
1177
1415
|
}
|
|
1178
1416
|
|
|
1417
|
+
function tokenizeProcessCommand(command: string): readonly string[] {
|
|
1418
|
+
const trimmed = command.trim();
|
|
1419
|
+
return trimmed.length === 0 ? [] : trimmed.split(/\s+/u);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function readCommandFlagValue(tokens: readonly string[], flag: string): string | null {
|
|
1423
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
1424
|
+
const token = tokens[index]!;
|
|
1425
|
+
if (token === flag) {
|
|
1426
|
+
const value = tokens[index + 1];
|
|
1427
|
+
return value === undefined ? null : value;
|
|
1428
|
+
}
|
|
1429
|
+
if (token.startsWith(`${flag}=`)) {
|
|
1430
|
+
const value = token.slice(flag.length + 1);
|
|
1431
|
+
return value.length === 0 ? null : value;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return null;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function parseGatewayDaemonProcessEntry(entry: ProcessTableEntry): ParsedGatewayDaemonEntry | null {
|
|
1438
|
+
if (!/\bcontrol-plane-daemon\.(?:ts|js)\b/u.test(entry.command)) {
|
|
1439
|
+
return null;
|
|
1440
|
+
}
|
|
1441
|
+
const tokens = tokenizeProcessCommand(entry.command);
|
|
1442
|
+
const host = readCommandFlagValue(tokens, '--host');
|
|
1443
|
+
const portRaw = readCommandFlagValue(tokens, '--port');
|
|
1444
|
+
const stateDbPath = readCommandFlagValue(tokens, '--state-db-path');
|
|
1445
|
+
const authToken = readCommandFlagValue(tokens, '--auth-token');
|
|
1446
|
+
if (host === null || portRaw === null || stateDbPath === null) {
|
|
1447
|
+
return null;
|
|
1448
|
+
}
|
|
1449
|
+
const port = Number.parseInt(portRaw, 10);
|
|
1450
|
+
if (!Number.isFinite(port) || !Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
1451
|
+
return null;
|
|
1452
|
+
}
|
|
1453
|
+
return {
|
|
1454
|
+
pid: entry.pid,
|
|
1455
|
+
host,
|
|
1456
|
+
port,
|
|
1457
|
+
authToken,
|
|
1458
|
+
stateDbPath: resolve(stateDbPath),
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function listGatewayDaemonProcesses(): readonly ParsedGatewayDaemonEntry[] {
|
|
1463
|
+
const parsed: ParsedGatewayDaemonEntry[] = [];
|
|
1464
|
+
for (const entry of readProcessTable()) {
|
|
1465
|
+
const daemon = parseGatewayDaemonProcessEntry(entry);
|
|
1466
|
+
if (daemon !== null) {
|
|
1467
|
+
parsed.push(daemon);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
return parsed;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function isPathWithinWorkspaceRuntimeScope(
|
|
1474
|
+
pathValue: string,
|
|
1475
|
+
invocationDirectory: string,
|
|
1476
|
+
): boolean {
|
|
1477
|
+
const runtimeRoot = resolveHarnessWorkspaceDirectory(invocationDirectory, process.env);
|
|
1478
|
+
const normalizedRoot = resolve(runtimeRoot);
|
|
1479
|
+
const normalizedPath = resolve(pathValue);
|
|
1480
|
+
return normalizedPath === normalizedRoot || normalizedPath.startsWith(`${normalizedRoot}/`);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1179
1483
|
function findOrphanSqlitePidsForDbPath(stateDbPath: string): readonly number[] {
|
|
1180
1484
|
const normalizedDbPath = resolve(stateDbPath);
|
|
1181
1485
|
return readProcessTable()
|
|
@@ -1398,10 +1702,9 @@ function resolveGatewaySettings(
|
|
|
1398
1702
|
const port = normalizeGatewayPort(
|
|
1399
1703
|
overrides.port ?? record?.port ?? env.HARNESS_CONTROL_PLANE_PORT,
|
|
1400
1704
|
);
|
|
1401
|
-
const
|
|
1402
|
-
overrides.stateDbPath ??
|
|
1403
|
-
|
|
1404
|
-
);
|
|
1705
|
+
const configuredStateDbPath =
|
|
1706
|
+
overrides.stateDbPath ?? env.HARNESS_CONTROL_PLANE_DB_PATH ?? defaultStateDbPath;
|
|
1707
|
+
const stateDbPathRaw = normalizeGatewayStateDbPath(configuredStateDbPath, defaultStateDbPath);
|
|
1405
1708
|
const stateDbPath = resolveHarnessRuntimePath(invocationDirectory, stateDbPathRaw, env);
|
|
1406
1709
|
|
|
1407
1710
|
const envToken =
|
|
@@ -1424,14 +1727,18 @@ function resolveGatewaySettings(
|
|
|
1424
1727
|
};
|
|
1425
1728
|
}
|
|
1426
1729
|
|
|
1427
|
-
async function
|
|
1730
|
+
async function probeGatewayEndpoint(
|
|
1731
|
+
host: string,
|
|
1732
|
+
port: number,
|
|
1733
|
+
authToken: string | null,
|
|
1734
|
+
): Promise<GatewayProbeResult> {
|
|
1428
1735
|
try {
|
|
1429
1736
|
const client = await connectControlPlaneStreamClient({
|
|
1430
|
-
host
|
|
1431
|
-
port
|
|
1432
|
-
...(
|
|
1737
|
+
host,
|
|
1738
|
+
port,
|
|
1739
|
+
...(authToken !== null
|
|
1433
1740
|
? {
|
|
1434
|
-
authToken
|
|
1741
|
+
authToken,
|
|
1435
1742
|
}
|
|
1436
1743
|
: {}),
|
|
1437
1744
|
});
|
|
@@ -1477,6 +1784,10 @@ async function probeGateway(record: GatewayRecord): Promise<GatewayProbeResult>
|
|
|
1477
1784
|
}
|
|
1478
1785
|
}
|
|
1479
1786
|
|
|
1787
|
+
async function probeGateway(record: GatewayRecord): Promise<GatewayProbeResult> {
|
|
1788
|
+
return await probeGatewayEndpoint(record.host, record.port, record.authToken);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1480
1791
|
async function waitForGatewayReady(record: GatewayRecord): Promise<void> {
|
|
1481
1792
|
const client = await connectControlPlaneStreamClient({
|
|
1482
1793
|
host: record.host,
|
|
@@ -1509,6 +1820,7 @@ async function startDetachedGateway(
|
|
|
1509
1820
|
): Promise<GatewayRecord> {
|
|
1510
1821
|
mkdirSync(dirname(logPath), { recursive: true });
|
|
1511
1822
|
const logFd = openSync(logPath, 'a');
|
|
1823
|
+
const gatewayRunId = randomUUID();
|
|
1512
1824
|
const daemonArgs = tsRuntimeArgs(
|
|
1513
1825
|
daemonScriptPath,
|
|
1514
1826
|
[
|
|
@@ -1530,6 +1842,7 @@ async function startDetachedGateway(
|
|
|
1530
1842
|
env: {
|
|
1531
1843
|
...process.env,
|
|
1532
1844
|
HARNESS_INVOKE_CWD: invocationDirectory,
|
|
1845
|
+
HARNESS_GATEWAY_RUN_ID: gatewayRunId,
|
|
1533
1846
|
},
|
|
1534
1847
|
});
|
|
1535
1848
|
closeSync(logFd);
|
|
@@ -1547,10 +1860,16 @@ async function startDetachedGateway(
|
|
|
1547
1860
|
stateDbPath: settings.stateDbPath,
|
|
1548
1861
|
startedAt: new Date().toISOString(),
|
|
1549
1862
|
workspaceRoot: invocationDirectory,
|
|
1863
|
+
gatewayRunId,
|
|
1550
1864
|
};
|
|
1551
1865
|
|
|
1552
1866
|
try {
|
|
1553
1867
|
await waitForGatewayReady(record);
|
|
1868
|
+
if (!isPidRunning(child.pid)) {
|
|
1869
|
+
throw new Error(
|
|
1870
|
+
`gateway daemon exited during startup (pid=${String(child.pid)}); possible duplicate start or port collision`,
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1554
1873
|
} catch (error: unknown) {
|
|
1555
1874
|
try {
|
|
1556
1875
|
process.kill(child.pid, 'SIGTERM');
|
|
@@ -1565,6 +1884,47 @@ async function startDetachedGateway(
|
|
|
1565
1884
|
return record;
|
|
1566
1885
|
}
|
|
1567
1886
|
|
|
1887
|
+
function authTokenMatches(
|
|
1888
|
+
candidate: ParsedGatewayDaemonEntry,
|
|
1889
|
+
expectedAuthToken: string | null,
|
|
1890
|
+
): boolean {
|
|
1891
|
+
if (expectedAuthToken === null) {
|
|
1892
|
+
return candidate.authToken === null;
|
|
1893
|
+
}
|
|
1894
|
+
return candidate.authToken === expectedAuthToken;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
function findReachableGatewayDaemonCandidates(
|
|
1898
|
+
invocationDirectory: string,
|
|
1899
|
+
settings: ResolvedGatewaySettings,
|
|
1900
|
+
): readonly ParsedGatewayDaemonEntry[] {
|
|
1901
|
+
return listGatewayDaemonProcesses().filter((candidate) => {
|
|
1902
|
+
if (candidate.host !== settings.host || candidate.port !== settings.port) {
|
|
1903
|
+
return false;
|
|
1904
|
+
}
|
|
1905
|
+
if (!authTokenMatches(candidate, settings.authToken)) {
|
|
1906
|
+
return false;
|
|
1907
|
+
}
|
|
1908
|
+
return isPathWithinWorkspaceRuntimeScope(candidate.stateDbPath, invocationDirectory);
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
function createAdoptedGatewayRecord(
|
|
1913
|
+
invocationDirectory: string,
|
|
1914
|
+
daemon: ParsedGatewayDaemonEntry,
|
|
1915
|
+
): GatewayRecord {
|
|
1916
|
+
return {
|
|
1917
|
+
version: GATEWAY_RECORD_VERSION,
|
|
1918
|
+
pid: daemon.pid,
|
|
1919
|
+
host: daemon.host,
|
|
1920
|
+
port: daemon.port,
|
|
1921
|
+
authToken: daemon.authToken,
|
|
1922
|
+
stateDbPath: daemon.stateDbPath,
|
|
1923
|
+
startedAt: new Date().toISOString(),
|
|
1924
|
+
workspaceRoot: invocationDirectory,
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1568
1928
|
async function ensureGatewayRunning(
|
|
1569
1929
|
invocationDirectory: string,
|
|
1570
1930
|
recordPath: string,
|
|
@@ -1598,6 +1958,33 @@ async function ensureGatewayRunning(
|
|
|
1598
1958
|
process.env,
|
|
1599
1959
|
defaultStateDbPath,
|
|
1600
1960
|
);
|
|
1961
|
+
if (existingRecord === null) {
|
|
1962
|
+
const endpointProbe = await probeGatewayEndpoint(
|
|
1963
|
+
settings.host,
|
|
1964
|
+
settings.port,
|
|
1965
|
+
settings.authToken,
|
|
1966
|
+
);
|
|
1967
|
+
if (endpointProbe.connected) {
|
|
1968
|
+
const candidates = findReachableGatewayDaemonCandidates(invocationDirectory, settings);
|
|
1969
|
+
if (candidates.length === 1) {
|
|
1970
|
+
const adopted = createAdoptedGatewayRecord(invocationDirectory, candidates[0]!);
|
|
1971
|
+
writeGatewayRecord(recordPath, adopted);
|
|
1972
|
+
return {
|
|
1973
|
+
record: adopted,
|
|
1974
|
+
started: false,
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
if (candidates.length > 1) {
|
|
1978
|
+
const pidList = candidates.map((candidate) => String(candidate.pid)).join(', ');
|
|
1979
|
+
throw new Error(
|
|
1980
|
+
`gateway endpoint reachable with multiple daemon candidates (${pidList}); stop with \`harness gateway stop --force\` and retry`,
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
throw new Error(
|
|
1984
|
+
'gateway endpoint is reachable but no matching daemon could be adopted; stop with `harness gateway stop --force` and retry',
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1601
1988
|
const record = await startDetachedGateway(
|
|
1602
1989
|
invocationDirectory,
|
|
1603
1990
|
recordPath,
|
|
@@ -1749,6 +2136,7 @@ async function runGatewayForeground(
|
|
|
1749
2136
|
settings: ResolvedGatewaySettings,
|
|
1750
2137
|
runtimeArgs: readonly string[] = [],
|
|
1751
2138
|
): Promise<number> {
|
|
2139
|
+
const gatewayRunId = randomUUID();
|
|
1752
2140
|
const existingRecord = readGatewayRecord(recordPath);
|
|
1753
2141
|
if (existingRecord !== null) {
|
|
1754
2142
|
const probe = await probeGateway(existingRecord);
|
|
@@ -1779,6 +2167,7 @@ async function runGatewayForeground(
|
|
|
1779
2167
|
env: {
|
|
1780
2168
|
...process.env,
|
|
1781
2169
|
HARNESS_INVOKE_CWD: invocationDirectory,
|
|
2170
|
+
HARNESS_GATEWAY_RUN_ID: gatewayRunId,
|
|
1782
2171
|
},
|
|
1783
2172
|
});
|
|
1784
2173
|
if (child.pid !== undefined) {
|
|
@@ -1791,6 +2180,7 @@ async function runGatewayForeground(
|
|
|
1791
2180
|
stateDbPath: settings.stateDbPath,
|
|
1792
2181
|
startedAt: new Date().toISOString(),
|
|
1793
2182
|
workspaceRoot: invocationDirectory,
|
|
2183
|
+
gatewayRunId,
|
|
1794
2184
|
});
|
|
1795
2185
|
}
|
|
1796
2186
|
|
|
@@ -1847,37 +2237,48 @@ async function runGatewayCommandEntry(
|
|
|
1847
2237
|
command: ParsedGatewayCommand,
|
|
1848
2238
|
invocationDirectory: string,
|
|
1849
2239
|
daemonScriptPath: string,
|
|
2240
|
+
lockPath: string,
|
|
1850
2241
|
recordPath: string,
|
|
1851
2242
|
logPath: string,
|
|
1852
2243
|
defaultStateDbPath: string,
|
|
1853
2244
|
runtimeOptions: RuntimeInspectOptions,
|
|
1854
2245
|
): Promise<number> {
|
|
2246
|
+
const withLock = async <T>(operation: () => Promise<T>): Promise<T> => {
|
|
2247
|
+
return await withGatewayControlLock(lockPath, invocationDirectory, operation);
|
|
2248
|
+
};
|
|
2249
|
+
|
|
1855
2250
|
if (command.type === 'status') {
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
2251
|
+
return await withLock(async () => {
|
|
2252
|
+
const record = readGatewayRecord(recordPath);
|
|
2253
|
+
if (record === null) {
|
|
2254
|
+
process.stdout.write('gateway status: stopped\n');
|
|
2255
|
+
return 0;
|
|
2256
|
+
}
|
|
2257
|
+
const pidRunning = isPidRunning(record.pid);
|
|
2258
|
+
const probe = await probeGateway(record);
|
|
2259
|
+
process.stdout.write(`gateway status: ${probe.connected ? 'running' : 'unreachable'}\n`);
|
|
2260
|
+
process.stdout.write(`record: ${recordPath}\n`);
|
|
2261
|
+
process.stdout.write(`lock: ${lockPath}\n`);
|
|
2262
|
+
process.stdout.write(
|
|
2263
|
+
`pid: ${String(record.pid)} (${pidRunning ? 'running' : 'not-running'})\n`,
|
|
2264
|
+
);
|
|
2265
|
+
process.stdout.write(`host: ${record.host}\n`);
|
|
2266
|
+
process.stdout.write(`port: ${String(record.port)}\n`);
|
|
2267
|
+
process.stdout.write(`auth: ${record.authToken === null ? 'off' : 'on'}\n`);
|
|
2268
|
+
process.stdout.write(`db: ${record.stateDbPath}\n`);
|
|
2269
|
+
process.stdout.write(`startedAt: ${record.startedAt}\n`);
|
|
2270
|
+
if (typeof record.gatewayRunId === 'string' && record.gatewayRunId.length > 0) {
|
|
2271
|
+
process.stdout.write(`runId: ${record.gatewayRunId}\n`);
|
|
2272
|
+
}
|
|
2273
|
+
process.stdout.write(
|
|
2274
|
+
`sessions: total=${String(probe.sessionCount)} live=${String(probe.liveSessionCount)}\n`,
|
|
2275
|
+
);
|
|
2276
|
+
if (!probe.connected) {
|
|
2277
|
+
process.stdout.write(`lastError: ${probe.error ?? 'unknown'}\n`);
|
|
2278
|
+
return 1;
|
|
2279
|
+
}
|
|
1859
2280
|
return 0;
|
|
1860
|
-
}
|
|
1861
|
-
const pidRunning = isPidRunning(record.pid);
|
|
1862
|
-
const probe = await probeGateway(record);
|
|
1863
|
-
process.stdout.write(`gateway status: ${probe.connected ? 'running' : 'unreachable'}\n`);
|
|
1864
|
-
process.stdout.write(`record: ${recordPath}\n`);
|
|
1865
|
-
process.stdout.write(
|
|
1866
|
-
`pid: ${String(record.pid)} (${pidRunning ? 'running' : 'not-running'})\n`,
|
|
1867
|
-
);
|
|
1868
|
-
process.stdout.write(`host: ${record.host}\n`);
|
|
1869
|
-
process.stdout.write(`port: ${String(record.port)}\n`);
|
|
1870
|
-
process.stdout.write(`auth: ${record.authToken === null ? 'off' : 'on'}\n`);
|
|
1871
|
-
process.stdout.write(`db: ${record.stateDbPath}\n`);
|
|
1872
|
-
process.stdout.write(`startedAt: ${record.startedAt}\n`);
|
|
1873
|
-
process.stdout.write(
|
|
1874
|
-
`sessions: total=${String(probe.sessionCount)} live=${String(probe.liveSessionCount)}\n`,
|
|
1875
|
-
);
|
|
1876
|
-
if (!probe.connected) {
|
|
1877
|
-
process.stdout.write(`lastError: ${probe.error ?? 'unknown'}\n`);
|
|
1878
|
-
return 1;
|
|
1879
|
-
}
|
|
1880
|
-
return 0;
|
|
2281
|
+
});
|
|
1881
2282
|
}
|
|
1882
2283
|
|
|
1883
2284
|
if (command.type === 'stop') {
|
|
@@ -1886,26 +2287,32 @@ async function runGatewayCommandEntry(
|
|
|
1886
2287
|
timeoutMs: DEFAULT_GATEWAY_STOP_TIMEOUT_MS,
|
|
1887
2288
|
cleanupOrphans: true,
|
|
1888
2289
|
};
|
|
1889
|
-
const stopped = await
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
2290
|
+
const stopped = await withLock(
|
|
2291
|
+
async () =>
|
|
2292
|
+
await stopGateway(
|
|
2293
|
+
invocationDirectory,
|
|
2294
|
+
daemonScriptPath,
|
|
2295
|
+
recordPath,
|
|
2296
|
+
defaultStateDbPath,
|
|
2297
|
+
stopOptions,
|
|
2298
|
+
),
|
|
1895
2299
|
);
|
|
1896
2300
|
process.stdout.write(`${stopped.message}\n`);
|
|
1897
2301
|
return stopped.stopped ? 0 : 1;
|
|
1898
2302
|
}
|
|
1899
2303
|
|
|
1900
2304
|
if (command.type === 'start') {
|
|
1901
|
-
const ensured = await
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
2305
|
+
const ensured = await withLock(
|
|
2306
|
+
async () =>
|
|
2307
|
+
await ensureGatewayRunning(
|
|
2308
|
+
invocationDirectory,
|
|
2309
|
+
recordPath,
|
|
2310
|
+
logPath,
|
|
2311
|
+
daemonScriptPath,
|
|
2312
|
+
defaultStateDbPath,
|
|
2313
|
+
command.startOptions ?? {},
|
|
2314
|
+
runtimeOptions.gatewayRuntimeArgs,
|
|
2315
|
+
),
|
|
1909
2316
|
);
|
|
1910
2317
|
if (ensured.started) {
|
|
1911
2318
|
process.stdout.write(
|
|
@@ -1918,61 +2325,66 @@ async function runGatewayCommandEntry(
|
|
|
1918
2325
|
}
|
|
1919
2326
|
process.stdout.write(`record: ${recordPath}\n`);
|
|
1920
2327
|
process.stdout.write(`log: ${logPath}\n`);
|
|
2328
|
+
process.stdout.write(`lock: ${lockPath}\n`);
|
|
1921
2329
|
return 0;
|
|
1922
2330
|
}
|
|
1923
2331
|
|
|
1924
2332
|
if (command.type === 'restart') {
|
|
1925
|
-
const stopResult = await
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
timeoutMs: DEFAULT_GATEWAY_STOP_TIMEOUT_MS,
|
|
1933
|
-
cleanupOrphans: true,
|
|
1934
|
-
},
|
|
2333
|
+
const stopResult = await withLock(
|
|
2334
|
+
async () =>
|
|
2335
|
+
await stopGateway(invocationDirectory, daemonScriptPath, recordPath, defaultStateDbPath, {
|
|
2336
|
+
force: true,
|
|
2337
|
+
timeoutMs: DEFAULT_GATEWAY_STOP_TIMEOUT_MS,
|
|
2338
|
+
cleanupOrphans: true,
|
|
2339
|
+
}),
|
|
1935
2340
|
);
|
|
1936
2341
|
process.stdout.write(`${stopResult.message}\n`);
|
|
1937
|
-
const ensured = await
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
2342
|
+
const ensured = await withLock(
|
|
2343
|
+
async () =>
|
|
2344
|
+
await ensureGatewayRunning(
|
|
2345
|
+
invocationDirectory,
|
|
2346
|
+
recordPath,
|
|
2347
|
+
logPath,
|
|
2348
|
+
daemonScriptPath,
|
|
2349
|
+
defaultStateDbPath,
|
|
2350
|
+
command.startOptions ?? {},
|
|
2351
|
+
runtimeOptions.gatewayRuntimeArgs,
|
|
2352
|
+
),
|
|
1945
2353
|
);
|
|
1946
2354
|
process.stdout.write(
|
|
1947
2355
|
`gateway restarted pid=${String(ensured.record.pid)} host=${ensured.record.host} port=${String(ensured.record.port)}\n`,
|
|
1948
2356
|
);
|
|
1949
2357
|
process.stdout.write(`record: ${recordPath}\n`);
|
|
1950
2358
|
process.stdout.write(`log: ${logPath}\n`);
|
|
2359
|
+
process.stdout.write(`lock: ${lockPath}\n`);
|
|
1951
2360
|
return 0;
|
|
1952
2361
|
}
|
|
1953
2362
|
|
|
1954
2363
|
if (command.type === 'run') {
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2364
|
+
return await withLock(async () => {
|
|
2365
|
+
const existingRecord = readGatewayRecord(recordPath);
|
|
2366
|
+
const settings = resolveGatewaySettings(
|
|
2367
|
+
invocationDirectory,
|
|
2368
|
+
existingRecord,
|
|
2369
|
+
command.startOptions ?? {},
|
|
2370
|
+
process.env,
|
|
2371
|
+
defaultStateDbPath,
|
|
2372
|
+
);
|
|
2373
|
+
process.stdout.write(
|
|
2374
|
+
`gateway foreground run host=${settings.host} port=${String(settings.port)} db=${settings.stateDbPath}\n`,
|
|
2375
|
+
);
|
|
2376
|
+
process.stdout.write(`lock: ${lockPath}\n`);
|
|
2377
|
+
return await runGatewayForeground(
|
|
2378
|
+
daemonScriptPath,
|
|
2379
|
+
invocationDirectory,
|
|
2380
|
+
recordPath,
|
|
2381
|
+
settings,
|
|
2382
|
+
runtimeOptions.gatewayRuntimeArgs,
|
|
2383
|
+
);
|
|
2384
|
+
});
|
|
1973
2385
|
}
|
|
1974
2386
|
|
|
1975
|
-
const record = readGatewayRecord(recordPath);
|
|
2387
|
+
const record = await withLock(async () => readGatewayRecord(recordPath));
|
|
1976
2388
|
if (record === null) {
|
|
1977
2389
|
throw new Error('gateway not running; start it first');
|
|
1978
2390
|
}
|
|
@@ -1986,6 +2398,7 @@ async function runDefaultClient(
|
|
|
1986
2398
|
invocationDirectory: string,
|
|
1987
2399
|
daemonScriptPath: string,
|
|
1988
2400
|
muxScriptPath: string,
|
|
2401
|
+
lockPath: string,
|
|
1989
2402
|
recordPath: string,
|
|
1990
2403
|
logPath: string,
|
|
1991
2404
|
defaultStateDbPath: string,
|
|
@@ -1993,14 +2406,19 @@ async function runDefaultClient(
|
|
|
1993
2406
|
sessionName: string | null,
|
|
1994
2407
|
runtimeOptions: RuntimeInspectOptions,
|
|
1995
2408
|
): Promise<number> {
|
|
1996
|
-
const ensured = await
|
|
2409
|
+
const ensured = await withGatewayControlLock(
|
|
2410
|
+
lockPath,
|
|
1997
2411
|
invocationDirectory,
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2412
|
+
async () =>
|
|
2413
|
+
await ensureGatewayRunning(
|
|
2414
|
+
invocationDirectory,
|
|
2415
|
+
recordPath,
|
|
2416
|
+
logPath,
|
|
2417
|
+
daemonScriptPath,
|
|
2418
|
+
defaultStateDbPath,
|
|
2419
|
+
{},
|
|
2420
|
+
runtimeOptions.gatewayRuntimeArgs,
|
|
2421
|
+
),
|
|
2004
2422
|
);
|
|
2005
2423
|
if (ensured.started) {
|
|
2006
2424
|
process.stdout.write(
|
|
@@ -2047,41 +2465,49 @@ async function runProfileRun(
|
|
|
2047
2465
|
removeActiveProfileState(sessionPaths.profileStatePath);
|
|
2048
2466
|
}
|
|
2049
2467
|
|
|
2050
|
-
const
|
|
2051
|
-
|
|
2052
|
-
const existingProbe = await probeGateway(existingRecord);
|
|
2053
|
-
if (existingProbe.connected || isPidRunning(existingRecord.pid)) {
|
|
2054
|
-
throw new Error('profile command requires the target session gateway to be stopped first');
|
|
2055
|
-
}
|
|
2056
|
-
removeGatewayRecord(sessionPaths.recordPath);
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
const host = normalizeGatewayHost(process.env.HARNESS_CONTROL_PLANE_HOST);
|
|
2060
|
-
const reservedPort = await reservePort(host);
|
|
2061
|
-
const settings = resolveGatewaySettings(
|
|
2468
|
+
const gateway = await withGatewayControlLock(
|
|
2469
|
+
sessionPaths.lockPath,
|
|
2062
2470
|
invocationDirectory,
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2471
|
+
async () => {
|
|
2472
|
+
const existingRecord = readGatewayRecord(sessionPaths.recordPath);
|
|
2473
|
+
if (existingRecord !== null) {
|
|
2474
|
+
const existingProbe = await probeGateway(existingRecord);
|
|
2475
|
+
if (existingProbe.connected || isPidRunning(existingRecord.pid)) {
|
|
2476
|
+
throw new Error(
|
|
2477
|
+
'profile command requires the target session gateway to be stopped first',
|
|
2478
|
+
);
|
|
2479
|
+
}
|
|
2480
|
+
removeGatewayRecord(sessionPaths.recordPath);
|
|
2481
|
+
}
|
|
2071
2482
|
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2483
|
+
const host = normalizeGatewayHost(process.env.HARNESS_CONTROL_PLANE_HOST);
|
|
2484
|
+
const reservedPort = await reservePort(host);
|
|
2485
|
+
const settings = resolveGatewaySettings(
|
|
2486
|
+
invocationDirectory,
|
|
2487
|
+
null,
|
|
2488
|
+
{
|
|
2489
|
+
port: reservedPort,
|
|
2490
|
+
stateDbPath: sessionPaths.defaultStateDbPath,
|
|
2491
|
+
},
|
|
2492
|
+
process.env,
|
|
2493
|
+
sessionPaths.defaultStateDbPath,
|
|
2494
|
+
);
|
|
2495
|
+
|
|
2496
|
+
return await startDetachedGateway(
|
|
2497
|
+
invocationDirectory,
|
|
2498
|
+
sessionPaths.recordPath,
|
|
2499
|
+
sessionPaths.logPath,
|
|
2500
|
+
settings,
|
|
2501
|
+
daemonScriptPath,
|
|
2502
|
+
[
|
|
2503
|
+
...runtimeOptions.gatewayRuntimeArgs,
|
|
2504
|
+
...buildCpuProfileRuntimeArgs({
|
|
2505
|
+
cpuProfileDir: profileDir,
|
|
2506
|
+
cpuProfileName: PROFILE_GATEWAY_FILE_NAME,
|
|
2507
|
+
}),
|
|
2508
|
+
],
|
|
2509
|
+
);
|
|
2510
|
+
},
|
|
2085
2511
|
);
|
|
2086
2512
|
|
|
2087
2513
|
let clientExitCode = 1;
|
|
@@ -2105,16 +2531,21 @@ async function runProfileRun(
|
|
|
2105
2531
|
clientError = error instanceof Error ? error : new Error(String(error));
|
|
2106
2532
|
}
|
|
2107
2533
|
|
|
2108
|
-
const stopped = await
|
|
2534
|
+
const stopped = await withGatewayControlLock(
|
|
2535
|
+
sessionPaths.lockPath,
|
|
2109
2536
|
invocationDirectory,
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2537
|
+
async () =>
|
|
2538
|
+
await stopGateway(
|
|
2539
|
+
invocationDirectory,
|
|
2540
|
+
daemonScriptPath,
|
|
2541
|
+
sessionPaths.recordPath,
|
|
2542
|
+
sessionPaths.defaultStateDbPath,
|
|
2543
|
+
{
|
|
2544
|
+
force: true,
|
|
2545
|
+
timeoutMs: DEFAULT_GATEWAY_STOP_TIMEOUT_MS,
|
|
2546
|
+
cleanupOrphans: true,
|
|
2547
|
+
},
|
|
2548
|
+
),
|
|
2118
2549
|
);
|
|
2119
2550
|
process.stdout.write(`${stopped.message}\n`);
|
|
2120
2551
|
if (!stopped.stopped) {
|
|
@@ -2519,6 +2950,7 @@ async function main(): Promise<number> {
|
|
|
2519
2950
|
command,
|
|
2520
2951
|
invocationDirectory,
|
|
2521
2952
|
daemonScriptPath,
|
|
2953
|
+
sessionPaths.lockPath,
|
|
2522
2954
|
sessionPaths.recordPath,
|
|
2523
2955
|
sessionPaths.logPath,
|
|
2524
2956
|
sessionPaths.defaultStateDbPath,
|
|
@@ -2577,6 +3009,7 @@ async function main(): Promise<number> {
|
|
|
2577
3009
|
invocationDirectory,
|
|
2578
3010
|
daemonScriptPath,
|
|
2579
3011
|
muxScriptPath,
|
|
3012
|
+
sessionPaths.lockPath,
|
|
2580
3013
|
sessionPaths.recordPath,
|
|
2581
3014
|
sessionPaths.logPath,
|
|
2582
3015
|
sessionPaths.defaultStateDbPath,
|