@jmoyers/harness 0.1.9 → 0.1.11
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 +36 -155
- package/package.json +3 -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 +265 -14
- package/scripts/control-plane-daemon.ts +33 -5
- package/scripts/harness.ts +579 -134
- package/src/cli/default-gateway-pointer.ts +193 -0
- package/src/cli/gateway-record.ts +16 -1
- package/src/config/config-core.ts +13 -2
- package/src/config/harness-paths.ts +4 -7
- package/src/config/harness-runtime-migration.ts +142 -19
- package/src/config/secrets-core.ts +92 -4
- package/src/control-plane/prompt/thread-title-namer.ts +316 -0
- package/src/control-plane/stream-command-parser.ts +12 -0
- package/src/control-plane/stream-protocol.ts +6 -0
- package/src/control-plane/stream-server-background.ts +18 -2
- package/src/control-plane/stream-server-command.ts +14 -0
- package/src/control-plane/stream-server.ts +460 -28
- package/src/domain/conversations.ts +11 -7
- package/src/domain/workspace.ts +9 -0
- package/src/mux/input-shortcuts.ts +38 -1
- package/src/mux/live-mux/git-parsing.ts +40 -0
- package/src/mux/live-mux/global-shortcut-handlers.ts +8 -0
- package/src/mux/live-mux/left-rail-conversation-click.ts +6 -3
- package/src/mux/live-mux/modal-input-reducers.ts +34 -1
- package/src/mux/live-mux/modal-overlays.ts +45 -0
- package/src/mux/live-mux/modal-prompt-handlers.ts +85 -0
- package/src/mux/render-frame.ts +1 -1
- package/src/mux/task-screen-keybindings.ts +29 -1
- package/src/services/control-plane.ts +22 -0
- package/src/services/runtime-control-actions.ts +69 -0
- package/src/services/runtime-conversation-activation.ts +25 -0
- package/src/services/runtime-conversation-starter.ts +31 -7
- package/src/services/runtime-input-router.ts +6 -0
- package/src/services/runtime-modal-input.ts +18 -0
- package/src/services/runtime-navigation-input.ts +4 -0
- package/src/services/runtime-rail-input.ts +5 -0
- package/src/services/runtime-repository-actions.ts +2 -0
- package/src/services/runtime-workspace-actions.ts +5 -0
- package/src/store/control-plane-store.ts +36 -0
- package/src/store/event-store.ts +36 -0
- package/src/ui/global-shortcut-input.ts +2 -0
- package/src/ui/input.ts +31 -0
- package/src/ui/modals/manager.ts +26 -0
package/scripts/harness.ts
CHANGED
|
@@ -19,6 +19,10 @@ import { connectControlPlaneStreamClient } from '../src/control-plane/stream-cli
|
|
|
19
19
|
import { parseStreamCommand } from '../src/control-plane/stream-command-parser.ts';
|
|
20
20
|
import type { StreamCommand } from '../src/control-plane/stream-protocol.ts';
|
|
21
21
|
import { runHarnessAnimate } from './harness-animate.ts';
|
|
22
|
+
import {
|
|
23
|
+
clearDefaultGatewayPointerForRecordPath,
|
|
24
|
+
writeDefaultGatewayPointerFromGatewayRecord,
|
|
25
|
+
} from '../src/cli/default-gateway-pointer.ts';
|
|
22
26
|
import {
|
|
23
27
|
GATEWAY_RECORD_VERSION,
|
|
24
28
|
DEFAULT_GATEWAY_DB_PATH,
|
|
@@ -26,6 +30,7 @@ import {
|
|
|
26
30
|
normalizeGatewayHost,
|
|
27
31
|
normalizeGatewayPort,
|
|
28
32
|
normalizeGatewayStateDbPath,
|
|
33
|
+
resolveGatewayLockPath,
|
|
29
34
|
parseGatewayRecordText,
|
|
30
35
|
resolveGatewayLogPath,
|
|
31
36
|
resolveGatewayRecordPath,
|
|
@@ -78,6 +83,9 @@ const DEFAULT_GATEWAY_START_RETRY_WINDOW_MS = 6000;
|
|
|
78
83
|
const DEFAULT_GATEWAY_START_RETRY_DELAY_MS = 40;
|
|
79
84
|
const DEFAULT_GATEWAY_STOP_TIMEOUT_MS = 5000;
|
|
80
85
|
const DEFAULT_GATEWAY_STOP_POLL_MS = 50;
|
|
86
|
+
const DEFAULT_GATEWAY_LOCK_TIMEOUT_MS = 7000;
|
|
87
|
+
const DEFAULT_GATEWAY_LOCK_POLL_MS = 40;
|
|
88
|
+
const GATEWAY_LOCK_VERSION = 1;
|
|
81
89
|
const DEFAULT_PROFILE_ROOT_PATH = 'profiles';
|
|
82
90
|
const DEFAULT_SESSION_ROOT_PATH = 'sessions';
|
|
83
91
|
const PROFILE_STATE_FILE_NAME = 'active-profile.json';
|
|
@@ -176,6 +184,7 @@ interface RuntimeInspectOptions {
|
|
|
176
184
|
interface SessionPaths {
|
|
177
185
|
recordPath: string;
|
|
178
186
|
logPath: string;
|
|
187
|
+
lockPath: string;
|
|
179
188
|
defaultStateDbPath: string;
|
|
180
189
|
profileDir: string;
|
|
181
190
|
profileStatePath: string;
|
|
@@ -222,6 +231,33 @@ interface OrphanProcessCleanupResult {
|
|
|
222
231
|
errorMessage: string | null;
|
|
223
232
|
}
|
|
224
233
|
|
|
234
|
+
interface GatewayProcessIdentity {
|
|
235
|
+
pid: number;
|
|
236
|
+
startedAt: string;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interface GatewayControlLockRecord {
|
|
240
|
+
version: number;
|
|
241
|
+
owner: GatewayProcessIdentity;
|
|
242
|
+
acquiredAt: string;
|
|
243
|
+
workspaceRoot: string;
|
|
244
|
+
token: string;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
interface GatewayControlLockHandle {
|
|
248
|
+
lockPath: string;
|
|
249
|
+
record: GatewayControlLockRecord;
|
|
250
|
+
release: () => void;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
interface ParsedGatewayDaemonEntry {
|
|
254
|
+
pid: number;
|
|
255
|
+
host: string;
|
|
256
|
+
port: number;
|
|
257
|
+
authToken: string | null;
|
|
258
|
+
stateDbPath: string;
|
|
259
|
+
}
|
|
260
|
+
|
|
225
261
|
interface ActiveProfileState {
|
|
226
262
|
version: number;
|
|
227
263
|
mode: typeof PROFILE_LIVE_INSPECT_MODE;
|
|
@@ -348,6 +384,7 @@ function resolveSessionPaths(
|
|
|
348
384
|
return {
|
|
349
385
|
recordPath: resolveGatewayRecordPath(invocationDirectory, process.env),
|
|
350
386
|
logPath: resolveGatewayLogPath(invocationDirectory, process.env),
|
|
387
|
+
lockPath: resolveGatewayLockPath(invocationDirectory, process.env),
|
|
351
388
|
defaultStateDbPath: resolveHarnessRuntimePath(
|
|
352
389
|
invocationDirectory,
|
|
353
390
|
DEFAULT_GATEWAY_DB_PATH,
|
|
@@ -365,6 +402,7 @@ function resolveSessionPaths(
|
|
|
365
402
|
return {
|
|
366
403
|
recordPath: resolve(sessionRoot, 'gateway.json'),
|
|
367
404
|
logPath: resolve(sessionRoot, 'gateway.log'),
|
|
405
|
+
lockPath: resolve(sessionRoot, 'gateway.lock'),
|
|
368
406
|
defaultStateDbPath: resolve(sessionRoot, 'control-plane.sqlite'),
|
|
369
407
|
profileDir: resolve(workspaceDirectory, DEFAULT_PROFILE_ROOT_PATH, sessionName),
|
|
370
408
|
profileStatePath: resolve(sessionRoot, PROFILE_STATE_FILE_NAME),
|
|
@@ -926,6 +964,7 @@ function writeTextFileAtomically(filePath: string, text: string): void {
|
|
|
926
964
|
|
|
927
965
|
function writeGatewayRecord(recordPath: string, record: GatewayRecord): void {
|
|
928
966
|
writeTextFileAtomically(recordPath, serializeGatewayRecord(record));
|
|
967
|
+
writeDefaultGatewayPointerFromGatewayRecord(recordPath, record, process.env);
|
|
929
968
|
}
|
|
930
969
|
|
|
931
970
|
function removeGatewayRecord(recordPath: string): void {
|
|
@@ -937,6 +976,211 @@ function removeGatewayRecord(recordPath: string): void {
|
|
|
937
976
|
throw error;
|
|
938
977
|
}
|
|
939
978
|
}
|
|
979
|
+
clearDefaultGatewayPointerForRecordPath(recordPath, process.cwd(), process.env);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function readProcessStartedAt(pid: number): string | null {
|
|
983
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
try {
|
|
987
|
+
const output = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
|
|
988
|
+
encoding: 'utf8',
|
|
989
|
+
}).trim();
|
|
990
|
+
return output.length > 0 ? output : null;
|
|
991
|
+
} catch {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function resolveCurrentProcessIdentity(): GatewayProcessIdentity {
|
|
997
|
+
const startedAt = readProcessStartedAt(process.pid);
|
|
998
|
+
if (startedAt === null) {
|
|
999
|
+
throw new Error(
|
|
1000
|
+
`failed to resolve current process start timestamp for pid=${String(process.pid)}`,
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
pid: process.pid,
|
|
1005
|
+
startedAt,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function parseGatewayControlLockText(text: string): GatewayControlLockRecord | null {
|
|
1010
|
+
let parsed: unknown;
|
|
1011
|
+
try {
|
|
1012
|
+
parsed = JSON.parse(text);
|
|
1013
|
+
} catch {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
const candidate = parsed as Record<string, unknown>;
|
|
1020
|
+
if (candidate['version'] !== GATEWAY_LOCK_VERSION) {
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
if (typeof candidate['acquiredAt'] !== 'string' || candidate['acquiredAt'].trim().length === 0) {
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
if (
|
|
1027
|
+
typeof candidate['workspaceRoot'] !== 'string' ||
|
|
1028
|
+
candidate['workspaceRoot'].trim().length === 0
|
|
1029
|
+
) {
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
if (typeof candidate['token'] !== 'string' || candidate['token'].trim().length === 0) {
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
const owner = candidate['owner'];
|
|
1036
|
+
if (typeof owner !== 'object' || owner === null || Array.isArray(owner)) {
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
const ownerRecord = owner as Record<string, unknown>;
|
|
1040
|
+
const pid = ownerRecord['pid'];
|
|
1041
|
+
const startedAt = ownerRecord['startedAt'];
|
|
1042
|
+
if (!Number.isInteger(pid) || (pid as number) <= 0) {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
if (typeof startedAt !== 'string' || startedAt.trim().length === 0) {
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
return {
|
|
1049
|
+
version: GATEWAY_LOCK_VERSION,
|
|
1050
|
+
owner: {
|
|
1051
|
+
pid: pid as number,
|
|
1052
|
+
startedAt,
|
|
1053
|
+
},
|
|
1054
|
+
acquiredAt: candidate['acquiredAt'] as string,
|
|
1055
|
+
workspaceRoot: candidate['workspaceRoot'] as string,
|
|
1056
|
+
token: candidate['token'] as string,
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function readGatewayControlLock(lockPath: string): GatewayControlLockRecord | null {
|
|
1061
|
+
if (!existsSync(lockPath)) {
|
|
1062
|
+
return null;
|
|
1063
|
+
}
|
|
1064
|
+
try {
|
|
1065
|
+
return parseGatewayControlLockText(readFileSync(lockPath, 'utf8'));
|
|
1066
|
+
} catch {
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function removeGatewayControlLock(lockPath: string): void {
|
|
1072
|
+
try {
|
|
1073
|
+
unlinkSync(lockPath);
|
|
1074
|
+
} catch (error: unknown) {
|
|
1075
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
1076
|
+
if (code !== 'ENOENT') {
|
|
1077
|
+
throw error;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function isGatewayControlLockOwnerAlive(record: GatewayControlLockRecord): boolean {
|
|
1083
|
+
if (!isPidRunning(record.owner.pid)) {
|
|
1084
|
+
return false;
|
|
1085
|
+
}
|
|
1086
|
+
const startedAt = readProcessStartedAt(record.owner.pid);
|
|
1087
|
+
if (startedAt === null) {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
return startedAt === record.owner.startedAt;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function createGatewayControlLockHandle(
|
|
1094
|
+
lockPath: string,
|
|
1095
|
+
record: GatewayControlLockRecord,
|
|
1096
|
+
): GatewayControlLockHandle {
|
|
1097
|
+
return {
|
|
1098
|
+
lockPath,
|
|
1099
|
+
record,
|
|
1100
|
+
release: () => {
|
|
1101
|
+
const current = readGatewayControlLock(lockPath);
|
|
1102
|
+
if (current === null) {
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (
|
|
1106
|
+
current.token !== record.token ||
|
|
1107
|
+
current.owner.pid !== record.owner.pid ||
|
|
1108
|
+
current.owner.startedAt !== record.owner.startedAt
|
|
1109
|
+
) {
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
removeGatewayControlLock(lockPath);
|
|
1113
|
+
},
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
async function acquireGatewayControlLock(
|
|
1118
|
+
lockPath: string,
|
|
1119
|
+
workspaceRoot: string,
|
|
1120
|
+
timeoutMs = DEFAULT_GATEWAY_LOCK_TIMEOUT_MS,
|
|
1121
|
+
): Promise<GatewayControlLockHandle> {
|
|
1122
|
+
const owner = resolveCurrentProcessIdentity();
|
|
1123
|
+
const deadlineMs = Date.now() + timeoutMs;
|
|
1124
|
+
const candidate: GatewayControlLockRecord = {
|
|
1125
|
+
version: GATEWAY_LOCK_VERSION,
|
|
1126
|
+
owner,
|
|
1127
|
+
acquiredAt: new Date().toISOString(),
|
|
1128
|
+
workspaceRoot,
|
|
1129
|
+
token: randomUUID(),
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
while (true) {
|
|
1133
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
1134
|
+
try {
|
|
1135
|
+
const fd = openSync(lockPath, 'wx');
|
|
1136
|
+
try {
|
|
1137
|
+
writeFileSync(fd, `${JSON.stringify(candidate, null, 2)}\n`, 'utf8');
|
|
1138
|
+
} finally {
|
|
1139
|
+
closeSync(fd);
|
|
1140
|
+
}
|
|
1141
|
+
return createGatewayControlLockHandle(lockPath, candidate);
|
|
1142
|
+
} catch (error: unknown) {
|
|
1143
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
1144
|
+
if (code !== 'EEXIST') {
|
|
1145
|
+
throw error;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const existing = readGatewayControlLock(lockPath);
|
|
1150
|
+
if (existing === null) {
|
|
1151
|
+
removeGatewayControlLock(lockPath);
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (existing.owner.pid === owner.pid && existing.owner.startedAt === owner.startedAt) {
|
|
1156
|
+
return createGatewayControlLockHandle(lockPath, existing);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (!isGatewayControlLockOwnerAlive(existing)) {
|
|
1160
|
+
removeGatewayControlLock(lockPath);
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
if (Date.now() >= deadlineMs) {
|
|
1165
|
+
throw new Error(
|
|
1166
|
+
`timed out waiting for gateway control lock: lockPath=${lockPath} ownerPid=${String(existing.owner.pid)} acquiredAt=${existing.acquiredAt}`,
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
await delay(DEFAULT_GATEWAY_LOCK_POLL_MS);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
async function withGatewayControlLock<T>(
|
|
1174
|
+
lockPath: string,
|
|
1175
|
+
workspaceRoot: string,
|
|
1176
|
+
operation: () => Promise<T>,
|
|
1177
|
+
): Promise<T> {
|
|
1178
|
+
const handle = await acquireGatewayControlLock(lockPath, workspaceRoot);
|
|
1179
|
+
try {
|
|
1180
|
+
return await operation();
|
|
1181
|
+
} finally {
|
|
1182
|
+
handle.release();
|
|
1183
|
+
}
|
|
940
1184
|
}
|
|
941
1185
|
|
|
942
1186
|
function parseActiveProfileState(raw: unknown): ActiveProfileState | null {
|
|
@@ -1176,6 +1420,73 @@ function readProcessTable(): readonly ProcessTableEntry[] {
|
|
|
1176
1420
|
return entries;
|
|
1177
1421
|
}
|
|
1178
1422
|
|
|
1423
|
+
function tokenizeProcessCommand(command: string): readonly string[] {
|
|
1424
|
+
const trimmed = command.trim();
|
|
1425
|
+
return trimmed.length === 0 ? [] : trimmed.split(/\s+/u);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function readCommandFlagValue(tokens: readonly string[], flag: string): string | null {
|
|
1429
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
1430
|
+
const token = tokens[index]!;
|
|
1431
|
+
if (token === flag) {
|
|
1432
|
+
const value = tokens[index + 1];
|
|
1433
|
+
return value === undefined ? null : value;
|
|
1434
|
+
}
|
|
1435
|
+
if (token.startsWith(`${flag}=`)) {
|
|
1436
|
+
const value = token.slice(flag.length + 1);
|
|
1437
|
+
return value.length === 0 ? null : value;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return null;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function parseGatewayDaemonProcessEntry(entry: ProcessTableEntry): ParsedGatewayDaemonEntry | null {
|
|
1444
|
+
if (!/\bcontrol-plane-daemon\.(?:ts|js)\b/u.test(entry.command)) {
|
|
1445
|
+
return null;
|
|
1446
|
+
}
|
|
1447
|
+
const tokens = tokenizeProcessCommand(entry.command);
|
|
1448
|
+
const host = readCommandFlagValue(tokens, '--host');
|
|
1449
|
+
const portRaw = readCommandFlagValue(tokens, '--port');
|
|
1450
|
+
const stateDbPath = readCommandFlagValue(tokens, '--state-db-path');
|
|
1451
|
+
const authToken = readCommandFlagValue(tokens, '--auth-token');
|
|
1452
|
+
if (host === null || portRaw === null || stateDbPath === null) {
|
|
1453
|
+
return null;
|
|
1454
|
+
}
|
|
1455
|
+
const port = Number.parseInt(portRaw, 10);
|
|
1456
|
+
if (!Number.isFinite(port) || !Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
return {
|
|
1460
|
+
pid: entry.pid,
|
|
1461
|
+
host,
|
|
1462
|
+
port,
|
|
1463
|
+
authToken,
|
|
1464
|
+
stateDbPath: resolve(stateDbPath),
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
function listGatewayDaemonProcesses(): readonly ParsedGatewayDaemonEntry[] {
|
|
1469
|
+
const parsed: ParsedGatewayDaemonEntry[] = [];
|
|
1470
|
+
for (const entry of readProcessTable()) {
|
|
1471
|
+
const daemon = parseGatewayDaemonProcessEntry(entry);
|
|
1472
|
+
if (daemon !== null) {
|
|
1473
|
+
parsed.push(daemon);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return parsed;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function isPathWithinWorkspaceRuntimeScope(
|
|
1480
|
+
pathValue: string,
|
|
1481
|
+
invocationDirectory: string,
|
|
1482
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
1483
|
+
): boolean {
|
|
1484
|
+
const runtimeRoot = resolveHarnessWorkspaceDirectory(invocationDirectory, env);
|
|
1485
|
+
const normalizedRoot = resolve(runtimeRoot);
|
|
1486
|
+
const normalizedPath = resolve(pathValue);
|
|
1487
|
+
return normalizedPath === normalizedRoot || normalizedPath.startsWith(`${normalizedRoot}/`);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1179
1490
|
function findOrphanSqlitePidsForDbPath(stateDbPath: string): readonly number[] {
|
|
1180
1491
|
const normalizedDbPath = resolve(stateDbPath);
|
|
1181
1492
|
return readProcessTable()
|
|
@@ -1398,11 +1709,15 @@ function resolveGatewaySettings(
|
|
|
1398
1709
|
const port = normalizeGatewayPort(
|
|
1399
1710
|
overrides.port ?? record?.port ?? env.HARNESS_CONTROL_PLANE_PORT,
|
|
1400
1711
|
);
|
|
1401
|
-
const
|
|
1402
|
-
|
|
1403
|
-
defaultStateDbPath,
|
|
1404
|
-
);
|
|
1712
|
+
const configuredStateDbPath = overrides.stateDbPath ?? defaultStateDbPath;
|
|
1713
|
+
const stateDbPathRaw = normalizeGatewayStateDbPath(configuredStateDbPath, defaultStateDbPath);
|
|
1405
1714
|
const stateDbPath = resolveHarnessRuntimePath(invocationDirectory, stateDbPathRaw, env);
|
|
1715
|
+
if (!isPathWithinWorkspaceRuntimeScope(stateDbPath, invocationDirectory, env)) {
|
|
1716
|
+
const runtimeRoot = resolveHarnessWorkspaceDirectory(invocationDirectory, env);
|
|
1717
|
+
throw new Error(
|
|
1718
|
+
`invalid --state-db-path: ${stateDbPath}. state db path must be under workspace runtime root ${runtimeRoot}`,
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1406
1721
|
|
|
1407
1722
|
const envToken =
|
|
1408
1723
|
typeof env.HARNESS_CONTROL_PLANE_AUTH_TOKEN === 'string' &&
|
|
@@ -1424,14 +1739,18 @@ function resolveGatewaySettings(
|
|
|
1424
1739
|
};
|
|
1425
1740
|
}
|
|
1426
1741
|
|
|
1427
|
-
async function
|
|
1742
|
+
async function probeGatewayEndpoint(
|
|
1743
|
+
host: string,
|
|
1744
|
+
port: number,
|
|
1745
|
+
authToken: string | null,
|
|
1746
|
+
): Promise<GatewayProbeResult> {
|
|
1428
1747
|
try {
|
|
1429
1748
|
const client = await connectControlPlaneStreamClient({
|
|
1430
|
-
host
|
|
1431
|
-
port
|
|
1432
|
-
...(
|
|
1749
|
+
host,
|
|
1750
|
+
port,
|
|
1751
|
+
...(authToken !== null
|
|
1433
1752
|
? {
|
|
1434
|
-
authToken
|
|
1753
|
+
authToken,
|
|
1435
1754
|
}
|
|
1436
1755
|
: {}),
|
|
1437
1756
|
});
|
|
@@ -1477,6 +1796,10 @@ async function probeGateway(record: GatewayRecord): Promise<GatewayProbeResult>
|
|
|
1477
1796
|
}
|
|
1478
1797
|
}
|
|
1479
1798
|
|
|
1799
|
+
async function probeGateway(record: GatewayRecord): Promise<GatewayProbeResult> {
|
|
1800
|
+
return await probeGatewayEndpoint(record.host, record.port, record.authToken);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1480
1803
|
async function waitForGatewayReady(record: GatewayRecord): Promise<void> {
|
|
1481
1804
|
const client = await connectControlPlaneStreamClient({
|
|
1482
1805
|
host: record.host,
|
|
@@ -1509,6 +1832,7 @@ async function startDetachedGateway(
|
|
|
1509
1832
|
): Promise<GatewayRecord> {
|
|
1510
1833
|
mkdirSync(dirname(logPath), { recursive: true });
|
|
1511
1834
|
const logFd = openSync(logPath, 'a');
|
|
1835
|
+
const gatewayRunId = randomUUID();
|
|
1512
1836
|
const daemonArgs = tsRuntimeArgs(
|
|
1513
1837
|
daemonScriptPath,
|
|
1514
1838
|
[
|
|
@@ -1530,6 +1854,7 @@ async function startDetachedGateway(
|
|
|
1530
1854
|
env: {
|
|
1531
1855
|
...process.env,
|
|
1532
1856
|
HARNESS_INVOKE_CWD: invocationDirectory,
|
|
1857
|
+
HARNESS_GATEWAY_RUN_ID: gatewayRunId,
|
|
1533
1858
|
},
|
|
1534
1859
|
});
|
|
1535
1860
|
closeSync(logFd);
|
|
@@ -1547,10 +1872,16 @@ async function startDetachedGateway(
|
|
|
1547
1872
|
stateDbPath: settings.stateDbPath,
|
|
1548
1873
|
startedAt: new Date().toISOString(),
|
|
1549
1874
|
workspaceRoot: invocationDirectory,
|
|
1875
|
+
gatewayRunId,
|
|
1550
1876
|
};
|
|
1551
1877
|
|
|
1552
1878
|
try {
|
|
1553
1879
|
await waitForGatewayReady(record);
|
|
1880
|
+
if (!isPidRunning(child.pid)) {
|
|
1881
|
+
throw new Error(
|
|
1882
|
+
`gateway daemon exited during startup (pid=${String(child.pid)}); possible duplicate start or port collision`,
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1554
1885
|
} catch (error: unknown) {
|
|
1555
1886
|
try {
|
|
1556
1887
|
process.kill(child.pid, 'SIGTERM');
|
|
@@ -1565,6 +1896,47 @@ async function startDetachedGateway(
|
|
|
1565
1896
|
return record;
|
|
1566
1897
|
}
|
|
1567
1898
|
|
|
1899
|
+
function authTokenMatches(
|
|
1900
|
+
candidate: ParsedGatewayDaemonEntry,
|
|
1901
|
+
expectedAuthToken: string | null,
|
|
1902
|
+
): boolean {
|
|
1903
|
+
if (expectedAuthToken === null) {
|
|
1904
|
+
return candidate.authToken === null;
|
|
1905
|
+
}
|
|
1906
|
+
return candidate.authToken === expectedAuthToken;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function findReachableGatewayDaemonCandidates(
|
|
1910
|
+
invocationDirectory: string,
|
|
1911
|
+
settings: ResolvedGatewaySettings,
|
|
1912
|
+
): readonly ParsedGatewayDaemonEntry[] {
|
|
1913
|
+
return listGatewayDaemonProcesses().filter((candidate) => {
|
|
1914
|
+
if (candidate.host !== settings.host || candidate.port !== settings.port) {
|
|
1915
|
+
return false;
|
|
1916
|
+
}
|
|
1917
|
+
if (!authTokenMatches(candidate, settings.authToken)) {
|
|
1918
|
+
return false;
|
|
1919
|
+
}
|
|
1920
|
+
return isPathWithinWorkspaceRuntimeScope(candidate.stateDbPath, invocationDirectory);
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
function createAdoptedGatewayRecord(
|
|
1925
|
+
invocationDirectory: string,
|
|
1926
|
+
daemon: ParsedGatewayDaemonEntry,
|
|
1927
|
+
): GatewayRecord {
|
|
1928
|
+
return {
|
|
1929
|
+
version: GATEWAY_RECORD_VERSION,
|
|
1930
|
+
pid: daemon.pid,
|
|
1931
|
+
host: daemon.host,
|
|
1932
|
+
port: daemon.port,
|
|
1933
|
+
authToken: daemon.authToken,
|
|
1934
|
+
stateDbPath: daemon.stateDbPath,
|
|
1935
|
+
startedAt: new Date().toISOString(),
|
|
1936
|
+
workspaceRoot: invocationDirectory,
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1568
1940
|
async function ensureGatewayRunning(
|
|
1569
1941
|
invocationDirectory: string,
|
|
1570
1942
|
recordPath: string,
|
|
@@ -1598,6 +1970,33 @@ async function ensureGatewayRunning(
|
|
|
1598
1970
|
process.env,
|
|
1599
1971
|
defaultStateDbPath,
|
|
1600
1972
|
);
|
|
1973
|
+
if (existingRecord === null) {
|
|
1974
|
+
const endpointProbe = await probeGatewayEndpoint(
|
|
1975
|
+
settings.host,
|
|
1976
|
+
settings.port,
|
|
1977
|
+
settings.authToken,
|
|
1978
|
+
);
|
|
1979
|
+
if (endpointProbe.connected) {
|
|
1980
|
+
const candidates = findReachableGatewayDaemonCandidates(invocationDirectory, settings);
|
|
1981
|
+
if (candidates.length === 1) {
|
|
1982
|
+
const adopted = createAdoptedGatewayRecord(invocationDirectory, candidates[0]!);
|
|
1983
|
+
writeGatewayRecord(recordPath, adopted);
|
|
1984
|
+
return {
|
|
1985
|
+
record: adopted,
|
|
1986
|
+
started: false,
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
if (candidates.length > 1) {
|
|
1990
|
+
const pidList = candidates.map((candidate) => String(candidate.pid)).join(', ');
|
|
1991
|
+
throw new Error(
|
|
1992
|
+
`gateway endpoint reachable with multiple daemon candidates (${pidList}); stop with \`harness gateway stop --force\` and retry`,
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
throw new Error(
|
|
1996
|
+
'gateway endpoint is reachable but no matching daemon could be adopted; stop with `harness gateway stop --force` and retry',
|
|
1997
|
+
);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
1601
2000
|
const record = await startDetachedGateway(
|
|
1602
2001
|
invocationDirectory,
|
|
1603
2002
|
recordPath,
|
|
@@ -1749,6 +2148,7 @@ async function runGatewayForeground(
|
|
|
1749
2148
|
settings: ResolvedGatewaySettings,
|
|
1750
2149
|
runtimeArgs: readonly string[] = [],
|
|
1751
2150
|
): Promise<number> {
|
|
2151
|
+
const gatewayRunId = randomUUID();
|
|
1752
2152
|
const existingRecord = readGatewayRecord(recordPath);
|
|
1753
2153
|
if (existingRecord !== null) {
|
|
1754
2154
|
const probe = await probeGateway(existingRecord);
|
|
@@ -1779,6 +2179,7 @@ async function runGatewayForeground(
|
|
|
1779
2179
|
env: {
|
|
1780
2180
|
...process.env,
|
|
1781
2181
|
HARNESS_INVOKE_CWD: invocationDirectory,
|
|
2182
|
+
HARNESS_GATEWAY_RUN_ID: gatewayRunId,
|
|
1782
2183
|
},
|
|
1783
2184
|
});
|
|
1784
2185
|
if (child.pid !== undefined) {
|
|
@@ -1791,6 +2192,7 @@ async function runGatewayForeground(
|
|
|
1791
2192
|
stateDbPath: settings.stateDbPath,
|
|
1792
2193
|
startedAt: new Date().toISOString(),
|
|
1793
2194
|
workspaceRoot: invocationDirectory,
|
|
2195
|
+
gatewayRunId,
|
|
1794
2196
|
});
|
|
1795
2197
|
}
|
|
1796
2198
|
|
|
@@ -1847,37 +2249,48 @@ async function runGatewayCommandEntry(
|
|
|
1847
2249
|
command: ParsedGatewayCommand,
|
|
1848
2250
|
invocationDirectory: string,
|
|
1849
2251
|
daemonScriptPath: string,
|
|
2252
|
+
lockPath: string,
|
|
1850
2253
|
recordPath: string,
|
|
1851
2254
|
logPath: string,
|
|
1852
2255
|
defaultStateDbPath: string,
|
|
1853
2256
|
runtimeOptions: RuntimeInspectOptions,
|
|
1854
2257
|
): Promise<number> {
|
|
2258
|
+
const withLock = async <T>(operation: () => Promise<T>): Promise<T> => {
|
|
2259
|
+
return await withGatewayControlLock(lockPath, invocationDirectory, operation);
|
|
2260
|
+
};
|
|
2261
|
+
|
|
1855
2262
|
if (command.type === 'status') {
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
2263
|
+
return await withLock(async () => {
|
|
2264
|
+
const record = readGatewayRecord(recordPath);
|
|
2265
|
+
if (record === null) {
|
|
2266
|
+
process.stdout.write('gateway status: stopped\n');
|
|
2267
|
+
return 0;
|
|
2268
|
+
}
|
|
2269
|
+
const pidRunning = isPidRunning(record.pid);
|
|
2270
|
+
const probe = await probeGateway(record);
|
|
2271
|
+
process.stdout.write(`gateway status: ${probe.connected ? 'running' : 'unreachable'}\n`);
|
|
2272
|
+
process.stdout.write(`record: ${recordPath}\n`);
|
|
2273
|
+
process.stdout.write(`lock: ${lockPath}\n`);
|
|
2274
|
+
process.stdout.write(
|
|
2275
|
+
`pid: ${String(record.pid)} (${pidRunning ? 'running' : 'not-running'})\n`,
|
|
2276
|
+
);
|
|
2277
|
+
process.stdout.write(`host: ${record.host}\n`);
|
|
2278
|
+
process.stdout.write(`port: ${String(record.port)}\n`);
|
|
2279
|
+
process.stdout.write(`auth: ${record.authToken === null ? 'off' : 'on'}\n`);
|
|
2280
|
+
process.stdout.write(`db: ${record.stateDbPath}\n`);
|
|
2281
|
+
process.stdout.write(`startedAt: ${record.startedAt}\n`);
|
|
2282
|
+
if (typeof record.gatewayRunId === 'string' && record.gatewayRunId.length > 0) {
|
|
2283
|
+
process.stdout.write(`runId: ${record.gatewayRunId}\n`);
|
|
2284
|
+
}
|
|
2285
|
+
process.stdout.write(
|
|
2286
|
+
`sessions: total=${String(probe.sessionCount)} live=${String(probe.liveSessionCount)}\n`,
|
|
2287
|
+
);
|
|
2288
|
+
if (!probe.connected) {
|
|
2289
|
+
process.stdout.write(`lastError: ${probe.error ?? 'unknown'}\n`);
|
|
2290
|
+
return 1;
|
|
2291
|
+
}
|
|
1859
2292
|
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;
|
|
2293
|
+
});
|
|
1881
2294
|
}
|
|
1882
2295
|
|
|
1883
2296
|
if (command.type === 'stop') {
|
|
@@ -1886,26 +2299,32 @@ async function runGatewayCommandEntry(
|
|
|
1886
2299
|
timeoutMs: DEFAULT_GATEWAY_STOP_TIMEOUT_MS,
|
|
1887
2300
|
cleanupOrphans: true,
|
|
1888
2301
|
};
|
|
1889
|
-
const stopped = await
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
2302
|
+
const stopped = await withLock(
|
|
2303
|
+
async () =>
|
|
2304
|
+
await stopGateway(
|
|
2305
|
+
invocationDirectory,
|
|
2306
|
+
daemonScriptPath,
|
|
2307
|
+
recordPath,
|
|
2308
|
+
defaultStateDbPath,
|
|
2309
|
+
stopOptions,
|
|
2310
|
+
),
|
|
1895
2311
|
);
|
|
1896
2312
|
process.stdout.write(`${stopped.message}\n`);
|
|
1897
2313
|
return stopped.stopped ? 0 : 1;
|
|
1898
2314
|
}
|
|
1899
2315
|
|
|
1900
2316
|
if (command.type === 'start') {
|
|
1901
|
-
const ensured = await
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
2317
|
+
const ensured = await withLock(
|
|
2318
|
+
async () =>
|
|
2319
|
+
await ensureGatewayRunning(
|
|
2320
|
+
invocationDirectory,
|
|
2321
|
+
recordPath,
|
|
2322
|
+
logPath,
|
|
2323
|
+
daemonScriptPath,
|
|
2324
|
+
defaultStateDbPath,
|
|
2325
|
+
command.startOptions ?? {},
|
|
2326
|
+
runtimeOptions.gatewayRuntimeArgs,
|
|
2327
|
+
),
|
|
1909
2328
|
);
|
|
1910
2329
|
if (ensured.started) {
|
|
1911
2330
|
process.stdout.write(
|
|
@@ -1918,61 +2337,66 @@ async function runGatewayCommandEntry(
|
|
|
1918
2337
|
}
|
|
1919
2338
|
process.stdout.write(`record: ${recordPath}\n`);
|
|
1920
2339
|
process.stdout.write(`log: ${logPath}\n`);
|
|
2340
|
+
process.stdout.write(`lock: ${lockPath}\n`);
|
|
1921
2341
|
return 0;
|
|
1922
2342
|
}
|
|
1923
2343
|
|
|
1924
2344
|
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
|
-
},
|
|
2345
|
+
const stopResult = await withLock(
|
|
2346
|
+
async () =>
|
|
2347
|
+
await stopGateway(invocationDirectory, daemonScriptPath, recordPath, defaultStateDbPath, {
|
|
2348
|
+
force: true,
|
|
2349
|
+
timeoutMs: DEFAULT_GATEWAY_STOP_TIMEOUT_MS,
|
|
2350
|
+
cleanupOrphans: true,
|
|
2351
|
+
}),
|
|
1935
2352
|
);
|
|
1936
2353
|
process.stdout.write(`${stopResult.message}\n`);
|
|
1937
|
-
const ensured = await
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
2354
|
+
const ensured = await withLock(
|
|
2355
|
+
async () =>
|
|
2356
|
+
await ensureGatewayRunning(
|
|
2357
|
+
invocationDirectory,
|
|
2358
|
+
recordPath,
|
|
2359
|
+
logPath,
|
|
2360
|
+
daemonScriptPath,
|
|
2361
|
+
defaultStateDbPath,
|
|
2362
|
+
command.startOptions ?? {},
|
|
2363
|
+
runtimeOptions.gatewayRuntimeArgs,
|
|
2364
|
+
),
|
|
1945
2365
|
);
|
|
1946
2366
|
process.stdout.write(
|
|
1947
2367
|
`gateway restarted pid=${String(ensured.record.pid)} host=${ensured.record.host} port=${String(ensured.record.port)}\n`,
|
|
1948
2368
|
);
|
|
1949
2369
|
process.stdout.write(`record: ${recordPath}\n`);
|
|
1950
2370
|
process.stdout.write(`log: ${logPath}\n`);
|
|
2371
|
+
process.stdout.write(`lock: ${lockPath}\n`);
|
|
1951
2372
|
return 0;
|
|
1952
2373
|
}
|
|
1953
2374
|
|
|
1954
2375
|
if (command.type === 'run') {
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2376
|
+
return await withLock(async () => {
|
|
2377
|
+
const existingRecord = readGatewayRecord(recordPath);
|
|
2378
|
+
const settings = resolveGatewaySettings(
|
|
2379
|
+
invocationDirectory,
|
|
2380
|
+
existingRecord,
|
|
2381
|
+
command.startOptions ?? {},
|
|
2382
|
+
process.env,
|
|
2383
|
+
defaultStateDbPath,
|
|
2384
|
+
);
|
|
2385
|
+
process.stdout.write(
|
|
2386
|
+
`gateway foreground run host=${settings.host} port=${String(settings.port)} db=${settings.stateDbPath}\n`,
|
|
2387
|
+
);
|
|
2388
|
+
process.stdout.write(`lock: ${lockPath}\n`);
|
|
2389
|
+
return await runGatewayForeground(
|
|
2390
|
+
daemonScriptPath,
|
|
2391
|
+
invocationDirectory,
|
|
2392
|
+
recordPath,
|
|
2393
|
+
settings,
|
|
2394
|
+
runtimeOptions.gatewayRuntimeArgs,
|
|
2395
|
+
);
|
|
2396
|
+
});
|
|
1973
2397
|
}
|
|
1974
2398
|
|
|
1975
|
-
const record = readGatewayRecord(recordPath);
|
|
2399
|
+
const record = await withLock(async () => readGatewayRecord(recordPath));
|
|
1976
2400
|
if (record === null) {
|
|
1977
2401
|
throw new Error('gateway not running; start it first');
|
|
1978
2402
|
}
|
|
@@ -1986,6 +2410,7 @@ async function runDefaultClient(
|
|
|
1986
2410
|
invocationDirectory: string,
|
|
1987
2411
|
daemonScriptPath: string,
|
|
1988
2412
|
muxScriptPath: string,
|
|
2413
|
+
lockPath: string,
|
|
1989
2414
|
recordPath: string,
|
|
1990
2415
|
logPath: string,
|
|
1991
2416
|
defaultStateDbPath: string,
|
|
@@ -1993,14 +2418,19 @@ async function runDefaultClient(
|
|
|
1993
2418
|
sessionName: string | null,
|
|
1994
2419
|
runtimeOptions: RuntimeInspectOptions,
|
|
1995
2420
|
): Promise<number> {
|
|
1996
|
-
const ensured = await
|
|
2421
|
+
const ensured = await withGatewayControlLock(
|
|
2422
|
+
lockPath,
|
|
1997
2423
|
invocationDirectory,
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2424
|
+
async () =>
|
|
2425
|
+
await ensureGatewayRunning(
|
|
2426
|
+
invocationDirectory,
|
|
2427
|
+
recordPath,
|
|
2428
|
+
logPath,
|
|
2429
|
+
daemonScriptPath,
|
|
2430
|
+
defaultStateDbPath,
|
|
2431
|
+
{},
|
|
2432
|
+
runtimeOptions.gatewayRuntimeArgs,
|
|
2433
|
+
),
|
|
2004
2434
|
);
|
|
2005
2435
|
if (ensured.started) {
|
|
2006
2436
|
process.stdout.write(
|
|
@@ -2047,41 +2477,49 @@ async function runProfileRun(
|
|
|
2047
2477
|
removeActiveProfileState(sessionPaths.profileStatePath);
|
|
2048
2478
|
}
|
|
2049
2479
|
|
|
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(
|
|
2480
|
+
const gateway = await withGatewayControlLock(
|
|
2481
|
+
sessionPaths.lockPath,
|
|
2062
2482
|
invocationDirectory,
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2483
|
+
async () => {
|
|
2484
|
+
const existingRecord = readGatewayRecord(sessionPaths.recordPath);
|
|
2485
|
+
if (existingRecord !== null) {
|
|
2486
|
+
const existingProbe = await probeGateway(existingRecord);
|
|
2487
|
+
if (existingProbe.connected || isPidRunning(existingRecord.pid)) {
|
|
2488
|
+
throw new Error(
|
|
2489
|
+
'profile command requires the target session gateway to be stopped first',
|
|
2490
|
+
);
|
|
2491
|
+
}
|
|
2492
|
+
removeGatewayRecord(sessionPaths.recordPath);
|
|
2493
|
+
}
|
|
2071
2494
|
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2495
|
+
const host = normalizeGatewayHost(process.env.HARNESS_CONTROL_PLANE_HOST);
|
|
2496
|
+
const reservedPort = await reservePort(host);
|
|
2497
|
+
const settings = resolveGatewaySettings(
|
|
2498
|
+
invocationDirectory,
|
|
2499
|
+
null,
|
|
2500
|
+
{
|
|
2501
|
+
port: reservedPort,
|
|
2502
|
+
stateDbPath: sessionPaths.defaultStateDbPath,
|
|
2503
|
+
},
|
|
2504
|
+
process.env,
|
|
2505
|
+
sessionPaths.defaultStateDbPath,
|
|
2506
|
+
);
|
|
2507
|
+
|
|
2508
|
+
return await startDetachedGateway(
|
|
2509
|
+
invocationDirectory,
|
|
2510
|
+
sessionPaths.recordPath,
|
|
2511
|
+
sessionPaths.logPath,
|
|
2512
|
+
settings,
|
|
2513
|
+
daemonScriptPath,
|
|
2514
|
+
[
|
|
2515
|
+
...runtimeOptions.gatewayRuntimeArgs,
|
|
2516
|
+
...buildCpuProfileRuntimeArgs({
|
|
2517
|
+
cpuProfileDir: profileDir,
|
|
2518
|
+
cpuProfileName: PROFILE_GATEWAY_FILE_NAME,
|
|
2519
|
+
}),
|
|
2520
|
+
],
|
|
2521
|
+
);
|
|
2522
|
+
},
|
|
2085
2523
|
);
|
|
2086
2524
|
|
|
2087
2525
|
let clientExitCode = 1;
|
|
@@ -2105,16 +2543,21 @@ async function runProfileRun(
|
|
|
2105
2543
|
clientError = error instanceof Error ? error : new Error(String(error));
|
|
2106
2544
|
}
|
|
2107
2545
|
|
|
2108
|
-
const stopped = await
|
|
2546
|
+
const stopped = await withGatewayControlLock(
|
|
2547
|
+
sessionPaths.lockPath,
|
|
2109
2548
|
invocationDirectory,
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2549
|
+
async () =>
|
|
2550
|
+
await stopGateway(
|
|
2551
|
+
invocationDirectory,
|
|
2552
|
+
daemonScriptPath,
|
|
2553
|
+
sessionPaths.recordPath,
|
|
2554
|
+
sessionPaths.defaultStateDbPath,
|
|
2555
|
+
{
|
|
2556
|
+
force: true,
|
|
2557
|
+
timeoutMs: DEFAULT_GATEWAY_STOP_TIMEOUT_MS,
|
|
2558
|
+
cleanupOrphans: true,
|
|
2559
|
+
},
|
|
2560
|
+
),
|
|
2118
2561
|
);
|
|
2119
2562
|
process.stdout.write(`${stopped.message}\n`);
|
|
2120
2563
|
if (!stopped.stopped) {
|
|
@@ -2485,7 +2928,7 @@ async function main(): Promise<number> {
|
|
|
2485
2928
|
const migration = migrateLegacyHarnessLayout(invocationDirectory, process.env);
|
|
2486
2929
|
if (migration.migrated) {
|
|
2487
2930
|
process.stdout.write(
|
|
2488
|
-
`[migration] local .harness migrated to global runtime layout (${String(migration.migratedEntries)} entries, configCopied=${String(migration.configCopied)}, secretsCopied=${String(migration.secretsCopied)})\n`,
|
|
2931
|
+
`[migration] local .harness migrated to global runtime layout (${String(migration.migratedEntries)} entries, configCopied=${String(migration.configCopied)}, secretsCopied=${String(migration.secretsCopied)}, legacyRootRemoved=${String(migration.legacyRootRemoved)})\n`,
|
|
2489
2932
|
);
|
|
2490
2933
|
}
|
|
2491
2934
|
loadHarnessSecrets({ cwd: invocationDirectory });
|
|
@@ -2519,6 +2962,7 @@ async function main(): Promise<number> {
|
|
|
2519
2962
|
command,
|
|
2520
2963
|
invocationDirectory,
|
|
2521
2964
|
daemonScriptPath,
|
|
2965
|
+
sessionPaths.lockPath,
|
|
2522
2966
|
sessionPaths.recordPath,
|
|
2523
2967
|
sessionPaths.logPath,
|
|
2524
2968
|
sessionPaths.defaultStateDbPath,
|
|
@@ -2577,6 +3021,7 @@ async function main(): Promise<number> {
|
|
|
2577
3021
|
invocationDirectory,
|
|
2578
3022
|
daemonScriptPath,
|
|
2579
3023
|
muxScriptPath,
|
|
3024
|
+
sessionPaths.lockPath,
|
|
2580
3025
|
sessionPaths.recordPath,
|
|
2581
3026
|
sessionPaths.logPath,
|
|
2582
3027
|
sessionPaths.defaultStateDbPath,
|