@openscout/scout 0.2.72 → 0.2.74
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/LICENSE +202 -0
- package/README.md +41 -7
- package/bin/scout +34 -0
- package/bin/scout.mjs +195 -18
- package/bin/scoutd +0 -0
- package/dist/build-manifest.json +9 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/RepoDiffViewer-CTXJvjIL.js +56 -0
- package/dist/client/assets/{addon-fit-C5ueiijZ.js → addon-fit-3IrvLm2n.js} +1 -1
- package/dist/client/assets/{addon-webgl-Cm7sl8RO.js → addon-webgl-BgxIDUI-.js} +1 -1
- package/dist/client/assets/arc.es-Bh8P_xre.js +188 -0
- package/dist/client/assets/c-BIGW1oBm.js +1 -0
- package/dist/client/assets/cpp-DIPi6g--.js +1 -0
- package/dist/client/assets/css-CLj8gQPS.js +1 -0
- package/dist/client/assets/dart-bE4Kk8sk.js +1 -0
- package/dist/client/assets/docker-BcOcwvcX.js +1 -0
- package/dist/client/assets/embed-entry-wAEtYUEM.js +1 -0
- package/dist/client/assets/go-C27-OAKa.js +1 -0
- package/dist/client/assets/html-pp8916En.js +1 -0
- package/dist/client/assets/index-BK3mRYyN.js +272 -0
- package/dist/client/assets/index-Dm1ZQS9G.css +1 -0
- package/dist/client/assets/java-CylS5w8V.js +1 -0
- package/dist/client/assets/javascript-wDzz0qaB.js +1 -0
- package/dist/client/assets/json-Cp-IABpG.js +1 -0
- package/dist/client/assets/jsx-g9-lgVsj.js +1 -0
- package/dist/client/assets/kotlin-BdnUsdx6.js +1 -0
- package/dist/client/assets/make-CHLpvVh8.js +1 -0
- package/dist/client/assets/markdown-Cvjx9yec.js +1 -0
- package/dist/client/assets/objective-c-DXmwc3jG.js +1 -0
- package/dist/client/assets/proto-C7zT0LnQ.js +1 -0
- package/dist/client/assets/python-B6aJPvgy.js +1 -0
- package/dist/client/assets/ruby-B13GW9FM.js +1 -0
- package/dist/client/assets/rust-B1yitclQ.js +1 -0
- package/dist/client/assets/scss-D5BDwBP9.js +1 -0
- package/dist/client/assets/shellscript-Yzrsuije.js +1 -0
- package/dist/client/assets/sql-CRqJ_cUM.js +1 -0
- package/dist/client/assets/swift-D82vCrfD.js +1 -0
- package/dist/client/assets/toml-vGWfd6FD.js +1 -0
- package/dist/client/assets/tsx-COt5Ahok.js +1 -0
- package/dist/client/assets/typescript-BPQ3VLAy.js +1 -0
- package/dist/client/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/client/assets/xml-sdJ4AIDG.js +1 -0
- package/dist/client/assets/{xterm-FFux-PdF.js → xterm-CoGJTxj9.js} +1 -1
- package/dist/client/assets/yaml-Buea-lGh.js +1 -0
- package/dist/client/favicon-16.png +0 -0
- package/dist/client/favicon-32.png +0 -0
- package/dist/client/favicon.ico +0 -0
- package/dist/client/favicon.svg +48 -0
- package/dist/client/index.html +8 -2
- package/dist/client/openscout-icon.png +0 -0
- package/dist/client/site.webmanifest +19 -0
- package/dist/client/web-app-icon-192.png +0 -0
- package/dist/client/web-app-icon-512.png +0 -0
- package/dist/drizzle/0000_curly_iron_monger.sql +592 -0
- package/dist/drizzle/0001_invocation-status-columns.sql +7 -0
- package/dist/drizzle/0002_invocation-flight-metadata.sql +21 -0
- package/dist/drizzle/README.md +81 -0
- package/dist/drizzle/meta/0000_snapshot.json +4575 -0
- package/dist/drizzle/meta/0001_snapshot.json +4624 -0
- package/dist/drizzle/meta/0002_snapshot.json +4631 -0
- package/dist/drizzle/meta/_journal.json +27 -0
- package/dist/main.mjs +75403 -63487
- package/dist/node/main.mjs +7721 -0
- package/dist/openscout-terminal-relay.mjs +3819 -158
- package/dist/{pair-supervisor.mjs → pairing-runtime-controller.mjs} +52639 -46285
- package/dist/runtime/base-daemon.mjs +2229 -521
- package/dist/runtime/broker-daemon.mjs +35050 -26658
- package/dist/runtime/broker-process-manager.mjs +2068 -422
- package/dist/runtime/mesh-discover.mjs +2032 -421
- package/dist/scout-control-plane-web.mjs +56491 -40036
- package/dist/scout-web-server.mjs +56491 -40036
- package/dist/statusline.mjs +287 -0
- package/package.json +7 -3
- package/dist/client/assets/RepoDiffViewer-DRzXs-TP.js +0 -1
- package/dist/client/assets/index-CYs6TkOv.js +0 -191
- package/dist/client/assets/index-n_fpvwq8.css +0 -1
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/runtime/src/base-daemon.ts
|
|
3
|
-
import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { homedir as
|
|
6
|
-
import { dirname as dirname4, join as
|
|
3
|
+
import { spawn as spawn3, spawnSync as spawnSync2 } from "child_process";
|
|
4
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, openSync, writeFileSync as writeFileSync2 } from "fs";
|
|
5
|
+
import { homedir as homedir6 } from "os";
|
|
6
|
+
import { dirname as dirname4, join as join6, resolve as resolve3 } from "path";
|
|
7
7
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8
8
|
|
|
9
9
|
// packages/runtime/src/broker-process-manager.ts
|
|
10
|
-
import { spawnSync } from "child_process";
|
|
11
|
-
import {
|
|
12
|
-
import { homedir as
|
|
13
|
-
import { dirname as
|
|
10
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
11
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
12
|
+
import { homedir as homedir5 } from "os";
|
|
13
|
+
import { dirname as dirname3, join as join5, resolve as resolve2 } from "path";
|
|
14
14
|
import { fileURLToPath } from "url";
|
|
15
15
|
|
|
16
16
|
// packages/runtime/src/broker-api.ts
|
|
17
|
+
import { existsSync } from "fs";
|
|
17
18
|
import { request as httpRequest } from "http";
|
|
18
19
|
var DEFAULT_BROKER_JSON_REQUEST_TIMEOUT_MS = 30000;
|
|
19
20
|
function normalizeBaseUrl(baseUrl) {
|
|
@@ -62,13 +63,17 @@ async function requestBrokerOverHttp(baseUrl, path, options) {
|
|
|
62
63
|
}
|
|
63
64
|
function shouldFallbackFromUnixSocket(error) {
|
|
64
65
|
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
65
|
-
return code === "ENOENT" || code === "ECONNREFUSED" || code === "ENOTSOCK" || code === "EACCES" || code === "FailedToOpenSocket";
|
|
66
|
+
return code === "ENOENT" || code === "ECONNREFUSED" || code === "ENOTSOCK" || code === "EACCES" || code === "EINVAL" || code === "FailedToOpenSocket";
|
|
66
67
|
}
|
|
67
68
|
function errorMessage(error) {
|
|
68
69
|
return error instanceof Error ? error.message : String(error);
|
|
69
70
|
}
|
|
70
71
|
function requestBrokerOverUnixSocket(socketPath, baseUrl, path, options) {
|
|
71
72
|
return new Promise((resolve, reject) => {
|
|
73
|
+
if (!existsSync(socketPath)) {
|
|
74
|
+
reject(Object.assign(new Error(`Scout broker socket does not exist: ${socketPath}`), { code: "ENOENT" }));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
72
77
|
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
73
78
|
const url = new URL(path, normalizedBaseUrl);
|
|
74
79
|
const body = requestBody(options);
|
|
@@ -180,7 +185,6 @@ async function requestScoutBrokerJsonWithTrace(baseUrl, path, options = {}) {
|
|
|
180
185
|
}
|
|
181
186
|
|
|
182
187
|
// packages/runtime/src/support-paths.ts
|
|
183
|
-
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
|
|
184
188
|
import { homedir } from "os";
|
|
185
189
|
import { join } from "path";
|
|
186
190
|
var OPENSCOUT_RPC_CUTOVER_MARKER = "rpc-runtime-cutover-v1";
|
|
@@ -204,6 +208,7 @@ function resolveOpenScoutSupportPaths() {
|
|
|
204
208
|
harnessCatalogPath: join(catalogDirectory, "harness-catalog.json"),
|
|
205
209
|
relayAgentsRegistryPath: join(supportDirectory, "relay-agents.json"),
|
|
206
210
|
managedInstallsPath: join(supportDirectory, "managed-installs.json"),
|
|
211
|
+
hostInfoPath: join(supportDirectory, ".host-info"),
|
|
207
212
|
relayHubDirectory: process.env.OPENSCOUT_RELAY_HUB ?? join(home, ".openscout", "relay"),
|
|
208
213
|
controlHome,
|
|
209
214
|
knowledgeDirectory,
|
|
@@ -214,35 +219,1552 @@ function resolveOpenScoutSupportPaths() {
|
|
|
214
219
|
cutoverMarkerPath: join(supportDirectory, OPENSCOUT_RPC_CUTOVER_MARKER)
|
|
215
220
|
};
|
|
216
221
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
222
|
+
|
|
223
|
+
// packages/runtime/src/open-scout-network.ts
|
|
224
|
+
import { readFileSync } from "fs";
|
|
225
|
+
var DEFAULT_OPENSCOUT_NETWORK_RENDEZVOUS_URL = "https://mesh.oscout.net";
|
|
226
|
+
var DEFAULT_OPENSCOUT_NETWORK_PAIRING_RELAY_URL = "wss://mesh.oscout.net/v1/relay";
|
|
227
|
+
function defaultOpenScoutNetworkSettings() {
|
|
228
|
+
return {
|
|
229
|
+
discoveryEnabled: false,
|
|
230
|
+
rendezvousUrl: DEFAULT_OPENSCOUT_NETWORK_RENDEZVOUS_URL,
|
|
231
|
+
pairingRelayUrl: DEFAULT_OPENSCOUT_NETWORK_PAIRING_RELAY_URL,
|
|
232
|
+
keepPairingRelayRunning: true
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function normalizeOpenScoutNetworkSettings(input) {
|
|
236
|
+
const base = defaultOpenScoutNetworkSettings();
|
|
237
|
+
const record = isRecord(input) ? input : {};
|
|
238
|
+
return {
|
|
239
|
+
discoveryEnabled: typeof record.discoveryEnabled === "boolean" ? record.discoveryEnabled : base.discoveryEnabled,
|
|
240
|
+
rendezvousUrl: normalizeUrlString(record.rendezvousUrl, base.rendezvousUrl),
|
|
241
|
+
pairingRelayUrl: normalizeUrlString(record.pairingRelayUrl, base.pairingRelayUrl),
|
|
242
|
+
keepPairingRelayRunning: typeof record.keepPairingRelayRunning === "boolean" ? record.keepPairingRelayRunning : base.keepPairingRelayRunning
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function readOpenScoutNetworkSettingsSync() {
|
|
246
|
+
try {
|
|
247
|
+
const raw = readFileSync(resolveOpenScoutSupportPaths().settingsPath, "utf8");
|
|
248
|
+
const parsed = JSON.parse(raw);
|
|
249
|
+
const network = isRecord(parsed) && isRecord(parsed.network) ? parsed.network : {};
|
|
250
|
+
return normalizeOpenScoutNetworkSettings(isRecord(network.openScoutNetwork) ? network.openScoutNetwork : {});
|
|
251
|
+
} catch {
|
|
252
|
+
return defaultOpenScoutNetworkSettings();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function openScoutNetworkDiscoveryEnabled(env = process.env) {
|
|
256
|
+
const explicit = readBooleanEnv(env.OPENSCOUT_NETWORK_DISCOVERY_ENABLED) ?? readBooleanEnv(env.OPENSCOUT_OSN_DISCOVERY_ENABLED);
|
|
257
|
+
if (explicit !== undefined) {
|
|
258
|
+
return explicit;
|
|
259
|
+
}
|
|
260
|
+
return readOpenScoutNetworkSettingsSync().discoveryEnabled;
|
|
261
|
+
}
|
|
262
|
+
function openScoutNetworkServiceEnvironment(env = process.env) {
|
|
263
|
+
const settings = readOpenScoutNetworkSettingsSync();
|
|
264
|
+
if (!settings.discoveryEnabled) {
|
|
265
|
+
return {};
|
|
266
|
+
}
|
|
267
|
+
const next = {};
|
|
268
|
+
if (!hasEnv(env, "OPENSCOUT_MESH_RENDEZVOUS_URL")) {
|
|
269
|
+
next.OPENSCOUT_MESH_RENDEZVOUS_URL = settings.rendezvousUrl;
|
|
270
|
+
}
|
|
271
|
+
if (!hasEnv(env, "OPENSCOUT_PAIRING_RELAY_URL") && !hasEnv(env, "OPENSCOUT_MOBILE_PAIRING_RELAY_URL")) {
|
|
272
|
+
next.OPENSCOUT_PAIRING_RELAY_URL = settings.pairingRelayUrl;
|
|
273
|
+
}
|
|
274
|
+
return next;
|
|
275
|
+
}
|
|
276
|
+
function normalizeUrlString(value, fallback) {
|
|
277
|
+
if (typeof value !== "string") {
|
|
278
|
+
return fallback;
|
|
279
|
+
}
|
|
280
|
+
const trimmed = value.trim().replace(/\/$/, "");
|
|
281
|
+
return trimmed || fallback;
|
|
282
|
+
}
|
|
283
|
+
function readBooleanEnv(value) {
|
|
284
|
+
const normalized = value?.trim().toLowerCase();
|
|
285
|
+
if (!normalized)
|
|
220
286
|
return;
|
|
287
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
288
|
+
return true;
|
|
289
|
+
if (["0", "false", "no", "off"].includes(normalized))
|
|
290
|
+
return false;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
function hasEnv(env, key) {
|
|
294
|
+
return typeof env[key] === "string" && env[key].trim().length > 0;
|
|
295
|
+
}
|
|
296
|
+
function isRecord(value) {
|
|
297
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// packages/runtime/src/tailscale.ts
|
|
301
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
302
|
+
import { execFileSync } from "child_process";
|
|
303
|
+
|
|
304
|
+
// packages/runtime/src/system-probes/tailscale-status.ts
|
|
305
|
+
import { readFile } from "fs/promises";
|
|
306
|
+
|
|
307
|
+
// packages/runtime/src/system-probes/registry.ts
|
|
308
|
+
var DEFAULT_MIN_MAX_STALE_MS = 2 * 60000;
|
|
309
|
+
var PROBE_RUN_OUTPUT = Symbol("openscout.probeRunOutput");
|
|
310
|
+
var registeredProbes = [];
|
|
311
|
+
|
|
312
|
+
class ProbeBackendError extends Error {
|
|
313
|
+
backend;
|
|
314
|
+
fallbackSince;
|
|
315
|
+
fallbackReason;
|
|
316
|
+
code;
|
|
317
|
+
timedOut;
|
|
318
|
+
constructor(message, metadata, cause) {
|
|
319
|
+
super(message);
|
|
320
|
+
this.name = "ProbeBackendError";
|
|
321
|
+
this.backend = metadata.backend;
|
|
322
|
+
this.fallbackSince = metadata.fallbackSince;
|
|
323
|
+
this.fallbackReason = metadata.fallbackReason;
|
|
324
|
+
const details = probeErrorDetails(cause);
|
|
325
|
+
if (details.code) {
|
|
326
|
+
this.code = details.code;
|
|
327
|
+
}
|
|
328
|
+
if (details.timedOut !== undefined) {
|
|
329
|
+
this.timedOut = details.timedOut;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function probeRunOutput(value, metadata) {
|
|
334
|
+
return {
|
|
335
|
+
[PROBE_RUN_OUTPUT]: true,
|
|
336
|
+
value,
|
|
337
|
+
...metadata
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function isProbeRunOutput(value) {
|
|
341
|
+
return typeof value === "object" && value !== null && value[PROBE_RUN_OUTPUT] === true;
|
|
342
|
+
}
|
|
343
|
+
function backendMetadataFromError(error) {
|
|
344
|
+
if (error instanceof ProbeBackendError) {
|
|
345
|
+
return {
|
|
346
|
+
backend: error.backend,
|
|
347
|
+
fallbackSince: error.fallbackSince,
|
|
348
|
+
fallbackReason: error.fallbackReason
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
if (typeof error === "object" && error !== null) {
|
|
352
|
+
const record = error;
|
|
353
|
+
if (record.backend === "local" || record.backend === "scoutd" || record.backend === "local-fallback") {
|
|
354
|
+
return {
|
|
355
|
+
backend: record.backend,
|
|
356
|
+
fallbackSince: typeof record.fallbackSince === "number" ? record.fallbackSince : undefined,
|
|
357
|
+
fallbackReason: typeof record.fallbackReason === "string" ? record.fallbackReason : undefined
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
function probeErrorDetails(error) {
|
|
364
|
+
if (typeof error !== "object" || error === null) {
|
|
365
|
+
return {};
|
|
366
|
+
}
|
|
367
|
+
const record = error;
|
|
368
|
+
const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : undefined;
|
|
369
|
+
const timedOut = record.timedOut === true || code === "timeout" ? true : record.timedOut === false ? false : undefined;
|
|
370
|
+
return { code, timedOut };
|
|
371
|
+
}
|
|
372
|
+
function assertProbeSpec(spec) {
|
|
373
|
+
if (!spec.id.trim()) {
|
|
374
|
+
throw new Error("Probe id is required");
|
|
375
|
+
}
|
|
376
|
+
if (!Number.isFinite(spec.ttlMs) || spec.ttlMs <= 0) {
|
|
377
|
+
throw new Error(`Probe ${spec.id} must declare a positive ttlMs`);
|
|
378
|
+
}
|
|
379
|
+
if (!Number.isFinite(spec.timeoutMs) || spec.timeoutMs <= 0) {
|
|
380
|
+
throw new Error(`Probe ${spec.id} must declare a positive timeoutMs`);
|
|
381
|
+
}
|
|
382
|
+
if (spec.maxStaleMs !== undefined && (!Number.isFinite(spec.maxStaleMs) || spec.maxStaleMs <= 0)) {
|
|
383
|
+
throw new Error(`Probe ${spec.id} maxStaleMs must be positive when set`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function maxStaleMsFor(spec) {
|
|
387
|
+
return spec.maxStaleMs ?? Math.max(DEFAULT_MIN_MAX_STALE_MS, spec.ttlMs * 10);
|
|
388
|
+
}
|
|
389
|
+
function failureBackoffMs(consecutiveFailures) {
|
|
390
|
+
if (consecutiveFailures <= 0) {
|
|
391
|
+
return 0;
|
|
392
|
+
}
|
|
393
|
+
return Math.min(30000, 1000 * 2 ** Math.min(consecutiveFailures - 1, 5));
|
|
394
|
+
}
|
|
395
|
+
function probeErrorFromUnknown(error, at, timedOut = false) {
|
|
396
|
+
if (typeof error === "object" && error !== null) {
|
|
397
|
+
const record = error;
|
|
398
|
+
const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : timedOut ? "timeout" : typeof record.name === "string" && record.name.trim() ? record.name.trim() : "error";
|
|
399
|
+
const message = typeof record.message === "string" && record.message.trim() ? record.message : String(error);
|
|
400
|
+
return { code, message, at, ...timedOut ? { timedOut: true } : {} };
|
|
221
401
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
402
|
+
return {
|
|
403
|
+
code: timedOut ? "timeout" : "error",
|
|
404
|
+
message: String(error),
|
|
405
|
+
at,
|
|
406
|
+
...timedOut ? { timedOut: true } : {}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function timeoutError(probeId, timeoutMs) {
|
|
410
|
+
return {
|
|
411
|
+
code: "timeout",
|
|
412
|
+
message: `Probe ${probeId} timed out after ${timeoutMs}ms`,
|
|
413
|
+
at: Date.now(),
|
|
414
|
+
timedOut: true
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function staleTooLongError(probeId, ageMs, maxStaleMs) {
|
|
418
|
+
return {
|
|
419
|
+
code: "max_stale_exceeded",
|
|
420
|
+
message: `Probe ${probeId} last good snapshot is ${ageMs}ms old, exceeding maxStaleMs ${maxStaleMs}`,
|
|
421
|
+
at: Date.now()
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function isFreshEnough(state, maxAgeMs) {
|
|
425
|
+
if (state.at === null || state.invalidated) {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
return Date.now() - state.at <= maxAgeMs;
|
|
429
|
+
}
|
|
430
|
+
function logBackendTransition(input) {
|
|
431
|
+
if (input.from === input.to) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const keySuffix = input.key === undefined ? "" : ` key=${JSON.stringify(input.key)}`;
|
|
435
|
+
const reasonSuffix = input.reason ? ` (${input.reason})` : "";
|
|
436
|
+
console.warn(`[openscout] system probe ${input.id}${keySuffix} backend ${input.from} -> ${input.to}${reasonSuffix}`);
|
|
228
437
|
}
|
|
229
438
|
|
|
230
|
-
|
|
231
|
-
|
|
439
|
+
class ProbeInstance {
|
|
440
|
+
spec;
|
|
441
|
+
key;
|
|
442
|
+
runProbe;
|
|
443
|
+
scheduleRun;
|
|
444
|
+
state = {
|
|
445
|
+
value: null,
|
|
446
|
+
at: null,
|
|
447
|
+
error: null,
|
|
448
|
+
consecutiveFailures: 0,
|
|
449
|
+
inFlight: null,
|
|
450
|
+
invalidated: false,
|
|
451
|
+
invalidationReason: null,
|
|
452
|
+
invalidationSerial: 0,
|
|
453
|
+
nextRetryAt: 0,
|
|
454
|
+
backend: "local",
|
|
455
|
+
fallbackSince: null,
|
|
456
|
+
fallbackReason: null
|
|
457
|
+
};
|
|
458
|
+
metricState;
|
|
459
|
+
lastAccessAt = Date.now();
|
|
460
|
+
constructor(options) {
|
|
461
|
+
this.spec = options.spec;
|
|
462
|
+
this.key = options.key;
|
|
463
|
+
this.runProbe = options.run;
|
|
464
|
+
this.scheduleRun = options.scheduleRun ?? ((task) => task());
|
|
465
|
+
this.metricState = {
|
|
466
|
+
id: options.spec.id,
|
|
467
|
+
...options.key !== undefined ? { key: options.key } : {},
|
|
468
|
+
backend: "local",
|
|
469
|
+
runCount: 0,
|
|
470
|
+
failureCount: 0,
|
|
471
|
+
timeoutCount: 0,
|
|
472
|
+
staleServedCount: 0,
|
|
473
|
+
lastRunAt: null,
|
|
474
|
+
lastDurationMs: null,
|
|
475
|
+
lastSuccessAt: null
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
read() {
|
|
479
|
+
this.touch();
|
|
480
|
+
if (this.shouldRefreshForRead(Date.now())) {
|
|
481
|
+
this.ensureRefresh(false, this.spec.ttlMs);
|
|
482
|
+
}
|
|
483
|
+
const snap = this.snapshot();
|
|
484
|
+
if (snap.status === "stale" || snap.status === "failed" && snap.at !== null) {
|
|
485
|
+
this.metricState.staleServedCount += 1;
|
|
486
|
+
}
|
|
487
|
+
return snap;
|
|
488
|
+
}
|
|
489
|
+
async fresh(options = {}) {
|
|
490
|
+
this.touch();
|
|
491
|
+
const maxAgeMs = options.maxAgeMs ?? this.spec.ttlMs;
|
|
492
|
+
for (let attempt = 0;attempt < 3; attempt += 1) {
|
|
493
|
+
if (isFreshEnough(this.state, maxAgeMs)) {
|
|
494
|
+
return this.snapshot();
|
|
495
|
+
}
|
|
496
|
+
await this.ensureRefresh(true, maxAgeMs);
|
|
497
|
+
if (!this.state.invalidated) {
|
|
498
|
+
return this.snapshot();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return this.snapshot();
|
|
502
|
+
}
|
|
503
|
+
snapshot() {
|
|
504
|
+
const now = Date.now();
|
|
505
|
+
const ageMs = this.state.at === null ? null : Math.max(0, now - this.state.at);
|
|
506
|
+
const maxStaleMs = maxStaleMsFor(this.spec);
|
|
507
|
+
const exceededMaxStale = ageMs !== null && ageMs > maxStaleMs;
|
|
508
|
+
const fresh = ageMs !== null && !this.state.invalidated && ageMs <= this.spec.ttlMs;
|
|
509
|
+
const status = this.state.at === null ? this.state.error ? "failed" : "empty" : exceededMaxStale ? "failed" : fresh ? "fresh" : "stale";
|
|
510
|
+
const error = exceededMaxStale ? this.state.error ?? staleTooLongError(this.spec.id, ageMs ?? 0, maxStaleMs) : this.state.error;
|
|
511
|
+
return {
|
|
512
|
+
id: this.spec.id,
|
|
513
|
+
...this.key !== undefined ? { key: this.key } : {},
|
|
514
|
+
value: status === "failed" && exceededMaxStale ? null : this.state.value,
|
|
515
|
+
at: this.state.at,
|
|
516
|
+
ageMs,
|
|
517
|
+
stale: status === "stale" || status === "failed" && this.state.at !== null,
|
|
518
|
+
refreshing: this.state.inFlight !== null,
|
|
519
|
+
status,
|
|
520
|
+
error,
|
|
521
|
+
consecutiveFailures: this.state.consecutiveFailures,
|
|
522
|
+
backend: this.state.backend,
|
|
523
|
+
...this.state.fallbackSince !== null ? { fallbackSince: this.state.fallbackSince } : {},
|
|
524
|
+
...this.state.fallbackReason !== null ? { fallbackReason: this.state.fallbackReason } : {}
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
invalidate(reason) {
|
|
528
|
+
this.touch();
|
|
529
|
+
this.state.invalidated = true;
|
|
530
|
+
this.state.invalidationReason = reason ?? null;
|
|
531
|
+
this.state.invalidationSerial += 1;
|
|
532
|
+
this.state.nextRetryAt = 0;
|
|
533
|
+
}
|
|
534
|
+
metrics() {
|
|
535
|
+
return {
|
|
536
|
+
...this.metricState,
|
|
537
|
+
consecutiveFailures: this.state.consecutiveFailures,
|
|
538
|
+
inFlight: this.state.inFlight !== null,
|
|
539
|
+
...this.state.fallbackSince !== null ? { fallbackSince: this.state.fallbackSince } : {},
|
|
540
|
+
...this.state.fallbackReason !== null ? { fallbackReason: this.state.fallbackReason } : {}
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
isRefreshing() {
|
|
544
|
+
return this.state.inFlight !== null;
|
|
545
|
+
}
|
|
546
|
+
touch() {
|
|
547
|
+
this.lastAccessAt = Date.now();
|
|
548
|
+
}
|
|
549
|
+
shouldRefreshForRead(now) {
|
|
550
|
+
if (this.state.inFlight !== null) {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
if (this.state.nextRetryAt > now) {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
if (this.state.at === null) {
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
if (this.state.invalidated) {
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
return now - this.state.at > this.spec.ttlMs;
|
|
563
|
+
}
|
|
564
|
+
ensureRefresh(force, maxAgeMs) {
|
|
565
|
+
if (this.state.inFlight) {
|
|
566
|
+
return this.state.inFlight;
|
|
567
|
+
}
|
|
568
|
+
const now = Date.now();
|
|
569
|
+
if (!force && this.state.nextRetryAt > now) {
|
|
570
|
+
return Promise.resolve();
|
|
571
|
+
}
|
|
572
|
+
const inFlight = this.scheduleRun(() => this.executeRun(maxAgeMs)).finally(() => {
|
|
573
|
+
if (this.state.inFlight === inFlight) {
|
|
574
|
+
this.state.inFlight = null;
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
this.state.inFlight = inFlight;
|
|
578
|
+
return inFlight;
|
|
579
|
+
}
|
|
580
|
+
async executeRun(maxAgeMs) {
|
|
581
|
+
const startedAt = Date.now();
|
|
582
|
+
const previousBackend = this.state.backend;
|
|
583
|
+
const invalidationSerialAtStart = this.state.invalidationSerial;
|
|
584
|
+
const controller = new AbortController;
|
|
585
|
+
let timeout = null;
|
|
586
|
+
this.metricState.runCount += 1;
|
|
587
|
+
this.metricState.lastRunAt = startedAt;
|
|
588
|
+
const ctx = {
|
|
589
|
+
probeId: this.spec.id,
|
|
590
|
+
...this.key !== undefined ? { key: this.key } : {},
|
|
591
|
+
signal: controller.signal,
|
|
592
|
+
timeoutMs: this.spec.timeoutMs,
|
|
593
|
+
maxAgeMs,
|
|
594
|
+
startedAt
|
|
595
|
+
};
|
|
596
|
+
const timeoutProbeError = timeoutError(this.spec.id, this.spec.timeoutMs);
|
|
597
|
+
const runPromise = this.runProbe(ctx);
|
|
598
|
+
runPromise.catch(() => {
|
|
599
|
+
return;
|
|
600
|
+
});
|
|
601
|
+
try {
|
|
602
|
+
const result = await new Promise((resolve, reject) => {
|
|
603
|
+
timeout = setTimeout(() => {
|
|
604
|
+
controller.abort(timeoutProbeError);
|
|
605
|
+
reject(timeoutProbeError);
|
|
606
|
+
}, this.spec.timeoutMs);
|
|
607
|
+
runPromise.then(resolve, reject);
|
|
608
|
+
});
|
|
609
|
+
const output = isProbeRunOutput(result) ? result : probeRunOutput(result, { backend: "local" });
|
|
610
|
+
this.state.value = output.value;
|
|
611
|
+
this.state.at = output.generatedAt ?? Date.now();
|
|
612
|
+
this.state.error = null;
|
|
613
|
+
this.state.consecutiveFailures = 0;
|
|
614
|
+
if (this.state.invalidationSerial === invalidationSerialAtStart) {
|
|
615
|
+
this.state.invalidated = false;
|
|
616
|
+
this.state.invalidationReason = null;
|
|
617
|
+
}
|
|
618
|
+
this.state.nextRetryAt = 0;
|
|
619
|
+
this.state.backend = output.backend;
|
|
620
|
+
this.state.fallbackSince = output.fallbackSince ?? null;
|
|
621
|
+
this.state.fallbackReason = output.fallbackReason ?? null;
|
|
622
|
+
this.metricState.backend = output.backend;
|
|
623
|
+
this.metricState.lastSuccessAt = this.state.at;
|
|
624
|
+
logBackendTransition({
|
|
625
|
+
id: this.spec.id,
|
|
626
|
+
key: this.key,
|
|
627
|
+
from: previousBackend,
|
|
628
|
+
to: output.backend,
|
|
629
|
+
reason: output.fallbackReason
|
|
630
|
+
});
|
|
631
|
+
} catch (error) {
|
|
632
|
+
const timedOut = error === timeoutProbeError || typeof error === "object" && error !== null && (error.code === "timeout" || error.timedOut === true);
|
|
633
|
+
const at = Date.now();
|
|
634
|
+
this.state.error = error === timeoutProbeError ? timeoutProbeError : probeErrorFromUnknown(error, at, timedOut);
|
|
635
|
+
this.state.consecutiveFailures += 1;
|
|
636
|
+
this.state.nextRetryAt = at + failureBackoffMs(this.state.consecutiveFailures);
|
|
637
|
+
const backendMetadata = backendMetadataFromError(error);
|
|
638
|
+
if (backendMetadata) {
|
|
639
|
+
this.state.backend = backendMetadata.backend;
|
|
640
|
+
this.state.fallbackSince = backendMetadata.fallbackSince ?? null;
|
|
641
|
+
this.state.fallbackReason = backendMetadata.fallbackReason ?? null;
|
|
642
|
+
this.metricState.backend = backendMetadata.backend;
|
|
643
|
+
logBackendTransition({
|
|
644
|
+
id: this.spec.id,
|
|
645
|
+
key: this.key,
|
|
646
|
+
from: previousBackend,
|
|
647
|
+
to: backendMetadata.backend,
|
|
648
|
+
reason: backendMetadata.fallbackReason
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
this.metricState.failureCount += 1;
|
|
652
|
+
if (this.state.error.timedOut || this.state.error.code === "timeout") {
|
|
653
|
+
this.metricState.timeoutCount += 1;
|
|
654
|
+
}
|
|
655
|
+
} finally {
|
|
656
|
+
if (timeout) {
|
|
657
|
+
clearTimeout(timeout);
|
|
658
|
+
}
|
|
659
|
+
this.metricState.lastDurationMs = Date.now() - startedAt;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
class FamilyLimiter {
|
|
665
|
+
maxConcurrent;
|
|
666
|
+
active = 0;
|
|
667
|
+
queue = [];
|
|
668
|
+
constructor(maxConcurrent) {
|
|
669
|
+
this.maxConcurrent = maxConcurrent;
|
|
670
|
+
}
|
|
671
|
+
queued() {
|
|
672
|
+
return this.queue.length;
|
|
673
|
+
}
|
|
674
|
+
async run(task) {
|
|
675
|
+
if (this.active >= this.maxConcurrent) {
|
|
676
|
+
await new Promise((resolve) => this.queue.push(resolve));
|
|
677
|
+
}
|
|
678
|
+
this.active += 1;
|
|
679
|
+
try {
|
|
680
|
+
return await task();
|
|
681
|
+
} finally {
|
|
682
|
+
this.active -= 1;
|
|
683
|
+
const next = this.queue.shift();
|
|
684
|
+
if (next) {
|
|
685
|
+
next();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
class ProbeFamily {
|
|
692
|
+
spec;
|
|
693
|
+
entries = new Map;
|
|
694
|
+
limiter;
|
|
695
|
+
constructor(spec) {
|
|
696
|
+
this.spec = spec;
|
|
697
|
+
assertProbeSpec(spec);
|
|
698
|
+
if (!Number.isInteger(spec.maxKeys) || spec.maxKeys <= 0) {
|
|
699
|
+
throw new Error(`Probe family ${spec.id} must declare a positive maxKeys`);
|
|
700
|
+
}
|
|
701
|
+
if (!Number.isFinite(spec.idleKeyTtlMs) || spec.idleKeyTtlMs <= 0) {
|
|
702
|
+
throw new Error(`Probe family ${spec.id} must declare a positive idleKeyTtlMs`);
|
|
703
|
+
}
|
|
704
|
+
if (!Number.isInteger(spec.maxConcurrentKeys) || spec.maxConcurrentKeys <= 0) {
|
|
705
|
+
throw new Error(`Probe family ${spec.id} must declare a positive maxConcurrentKeys`);
|
|
706
|
+
}
|
|
707
|
+
this.limiter = new FamilyLimiter(spec.maxConcurrentKeys);
|
|
708
|
+
}
|
|
709
|
+
for(rawKey) {
|
|
710
|
+
const key = this.normalize(rawKey);
|
|
711
|
+
this.cleanupIdle(Date.now());
|
|
712
|
+
let entry = this.entries.get(key);
|
|
713
|
+
if (!entry) {
|
|
714
|
+
entry = new ProbeInstance({
|
|
715
|
+
spec: this.spec,
|
|
716
|
+
key,
|
|
717
|
+
run: (ctx) => this.spec.run(key, ctx),
|
|
718
|
+
scheduleRun: (task) => this.limiter.run(task)
|
|
719
|
+
});
|
|
720
|
+
this.entries.set(key, entry);
|
|
721
|
+
}
|
|
722
|
+
entry.lastAccessAt = Date.now();
|
|
723
|
+
this.evictLruIfNeeded();
|
|
724
|
+
return entry;
|
|
725
|
+
}
|
|
726
|
+
snapshot(key) {
|
|
727
|
+
return this.for(key).snapshot();
|
|
728
|
+
}
|
|
729
|
+
invalidate(key, reason) {
|
|
730
|
+
this.for(key).invalidate(reason);
|
|
731
|
+
}
|
|
732
|
+
metrics() {
|
|
733
|
+
this.cleanupIdle(Date.now());
|
|
734
|
+
return {
|
|
735
|
+
id: this.spec.id,
|
|
736
|
+
keyCount: this.entries.size,
|
|
737
|
+
maxKeys: this.spec.maxKeys,
|
|
738
|
+
activeRuns: this.limiter.active,
|
|
739
|
+
queuedRuns: this.limiter.queued(),
|
|
740
|
+
keys: Array.from(this.entries.values(), (entry) => entry.metrics())
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
keys() {
|
|
744
|
+
this.cleanupIdle(Date.now());
|
|
745
|
+
return Array.from(this.entries.keys());
|
|
746
|
+
}
|
|
747
|
+
normalize(rawKey) {
|
|
748
|
+
const key = this.spec.normalizeKey(rawKey).trim();
|
|
749
|
+
if (!key) {
|
|
750
|
+
throw new Error(`Probe family ${this.spec.id} normalized an empty key`);
|
|
751
|
+
}
|
|
752
|
+
return key;
|
|
753
|
+
}
|
|
754
|
+
cleanupIdle(now) {
|
|
755
|
+
for (const [key, entry] of this.entries) {
|
|
756
|
+
if (!entry.isRefreshing() && now - entry.lastAccessAt > this.spec.idleKeyTtlMs) {
|
|
757
|
+
this.entries.delete(key);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
evictLruIfNeeded() {
|
|
762
|
+
while (this.entries.size > this.spec.maxKeys) {
|
|
763
|
+
let oldestKey = null;
|
|
764
|
+
let oldestAccess = Number.POSITIVE_INFINITY;
|
|
765
|
+
for (const [key, entry] of this.entries) {
|
|
766
|
+
if (entry.isRefreshing()) {
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
if (entry.lastAccessAt < oldestAccess) {
|
|
770
|
+
oldestAccess = entry.lastAccessAt;
|
|
771
|
+
oldestKey = key;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (!oldestKey) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
this.entries.delete(oldestKey);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
function defineProbe(spec) {
|
|
782
|
+
assertProbeSpec(spec);
|
|
783
|
+
const handle = new ProbeInstance({
|
|
784
|
+
spec,
|
|
785
|
+
run: spec.run
|
|
786
|
+
});
|
|
787
|
+
registeredProbes.push({
|
|
788
|
+
kind: "probe",
|
|
789
|
+
id: spec.id,
|
|
790
|
+
handle
|
|
791
|
+
});
|
|
792
|
+
return handle;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// packages/runtime/src/system-probes/exec.ts
|
|
796
|
+
import { spawn } from "child_process";
|
|
797
|
+
|
|
798
|
+
// packages/runtime/src/system-probes/scoutd-client.ts
|
|
799
|
+
import { existsSync as existsSync2 } from "fs";
|
|
232
800
|
import { homedir as homedir2 } from "os";
|
|
233
|
-
import {
|
|
801
|
+
import { join as join2 } from "path";
|
|
802
|
+
import { Socket } from "net";
|
|
803
|
+
|
|
804
|
+
// packages/runtime/src/system-probes/scout-host-catalog.ts
|
|
805
|
+
var SCOUT_HOST_PROBE_SCHEMA_VERSIONS = {
|
|
806
|
+
"tailscale.status": 1,
|
|
807
|
+
"git.buildInfo": 1,
|
|
808
|
+
"git.revParse": 1,
|
|
809
|
+
"git.diffShortstat": 1,
|
|
810
|
+
"git.statusPorcelain": 1,
|
|
811
|
+
"git.mergeBase": 1,
|
|
812
|
+
"git.logLastCommitUnix": 1,
|
|
813
|
+
"git.worktreeListPorcelain": 1,
|
|
814
|
+
"tmux.sessions": 1,
|
|
815
|
+
"tmux.panes": 1,
|
|
816
|
+
"zellij.sessions": 1,
|
|
817
|
+
"ps.runtime": 1,
|
|
818
|
+
"ps.discovery": 1,
|
|
819
|
+
"ps.cwd": 1,
|
|
820
|
+
"net.listeners": 1,
|
|
821
|
+
"repo.scan": 1,
|
|
822
|
+
"repo.diff": 1
|
|
823
|
+
};
|
|
824
|
+
var SCOUT_HOST_EXEC_VERB_SCHEMA_VERSIONS = {
|
|
825
|
+
"tmux.sendKeys": 1,
|
|
826
|
+
"tmux.sendKeysLiteral": 1,
|
|
827
|
+
"tmux.loadBuffer": 1,
|
|
828
|
+
"tmux.pasteBuffer": 1,
|
|
829
|
+
"tmux.deleteBuffer": 1,
|
|
830
|
+
"tmux.killSession": 1,
|
|
831
|
+
"tmux.newSession": 1,
|
|
832
|
+
"tmux.detachClient": 1,
|
|
833
|
+
"tailscale.cert": 1,
|
|
834
|
+
"reveal.open": 1
|
|
835
|
+
};
|
|
836
|
+
function expectedScoutHostProbeSchemaVersion(probeId) {
|
|
837
|
+
return Object.hasOwn(SCOUT_HOST_PROBE_SCHEMA_VERSIONS, probeId) ? SCOUT_HOST_PROBE_SCHEMA_VERSIONS[probeId] : null;
|
|
838
|
+
}
|
|
839
|
+
function expectedScoutHostExecVerbSchemaVersion(verb) {
|
|
840
|
+
return Object.hasOwn(SCOUT_HOST_EXEC_VERB_SCHEMA_VERSIONS, verb) ? SCOUT_HOST_EXEC_VERB_SCHEMA_VERSIONS[verb] : null;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// packages/runtime/src/system-probes/scoutd-client.ts
|
|
844
|
+
var CAPABILITIES_SCHEMA = "openscout.probe.capabilities/v1";
|
|
845
|
+
var REQUEST_SCHEMA = "openscout.probe.request/v1";
|
|
846
|
+
var SNAPSHOT_SCHEMA = "openscout.probe.snapshot/v1";
|
|
847
|
+
var ERROR_SCHEMA = "openscout.probe.error/v1";
|
|
848
|
+
var EXEC_REQUEST_SCHEMA = "openscout.exec.request/v1";
|
|
849
|
+
var EXEC_RESPONSE_SCHEMA = "openscout.exec.response/v1";
|
|
850
|
+
var CAPABILITY_RECHECK_MS = 1e4;
|
|
851
|
+
var SOCKET_TIMEOUT_MS = 900;
|
|
852
|
+
var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
853
|
+
function readString(value) {
|
|
854
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
855
|
+
}
|
|
856
|
+
function readNumber(value) {
|
|
857
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
858
|
+
}
|
|
859
|
+
function isRecord2(value) {
|
|
860
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
861
|
+
}
|
|
862
|
+
function fallbackMessage(error) {
|
|
863
|
+
if (error instanceof Error && error.message.trim()) {
|
|
864
|
+
return error.message;
|
|
865
|
+
}
|
|
866
|
+
return String(error);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
class ScoutdExecResponseError extends Error {
|
|
870
|
+
code;
|
|
871
|
+
timedOut;
|
|
872
|
+
constructor(message, options = {}) {
|
|
873
|
+
super(message);
|
|
874
|
+
this.name = "ScoutdExecResponseError";
|
|
875
|
+
this.code = options.code ?? "exec_error";
|
|
876
|
+
this.timedOut = options.timedOut ?? false;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
class ScoutdProbeResponseError extends Error {
|
|
881
|
+
code;
|
|
882
|
+
timedOut;
|
|
883
|
+
constructor(message, options = {}) {
|
|
884
|
+
super(message);
|
|
885
|
+
this.name = "ScoutdProbeResponseError";
|
|
886
|
+
this.code = options.code ?? "probe_error";
|
|
887
|
+
this.timedOut = options.timedOut ?? false;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
function resolveScoutdProbesSocketPath(env = process.env) {
|
|
891
|
+
const explicit = env.OPENSCOUT_PROBES_SOCKET?.trim();
|
|
892
|
+
if (explicit) {
|
|
893
|
+
return explicit;
|
|
894
|
+
}
|
|
895
|
+
const openScoutHome = env.OPENSCOUT_HOME?.trim() || join2(homedir2(), ".openscout");
|
|
896
|
+
return join2(openScoutHome, "run", "scoutd-probes.sock");
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
class ScoutdProbeClient {
|
|
900
|
+
env;
|
|
901
|
+
capabilities = null;
|
|
902
|
+
lastCapabilityCheckAt = null;
|
|
903
|
+
lastError = null;
|
|
904
|
+
daemonObserved = false;
|
|
905
|
+
fallbackByProbe = new Map;
|
|
906
|
+
fallbackByExec = new Map;
|
|
907
|
+
constructor(env = process.env) {
|
|
908
|
+
this.env = env;
|
|
909
|
+
}
|
|
910
|
+
async requestProbe(input) {
|
|
911
|
+
const socketPath = resolveScoutdProbesSocketPath(this.env);
|
|
912
|
+
const socketTimeoutMs = probeSocketTimeoutMs(input.timeoutMs);
|
|
913
|
+
const socketExists = existsSync2(socketPath);
|
|
914
|
+
if (!socketExists) {
|
|
915
|
+
this.capabilities = null;
|
|
916
|
+
this.lastError = null;
|
|
917
|
+
return this.daemonObserved ? this.fallback(input.probeId, input.key, `probe socket is missing: ${socketPath}`) : { state: "local" };
|
|
918
|
+
}
|
|
919
|
+
this.daemonObserved = true;
|
|
920
|
+
const capabilities = await this.ensureCapabilities(socketPath, socketTimeoutMs);
|
|
921
|
+
if (!capabilities) {
|
|
922
|
+
return this.fallback(input.probeId, input.key, this.lastError ?? "probe capabilities unavailable");
|
|
923
|
+
}
|
|
924
|
+
const capability = capabilities.families.get(input.probeId);
|
|
925
|
+
if (!capability) {
|
|
926
|
+
return this.fallback(input.probeId, input.key, `scoutd does not serve ${input.probeId}`);
|
|
927
|
+
}
|
|
928
|
+
const expectedSchemaVersion = expectedScoutHostProbeSchemaVersion(input.probeId);
|
|
929
|
+
if (expectedSchemaVersion === null) {
|
|
930
|
+
return this.fallback(input.probeId, input.key, `client has no compiled schema version for ${input.probeId}`);
|
|
931
|
+
}
|
|
932
|
+
if (capability.schemaVersion !== expectedSchemaVersion) {
|
|
933
|
+
return this.fallback(input.probeId, input.key, `scoutd serves ${input.probeId} schema v${capability.schemaVersion}, expected v${expectedSchemaVersion}`);
|
|
934
|
+
}
|
|
935
|
+
try {
|
|
936
|
+
const response = await requestJson(socketPath, {
|
|
937
|
+
schema: REQUEST_SCHEMA,
|
|
938
|
+
schemaVersion: expectedSchemaVersion,
|
|
939
|
+
probeId: input.probeId,
|
|
940
|
+
key: input.key ?? null,
|
|
941
|
+
maxAgeMs: input.maxAgeMs,
|
|
942
|
+
opTimeoutMs: requestOpTimeoutMs(input.timeoutMs)
|
|
943
|
+
}, { timeoutMs: socketTimeoutMs });
|
|
944
|
+
if (!isRecord2(response) || response.schema !== SNAPSHOT_SCHEMA) {
|
|
945
|
+
throw new Error("scoutd returned an invalid probe snapshot envelope");
|
|
946
|
+
}
|
|
947
|
+
if (response.error) {
|
|
948
|
+
const error = isRecord2(response.error) ? response.error : {};
|
|
949
|
+
const code = readString(error.code) ?? "probe_error";
|
|
950
|
+
const message = readString(error.message) ?? code;
|
|
951
|
+
const timedOut = error.timedOut === true || error.timed_out === true;
|
|
952
|
+
throw new ScoutdProbeResponseError(message, { code, timedOut });
|
|
953
|
+
}
|
|
954
|
+
this.fallbackByProbe.delete(fallbackKey(input.probeId, input.key));
|
|
955
|
+
this.lastError = null;
|
|
956
|
+
return {
|
|
957
|
+
state: "scoutd",
|
|
958
|
+
value: response.value,
|
|
959
|
+
generatedAt: readNumber(response.generatedAt) ?? Date.now(),
|
|
960
|
+
daemonVersion: readString(response.daemonVersion) ?? capabilities.daemonVersion
|
|
961
|
+
};
|
|
962
|
+
} catch (error) {
|
|
963
|
+
if (!(error instanceof ScoutdProbeResponseError)) {
|
|
964
|
+
this.capabilities = null;
|
|
965
|
+
this.lastCapabilityCheckAt = null;
|
|
966
|
+
}
|
|
967
|
+
this.lastError = fallbackMessage(error);
|
|
968
|
+
return this.fallback(input.probeId, input.key, this.lastError);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
async requestExecVerb(input) {
|
|
972
|
+
const socketPath = resolveScoutdProbesSocketPath(this.env);
|
|
973
|
+
const socketTimeoutMs = execSocketTimeoutMs(input.args);
|
|
974
|
+
const socketExists = existsSync2(socketPath);
|
|
975
|
+
if (!socketExists) {
|
|
976
|
+
this.capabilities = null;
|
|
977
|
+
this.lastError = null;
|
|
978
|
+
return this.daemonObserved ? this.execFallback(input.verb, `probe socket is missing: ${socketPath}`) : { state: "local" };
|
|
979
|
+
}
|
|
980
|
+
this.daemonObserved = true;
|
|
981
|
+
const capabilities = await this.ensureCapabilities(socketPath, socketTimeoutMs);
|
|
982
|
+
if (!capabilities) {
|
|
983
|
+
return this.execFallback(input.verb, this.lastError ?? "probe capabilities unavailable");
|
|
984
|
+
}
|
|
985
|
+
const capability = capabilities.verbs.get(input.verb);
|
|
986
|
+
if (!capability) {
|
|
987
|
+
return this.execFallback(input.verb, `scoutd does not serve ${input.verb}`);
|
|
988
|
+
}
|
|
989
|
+
const expectedSchemaVersion = expectedScoutHostExecVerbSchemaVersion(input.verb);
|
|
990
|
+
if (expectedSchemaVersion === null) {
|
|
991
|
+
return this.execFallback(input.verb, `client has no compiled schema version for ${input.verb}`);
|
|
992
|
+
}
|
|
993
|
+
if (capability.schemaVersion !== expectedSchemaVersion) {
|
|
994
|
+
return this.execFallback(input.verb, `scoutd serves ${input.verb} schema v${capability.schemaVersion}, expected v${expectedSchemaVersion}`);
|
|
995
|
+
}
|
|
996
|
+
try {
|
|
997
|
+
const response = await requestJson(socketPath, {
|
|
998
|
+
schema: EXEC_REQUEST_SCHEMA,
|
|
999
|
+
schemaVersion: expectedSchemaVersion,
|
|
1000
|
+
verb: input.verb,
|
|
1001
|
+
args: input.args
|
|
1002
|
+
}, { timeoutMs: socketTimeoutMs });
|
|
1003
|
+
if (!isRecord2(response) || response.schema !== EXEC_RESPONSE_SCHEMA) {
|
|
1004
|
+
throw new Error("scoutd returned an invalid exec response envelope");
|
|
1005
|
+
}
|
|
1006
|
+
if (!response.ok) {
|
|
1007
|
+
const error = isRecord2(response.error) ? response.error : {};
|
|
1008
|
+
const code = readString(error.code) ?? "exec_error";
|
|
1009
|
+
const message = readString(error.message) ?? code;
|
|
1010
|
+
const timedOut = error.timedOut === true || error.timed_out === true;
|
|
1011
|
+
throw new ScoutdExecResponseError(message, { code, timedOut });
|
|
1012
|
+
}
|
|
1013
|
+
this.fallbackByExec.delete(input.verb);
|
|
1014
|
+
this.lastError = null;
|
|
1015
|
+
return {
|
|
1016
|
+
state: "scoutd",
|
|
1017
|
+
value: response.value,
|
|
1018
|
+
daemonVersion: readString(response.daemonVersion) ?? capabilities.daemonVersion
|
|
1019
|
+
};
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
if (error instanceof ScoutdExecResponseError) {
|
|
1022
|
+
throw error;
|
|
1023
|
+
}
|
|
1024
|
+
this.capabilities = null;
|
|
1025
|
+
this.lastCapabilityCheckAt = null;
|
|
1026
|
+
this.lastError = fallbackMessage(error);
|
|
1027
|
+
return this.execFallback(input.verb, this.lastError);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
diagnostics() {
|
|
1031
|
+
const socketPath = resolveScoutdProbesSocketPath(this.env);
|
|
1032
|
+
return {
|
|
1033
|
+
socketPath,
|
|
1034
|
+
socketExists: existsSync2(socketPath),
|
|
1035
|
+
daemonObserved: this.daemonObserved,
|
|
1036
|
+
daemonVersion: this.capabilities?.daemonVersion ?? null,
|
|
1037
|
+
supportedProbeIds: this.capabilities ? [...this.capabilities.families.keys()].sort() : [],
|
|
1038
|
+
supportedExecVerbs: this.capabilities ? [...this.capabilities.verbs.keys()].sort() : [],
|
|
1039
|
+
lastCapabilityCheckAt: this.lastCapabilityCheckAt,
|
|
1040
|
+
lastError: this.lastError
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
resetForTests() {
|
|
1044
|
+
this.capabilities = null;
|
|
1045
|
+
this.lastCapabilityCheckAt = null;
|
|
1046
|
+
this.lastError = null;
|
|
1047
|
+
this.daemonObserved = false;
|
|
1048
|
+
this.fallbackByProbe.clear();
|
|
1049
|
+
this.fallbackByExec.clear();
|
|
1050
|
+
}
|
|
1051
|
+
async ensureCapabilities(socketPath, timeoutMs = SOCKET_TIMEOUT_MS) {
|
|
1052
|
+
const now = Date.now();
|
|
1053
|
+
if (this.capabilities && this.lastCapabilityCheckAt !== null && now - this.lastCapabilityCheckAt < CAPABILITY_RECHECK_MS) {
|
|
1054
|
+
return this.capabilities;
|
|
1055
|
+
}
|
|
1056
|
+
this.lastCapabilityCheckAt = now;
|
|
1057
|
+
try {
|
|
1058
|
+
const response = await requestJson(socketPath, { schema: CAPABILITIES_SCHEMA }, { timeoutMs });
|
|
1059
|
+
if (!isRecord2(response) || response.schema !== CAPABILITIES_SCHEMA) {
|
|
1060
|
+
throw new Error("scoutd returned an invalid capabilities envelope");
|
|
1061
|
+
}
|
|
1062
|
+
const daemonVersion = readString(response.daemonVersion) ?? "unknown";
|
|
1063
|
+
const families = new Map;
|
|
1064
|
+
if (Array.isArray(response.families)) {
|
|
1065
|
+
for (const entry of response.families) {
|
|
1066
|
+
if (!isRecord2(entry))
|
|
1067
|
+
continue;
|
|
1068
|
+
const probeId = readString(entry.probeId);
|
|
1069
|
+
const schemaVersion = readNumber(entry.schemaVersion);
|
|
1070
|
+
const ttlMs = readNumber(entry.ttlMs);
|
|
1071
|
+
if (probeId && schemaVersion !== null && ttlMs !== null) {
|
|
1072
|
+
families.set(probeId, { probeId, schemaVersion, ttlMs });
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const verbs = new Map;
|
|
1077
|
+
if (Array.isArray(response.verbs)) {
|
|
1078
|
+
for (const entry of response.verbs) {
|
|
1079
|
+
if (!isRecord2(entry))
|
|
1080
|
+
continue;
|
|
1081
|
+
const verb = readString(entry.verb);
|
|
1082
|
+
const schemaVersion = readNumber(entry.schemaVersion);
|
|
1083
|
+
if (verb && schemaVersion !== null) {
|
|
1084
|
+
verbs.set(verb, { verb, schemaVersion });
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
this.capabilities = { daemonVersion, families, verbs };
|
|
1089
|
+
this.daemonObserved = true;
|
|
1090
|
+
this.lastError = null;
|
|
1091
|
+
return this.capabilities;
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
this.capabilities = null;
|
|
1094
|
+
this.lastError = fallbackMessage(error);
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
fallback(probeId, key, reason) {
|
|
1099
|
+
const id = fallbackKey(probeId, key);
|
|
1100
|
+
let state = this.fallbackByProbe.get(id);
|
|
1101
|
+
if (!state || state.reason !== reason) {
|
|
1102
|
+
state = { since: Date.now(), reason };
|
|
1103
|
+
this.fallbackByProbe.set(id, state);
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
state: "local",
|
|
1107
|
+
fallbackSince: state.since,
|
|
1108
|
+
fallbackReason: state.reason
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
execFallback(verb, reason) {
|
|
1112
|
+
let state = this.fallbackByExec.get(verb);
|
|
1113
|
+
if (!state || state.reason !== reason) {
|
|
1114
|
+
state = { since: Date.now(), reason };
|
|
1115
|
+
this.fallbackByExec.set(verb, state);
|
|
1116
|
+
}
|
|
1117
|
+
return {
|
|
1118
|
+
state: "local",
|
|
1119
|
+
fallbackSince: state.since,
|
|
1120
|
+
fallbackReason: state.reason
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
var singletonClient = null;
|
|
1125
|
+
function getScoutdProbeClient() {
|
|
1126
|
+
singletonClient ??= new ScoutdProbeClient;
|
|
1127
|
+
return singletonClient;
|
|
1128
|
+
}
|
|
1129
|
+
async function runWithScoutdFallback(input) {
|
|
1130
|
+
const client = getScoutdProbeClient();
|
|
1131
|
+
const scoutd = await client.requestProbe({
|
|
1132
|
+
probeId: input.probeId,
|
|
1133
|
+
key: input.key,
|
|
1134
|
+
maxAgeMs: input.ctx.maxAgeMs,
|
|
1135
|
+
timeoutMs: input.ctx.timeoutMs
|
|
1136
|
+
});
|
|
1137
|
+
if (scoutd.state === "scoutd") {
|
|
1138
|
+
return probeRunOutput(scoutd.value, {
|
|
1139
|
+
backend: "scoutd",
|
|
1140
|
+
generatedAt: scoutd.generatedAt
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
const metadata = scoutd.fallbackSince ? {
|
|
1144
|
+
backend: "local-fallback",
|
|
1145
|
+
fallbackSince: scoutd.fallbackSince,
|
|
1146
|
+
fallbackReason: scoutd.fallbackReason ?? "scoutd unavailable"
|
|
1147
|
+
} : { backend: "local" };
|
|
1148
|
+
try {
|
|
1149
|
+
const local = await input.local();
|
|
1150
|
+
return probeRunOutput(local, metadata);
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
throw new ProbeBackendError(error instanceof Error ? error.message : String(error), metadata, error);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
function probeSocketTimeoutMs(timeoutMs) {
|
|
1156
|
+
if (timeoutMs === undefined || !Number.isFinite(timeoutMs)) {
|
|
1157
|
+
return SOCKET_TIMEOUT_MS;
|
|
1158
|
+
}
|
|
1159
|
+
return Math.max(SOCKET_TIMEOUT_MS, Math.min(Math.max(0, timeoutMs) + 1000, 31000));
|
|
1160
|
+
}
|
|
1161
|
+
function requestOpTimeoutMs(timeoutMs) {
|
|
1162
|
+
if (timeoutMs === undefined || !Number.isFinite(timeoutMs)) {
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
return Math.max(0, Math.floor(timeoutMs));
|
|
1166
|
+
}
|
|
1167
|
+
function execSocketTimeoutMs(args) {
|
|
1168
|
+
const timeoutMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) ? Math.max(0, args.timeoutMs) : 5000;
|
|
1169
|
+
return Math.max(SOCKET_TIMEOUT_MS, Math.min(timeoutMs + 1000, 31000));
|
|
1170
|
+
}
|
|
1171
|
+
async function requestJson(socketPath, payload, options = {}) {
|
|
1172
|
+
const bun = globalThis.Bun;
|
|
1173
|
+
if (bun?.connect) {
|
|
1174
|
+
return await requestJsonWithBun(bun.connect, socketPath, payload, options);
|
|
1175
|
+
}
|
|
1176
|
+
return await new Promise((resolve, reject) => {
|
|
1177
|
+
const socket = new Socket;
|
|
1178
|
+
let response = "";
|
|
1179
|
+
let settled = false;
|
|
1180
|
+
const finish = (error, value) => {
|
|
1181
|
+
if (settled)
|
|
1182
|
+
return;
|
|
1183
|
+
settled = true;
|
|
1184
|
+
clearTimeout(timer);
|
|
1185
|
+
socket.removeAllListeners();
|
|
1186
|
+
socket.destroy();
|
|
1187
|
+
if (error) {
|
|
1188
|
+
reject(error);
|
|
1189
|
+
} else {
|
|
1190
|
+
resolve(value);
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
const timeoutMs = options.timeoutMs ?? SOCKET_TIMEOUT_MS;
|
|
1194
|
+
const timer = setTimeout(() => {
|
|
1195
|
+
finish(new Error(`scoutd probe socket timed out after ${timeoutMs}ms`));
|
|
1196
|
+
}, timeoutMs);
|
|
1197
|
+
timer.unref?.();
|
|
1198
|
+
const finishFromResponse = () => {
|
|
1199
|
+
if (settled)
|
|
1200
|
+
return;
|
|
1201
|
+
try {
|
|
1202
|
+
const parsed = JSON.parse(response);
|
|
1203
|
+
if (isRecord2(parsed) && parsed.schema === ERROR_SCHEMA) {
|
|
1204
|
+
const error = isRecord2(parsed.error) ? parsed.error : {};
|
|
1205
|
+
const message = readString(error.message) ?? readString(error.code) ?? "scoutd probe request failed";
|
|
1206
|
+
finish(new Error(message));
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
finish(null, parsed);
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
finish(new Error(`scoutd probe response was not JSON: ${fallbackMessage(error)}`));
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
socket.setEncoding("utf8");
|
|
1215
|
+
socket.on("connect", () => {
|
|
1216
|
+
socket.write(`${JSON.stringify(payload)}
|
|
1217
|
+
`);
|
|
1218
|
+
});
|
|
1219
|
+
socket.on("data", (chunk) => {
|
|
1220
|
+
response += chunk;
|
|
1221
|
+
if (Buffer.byteLength(response, "utf8") > MAX_RESPONSE_BYTES) {
|
|
1222
|
+
finish(new Error("scoutd probe response exceeded output limit"));
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
socket.on("error", (error) => finish(error));
|
|
1226
|
+
socket.on("end", finishFromResponse);
|
|
1227
|
+
socket.on("close", () => {
|
|
1228
|
+
if (!settled) {
|
|
1229
|
+
if (response.length === 0) {
|
|
1230
|
+
finish(new Error("scoutd probe socket closed without a response"));
|
|
1231
|
+
} else {
|
|
1232
|
+
finishFromResponse();
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
socket.connect(socketPath);
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
async function requestJsonWithBun(connect, socketPath, payload, options = {}) {
|
|
1240
|
+
return await new Promise((resolve, reject) => {
|
|
1241
|
+
let response = "";
|
|
1242
|
+
let settled = false;
|
|
1243
|
+
let socketRef = null;
|
|
1244
|
+
const decoder = new TextDecoder;
|
|
1245
|
+
const finish = (error, value) => {
|
|
1246
|
+
if (settled)
|
|
1247
|
+
return;
|
|
1248
|
+
settled = true;
|
|
1249
|
+
clearTimeout(timer);
|
|
1250
|
+
try {
|
|
1251
|
+
socketRef?.end?.();
|
|
1252
|
+
} catch {}
|
|
1253
|
+
if (error) {
|
|
1254
|
+
reject(error);
|
|
1255
|
+
} else {
|
|
1256
|
+
resolve(value);
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
const finishFromResponse = () => {
|
|
1260
|
+
if (settled)
|
|
1261
|
+
return;
|
|
1262
|
+
try {
|
|
1263
|
+
const parsed = JSON.parse(response);
|
|
1264
|
+
if (isRecord2(parsed) && parsed.schema === ERROR_SCHEMA) {
|
|
1265
|
+
const error = isRecord2(parsed.error) ? parsed.error : {};
|
|
1266
|
+
const message = readString(error.message) ?? readString(error.code) ?? "scoutd probe request failed";
|
|
1267
|
+
finish(new Error(message));
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
finish(null, parsed);
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
finish(new Error(`scoutd probe response was not JSON: ${fallbackMessage(error)}`));
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
const timeoutMs = options.timeoutMs ?? SOCKET_TIMEOUT_MS;
|
|
1276
|
+
const timer = setTimeout(() => {
|
|
1277
|
+
finish(new Error(`scoutd probe socket timed out after ${timeoutMs}ms`));
|
|
1278
|
+
}, timeoutMs);
|
|
1279
|
+
timer.unref?.();
|
|
1280
|
+
connect({
|
|
1281
|
+
unix: socketPath,
|
|
1282
|
+
socket: {
|
|
1283
|
+
open(socket) {
|
|
1284
|
+
socketRef = socket;
|
|
1285
|
+
socket.write(`${JSON.stringify(payload)}
|
|
1286
|
+
`);
|
|
1287
|
+
},
|
|
1288
|
+
data(_socket, data) {
|
|
1289
|
+
response += decoder.decode(data, { stream: true });
|
|
1290
|
+
if (Buffer.byteLength(response, "utf8") > MAX_RESPONSE_BYTES) {
|
|
1291
|
+
finish(new Error("scoutd probe response exceeded output limit"));
|
|
1292
|
+
}
|
|
1293
|
+
},
|
|
1294
|
+
close() {
|
|
1295
|
+
if (response.length === 0) {
|
|
1296
|
+
finish(new Error("scoutd probe socket closed without a response"));
|
|
1297
|
+
} else {
|
|
1298
|
+
response += decoder.decode();
|
|
1299
|
+
finishFromResponse();
|
|
1300
|
+
}
|
|
1301
|
+
},
|
|
1302
|
+
error(_socket, error) {
|
|
1303
|
+
finish(error);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}).then((socket) => {
|
|
1307
|
+
socketRef = socket;
|
|
1308
|
+
}, (error) => {
|
|
1309
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
1310
|
+
});
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
function fallbackKey(probeId, key) {
|
|
1314
|
+
return `${probeId}\x00${key ?? ""}`;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// packages/runtime/src/system-probes/exec.ts
|
|
1318
|
+
class ProbeCommandError extends Error {
|
|
1319
|
+
code;
|
|
1320
|
+
exitCode;
|
|
1321
|
+
signal;
|
|
1322
|
+
constructor(message, options) {
|
|
1323
|
+
super(message);
|
|
1324
|
+
this.name = "ProbeCommandError";
|
|
1325
|
+
this.code = options.code;
|
|
1326
|
+
this.exitCode = options.exitCode;
|
|
1327
|
+
this.signal = options.signal;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
var DEFAULT_STDOUT_CAP_BYTES = 1024 * 1024;
|
|
1331
|
+
var DEFAULT_STDERR_CAP_BYTES = 128 * 1024;
|
|
1332
|
+
var SIGKILL_DELAY_MS = 500;
|
|
1333
|
+
var EXEC_SYSTEM_TRANSPORT_METADATA = Symbol.for("openscout.execSystem.transport");
|
|
1334
|
+
var spawnProcess = spawn;
|
|
1335
|
+
function bufferByteLength(chunks) {
|
|
1336
|
+
return chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
1337
|
+
}
|
|
1338
|
+
function abortMessage(ctx) {
|
|
1339
|
+
const reason = ctx.signal.reason;
|
|
1340
|
+
if (typeof reason === "object" && reason !== null && "message" in reason) {
|
|
1341
|
+
const message = reason.message;
|
|
1342
|
+
if (typeof message === "string" && message.trim()) {
|
|
1343
|
+
return message;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return `Probe ${ctx.probeId} aborted`;
|
|
1347
|
+
}
|
|
1348
|
+
async function execProbeFile(ctx, file, args, options = {}) {
|
|
1349
|
+
const maxStdoutBytes = options.maxStdoutBytes ?? DEFAULT_STDOUT_CAP_BYTES;
|
|
1350
|
+
const maxStderrBytes = options.maxStderrBytes ?? DEFAULT_STDERR_CAP_BYTES;
|
|
1351
|
+
return await new Promise((resolve, reject) => {
|
|
1352
|
+
if (ctx.signal.aborted) {
|
|
1353
|
+
reject(new ProbeCommandError(abortMessage(ctx), { code: "aborted" }));
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
const stdoutChunks = [];
|
|
1357
|
+
const stderrChunks = [];
|
|
1358
|
+
let settled = false;
|
|
1359
|
+
let killTimer = null;
|
|
1360
|
+
const child = spawnProcess(file, [...args], {
|
|
1361
|
+
cwd: options.cwd,
|
|
1362
|
+
env: options.env,
|
|
1363
|
+
stdio: [options.input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
1364
|
+
windowsHide: true
|
|
1365
|
+
});
|
|
1366
|
+
const cleanup = () => {
|
|
1367
|
+
ctx.signal.removeEventListener("abort", onAbort);
|
|
1368
|
+
if (killTimer) {
|
|
1369
|
+
clearTimeout(killTimer);
|
|
1370
|
+
killTimer = null;
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
const fail = (error) => {
|
|
1374
|
+
if (settled) {
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
settled = true;
|
|
1378
|
+
cleanup();
|
|
1379
|
+
if (!child.killed) {
|
|
1380
|
+
child.kill("SIGTERM");
|
|
1381
|
+
}
|
|
1382
|
+
reject(error);
|
|
1383
|
+
};
|
|
1384
|
+
const onAbort = () => {
|
|
1385
|
+
if (settled) {
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
settled = true;
|
|
1389
|
+
ctx.signal.removeEventListener("abort", onAbort);
|
|
1390
|
+
child.kill("SIGTERM");
|
|
1391
|
+
killTimer = setTimeout(() => {
|
|
1392
|
+
child.kill("SIGKILL");
|
|
1393
|
+
}, SIGKILL_DELAY_MS);
|
|
1394
|
+
reject(new ProbeCommandError(abortMessage(ctx), { code: "timeout" }));
|
|
1395
|
+
};
|
|
1396
|
+
ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
1397
|
+
if (options.input !== undefined) {
|
|
1398
|
+
child.stdin?.end(options.input);
|
|
1399
|
+
}
|
|
1400
|
+
child.stdout?.on("data", (chunk) => {
|
|
1401
|
+
if (settled) {
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
stdoutChunks.push(chunk);
|
|
1405
|
+
if (bufferByteLength(stdoutChunks) > maxStdoutBytes) {
|
|
1406
|
+
fail(new ProbeCommandError(`Probe ${ctx.probeId} stdout exceeded ${maxStdoutBytes} bytes`, { code: "output_cap" }));
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1409
|
+
child.stderr?.on("data", (chunk) => {
|
|
1410
|
+
if (settled) {
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
stderrChunks.push(chunk);
|
|
1414
|
+
if (bufferByteLength(stderrChunks) > maxStderrBytes) {
|
|
1415
|
+
fail(new ProbeCommandError(`Probe ${ctx.probeId} stderr exceeded ${maxStderrBytes} bytes`, { code: "output_cap" }));
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
child.once("error", (error) => {
|
|
1419
|
+
if (settled) {
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
settled = true;
|
|
1423
|
+
cleanup();
|
|
1424
|
+
const code = typeof error.code === "string" ? String(error.code) : "spawn";
|
|
1425
|
+
reject(new ProbeCommandError(error.message, { code }));
|
|
1426
|
+
});
|
|
1427
|
+
child.once("close", (exitCode, signal) => {
|
|
1428
|
+
if (settled) {
|
|
1429
|
+
cleanup();
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
settled = true;
|
|
1433
|
+
cleanup();
|
|
1434
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
1435
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
|
1436
|
+
if (exitCode === 0) {
|
|
1437
|
+
resolve({ stdout, stderr, exitCode });
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
reject(new ProbeCommandError(`${file} exited with ${exitCode ?? signal ?? "unknown status"}`, { code: "exit", exitCode, signal }));
|
|
1441
|
+
});
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// packages/runtime/src/system-probes/tailscale-status.ts
|
|
1446
|
+
var DEFAULT_TAILSCALE_STATUS_TIMEOUT_MS = 1500;
|
|
1447
|
+
function parseTailscaleStatusJson(raw) {
|
|
1448
|
+
return JSON.parse(raw);
|
|
1449
|
+
}
|
|
1450
|
+
function parsePeers(status) {
|
|
1451
|
+
const peers = Object.entries(status.Peer ?? {});
|
|
1452
|
+
return peers.map(([fallbackId, peer]) => ({
|
|
1453
|
+
id: peer.ID ?? fallbackId,
|
|
1454
|
+
name: peer.HostName ?? peer.DNSName ?? fallbackId,
|
|
1455
|
+
dnsName: peer.DNSName,
|
|
1456
|
+
addresses: peer.TailscaleIPs ?? [],
|
|
1457
|
+
online: peer.Online ?? false,
|
|
1458
|
+
hostName: peer.HostName,
|
|
1459
|
+
os: peer.OS,
|
|
1460
|
+
tags: peer.Tags ?? []
|
|
1461
|
+
}));
|
|
1462
|
+
}
|
|
1463
|
+
function parseSelf(status) {
|
|
1464
|
+
const self = status.Self;
|
|
1465
|
+
if (!self) {
|
|
1466
|
+
return null;
|
|
1467
|
+
}
|
|
1468
|
+
return {
|
|
1469
|
+
id: self.ID ?? self.DNSName ?? self.HostName ?? "self",
|
|
1470
|
+
name: self.HostName ?? self.DNSName ?? "self",
|
|
1471
|
+
dnsName: self.DNSName,
|
|
1472
|
+
addresses: self.TailscaleIPs ?? [],
|
|
1473
|
+
online: self.Online ?? true,
|
|
1474
|
+
hostName: self.HostName,
|
|
1475
|
+
os: self.OS,
|
|
1476
|
+
tailnetName: status.CurrentTailnet?.Name,
|
|
1477
|
+
magicDnsSuffix: status.CurrentTailnet?.MagicDNSSuffix
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
function isTailscaleBackendRunning(status) {
|
|
1481
|
+
return (status.BackendState ?? "").trim().toLowerCase() === "running";
|
|
1482
|
+
}
|
|
1483
|
+
function normalizeHost(value) {
|
|
1484
|
+
const normalized = value?.trim().replace(/\.$/, "").toLowerCase();
|
|
1485
|
+
if (!normalized || normalized.includes("/") || /\s/.test(normalized)) {
|
|
1486
|
+
return null;
|
|
1487
|
+
}
|
|
1488
|
+
return normalized;
|
|
1489
|
+
}
|
|
1490
|
+
function uniq(values) {
|
|
1491
|
+
const seen = new Set;
|
|
1492
|
+
const out = [];
|
|
1493
|
+
for (const value of values) {
|
|
1494
|
+
const normalized = normalizeHost(value ?? undefined);
|
|
1495
|
+
if (!normalized || seen.has(normalized)) {
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
seen.add(normalized);
|
|
1499
|
+
out.push(normalized);
|
|
1500
|
+
}
|
|
1501
|
+
return out;
|
|
1502
|
+
}
|
|
1503
|
+
function tailscaleSelfWebHosts(self) {
|
|
1504
|
+
if (!self) {
|
|
1505
|
+
return [];
|
|
1506
|
+
}
|
|
1507
|
+
return uniq([
|
|
1508
|
+
self.dnsName,
|
|
1509
|
+
self.hostName && self.magicDnsSuffix ? `${self.hostName}.${self.magicDnsSuffix}` : undefined,
|
|
1510
|
+
...self.addresses
|
|
1511
|
+
]);
|
|
1512
|
+
}
|
|
1513
|
+
function summarizeTailscaleStatus(status) {
|
|
1514
|
+
return {
|
|
1515
|
+
backendState: status.BackendState ?? null,
|
|
1516
|
+
running: isTailscaleBackendRunning(status),
|
|
1517
|
+
health: status.Health ?? [],
|
|
1518
|
+
peers: parsePeers(status),
|
|
1519
|
+
self: parseSelf(status)
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
function statusTimeoutMs(env) {
|
|
1523
|
+
const parsed = Number.parseInt(env.OPENSCOUT_TAILSCALE_STATUS_TIMEOUT_MS ?? "", 10);
|
|
1524
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TAILSCALE_STATUS_TIMEOUT_MS;
|
|
1525
|
+
}
|
|
1526
|
+
async function readStatusJsonFromFile(filePath) {
|
|
1527
|
+
try {
|
|
1528
|
+
const raw = await readFile(filePath, "utf8");
|
|
1529
|
+
return parseTailscaleStatusJson(raw);
|
|
1530
|
+
} catch {
|
|
1531
|
+
return null;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
function isDomainUnavailableError(error) {
|
|
1535
|
+
if (!(error instanceof ProbeCommandError)) {
|
|
1536
|
+
return false;
|
|
1537
|
+
}
|
|
1538
|
+
return error.code === "ENOENT" || error.code === "spawn" || error.code === "exit";
|
|
1539
|
+
}
|
|
1540
|
+
async function readTailscaleStatusSummaryLocal(ctx) {
|
|
1541
|
+
const fixturePath = process.env.OPENSCOUT_TAILSCALE_STATUS_JSON;
|
|
1542
|
+
if (fixturePath) {
|
|
1543
|
+
const status = await readStatusJsonFromFile(fixturePath);
|
|
1544
|
+
return status ? summarizeTailscaleStatus(status) : null;
|
|
1545
|
+
}
|
|
1546
|
+
const tailscaleBin = process.env.OPENSCOUT_TAILSCALE_BIN ?? "tailscale";
|
|
1547
|
+
try {
|
|
1548
|
+
const { stdout } = await execProbeFile(ctx, tailscaleBin, ["status", "--json"], {
|
|
1549
|
+
maxStdoutBytes: 4 * 1024 * 1024,
|
|
1550
|
+
maxStderrBytes: 256 * 1024
|
|
1551
|
+
});
|
|
1552
|
+
return summarizeTailscaleStatus(parseTailscaleStatusJson(stdout));
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
if (isDomainUnavailableError(error)) {
|
|
1555
|
+
return null;
|
|
1556
|
+
}
|
|
1557
|
+
throw error;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
var tailscaleStatusProbe = defineProbe({
|
|
1561
|
+
id: "tailscale.status",
|
|
1562
|
+
ttlMs: 30000,
|
|
1563
|
+
timeoutMs: statusTimeoutMs(process.env),
|
|
1564
|
+
run: (ctx) => runWithScoutdFallback({
|
|
1565
|
+
probeId: "tailscale.status",
|
|
1566
|
+
ctx,
|
|
1567
|
+
local: () => readTailscaleStatusSummaryLocal(ctx)
|
|
1568
|
+
})
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
// packages/runtime/src/tailscale.ts
|
|
1572
|
+
var DEFAULT_TAILSCALE_STATUS_TIMEOUT_MS2 = 1500;
|
|
1573
|
+
function readStatusJsonFromFileSync(filePath) {
|
|
1574
|
+
const raw = readFileSync2(filePath, "utf8");
|
|
1575
|
+
return parseTailscaleStatusJson(raw);
|
|
1576
|
+
}
|
|
1577
|
+
function statusTimeoutMs2(env) {
|
|
1578
|
+
const parsed = Number.parseInt(env.OPENSCOUT_TAILSCALE_STATUS_TIMEOUT_MS ?? "", 10);
|
|
1579
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TAILSCALE_STATUS_TIMEOUT_MS2;
|
|
1580
|
+
}
|
|
1581
|
+
function readStatusJsonSync(env = process.env) {
|
|
1582
|
+
if (env.OPENSCOUT_TAILSCALE_AUTO_HOSTS === "0") {
|
|
1583
|
+
return null;
|
|
1584
|
+
}
|
|
1585
|
+
const fixturePath = env.OPENSCOUT_TAILSCALE_STATUS_JSON;
|
|
1586
|
+
if (fixturePath) {
|
|
1587
|
+
try {
|
|
1588
|
+
return readStatusJsonFromFileSync(fixturePath);
|
|
1589
|
+
} catch {
|
|
1590
|
+
return null;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
const tailscaleBin = env.OPENSCOUT_TAILSCALE_BIN ?? (env === process.env ? "tailscale" : undefined);
|
|
1594
|
+
if (!tailscaleBin) {
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
try {
|
|
1598
|
+
const stdout = execFileSync(tailscaleBin, ["status", "--json"], {
|
|
1599
|
+
encoding: "utf8",
|
|
1600
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1601
|
+
timeout: statusTimeoutMs2(env),
|
|
1602
|
+
windowsHide: true
|
|
1603
|
+
});
|
|
1604
|
+
return parseTailscaleStatusJson(stdout);
|
|
1605
|
+
} catch {
|
|
1606
|
+
return null;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
function readTailscaleSelfWebHostsSync(env = process.env) {
|
|
1610
|
+
const status = readStatusJsonSync(env);
|
|
1611
|
+
if (!status || !isTailscaleBackendRunning(status)) {
|
|
1612
|
+
return [];
|
|
1613
|
+
}
|
|
1614
|
+
return tailscaleSelfWebHosts(summarizeTailscaleStatus(status).self);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// packages/runtime/src/local-config.ts
|
|
1618
|
+
import {
|
|
1619
|
+
existsSync as existsSync3,
|
|
1620
|
+
mkdirSync,
|
|
1621
|
+
readFileSync as readFileSync3,
|
|
1622
|
+
renameSync,
|
|
1623
|
+
writeFileSync
|
|
1624
|
+
} from "fs";
|
|
1625
|
+
import { homedir as homedir3, hostname as osHostname } from "os";
|
|
1626
|
+
import { dirname, join as join3 } from "path";
|
|
1627
|
+
var LOCAL_CONFIG_VERSION = 1;
|
|
1628
|
+
var DEFAULT_SCOUT_WEB_PORTAL_HOST = "scout.local";
|
|
1629
|
+
var DEFAULT_SCOUT_WEB_DEV_HOST = `dev.${DEFAULT_SCOUT_WEB_PORTAL_HOST}`;
|
|
1630
|
+
var DEFAULT_SCOUT_WEB_VITE_HMR_PATH = "/ws/hmr";
|
|
1631
|
+
var OPENSCOUT_PORTS = {
|
|
1632
|
+
broker: 43110,
|
|
1633
|
+
web: 43120,
|
|
1634
|
+
webTerminalRelay: 43121,
|
|
1635
|
+
vite: 43122,
|
|
1636
|
+
pairingBridge: 43130,
|
|
1637
|
+
pairingRelay: 43131,
|
|
1638
|
+
pairingFileServer: 43132,
|
|
1639
|
+
studio: 43140
|
|
1640
|
+
};
|
|
1641
|
+
var DEFAULT_LOCAL_CONFIG = {
|
|
1642
|
+
version: LOCAL_CONFIG_VERSION,
|
|
1643
|
+
host: "127.0.0.1",
|
|
1644
|
+
webLocalName: undefined,
|
|
1645
|
+
ports: {
|
|
1646
|
+
broker: OPENSCOUT_PORTS.broker,
|
|
1647
|
+
web: OPENSCOUT_PORTS.web,
|
|
1648
|
+
pairing: OPENSCOUT_PORTS.pairingBridge
|
|
1649
|
+
}
|
|
1650
|
+
};
|
|
1651
|
+
function normalizeLocalHostnameLabel(value) {
|
|
1652
|
+
const firstLabel = value?.trim().replace(/\.local\.?$/i, "").split(".").find((part) => part.trim().length > 0)?.trim();
|
|
1653
|
+
const normalized = firstLabel?.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
|
|
1654
|
+
return normalized || "localhost";
|
|
1655
|
+
}
|
|
1656
|
+
function normalizeLocalHostname(value) {
|
|
1657
|
+
const trimmed = value?.trim().replace(/\.$/, "").toLowerCase();
|
|
1658
|
+
const labels = trimmed?.split(".").map((label) => label.trim().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-")).filter(Boolean);
|
|
1659
|
+
return labels && labels.length > 0 ? labels.join(".") : "localhost";
|
|
1660
|
+
}
|
|
1661
|
+
function resolveScoutWebNamedHostname(name) {
|
|
1662
|
+
const normalized = normalizeLocalHostname(name);
|
|
1663
|
+
return normalized.includes(".") ? normalized : `${normalized}.${DEFAULT_SCOUT_WEB_PORTAL_HOST}`;
|
|
1664
|
+
}
|
|
1665
|
+
function resolveScoutWebDevHostname(portalHost = DEFAULT_SCOUT_WEB_PORTAL_HOST) {
|
|
1666
|
+
const normalized = portalHost.trim().replace(/\.$/, "").toLowerCase();
|
|
1667
|
+
if (!normalized || normalized.startsWith("dev.")) {
|
|
1668
|
+
return normalized || DEFAULT_SCOUT_WEB_DEV_HOST;
|
|
1669
|
+
}
|
|
1670
|
+
return `dev.${normalized}`;
|
|
1671
|
+
}
|
|
1672
|
+
function resolveConfiguredScoutWebHostname(config = loadLocalConfig(), machineHostname = osHostname()) {
|
|
1673
|
+
if (config.webLocalName) {
|
|
1674
|
+
return resolveScoutWebNamedHostname(config.webLocalName);
|
|
1675
|
+
}
|
|
1676
|
+
return `${normalizeLocalHostnameLabel(machineHostname)}.${DEFAULT_SCOUT_WEB_PORTAL_HOST}`;
|
|
1677
|
+
}
|
|
1678
|
+
function localConfigHome() {
|
|
1679
|
+
return process.env.OPENSCOUT_HOME ?? join3(homedir3(), ".openscout");
|
|
1680
|
+
}
|
|
1681
|
+
function localConfigPath() {
|
|
1682
|
+
return join3(localConfigHome(), "config.json");
|
|
1683
|
+
}
|
|
1684
|
+
function loadLocalConfig() {
|
|
1685
|
+
const configPath = localConfigPath();
|
|
1686
|
+
if (!existsSync3(configPath))
|
|
1687
|
+
return { version: LOCAL_CONFIG_VERSION };
|
|
1688
|
+
try {
|
|
1689
|
+
return validateLocalConfig(JSON.parse(readFileSync3(configPath, "utf8")));
|
|
1690
|
+
} catch {
|
|
1691
|
+
return { version: LOCAL_CONFIG_VERSION };
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
function validateLocalConfig(input) {
|
|
1695
|
+
if (!input || typeof input !== "object")
|
|
1696
|
+
return { version: LOCAL_CONFIG_VERSION };
|
|
1697
|
+
const raw = input;
|
|
1698
|
+
const out = { version: LOCAL_CONFIG_VERSION };
|
|
1699
|
+
if (typeof raw.host === "string" && raw.host.trim().length > 0) {
|
|
1700
|
+
out.host = raw.host.trim();
|
|
1701
|
+
}
|
|
1702
|
+
if (typeof raw.webLocalName === "string" && raw.webLocalName.trim().length > 0) {
|
|
1703
|
+
out.webLocalName = raw.webLocalName.trim();
|
|
1704
|
+
}
|
|
1705
|
+
if (raw.ports && typeof raw.ports === "object") {
|
|
1706
|
+
const ports = raw.ports;
|
|
1707
|
+
const p = {};
|
|
1708
|
+
if (isValidPort(ports.broker))
|
|
1709
|
+
p.broker = ports.broker;
|
|
1710
|
+
if (isValidPort(ports.web))
|
|
1711
|
+
p.web = ports.web;
|
|
1712
|
+
if (isValidPort(ports.pairing))
|
|
1713
|
+
p.pairing = ports.pairing;
|
|
1714
|
+
if (Object.keys(p).length > 0)
|
|
1715
|
+
out.ports = p;
|
|
1716
|
+
}
|
|
1717
|
+
return out;
|
|
1718
|
+
}
|
|
1719
|
+
function isValidPort(value) {
|
|
1720
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 && value < 65536;
|
|
1721
|
+
}
|
|
1722
|
+
function parseEnvPort(name) {
|
|
1723
|
+
const raw = process.env[name]?.trim();
|
|
1724
|
+
if (!raw) {
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1728
|
+
return isValidPort(parsed) ? parsed : undefined;
|
|
1729
|
+
}
|
|
1730
|
+
function resolveEffectiveLocalConfig() {
|
|
1731
|
+
const file = loadLocalConfig();
|
|
1732
|
+
const host = process.env.OPENSCOUT_BROKER_HOST?.trim() || process.env.OPENSCOUT_HOST?.trim() || file.host;
|
|
1733
|
+
const ports = {
|
|
1734
|
+
broker: parseEnvPort("OPENSCOUT_BROKER_PORT") ?? file.ports?.broker,
|
|
1735
|
+
web: parseEnvPort("OPENSCOUT_WEB_PORT") ?? parseEnvPort("SCOUT_WEB_PORT") ?? file.ports?.web,
|
|
1736
|
+
pairing: parseEnvPort("OPENSCOUT_PAIRING_PORT") ?? file.ports?.pairing
|
|
1737
|
+
};
|
|
1738
|
+
return {
|
|
1739
|
+
version: LOCAL_CONFIG_VERSION,
|
|
1740
|
+
...host ? { host } : {},
|
|
1741
|
+
...file.webLocalName ? { webLocalName: file.webLocalName } : {},
|
|
1742
|
+
...ports.broker || ports.web || ports.pairing ? { ports } : {}
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
function resolveBrokerPort() {
|
|
1746
|
+
return resolveEffectiveLocalConfig().ports?.broker ?? DEFAULT_LOCAL_CONFIG.ports.broker;
|
|
1747
|
+
}
|
|
1748
|
+
function resolveWebPort() {
|
|
1749
|
+
return resolveEffectiveLocalConfig().ports?.web ?? DEFAULT_LOCAL_CONFIG.ports.web;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// packages/runtime/src/tool-resolution.ts
|
|
1753
|
+
import { accessSync, constants, existsSync as existsSync4 } from "fs";
|
|
1754
|
+
import { homedir as homedir4 } from "os";
|
|
1755
|
+
import { basename, dirname as dirname2, join as join4, resolve } from "path";
|
|
234
1756
|
var DEFAULT_COMMON_EXECUTABLE_DIRECTORIES = [
|
|
235
|
-
|
|
1757
|
+
join4(homedir4(), ".bun", "bin"),
|
|
236
1758
|
"/opt/homebrew/bin",
|
|
237
1759
|
"/usr/local/bin"
|
|
238
1760
|
];
|
|
239
1761
|
function expandHomePath(value, env = process.env) {
|
|
240
|
-
const home = env.HOME ??
|
|
1762
|
+
const home = env.HOME ?? homedir4();
|
|
241
1763
|
if (value === "~") {
|
|
242
1764
|
return home;
|
|
243
1765
|
}
|
|
244
1766
|
if (value.startsWith("~/")) {
|
|
245
|
-
return
|
|
1767
|
+
return join4(home, value.slice(2));
|
|
246
1768
|
}
|
|
247
1769
|
return value;
|
|
248
1770
|
}
|
|
@@ -299,7 +1821,7 @@ function resolveExecutableFromSearch(options) {
|
|
|
299
1821
|
], env);
|
|
300
1822
|
for (const directory of searchDirectories) {
|
|
301
1823
|
for (const name of options.names) {
|
|
302
|
-
const candidate =
|
|
1824
|
+
const candidate = join4(directory, name);
|
|
303
1825
|
if (isExecutablePath(candidate)) {
|
|
304
1826
|
return {
|
|
305
1827
|
path: candidate,
|
|
@@ -323,7 +1845,7 @@ function findExecutableOnSearchPath(name, env) {
|
|
|
323
1845
|
...DEFAULT_COMMON_EXECUTABLE_DIRECTORIES
|
|
324
1846
|
], env);
|
|
325
1847
|
for (const directory of searchDirectories) {
|
|
326
|
-
const candidate =
|
|
1848
|
+
const candidate = join4(directory, name);
|
|
327
1849
|
if (isExecutablePath(candidate)) {
|
|
328
1850
|
return { path: candidate, source: "path" };
|
|
329
1851
|
}
|
|
@@ -331,37 +1853,116 @@ function findExecutableOnSearchPath(name, env) {
|
|
|
331
1853
|
return null;
|
|
332
1854
|
}
|
|
333
1855
|
|
|
1856
|
+
// packages/runtime/src/runtime-adapters.ts
|
|
1857
|
+
function normalizeRuntimeHost(value) {
|
|
1858
|
+
const normalized = value?.trim().toLowerCase();
|
|
1859
|
+
if (normalized === "bun")
|
|
1860
|
+
return "bun";
|
|
1861
|
+
if (normalized === "node")
|
|
1862
|
+
return "node";
|
|
1863
|
+
return null;
|
|
1864
|
+
}
|
|
1865
|
+
function normalizeRuntimeServiceAdapter(value) {
|
|
1866
|
+
const normalized = value?.trim().toLowerCase().replaceAll("_", "-");
|
|
1867
|
+
if (normalized === "macos-scoutd" || normalized === "macos-launchd")
|
|
1868
|
+
return "macos-scoutd";
|
|
1869
|
+
if (normalized === "linux-systemd-user" || normalized === "systemd-user")
|
|
1870
|
+
return "linux-systemd-user";
|
|
1871
|
+
if (normalized === "headless-foreground" || normalized === "foreground" || normalized === "headless") {
|
|
1872
|
+
return "headless-foreground";
|
|
1873
|
+
}
|
|
1874
|
+
if (normalized === "windows-service")
|
|
1875
|
+
return "windows-service";
|
|
1876
|
+
return null;
|
|
1877
|
+
}
|
|
1878
|
+
function defaultServiceAdapterForPlatform(platform = process.platform, env = process.env) {
|
|
1879
|
+
const explicit = normalizeRuntimeServiceAdapter(env.OPENSCOUT_SERVICE_ADAPTER);
|
|
1880
|
+
if (explicit)
|
|
1881
|
+
return explicit;
|
|
1882
|
+
if (normalizeRuntimeHost(env.OPENSCOUT_RUNTIME_HOST) === "node") {
|
|
1883
|
+
return "headless-foreground";
|
|
1884
|
+
}
|
|
1885
|
+
if (platform === "darwin")
|
|
1886
|
+
return "macos-scoutd";
|
|
1887
|
+
if (platform === "linux")
|
|
1888
|
+
return "headless-foreground";
|
|
1889
|
+
if (platform === "win32")
|
|
1890
|
+
return "windows-service";
|
|
1891
|
+
return "headless-foreground";
|
|
1892
|
+
}
|
|
1893
|
+
|
|
334
1894
|
// packages/runtime/src/broker-process-manager.ts
|
|
335
1895
|
function isTmpPath(p) {
|
|
336
1896
|
return /^\/(?:private\/)?tmp\//.test(p);
|
|
337
1897
|
}
|
|
338
1898
|
var DEFAULT_BROKER_HOST = "127.0.0.1";
|
|
339
1899
|
var DEFAULT_BROKER_HOST_MESH = "0.0.0.0";
|
|
340
|
-
var DEFAULT_BROKER_PORT =
|
|
1900
|
+
var DEFAULT_BROKER_PORT = OPENSCOUT_PORTS.broker;
|
|
341
1901
|
var DEFAULT_ADVERTISE_SCOPE = "local";
|
|
342
|
-
function resolveAdvertiseScope() {
|
|
343
|
-
|
|
1902
|
+
function resolveAdvertiseScope(env = process.env) {
|
|
1903
|
+
if (openScoutNetworkDiscoveryEnabled(env))
|
|
1904
|
+
return "mesh";
|
|
1905
|
+
const raw = (env.OPENSCOUT_ADVERTISE_SCOPE ?? "").trim().toLowerCase();
|
|
344
1906
|
if (raw === "mesh")
|
|
345
1907
|
return "mesh";
|
|
346
1908
|
if (raw === "local")
|
|
347
1909
|
return "local";
|
|
348
1910
|
return DEFAULT_ADVERTISE_SCOPE;
|
|
349
1911
|
}
|
|
350
|
-
function resolveBrokerHost(scope = resolveAdvertiseScope()) {
|
|
351
|
-
const explicit =
|
|
352
|
-
if (
|
|
1912
|
+
function resolveBrokerHost(scope = resolveAdvertiseScope(), env = process.env) {
|
|
1913
|
+
const explicit = env.OPENSCOUT_BROKER_HOST?.trim();
|
|
1914
|
+
if (explicit) {
|
|
1915
|
+
if (scope === "mesh" && isLoopbackHost(explicit)) {
|
|
1916
|
+
return DEFAULT_BROKER_HOST_MESH;
|
|
1917
|
+
}
|
|
353
1918
|
return explicit;
|
|
354
1919
|
}
|
|
355
1920
|
return scope === "mesh" ? DEFAULT_BROKER_HOST_MESH : DEFAULT_BROKER_HOST;
|
|
356
1921
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
1922
|
+
function isLoopbackHost(host) {
|
|
1923
|
+
const trimmed = host.trim();
|
|
1924
|
+
return trimmed === "127.0.0.1" || trimmed === "::1" || trimmed === "localhost";
|
|
1925
|
+
}
|
|
1926
|
+
function localBrokerControlHost(host) {
|
|
1927
|
+
const trimmed = host.trim();
|
|
1928
|
+
if (!trimmed || isWildcardHost(trimmed)) {
|
|
1929
|
+
return DEFAULT_BROKER_HOST;
|
|
1930
|
+
}
|
|
1931
|
+
return trimmed;
|
|
1932
|
+
}
|
|
360
1933
|
function buildDefaultBrokerUrl(host = DEFAULT_BROKER_HOST, port = DEFAULT_BROKER_PORT) {
|
|
361
1934
|
return `http://${host}:${port}`;
|
|
362
1935
|
}
|
|
1936
|
+
function resolveBrokerUrl(host, port, scope, env = process.env) {
|
|
1937
|
+
const explicit = env.OPENSCOUT_BROKER_URL?.trim();
|
|
1938
|
+
if (explicit && !(scope === "mesh" && isUnreachableMeshBrokerUrl(explicit))) {
|
|
1939
|
+
return explicit;
|
|
1940
|
+
}
|
|
1941
|
+
if (scope === "mesh") {
|
|
1942
|
+
const tailnetHost = readTailscaleSelfWebHostsSync(env)[0];
|
|
1943
|
+
if (tailnetHost) {
|
|
1944
|
+
return buildDefaultBrokerUrl(tailnetHost, port);
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
return buildDefaultBrokerUrl(host, port);
|
|
1948
|
+
}
|
|
1949
|
+
function isUnreachableMeshBrokerUrl(value) {
|
|
1950
|
+
try {
|
|
1951
|
+
const host = new URL(value).hostname;
|
|
1952
|
+
return isLoopbackHost(host) || isWildcardHost(host);
|
|
1953
|
+
} catch {
|
|
1954
|
+
return false;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
function isWildcardHost(host) {
|
|
1958
|
+
const trimmed = host.trim();
|
|
1959
|
+
return trimmed === "0.0.0.0" || trimmed === "::" || trimmed === "[::]";
|
|
1960
|
+
}
|
|
1961
|
+
function buildLocalBrokerControlUrl(host = DEFAULT_BROKER_HOST, port = DEFAULT_BROKER_PORT) {
|
|
1962
|
+
return buildDefaultBrokerUrl(localBrokerControlHost(host), port);
|
|
1963
|
+
}
|
|
363
1964
|
function buildDefaultBrokerSocketPath(runtimeDirectory) {
|
|
364
|
-
return
|
|
1965
|
+
return join5(runtimeDirectory, "broker.sock");
|
|
365
1966
|
}
|
|
366
1967
|
var DEFAULT_BROKER_URL = buildDefaultBrokerUrl();
|
|
367
1968
|
function runtimePackageDir() {
|
|
@@ -377,17 +1978,17 @@ function runtimePackageDir() {
|
|
|
377
1978
|
const fromGlobal = findGlobalRuntimeDir();
|
|
378
1979
|
if (fromGlobal)
|
|
379
1980
|
return fromGlobal;
|
|
380
|
-
const moduleDir =
|
|
1981
|
+
const moduleDir = dirname3(fileURLToPath(import.meta.url));
|
|
381
1982
|
return resolve2(moduleDir, "..");
|
|
382
1983
|
}
|
|
383
1984
|
function isInstalledRuntimePackageDir(candidate) {
|
|
384
|
-
return
|
|
1985
|
+
return existsSync5(join5(candidate, "package.json")) && existsSync5(join5(candidate, "bin", "openscout-runtime.mjs"));
|
|
385
1986
|
}
|
|
386
1987
|
function findGlobalRuntimeDir() {
|
|
387
1988
|
const candidates = [
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
1989
|
+
join5(homedir5(), ".bun", "node_modules", "@openscout", "runtime"),
|
|
1990
|
+
join5(homedir5(), ".bun", "install", "global", "node_modules", "@openscout", "runtime"),
|
|
1991
|
+
join5(homedir5(), ".bun", "install", "global", "node_modules", "@openscout", "scout", "node_modules", "@openscout", "runtime")
|
|
391
1992
|
];
|
|
392
1993
|
for (const c of candidates) {
|
|
393
1994
|
if (isInstalledRuntimePackageDir(c))
|
|
@@ -400,7 +2001,7 @@ function findGlobalRuntimeDir() {
|
|
|
400
2001
|
const scoutPkg = resolve2(scoutBin, "..", "..");
|
|
401
2002
|
if (isInstalledRuntimePackageDir(scoutPkg))
|
|
402
2003
|
return scoutPkg;
|
|
403
|
-
const nested =
|
|
2004
|
+
const nested = join5(scoutPkg, "node_modules", "@openscout", "runtime");
|
|
404
2005
|
if (isInstalledRuntimePackageDir(nested))
|
|
405
2006
|
return nested;
|
|
406
2007
|
const sibling = resolve2(scoutPkg, "..", "runtime");
|
|
@@ -411,30 +2012,43 @@ function findGlobalRuntimeDir() {
|
|
|
411
2012
|
return null;
|
|
412
2013
|
}
|
|
413
2014
|
function findBundledRuntimeDir() {
|
|
414
|
-
const moduleDir =
|
|
415
|
-
|
|
416
|
-
|
|
2015
|
+
const moduleDir = dirname3(fileURLToPath(import.meta.url));
|
|
2016
|
+
return resolveBundledRuntimeDirFromModuleDir(moduleDir);
|
|
2017
|
+
}
|
|
2018
|
+
function resolveBundledRuntimeDirFromModuleDir(moduleDir) {
|
|
2019
|
+
const candidates = [
|
|
2020
|
+
resolve2(moduleDir, ".."),
|
|
2021
|
+
resolve2(moduleDir, "..", "..")
|
|
2022
|
+
];
|
|
2023
|
+
for (const candidate of candidates) {
|
|
2024
|
+
if (isInstalledRuntimePackageDir(candidate))
|
|
2025
|
+
return candidate;
|
|
2026
|
+
}
|
|
2027
|
+
return null;
|
|
417
2028
|
}
|
|
418
2029
|
function findWorkspaceRuntimeDir(startDir) {
|
|
419
2030
|
let current = resolve2(startDir);
|
|
420
2031
|
while (true) {
|
|
421
|
-
const candidate =
|
|
422
|
-
if (
|
|
2032
|
+
const candidate = join5(current, "packages", "runtime");
|
|
2033
|
+
if (existsSync5(join5(candidate, "package.json")) && existsSync5(join5(candidate, "src"))) {
|
|
423
2034
|
return candidate;
|
|
424
2035
|
}
|
|
425
|
-
const parent =
|
|
2036
|
+
const parent = dirname3(current);
|
|
426
2037
|
if (parent === current)
|
|
427
2038
|
return null;
|
|
428
2039
|
current = parent;
|
|
429
2040
|
}
|
|
430
2041
|
}
|
|
431
2042
|
function resolveBunExecutable2() {
|
|
432
|
-
const bun =
|
|
2043
|
+
const bun = resolveOptionalBunExecutable();
|
|
433
2044
|
if (bun) {
|
|
434
|
-
return bun
|
|
2045
|
+
return bun;
|
|
435
2046
|
}
|
|
436
2047
|
throw new Error("Unable to locate Bun for broker service management. Install Bun or set OPENSCOUT_BUN_BIN.");
|
|
437
2048
|
}
|
|
2049
|
+
function resolveOptionalBunExecutable() {
|
|
2050
|
+
return resolveBunExecutable(process.env)?.path ?? null;
|
|
2051
|
+
}
|
|
438
2052
|
function resolveBrokerServiceMode() {
|
|
439
2053
|
const explicit = (process.env.OPENSCOUT_BROKER_SERVICE_MODE ?? "").trim().toLowerCase();
|
|
440
2054
|
if (explicit === "prod" || explicit === "production") {
|
|
@@ -443,21 +2057,7 @@ function resolveBrokerServiceMode() {
|
|
|
443
2057
|
if (explicit === "custom") {
|
|
444
2058
|
return "custom";
|
|
445
2059
|
}
|
|
446
|
-
return "dev";
|
|
447
|
-
}
|
|
448
|
-
function resolveBrokerStartTimeoutMs() {
|
|
449
|
-
const explicit = Number.parseInt(process.env.OPENSCOUT_BROKER_START_TIMEOUT_MS ?? "", 10);
|
|
450
|
-
if (Number.isFinite(explicit) && explicit > 0) {
|
|
451
|
-
return Math.max(explicit, BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
452
|
-
}
|
|
453
|
-
return DEFAULT_BROKER_START_TIMEOUT_MS;
|
|
454
|
-
}
|
|
455
|
-
function resolveBrokerStopTimeoutMs() {
|
|
456
|
-
const explicit = Number.parseInt(process.env.OPENSCOUT_BROKER_STOP_TIMEOUT_MS ?? "", 10);
|
|
457
|
-
if (Number.isFinite(explicit) && explicit > 0) {
|
|
458
|
-
return Math.max(explicit, BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
459
|
-
}
|
|
460
|
-
return DEFAULT_BROKER_STOP_TIMEOUT_MS;
|
|
2060
|
+
return "dev";
|
|
461
2061
|
}
|
|
462
2062
|
function resolveBrokerServiceLabel(mode) {
|
|
463
2063
|
const explicit = process.env.OPENSCOUT_SERVICE_LABEL?.trim() || process.env.OPENSCOUT_BROKER_SERVICE_LABEL?.trim();
|
|
@@ -474,33 +2074,23 @@ function resolveBrokerServiceLabel(mode) {
|
|
|
474
2074
|
return "dev.openscout";
|
|
475
2075
|
}
|
|
476
2076
|
}
|
|
477
|
-
function legacyBrokerServiceLabel(mode) {
|
|
478
|
-
switch (mode) {
|
|
479
|
-
case "prod":
|
|
480
|
-
return "com.openscout.broker";
|
|
481
|
-
case "custom":
|
|
482
|
-
return "com.openscout.broker.custom";
|
|
483
|
-
case "dev":
|
|
484
|
-
default:
|
|
485
|
-
return "dev.openscout.broker";
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
2077
|
function resolveBrokerServiceConfig() {
|
|
489
2078
|
const mode = resolveBrokerServiceMode();
|
|
490
2079
|
const label = resolveBrokerServiceLabel(mode);
|
|
2080
|
+
const serviceAdapter = resolveBrokerServiceAdapter();
|
|
491
2081
|
const uid = typeof process.getuid === "function" ? process.getuid() : Number.parseInt(process.env.UID ?? "0", 10);
|
|
492
2082
|
const supportPaths = resolveOpenScoutSupportPaths();
|
|
493
|
-
const defaultSupportDir =
|
|
2083
|
+
const defaultSupportDir = join5(homedir5(), "Library", "Application Support", "OpenScout");
|
|
494
2084
|
const supportDirectory = isTmpPath(supportPaths.supportDirectory) ? defaultSupportDir : supportPaths.supportDirectory;
|
|
495
|
-
const runtimeDirectory =
|
|
496
|
-
const logsDirectory =
|
|
497
|
-
const controlHome = isTmpPath(supportPaths.controlHome) ?
|
|
2085
|
+
const runtimeDirectory = join5(supportDirectory, "runtime");
|
|
2086
|
+
const logsDirectory = join5(supportDirectory, "logs", "broker");
|
|
2087
|
+
const controlHome = isTmpPath(supportPaths.controlHome) ? join5(homedir5(), ".openscout", "control-plane") : supportPaths.controlHome;
|
|
498
2088
|
const advertiseScope = resolveAdvertiseScope();
|
|
499
2089
|
const brokerHost = resolveBrokerHost(advertiseScope);
|
|
500
|
-
const brokerPort = Number.parseInt(process.env.OPENSCOUT_BROKER_PORT ?? String(
|
|
501
|
-
const brokerUrl =
|
|
2090
|
+
const brokerPort = Number.parseInt(process.env.OPENSCOUT_BROKER_PORT ?? String(resolveBrokerPort()), 10);
|
|
2091
|
+
const brokerUrl = resolveBrokerUrl(brokerHost, brokerPort, advertiseScope);
|
|
502
2092
|
const brokerSocketPath = process.env.OPENSCOUT_BROKER_SOCKET_PATH ?? buildDefaultBrokerSocketPath(runtimeDirectory);
|
|
503
|
-
const launchAgentPath =
|
|
2093
|
+
const launchAgentPath = join5(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
|
|
504
2094
|
return {
|
|
505
2095
|
label,
|
|
506
2096
|
mode,
|
|
@@ -511,11 +2101,11 @@ function resolveBrokerServiceConfig() {
|
|
|
511
2101
|
supportDirectory,
|
|
512
2102
|
runtimeDirectory,
|
|
513
2103
|
logsDirectory,
|
|
514
|
-
stdoutLogPath:
|
|
515
|
-
stderrLogPath:
|
|
2104
|
+
stdoutLogPath: join5(logsDirectory, "stdout.log"),
|
|
2105
|
+
stderrLogPath: join5(logsDirectory, "stderr.log"),
|
|
516
2106
|
controlHome,
|
|
517
2107
|
runtimePackageDir: runtimePackageDir(),
|
|
518
|
-
bunExecutable: resolveBunExecutable2(),
|
|
2108
|
+
bunExecutable: serviceAdapter === "macos-scoutd" ? resolveBunExecutable2() : resolveOptionalBunExecutable(),
|
|
519
2109
|
brokerHost,
|
|
520
2110
|
brokerPort,
|
|
521
2111
|
brokerUrl,
|
|
@@ -524,86 +2114,284 @@ function resolveBrokerServiceConfig() {
|
|
|
524
2114
|
coreAgents: readCoreAgentsSync()
|
|
525
2115
|
};
|
|
526
2116
|
}
|
|
527
|
-
function
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
2117
|
+
function resolveBrokerServiceAdapter(env = process.env, platform = process.platform) {
|
|
2118
|
+
return defaultServiceAdapterForPlatform(platform, env);
|
|
2119
|
+
}
|
|
2120
|
+
function executableCandidate(path) {
|
|
2121
|
+
return isExecutablePath(path) ? path : null;
|
|
2122
|
+
}
|
|
2123
|
+
function resolveExecutableName(name) {
|
|
2124
|
+
return resolveExecutableFromSearch({ names: [name] })?.path ?? null;
|
|
2125
|
+
}
|
|
2126
|
+
function resolveEnvExecutable(value) {
|
|
2127
|
+
const trimmed = value?.trim();
|
|
2128
|
+
if (!trimmed)
|
|
2129
|
+
return null;
|
|
2130
|
+
if (trimmed.includes("/") || trimmed.startsWith(".")) {
|
|
2131
|
+
const expanded = resolve2(expandHomePath(trimmed));
|
|
2132
|
+
return executableCandidate(expanded);
|
|
2133
|
+
}
|
|
2134
|
+
return resolveExecutableName(trimmed);
|
|
2135
|
+
}
|
|
2136
|
+
function findWorkspaceRootFromRuntimeDir(runtimePackageDir2) {
|
|
2137
|
+
let current = resolve2(runtimePackageDir2);
|
|
2138
|
+
while (true) {
|
|
2139
|
+
if (existsSync5(join5(current, "Cargo.toml")) && existsSync5(join5(current, "crates", "scoutd", "Cargo.toml"))) {
|
|
2140
|
+
return current;
|
|
2141
|
+
}
|
|
2142
|
+
const parent = dirname3(current);
|
|
2143
|
+
if (parent === current)
|
|
2144
|
+
return null;
|
|
2145
|
+
current = parent;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
function workspaceScoutdAllowed() {
|
|
2149
|
+
const raw = process.env.OPENSCOUT_ALLOW_WORKSPACE_SCOUTD?.trim().toLowerCase();
|
|
2150
|
+
return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
|
|
2151
|
+
}
|
|
2152
|
+
function resolveScoutdCommand(config = resolveBrokerServiceConfig()) {
|
|
2153
|
+
const explicit = resolveEnvExecutable(process.env.OPENSCOUT_SCOUTD_BIN);
|
|
2154
|
+
if (explicit) {
|
|
2155
|
+
return { path: explicit, source: "env" };
|
|
2156
|
+
}
|
|
2157
|
+
const workspaceRoot = findWorkspaceRootFromRuntimeDir(config.runtimePackageDir);
|
|
2158
|
+
const packageCandidates = [
|
|
2159
|
+
join5(config.runtimePackageDir, "bin", "scoutd"),
|
|
2160
|
+
join5(config.runtimePackageDir, "native", "scoutd"),
|
|
2161
|
+
join5(config.runtimePackageDir, "scoutd"),
|
|
2162
|
+
workspaceRoot ? join5(workspaceRoot, "packages", "cli", "bin", "scoutd") : null,
|
|
2163
|
+
workspaceRoot ? join5(workspaceRoot, "packages", "runtime", "bin", "scoutd") : null,
|
|
2164
|
+
join5(config.runtimeDirectory, "scoutd"),
|
|
2165
|
+
join5(dirname3(config.runtimePackageDir), "scout", "bin", "scoutd")
|
|
2166
|
+
];
|
|
2167
|
+
for (const candidate of packageCandidates) {
|
|
2168
|
+
const resolved = executableCandidate(candidate);
|
|
2169
|
+
if (resolved) {
|
|
2170
|
+
return { path: resolved, source: "package" };
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
const fromPath = resolveExecutableName("scoutd");
|
|
2174
|
+
if (fromPath) {
|
|
2175
|
+
return { path: fromPath, source: "path" };
|
|
2176
|
+
}
|
|
2177
|
+
if (workspaceRoot && workspaceScoutdAllowed()) {
|
|
2178
|
+
for (const candidate of [
|
|
2179
|
+
join5(workspaceRoot, "target", "release", "scoutd"),
|
|
2180
|
+
join5(workspaceRoot, "target", "debug", "scoutd")
|
|
2181
|
+
]) {
|
|
2182
|
+
const resolved = executableCandidate(candidate);
|
|
2183
|
+
if (resolved) {
|
|
2184
|
+
return { path: resolved, source: "workspace" };
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
return null;
|
|
2189
|
+
}
|
|
2190
|
+
function nativeServiceEnvironment(config, scoutdPath) {
|
|
2191
|
+
const env = {
|
|
2192
|
+
...process.env,
|
|
2193
|
+
OPENSCOUT_SCOUTD_BIN: scoutdPath,
|
|
2194
|
+
OPENSCOUT_RUNTIME_PACKAGE_DIR: config.runtimePackageDir,
|
|
2195
|
+
OPENSCOUT_SUPPORT_DIRECTORY: config.supportDirectory,
|
|
2196
|
+
OPENSCOUT_CONTROL_HOME: config.controlHome,
|
|
531
2197
|
OPENSCOUT_BROKER_HOST: config.brokerHost,
|
|
532
2198
|
OPENSCOUT_BROKER_PORT: String(config.brokerPort),
|
|
533
2199
|
OPENSCOUT_BROKER_URL: config.brokerUrl,
|
|
534
2200
|
OPENSCOUT_BROKER_SOCKET_PATH: config.brokerSocketPath,
|
|
535
|
-
OPENSCOUT_CONTROL_HOME: config.controlHome,
|
|
536
2201
|
OPENSCOUT_BROKER_SERVICE_MODE: config.mode,
|
|
537
2202
|
OPENSCOUT_BROKER_SERVICE_LABEL: config.label,
|
|
538
2203
|
OPENSCOUT_SERVICE_LABEL: config.label,
|
|
539
|
-
OPENSCOUT_ADVERTISE_SCOPE: config.advertiseScope
|
|
540
|
-
HOME: homedir3(),
|
|
541
|
-
PATH: launchPath,
|
|
542
|
-
...collectOptionalEnvVars([
|
|
543
|
-
"OPENSCOUT_MESH_ID",
|
|
544
|
-
"OPENSCOUT_MESH_SEEDS",
|
|
545
|
-
"OPENSCOUT_MESH_DISCOVERY_INTERVAL_MS",
|
|
546
|
-
"OPENSCOUT_NODE_NAME",
|
|
547
|
-
"OPENSCOUT_NODE_ID",
|
|
548
|
-
"OPENSCOUT_NODE_QUALIFIER",
|
|
549
|
-
"OPENSCOUT_TAILSCALE_BIN",
|
|
550
|
-
"OPENSCOUT_TAILSCALE_STATUS_JSON",
|
|
551
|
-
"OPENSCOUT_SSE_KEEPALIVE_MS",
|
|
552
|
-
"OPENSCOUT_WEB_EDGE_SCHEME",
|
|
553
|
-
"OPENSCOUT_WEB_PUBLIC_ORIGIN",
|
|
554
|
-
"OPENSCOUT_WEB_PORTAL_HOST",
|
|
555
|
-
"OPENSCOUT_WEB_LOCAL_NAME",
|
|
556
|
-
"OPENSCOUT_WEB_ADVERTISED_HOST",
|
|
557
|
-
"OPENSCOUT_WEB_TRUSTED_HOSTS",
|
|
558
|
-
"OPENSCOUT_WEB_TRUSTED_ORIGINS",
|
|
559
|
-
"OPENSCOUT_WEB_PORT",
|
|
560
|
-
"OPENSCOUT_WEB_FLAG_BUNDLE",
|
|
561
|
-
"OPENSCOUT_WEB_EXPERIENCE",
|
|
562
|
-
"OPENSCOUT_WEB_AB_VARIANT"
|
|
563
|
-
])
|
|
2204
|
+
OPENSCOUT_ADVERTISE_SCOPE: config.advertiseScope
|
|
564
2205
|
};
|
|
565
|
-
if (
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
return
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
2206
|
+
if (config.bunExecutable) {
|
|
2207
|
+
env.OPENSCOUT_BUN_BIN = config.bunExecutable;
|
|
2208
|
+
}
|
|
2209
|
+
if (config.coreAgents.length > 0) {
|
|
2210
|
+
env.OPENSCOUT_CORE_AGENTS = config.coreAgents.join(",");
|
|
2211
|
+
}
|
|
2212
|
+
return env;
|
|
2213
|
+
}
|
|
2214
|
+
function isRecord3(value) {
|
|
2215
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2216
|
+
}
|
|
2217
|
+
function readString2(value) {
|
|
2218
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
2219
|
+
}
|
|
2220
|
+
function readBoolean(value) {
|
|
2221
|
+
return typeof value === "boolean" ? value : undefined;
|
|
2222
|
+
}
|
|
2223
|
+
function readNumber2(value) {
|
|
2224
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
2225
|
+
}
|
|
2226
|
+
function readHealthTransport(value) {
|
|
2227
|
+
return value === "unix_socket" || value === "http" || value === "in_process" ? value : undefined;
|
|
2228
|
+
}
|
|
2229
|
+
function readRuntimeFreshness(value) {
|
|
2230
|
+
if (!isRecord3(value))
|
|
2231
|
+
return;
|
|
2232
|
+
const state = readString2(value.state);
|
|
2233
|
+
const basis = readString2(value.basis);
|
|
2234
|
+
const detail = readString2(value.detail);
|
|
2235
|
+
if (!state || !basis || !detail)
|
|
2236
|
+
return;
|
|
2237
|
+
return {
|
|
2238
|
+
state,
|
|
2239
|
+
intentional: readBoolean(value.intentional) ?? false,
|
|
2240
|
+
basis,
|
|
2241
|
+
artifactCommit: readString2(value.artifactCommit) ?? null,
|
|
2242
|
+
expectedCommit: readString2(value.expectedCommit) ?? null,
|
|
2243
|
+
pin: readString2(value.pin) ?? null,
|
|
2244
|
+
pinReason: readString2(value.pinReason) ?? null,
|
|
2245
|
+
manifestPath: readString2(value.manifestPath) ?? null,
|
|
2246
|
+
version: readString2(value.version) ?? null,
|
|
2247
|
+
builtAt: readString2(value.builtAt) ?? null,
|
|
2248
|
+
sourceDirty: readBoolean(value.sourceDirty) ?? null,
|
|
2249
|
+
detail
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
function normalizeNativeServiceStatus(input, config) {
|
|
2253
|
+
const healthRecord = isRecord3(input.health) ? input.health : {};
|
|
2254
|
+
const healthReachable = readBoolean(healthRecord.reachable) ?? readBoolean(input.reachable) ?? false;
|
|
2255
|
+
const healthOk = readBoolean(healthRecord.ok) ?? (typeof input.health === "boolean" ? input.health : undefined) ?? false;
|
|
2256
|
+
const healthError = readString2(healthRecord.error) ?? readString2(input.healthError);
|
|
2257
|
+
const healthTransport = readHealthTransport(healthRecord.transport) ?? readHealthTransport(input.healthTransport);
|
|
2258
|
+
const healthNodeId = readString2(healthRecord.nodeId);
|
|
2259
|
+
const healthMeshId = readString2(healthRecord.meshId);
|
|
2260
|
+
const healthSocketFallbackError = readString2(healthRecord.socketFallbackError);
|
|
2261
|
+
const healthCounts = isRecord3(healthRecord.counts) ? healthRecord.counts : undefined;
|
|
2262
|
+
const installed = readBoolean(input.installed) ?? existsSync5(config.launchAgentPath);
|
|
2263
|
+
const loaded = readBoolean(input.loaded) ?? false;
|
|
2264
|
+
const stdoutLogPath = readString2(input.stdoutLogPath) ?? config.stdoutLogPath;
|
|
2265
|
+
const stderrLogPath = readString2(input.stderrLogPath) ?? config.stderrLogPath;
|
|
2266
|
+
const lastLogLine = readString2(input.lastLogLine) ?? (healthReachable ? readLastLogLine([stdoutLogPath, stderrLogPath]) : readLastLogLine([stderrLogPath, stdoutLogPath]));
|
|
2267
|
+
const runtimeFreshness = readRuntimeFreshness(input.runtimeFreshness);
|
|
2268
|
+
return {
|
|
2269
|
+
label: readString2(input.label) ?? config.label,
|
|
2270
|
+
mode: readString2(input.mode) ?? config.mode,
|
|
2271
|
+
launchAgentPath: readString2(input.launchAgentPath) ?? config.launchAgentPath,
|
|
2272
|
+
bootoutCommand: readString2(input.bootoutCommand) ?? bootoutCommand(config),
|
|
2273
|
+
brokerUrl: readString2(input.brokerUrl) ?? config.brokerUrl,
|
|
2274
|
+
brokerSocketPath: readString2(input.brokerSocketPath) ?? config.brokerSocketPath,
|
|
2275
|
+
supportDirectory: readString2(input.supportDirectory) ?? config.supportDirectory,
|
|
2276
|
+
runtimeDirectory: readString2(input.runtimeDirectory) ?? config.runtimeDirectory,
|
|
2277
|
+
controlHome: readString2(input.controlHome) ?? config.controlHome,
|
|
2278
|
+
stdoutLogPath,
|
|
2279
|
+
stderrLogPath,
|
|
2280
|
+
installed,
|
|
2281
|
+
loaded,
|
|
2282
|
+
pid: readNumber2(input.pid) ?? null,
|
|
2283
|
+
launchdState: readString2(input.launchdState) ?? null,
|
|
2284
|
+
lastExitStatus: readNumber2(input.lastExitStatus) ?? null,
|
|
2285
|
+
usesLaunchAgent: readBoolean(input.usesLaunchAgent) ?? (installed || loaded),
|
|
2286
|
+
reachable: healthReachable,
|
|
2287
|
+
serviceAdapter: "macos-scoutd",
|
|
2288
|
+
health: {
|
|
2289
|
+
reachable: healthReachable,
|
|
2290
|
+
ok: healthOk,
|
|
2291
|
+
checkedAt: readNumber2(healthRecord.checkedAt) ?? Date.now(),
|
|
2292
|
+
transport: healthTransport,
|
|
2293
|
+
socketPath: config.brokerSocketPath,
|
|
2294
|
+
...healthSocketFallbackError ? { socketFallbackError: healthSocketFallbackError } : {},
|
|
2295
|
+
...healthNodeId ? { nodeId: healthNodeId } : {},
|
|
2296
|
+
...healthMeshId ? { meshId: healthMeshId } : {},
|
|
2297
|
+
...healthCounts ? { counts: healthCounts } : {},
|
|
2298
|
+
...isRecord3(healthRecord.build) ? { build: healthRecord.build } : {},
|
|
2299
|
+
...isRecord3(healthRecord.services) ? { services: healthRecord.services } : {},
|
|
2300
|
+
error: healthError
|
|
2301
|
+
},
|
|
2302
|
+
...runtimeFreshness ? { runtimeFreshness } : {},
|
|
2303
|
+
lastLogLine
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
var SCOUTD_MAX_BUFFER = 2 * 1024 * 1024;
|
|
2307
|
+
var SCOUTD_DEFAULT_TIMEOUT_MS = 45000;
|
|
2308
|
+
var SCOUTD_KILL_GRACE_MS = 250;
|
|
2309
|
+
function spawnScoutdJson(scoutdPath, command, env, timeoutMs) {
|
|
2310
|
+
return new Promise((resolvePromise, reject) => {
|
|
2311
|
+
const child = spawn2(scoutdPath, [command, "--json"], {
|
|
2312
|
+
env,
|
|
2313
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2314
|
+
});
|
|
2315
|
+
let stdout = "";
|
|
2316
|
+
let stderr = "";
|
|
2317
|
+
let settled = false;
|
|
2318
|
+
let timedOut = false;
|
|
2319
|
+
const killTimer = setTimeout(() => {
|
|
2320
|
+
timedOut = true;
|
|
2321
|
+
terminate();
|
|
2322
|
+
fail(new Error(`scoutd ${command} timed out after ${timeoutMs}ms`));
|
|
2323
|
+
}, timeoutMs);
|
|
2324
|
+
killTimer.unref?.();
|
|
2325
|
+
function terminate() {
|
|
2326
|
+
child.kill("SIGTERM");
|
|
2327
|
+
const hardKillTimer = setTimeout(() => child.kill("SIGKILL"), SCOUTD_KILL_GRACE_MS);
|
|
2328
|
+
hardKillTimer.unref?.();
|
|
2329
|
+
}
|
|
2330
|
+
function cleanup() {
|
|
2331
|
+
clearTimeout(killTimer);
|
|
2332
|
+
}
|
|
2333
|
+
function fail(error) {
|
|
2334
|
+
if (settled)
|
|
2335
|
+
return;
|
|
2336
|
+
settled = true;
|
|
2337
|
+
cleanup();
|
|
2338
|
+
reject(error);
|
|
2339
|
+
}
|
|
2340
|
+
function succeed(output) {
|
|
2341
|
+
if (settled)
|
|
2342
|
+
return;
|
|
2343
|
+
settled = true;
|
|
2344
|
+
cleanup();
|
|
2345
|
+
resolvePromise(output);
|
|
2346
|
+
}
|
|
2347
|
+
function append(kind, chunk) {
|
|
2348
|
+
const text = typeof chunk === "string" ? chunk : String(chunk);
|
|
2349
|
+
if (kind === "stdout")
|
|
2350
|
+
stdout += text;
|
|
2351
|
+
else
|
|
2352
|
+
stderr += text;
|
|
2353
|
+
if (stdout.length + stderr.length > SCOUTD_MAX_BUFFER) {
|
|
2354
|
+
terminate();
|
|
2355
|
+
fail(new Error(`scoutd ${command} exceeded output limit`));
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
child.stdout.setEncoding("utf8");
|
|
2359
|
+
child.stderr.setEncoding("utf8");
|
|
2360
|
+
child.stdout.on("data", (chunk) => append("stdout", chunk));
|
|
2361
|
+
child.stderr.on("data", (chunk) => append("stderr", chunk));
|
|
2362
|
+
child.on("error", (error) => fail(new Error(`scoutd ${command} failed: ${error.message}`)));
|
|
2363
|
+
child.on("close", (code, signal) => {
|
|
2364
|
+
if (settled || timedOut)
|
|
2365
|
+
return;
|
|
2366
|
+
const trimmedStdout = stdout.trim();
|
|
2367
|
+
const trimmedStderr = stderr.trim();
|
|
2368
|
+
if ((code ?? 1) !== 0) {
|
|
2369
|
+
const detail = trimmedStderr || trimmedStdout || `exit ${signal ?? code ?? "unknown status"}`;
|
|
2370
|
+
fail(new Error(`scoutd ${command} failed: ${detail}`));
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
succeed(trimmedStdout);
|
|
2374
|
+
});
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2377
|
+
async function runScoutdServiceCommand(command, config, timeoutMs = SCOUTD_DEFAULT_TIMEOUT_MS, runScoutdJson = spawnScoutdJson) {
|
|
2378
|
+
const scoutd = resolveScoutdCommand(config);
|
|
2379
|
+
if (!scoutd) {
|
|
2380
|
+
throw new Error("Unable to locate scoutd for broker service management. Build scoutd with `npm run scoutd:build`, install a package that includes scoutd, or set OPENSCOUT_SCOUTD_BIN.");
|
|
2381
|
+
}
|
|
2382
|
+
const stdout = await runScoutdJson(scoutd.path, command, nativeServiceEnvironment(config, scoutd.path), timeoutMs);
|
|
2383
|
+
let parsed;
|
|
2384
|
+
try {
|
|
2385
|
+
parsed = JSON.parse(stdout);
|
|
2386
|
+
} catch {
|
|
2387
|
+
throw new Error(`scoutd ${command} returned non-JSON stdout: ${stdout.slice(0, 400)}`);
|
|
2388
|
+
}
|
|
2389
|
+
return normalizeNativeServiceStatus(parsed, config);
|
|
602
2390
|
}
|
|
603
2391
|
function readCoreAgentsSync() {
|
|
604
2392
|
try {
|
|
605
2393
|
const settingsPath = resolveOpenScoutSupportPaths().settingsPath;
|
|
606
|
-
const raw =
|
|
2394
|
+
const raw = readFileSync4(settingsPath, "utf8");
|
|
607
2395
|
const settings = JSON.parse(raw);
|
|
608
2396
|
const raw_agents = settings?.agents?.coreAgents;
|
|
609
2397
|
if (Array.isArray(raw_agents)) {
|
|
@@ -612,88 +2400,14 @@ function readCoreAgentsSync() {
|
|
|
612
2400
|
} catch {}
|
|
613
2401
|
return [];
|
|
614
2402
|
}
|
|
615
|
-
function collectOptionalEnvVars(keys) {
|
|
616
|
-
const entries = {};
|
|
617
|
-
for (const key of keys) {
|
|
618
|
-
const value = process.env[key];
|
|
619
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
620
|
-
entries[key] = value;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
return entries;
|
|
624
|
-
}
|
|
625
|
-
function resolveLaunchAgentPATH() {
|
|
626
|
-
const entries = [
|
|
627
|
-
join3(homedir3(), ".bun", "bin"),
|
|
628
|
-
...(process.env.PATH ?? "").split(":").filter(Boolean),
|
|
629
|
-
"/opt/homebrew/bin",
|
|
630
|
-
"/usr/local/bin",
|
|
631
|
-
"/usr/bin",
|
|
632
|
-
"/bin",
|
|
633
|
-
"/usr/sbin",
|
|
634
|
-
"/sbin"
|
|
635
|
-
];
|
|
636
|
-
return Array.from(new Set(entries)).filter((e) => !isTmpPath(e)).join(":");
|
|
637
|
-
}
|
|
638
|
-
function xmlEscape(value) {
|
|
639
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
640
|
-
}
|
|
641
|
-
function ensureParentDirectory(filePath) {
|
|
642
|
-
mkdirSync2(dirname2(filePath), { recursive: true });
|
|
643
|
-
}
|
|
644
|
-
function ensureServiceDirectories(config) {
|
|
645
|
-
ensureOpenScoutCleanSlateSync();
|
|
646
|
-
mkdirSync2(config.supportDirectory, { recursive: true });
|
|
647
|
-
mkdirSync2(config.runtimeDirectory, { recursive: true, mode: 448 });
|
|
648
|
-
chmodSync(config.runtimeDirectory, 448);
|
|
649
|
-
mkdirSync2(config.logsDirectory, { recursive: true });
|
|
650
|
-
mkdirSync2(config.controlHome, { recursive: true });
|
|
651
|
-
ensureParentDirectory(config.launchAgentPath);
|
|
652
|
-
}
|
|
653
|
-
function writeLaunchAgentPlist(config) {
|
|
654
|
-
ensureServiceDirectories(config);
|
|
655
|
-
writeFileSync2(config.launchAgentPath, renderLaunchAgentPlist(config), "utf8");
|
|
656
|
-
}
|
|
657
2403
|
function bootoutCommand(config) {
|
|
658
|
-
return
|
|
659
|
-
}
|
|
660
|
-
function legacyLaunchAgentPath(config) {
|
|
661
|
-
return join3(homedir3(), "Library", "LaunchAgents", `${legacyBrokerServiceLabel(config.mode)}.plist`);
|
|
662
|
-
}
|
|
663
|
-
function legacyServiceTarget(config) {
|
|
664
|
-
return `${config.domainTarget}/${legacyBrokerServiceLabel(config.mode)}`;
|
|
665
|
-
}
|
|
666
|
-
function bootoutLegacyBrokerService(config) {
|
|
667
|
-
const legacyTarget = legacyServiceTarget(config);
|
|
668
|
-
if (legacyTarget === config.serviceTarget) {
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
runCommand(launchctlPath(), ["bootout", legacyTarget], { allowFailure: true });
|
|
672
|
-
}
|
|
673
|
-
function runCommand(command, args, options) {
|
|
674
|
-
const result = spawnSync(command, args, {
|
|
675
|
-
env: {
|
|
676
|
-
...process.env,
|
|
677
|
-
...options?.env ?? {}
|
|
678
|
-
},
|
|
679
|
-
encoding: "utf8"
|
|
680
|
-
});
|
|
681
|
-
const stdout = (result.stdout ?? "").trim();
|
|
682
|
-
const stderr = (result.stderr ?? "").trim();
|
|
683
|
-
const exitCode = result.status ?? 1;
|
|
684
|
-
if (exitCode !== 0 && !options?.allowFailure) {
|
|
685
|
-
throw new Error(stderr || stdout || `${command} exited with status ${exitCode}`);
|
|
686
|
-
}
|
|
687
|
-
return { exitCode, stdout, stderr };
|
|
688
|
-
}
|
|
689
|
-
function launchctlPath() {
|
|
690
|
-
return "/bin/launchctl";
|
|
2404
|
+
return `/bin/launchctl bootout ${config.serviceTarget}`;
|
|
691
2405
|
}
|
|
692
2406
|
function readLogLines(path) {
|
|
693
|
-
if (!
|
|
2407
|
+
if (!existsSync5(path)) {
|
|
694
2408
|
return [];
|
|
695
2409
|
}
|
|
696
|
-
return
|
|
2410
|
+
return readFileSync4(path, "utf8").split(`
|
|
697
2411
|
`).map((line) => line.trim()).filter(Boolean);
|
|
698
2412
|
}
|
|
699
2413
|
function isPackageScriptBanner(line) {
|
|
@@ -726,170 +2440,120 @@ function readLastLogLine(paths) {
|
|
|
726
2440
|
}
|
|
727
2441
|
return fallback;
|
|
728
2442
|
}
|
|
729
|
-
function
|
|
730
|
-
const
|
|
731
|
-
const stateMatch = output.match(/\bstate = ([^\n]+)/);
|
|
732
|
-
const lastExitMatch = output.match(/\blast exit code = (-?\d+)/i) || output.match(/\blast exit status = (-?\d+)/i);
|
|
2443
|
+
async function readHeadlessBrokerHealth(config) {
|
|
2444
|
+
const result = await requestScoutBrokerJsonWithTrace(config.brokerUrl, "/health", { socketPath: config.brokerSocketPath });
|
|
733
2445
|
return {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
lastExitStatus: lastExitMatch ? Number.parseInt(lastExitMatch[1] ?? "0", 10) : null
|
|
2446
|
+
health: result.value,
|
|
2447
|
+
trace: result.trace
|
|
737
2448
|
};
|
|
738
2449
|
}
|
|
739
|
-
function
|
|
740
|
-
|
|
741
|
-
if (printResult.exitCode !== 0) {
|
|
742
|
-
return {
|
|
743
|
-
loaded: false,
|
|
744
|
-
pid: null,
|
|
745
|
-
launchdState: null,
|
|
746
|
-
lastExitStatus: null,
|
|
747
|
-
raw: printResult.stderr || printResult.stdout
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
return {
|
|
751
|
-
loaded: true,
|
|
752
|
-
raw: printResult.stdout,
|
|
753
|
-
...parseLaunchctlPrint(printResult.stdout)
|
|
754
|
-
};
|
|
2450
|
+
function headlessLifecycleError(command) {
|
|
2451
|
+
return new Error(`Next step: run \`openscout-runtime broker\` in this shell or under your process manager. ` + `The headless service adapter leaves broker ${command} to that foreground process.`);
|
|
755
2452
|
}
|
|
756
|
-
async function
|
|
757
|
-
|
|
758
|
-
|
|
2453
|
+
async function runHeadlessForegroundServiceCommand(command, config, readHealth = readHeadlessBrokerHealth) {
|
|
2454
|
+
if (command !== "status") {
|
|
2455
|
+
throw headlessLifecycleError(command);
|
|
2456
|
+
}
|
|
759
2457
|
const checkedAt = Date.now();
|
|
760
2458
|
try {
|
|
761
|
-
const {
|
|
762
|
-
|
|
763
|
-
socketPath: config.brokerSocketPath
|
|
764
|
-
});
|
|
2459
|
+
const { health, trace } = await readHealth(config);
|
|
2460
|
+
const reachable = true;
|
|
765
2461
|
return {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
2462
|
+
serviceAdapter: "headless-foreground",
|
|
2463
|
+
label: config.label,
|
|
2464
|
+
mode: config.mode,
|
|
2465
|
+
launchAgentPath: config.launchAgentPath,
|
|
2466
|
+
bootoutCommand: "headless-foreground does not use launchd",
|
|
2467
|
+
brokerUrl: config.brokerUrl,
|
|
2468
|
+
brokerSocketPath: config.brokerSocketPath,
|
|
2469
|
+
supportDirectory: config.supportDirectory,
|
|
2470
|
+
runtimeDirectory: config.runtimeDirectory,
|
|
2471
|
+
controlHome: config.controlHome,
|
|
2472
|
+
stdoutLogPath: config.stdoutLogPath,
|
|
2473
|
+
stderrLogPath: config.stderrLogPath,
|
|
2474
|
+
installed: false,
|
|
2475
|
+
loaded: reachable,
|
|
2476
|
+
pid: null,
|
|
2477
|
+
launchdState: null,
|
|
2478
|
+
lastExitStatus: null,
|
|
2479
|
+
usesLaunchAgent: false,
|
|
2480
|
+
reachable,
|
|
2481
|
+
health: {
|
|
2482
|
+
reachable,
|
|
2483
|
+
ok: Boolean(health.ok),
|
|
2484
|
+
checkedAt,
|
|
2485
|
+
transport: trace.transport,
|
|
2486
|
+
socketPath: trace.socketPath,
|
|
2487
|
+
socketFallbackError: trace.socketFallbackError,
|
|
2488
|
+
nodeId: health.nodeId ?? undefined,
|
|
2489
|
+
meshId: health.meshId ?? undefined,
|
|
2490
|
+
counts: health.counts ?? undefined,
|
|
2491
|
+
build: health.build,
|
|
2492
|
+
services: health.services
|
|
2493
|
+
},
|
|
2494
|
+
lastLogLine: readLastLogLine([config.stderrLogPath, config.stdoutLogPath])
|
|
794
2495
|
};
|
|
795
2496
|
} catch (error) {
|
|
796
2497
|
return {
|
|
2498
|
+
serviceAdapter: "headless-foreground",
|
|
2499
|
+
label: config.label,
|
|
2500
|
+
mode: config.mode,
|
|
2501
|
+
launchAgentPath: config.launchAgentPath,
|
|
2502
|
+
bootoutCommand: "headless-foreground does not use launchd",
|
|
2503
|
+
brokerUrl: config.brokerUrl,
|
|
2504
|
+
brokerSocketPath: config.brokerSocketPath,
|
|
2505
|
+
supportDirectory: config.supportDirectory,
|
|
2506
|
+
runtimeDirectory: config.runtimeDirectory,
|
|
2507
|
+
controlHome: config.controlHome,
|
|
2508
|
+
stdoutLogPath: config.stdoutLogPath,
|
|
2509
|
+
stderrLogPath: config.stderrLogPath,
|
|
2510
|
+
installed: false,
|
|
2511
|
+
loaded: false,
|
|
2512
|
+
pid: null,
|
|
2513
|
+
launchdState: null,
|
|
2514
|
+
lastExitStatus: null,
|
|
2515
|
+
usesLaunchAgent: false,
|
|
797
2516
|
reachable: false,
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
2517
|
+
health: {
|
|
2518
|
+
reachable: false,
|
|
2519
|
+
ok: false,
|
|
2520
|
+
checkedAt,
|
|
2521
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2522
|
+
},
|
|
2523
|
+
lastLogLine: readLastLogLine([config.stderrLogPath, config.stdoutLogPath])
|
|
802
2524
|
};
|
|
803
|
-
}
|
|
804
|
-
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
async function runBrokerServiceCommand(command, config) {
|
|
2528
|
+
const adapter = resolveBrokerServiceAdapter();
|
|
2529
|
+
switch (adapter) {
|
|
2530
|
+
case "macos-scoutd":
|
|
2531
|
+
return await runScoutdServiceCommand(command, config);
|
|
2532
|
+
case "headless-foreground":
|
|
2533
|
+
return await runHeadlessForegroundServiceCommand(command, config);
|
|
2534
|
+
case "linux-systemd-user":
|
|
2535
|
+
throw new Error("The linux-systemd-user service adapter is not implemented yet. " + "Use OPENSCOUT_SERVICE_ADAPTER=headless-foreground and run `openscout-runtime broker` under your process manager.");
|
|
2536
|
+
case "windows-service":
|
|
2537
|
+
throw new Error("The windows-service adapter is not implemented yet. " + "Use OPENSCOUT_SERVICE_ADAPTER=headless-foreground and run `openscout-runtime broker` from a shell.");
|
|
805
2538
|
}
|
|
806
2539
|
}
|
|
807
2540
|
async function brokerServiceStatus(config = resolveBrokerServiceConfig()) {
|
|
808
|
-
|
|
809
|
-
const launchctl = inspectLaunchctl(config);
|
|
810
|
-
const health = await fetchHealthSnapshot(config);
|
|
811
|
-
const installed = existsSync3(config.launchAgentPath);
|
|
812
|
-
const lastLogLine = health.reachable ? readLastLogLine([config.stdoutLogPath, config.stderrLogPath]) : readLastLogLine([config.stderrLogPath, config.stdoutLogPath]);
|
|
813
|
-
return {
|
|
814
|
-
label: config.label,
|
|
815
|
-
mode: config.mode,
|
|
816
|
-
launchAgentPath: config.launchAgentPath,
|
|
817
|
-
bootoutCommand: bootoutCommand(config),
|
|
818
|
-
brokerUrl: config.brokerUrl,
|
|
819
|
-
brokerSocketPath: config.brokerSocketPath,
|
|
820
|
-
supportDirectory: config.supportDirectory,
|
|
821
|
-
runtimeDirectory: config.runtimeDirectory,
|
|
822
|
-
controlHome: config.controlHome,
|
|
823
|
-
stdoutLogPath: config.stdoutLogPath,
|
|
824
|
-
stderrLogPath: config.stderrLogPath,
|
|
825
|
-
installed,
|
|
826
|
-
loaded: launchctl.loaded,
|
|
827
|
-
pid: launchctl.pid,
|
|
828
|
-
launchdState: launchctl.launchdState,
|
|
829
|
-
lastExitStatus: launchctl.lastExitStatus,
|
|
830
|
-
usesLaunchAgent: installed || launchctl.loaded,
|
|
831
|
-
reachable: health.reachable,
|
|
832
|
-
health,
|
|
833
|
-
lastLogLine
|
|
834
|
-
};
|
|
2541
|
+
return runBrokerServiceCommand("status", config);
|
|
835
2542
|
}
|
|
836
2543
|
async function installBrokerService(config = resolveBrokerServiceConfig()) {
|
|
837
|
-
|
|
838
|
-
writeLaunchAgentPlist(config);
|
|
839
|
-
return brokerServiceStatus(config);
|
|
2544
|
+
return runBrokerServiceCommand("install", config);
|
|
840
2545
|
}
|
|
841
2546
|
async function startBrokerService(config = resolveBrokerServiceConfig()) {
|
|
842
|
-
|
|
843
|
-
writeLaunchAgentPlist(config);
|
|
844
|
-
runCommand(launchctlPath(), ["bootout", config.serviceTarget], { allowFailure: true });
|
|
845
|
-
await waitForBrokerServiceStopped(config);
|
|
846
|
-
runCommand(launchctlPath(), ["bootstrap", config.domainTarget, config.launchAgentPath], { allowFailure: true });
|
|
847
|
-
runCommand(launchctlPath(), ["kickstart", "-k", config.serviceTarget], { allowFailure: true });
|
|
848
|
-
const attempts = Math.ceil(resolveBrokerStartTimeoutMs() / BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
849
|
-
for (let attempt = 0;attempt < attempts; attempt += 1) {
|
|
850
|
-
const status2 = await brokerServiceStatus(config);
|
|
851
|
-
if (status2.health.reachable) {
|
|
852
|
-
return status2;
|
|
853
|
-
}
|
|
854
|
-
await sleep(BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
855
|
-
}
|
|
856
|
-
const status = await brokerServiceStatus(config);
|
|
857
|
-
throw new Error(status.lastLogLine ?? status.health.error ?? "Broker service did not become healthy.");
|
|
858
|
-
}
|
|
859
|
-
function sleep(ms) {
|
|
860
|
-
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
861
|
-
}
|
|
862
|
-
async function waitForBrokerServiceStopped(config) {
|
|
863
|
-
let status = await brokerServiceStatus(config);
|
|
864
|
-
const attempts = Math.ceil(resolveBrokerStopTimeoutMs() / BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
865
|
-
for (let attempt = 0;attempt < attempts; attempt += 1) {
|
|
866
|
-
status = await brokerServiceStatus(config);
|
|
867
|
-
if (!status.loaded && !status.health.reachable) {
|
|
868
|
-
return status;
|
|
869
|
-
}
|
|
870
|
-
await sleep(BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
871
|
-
}
|
|
872
|
-
return status;
|
|
2547
|
+
return runBrokerServiceCommand("start", config);
|
|
873
2548
|
}
|
|
874
2549
|
async function stopBrokerService(config = resolveBrokerServiceConfig()) {
|
|
875
|
-
|
|
876
|
-
return waitForBrokerServiceStopped(config);
|
|
2550
|
+
return runBrokerServiceCommand("stop", config);
|
|
877
2551
|
}
|
|
878
2552
|
async function restartBrokerService(config = resolveBrokerServiceConfig()) {
|
|
879
|
-
|
|
880
|
-
return startBrokerService(config);
|
|
2553
|
+
return runBrokerServiceCommand("restart", config);
|
|
881
2554
|
}
|
|
882
2555
|
async function uninstallBrokerService(config = resolveBrokerServiceConfig()) {
|
|
883
|
-
|
|
884
|
-
if (existsSync3(config.launchAgentPath)) {
|
|
885
|
-
rmSync2(config.launchAgentPath, { force: true });
|
|
886
|
-
}
|
|
887
|
-
const legacyPath = legacyLaunchAgentPath(config);
|
|
888
|
-
bootoutLegacyBrokerService(config);
|
|
889
|
-
if (existsSync3(legacyPath)) {
|
|
890
|
-
rmSync2(legacyPath, { force: true });
|
|
891
|
-
}
|
|
892
|
-
return brokerServiceStatus(config);
|
|
2556
|
+
return runBrokerServiceCommand("uninstall", config);
|
|
893
2557
|
}
|
|
894
2558
|
async function main() {
|
|
895
2559
|
const command = process.argv[2] ?? "status";
|
|
@@ -927,6 +2591,7 @@ async function main() {
|
|
|
927
2591
|
}
|
|
928
2592
|
function formatBrokerServiceStatus(status) {
|
|
929
2593
|
const lines = [
|
|
2594
|
+
`service adapter: ${status.serviceAdapter ?? "unknown"}`,
|
|
930
2595
|
`label: ${status.label}`,
|
|
931
2596
|
`mode: ${status.mode}`,
|
|
932
2597
|
`launch agent: ${status.installed ? status.launchAgentPath : "not installed"}`,
|
|
@@ -939,8 +2604,12 @@ function formatBrokerServiceStatus(status) {
|
|
|
939
2604
|
`reachable: ${status.reachable ? "yes" : "no"}`,
|
|
940
2605
|
`health: ${status.health.ok ? "ok" : status.health.error ?? "unreachable"}`,
|
|
941
2606
|
`health transport: ${status.health.transport ?? "unknown"}`,
|
|
2607
|
+
`runtime freshness: ${status.runtimeFreshness?.state ?? "unavailable"}`,
|
|
942
2608
|
`logs: ${status.stdoutLogPath}`
|
|
943
2609
|
];
|
|
2610
|
+
if (status.runtimeFreshness) {
|
|
2611
|
+
lines.push(`runtime freshness detail: ${status.runtimeFreshness.detail}`);
|
|
2612
|
+
}
|
|
944
2613
|
if (status.health.socketFallbackError) {
|
|
945
2614
|
lines.push(`socket fallback: ${status.health.socketFallbackError}`);
|
|
946
2615
|
}
|
|
@@ -957,113 +2626,59 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1] && !pr
|
|
|
957
2626
|
});
|
|
958
2627
|
}
|
|
959
2628
|
|
|
960
|
-
// packages/runtime/src/local-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
renameSync,
|
|
966
|
-
writeFileSync as writeFileSync3
|
|
967
|
-
} from "fs";
|
|
968
|
-
import { homedir as homedir4, hostname as osHostname } from "os";
|
|
969
|
-
import { dirname as dirname3, join as join4 } from "path";
|
|
970
|
-
var LOCAL_CONFIG_VERSION = 1;
|
|
971
|
-
var DEFAULT_SCOUT_WEB_PORTAL_HOST = "scout.local";
|
|
972
|
-
var DEFAULT_LOCAL_CONFIG = {
|
|
973
|
-
version: LOCAL_CONFIG_VERSION,
|
|
974
|
-
host: "127.0.0.1",
|
|
975
|
-
webLocalName: undefined,
|
|
976
|
-
ports: {
|
|
977
|
-
broker: 65535,
|
|
978
|
-
web: 3200,
|
|
979
|
-
pairing: 7888
|
|
2629
|
+
// packages/runtime/src/local-edge.ts
|
|
2630
|
+
function normalizeViteHmrPath(value) {
|
|
2631
|
+
const trimmed = value?.trim();
|
|
2632
|
+
if (!trimmed) {
|
|
2633
|
+
return DEFAULT_SCOUT_WEB_VITE_HMR_PATH;
|
|
980
2634
|
}
|
|
981
|
-
};
|
|
982
|
-
|
|
983
|
-
const firstLabel = value?.trim().replace(/\.local\.?$/i, "").split(".").find((part) => part.trim().length > 0)?.trim();
|
|
984
|
-
const normalized = firstLabel?.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
|
|
985
|
-
return normalized || "localhost";
|
|
986
|
-
}
|
|
987
|
-
function normalizeLocalHostname(value) {
|
|
988
|
-
const trimmed = value?.trim().replace(/\.$/, "").toLowerCase();
|
|
989
|
-
const labels = trimmed?.split(".").map((label) => label.trim().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-")).filter(Boolean);
|
|
990
|
-
return labels && labels.length > 0 ? labels.join(".") : "localhost";
|
|
991
|
-
}
|
|
992
|
-
function resolveScoutWebNamedHostname(name) {
|
|
993
|
-
const normalized = normalizeLocalHostname(name);
|
|
994
|
-
return normalized.includes(".") ? normalized : `${normalized}.${DEFAULT_SCOUT_WEB_PORTAL_HOST}`;
|
|
2635
|
+
const normalized = `${trimmed.startsWith("/") ? "" : "/"}${trimmed}`.replace(/\/{2,}/g, "/").replace(/\/+$/g, "");
|
|
2636
|
+
return normalized || DEFAULT_SCOUT_WEB_VITE_HMR_PATH;
|
|
995
2637
|
}
|
|
996
|
-
function
|
|
997
|
-
|
|
998
|
-
|
|
2638
|
+
function resolveViteUpstream(input) {
|
|
2639
|
+
const url = input.viteDevUrl?.trim();
|
|
2640
|
+
if (url) {
|
|
2641
|
+
try {
|
|
2642
|
+
return new URL(url).host;
|
|
2643
|
+
} catch {
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
999
2646
|
}
|
|
1000
|
-
|
|
1001
|
-
}
|
|
1002
|
-
function localConfigHome() {
|
|
1003
|
-
return process.env.OPENSCOUT_HOME ?? join4(homedir4(), ".openscout");
|
|
1004
|
-
}
|
|
1005
|
-
function localConfigPath() {
|
|
1006
|
-
return join4(localConfigHome(), "config.json");
|
|
1007
|
-
}
|
|
1008
|
-
function loadLocalConfig() {
|
|
1009
|
-
const configPath = localConfigPath();
|
|
1010
|
-
if (!existsSync4(configPath))
|
|
1011
|
-
return { version: LOCAL_CONFIG_VERSION };
|
|
1012
|
-
try {
|
|
1013
|
-
return validateLocalConfig(JSON.parse(readFileSync2(configPath, "utf8")));
|
|
1014
|
-
} catch {
|
|
1015
|
-
return { version: LOCAL_CONFIG_VERSION };
|
|
2647
|
+
if (typeof input.vitePort === "number" && input.vitePort > 0) {
|
|
2648
|
+
return `127.0.0.1:${input.vitePort}`;
|
|
1016
2649
|
}
|
|
2650
|
+
return;
|
|
1017
2651
|
}
|
|
1018
|
-
function
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
const out = { version: LOCAL_CONFIG_VERSION };
|
|
1023
|
-
if (typeof raw.host === "string" && raw.host.trim().length > 0) {
|
|
1024
|
-
out.host = raw.host.trim();
|
|
1025
|
-
}
|
|
1026
|
-
if (typeof raw.webLocalName === "string" && raw.webLocalName.trim().length > 0) {
|
|
1027
|
-
out.webLocalName = raw.webLocalName.trim();
|
|
1028
|
-
}
|
|
1029
|
-
if (raw.ports && typeof raw.ports === "object") {
|
|
1030
|
-
const ports = raw.ports;
|
|
1031
|
-
const p = {};
|
|
1032
|
-
if (isValidPort(ports.broker))
|
|
1033
|
-
p.broker = ports.broker;
|
|
1034
|
-
if (isValidPort(ports.web))
|
|
1035
|
-
p.web = ports.web;
|
|
1036
|
-
if (isValidPort(ports.pairing))
|
|
1037
|
-
p.pairing = ports.pairing;
|
|
1038
|
-
if (Object.keys(p).length > 0)
|
|
1039
|
-
out.ports = p;
|
|
2652
|
+
function normalizeRouteHost(value) {
|
|
2653
|
+
const normalized = value?.trim().replace(/\.$/, "").toLowerCase();
|
|
2654
|
+
if (!normalized || normalized.includes("/") || /\s/.test(normalized)) {
|
|
2655
|
+
return null;
|
|
1040
2656
|
}
|
|
1041
|
-
return
|
|
1042
|
-
}
|
|
1043
|
-
function isValidPort(value) {
|
|
1044
|
-
return typeof value === "number" && Number.isInteger(value) && value > 0 && value < 65536;
|
|
1045
|
-
}
|
|
1046
|
-
function resolveBrokerPort() {
|
|
1047
|
-
return loadLocalConfig().ports?.broker ?? DEFAULT_LOCAL_CONFIG.ports.broker;
|
|
1048
|
-
}
|
|
1049
|
-
function resolveWebPort() {
|
|
1050
|
-
return loadLocalConfig().ports?.web ?? DEFAULT_LOCAL_CONFIG.ports.web;
|
|
2657
|
+
return normalized;
|
|
1051
2658
|
}
|
|
1052
|
-
|
|
1053
|
-
// packages/runtime/src/local-edge.ts
|
|
1054
2659
|
function uniqRoutes(routes) {
|
|
1055
2660
|
const seen = new Set;
|
|
1056
2661
|
const out = [];
|
|
1057
2662
|
for (const route of routes) {
|
|
1058
|
-
const
|
|
2663
|
+
const host = normalizeRouteHost(route.host);
|
|
2664
|
+
if (!host) {
|
|
2665
|
+
continue;
|
|
2666
|
+
}
|
|
2667
|
+
const key = `${host} ${route.upstream}`;
|
|
1059
2668
|
if (seen.has(key)) {
|
|
1060
2669
|
continue;
|
|
1061
2670
|
}
|
|
1062
2671
|
seen.add(key);
|
|
1063
|
-
out.push(route);
|
|
2672
|
+
out.push({ ...route, host });
|
|
1064
2673
|
}
|
|
1065
2674
|
return out;
|
|
1066
2675
|
}
|
|
2676
|
+
function formatCaddyHost(host) {
|
|
2677
|
+
if (host.includes(":") && !host.startsWith("[") && !host.endsWith("]")) {
|
|
2678
|
+
return `[${host}]`;
|
|
2679
|
+
}
|
|
2680
|
+
return host;
|
|
2681
|
+
}
|
|
1067
2682
|
function resolveOpenScoutLocalEdgeConfig(input = {}) {
|
|
1068
2683
|
const portalHost = resolveScoutWebNamedHostname(input.portalHost ?? DEFAULT_SCOUT_WEB_PORTAL_HOST);
|
|
1069
2684
|
const nodeHost = resolveScoutWebNamedHostname(input.nodeHost ?? resolveConfiguredScoutWebHostname());
|
|
@@ -1071,6 +2686,7 @@ function resolveOpenScoutLocalEdgeConfig(input = {}) {
|
|
|
1071
2686
|
const scheme = input.scheme ?? "http";
|
|
1072
2687
|
const upstream = `127.0.0.1:${input.webPort ?? resolveWebPort()}`;
|
|
1073
2688
|
const brokerUpstream = `127.0.0.1:${input.brokerPort ?? resolveBrokerPort()}`;
|
|
2689
|
+
const viteUpstream = resolveViteUpstream(input);
|
|
1074
2690
|
return {
|
|
1075
2691
|
portalHost,
|
|
1076
2692
|
nodeHost,
|
|
@@ -1079,8 +2695,18 @@ function resolveOpenScoutLocalEdgeConfig(input = {}) {
|
|
|
1079
2695
|
brokerUpstream,
|
|
1080
2696
|
routes: uniqRoutes([
|
|
1081
2697
|
{ host: portalHost, upstream },
|
|
1082
|
-
{ host: wildcardHost, upstream }
|
|
1083
|
-
|
|
2698
|
+
{ host: wildcardHost, upstream },
|
|
2699
|
+
...viteUpstream ? [{
|
|
2700
|
+
host: resolveScoutWebDevHostname(portalHost),
|
|
2701
|
+
upstream: viteUpstream,
|
|
2702
|
+
mode: "vite-dev"
|
|
2703
|
+
}] : [],
|
|
2704
|
+
...(input.extraHosts ?? []).map((host) => ({ host, upstream }))
|
|
2705
|
+
]),
|
|
2706
|
+
...viteUpstream ? {
|
|
2707
|
+
viteUpstream,
|
|
2708
|
+
viteHmrPath: normalizeViteHmrPath(input.viteHmrPath)
|
|
2709
|
+
} : {}
|
|
1084
2710
|
};
|
|
1085
2711
|
}
|
|
1086
2712
|
function renderOpenScoutStartPage(config) {
|
|
@@ -1281,12 +2907,10 @@ function renderOpenScoutStartPage(config) {
|
|
|
1281
2907
|
</body>
|
|
1282
2908
|
</html>`;
|
|
1283
2909
|
}
|
|
1284
|
-
function
|
|
1285
|
-
const
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
const host = scheme === "http" ? `http://${route.host}` : route.host;
|
|
1289
|
-
return `${host} {
|
|
2910
|
+
function renderCaddyDefaultRouteBlock(config, route, scheme, startPage) {
|
|
2911
|
+
const routeHost = formatCaddyHost(route.host);
|
|
2912
|
+
const host = scheme === "http" ? `http://${routeHost}` : routeHost;
|
|
2913
|
+
return `${host} {
|
|
1290
2914
|
` + (scheme === "https" ? ` tls internal
|
|
1291
2915
|
` : "") + ` handle /__openscout/web/start {
|
|
1292
2916
|
` + ` rewrite * /v1/web/start
|
|
@@ -1298,8 +2922,8 @@ function renderOpenScoutCaddyfile(config) {
|
|
|
1298
2922
|
` + ` }
|
|
1299
2923
|
` + ` handle {
|
|
1300
2924
|
` + ` reverse_proxy ${route.upstream} {
|
|
1301
|
-
` + ` lb_try_duration
|
|
1302
|
-
` + ` lb_try_interval
|
|
2925
|
+
` + ` lb_try_duration 15s
|
|
2926
|
+
` + ` lb_try_interval 500ms
|
|
1303
2927
|
` + ` }
|
|
1304
2928
|
` + ` }
|
|
1305
2929
|
` + ` handle_errors {
|
|
@@ -1309,7 +2933,24 @@ function renderOpenScoutCaddyfile(config) {
|
|
|
1309
2933
|
` + `HTML 200
|
|
1310
2934
|
` + ` }
|
|
1311
2935
|
` + `}`;
|
|
1312
|
-
|
|
2936
|
+
}
|
|
2937
|
+
function renderCaddyViteDevRouteBlock(route, scheme) {
|
|
2938
|
+
const routeHost = formatCaddyHost(route.host);
|
|
2939
|
+
const host = scheme === "http" ? `http://${routeHost}` : routeHost;
|
|
2940
|
+
return `${host} {
|
|
2941
|
+
` + (scheme === "https" ? ` tls internal
|
|
2942
|
+
` : "") + ` handle {
|
|
2943
|
+
` + ` reverse_proxy ${route.upstream} {
|
|
2944
|
+
` + ` lb_try_duration 15s
|
|
2945
|
+
` + ` lb_try_interval 500ms
|
|
2946
|
+
` + ` }
|
|
2947
|
+
` + ` }
|
|
2948
|
+
` + `}`;
|
|
2949
|
+
}
|
|
2950
|
+
function renderOpenScoutCaddyfile(config) {
|
|
2951
|
+
const schemes = config.scheme === "both" ? ["http", "https"] : [config.scheme];
|
|
2952
|
+
const startPage = renderOpenScoutStartPage(config);
|
|
2953
|
+
const blocks = schemes.flatMap((scheme) => config.routes.map((route) => route.mode === "vite-dev" ? renderCaddyViteDevRouteBlock(route, scheme) : renderCaddyDefaultRouteBlock(config, route, scheme, startPage))).join(`
|
|
1313
2954
|
|
|
1314
2955
|
`);
|
|
1315
2956
|
return `${blocks}
|
|
@@ -1322,8 +2963,9 @@ var RESTART_MAX_DELAY_MS = 30000;
|
|
|
1322
2963
|
var BROKER_HEALTH_TIMEOUT_MS = 30000;
|
|
1323
2964
|
var BROKER_HEALTH_POLL_MS = 250;
|
|
1324
2965
|
var CHILD_SHUTDOWN_TIMEOUT_MS = 12000;
|
|
1325
|
-
var
|
|
1326
|
-
var
|
|
2966
|
+
var WEB_START_RETRY_MS = 5000;
|
|
2967
|
+
var MENU_BUNDLE_ID = "app.openscout.scout.menu";
|
|
2968
|
+
var MENU_PROCESS_NAME = "ScoutMenu";
|
|
1327
2969
|
var PROCESS_NAME = "scout-base";
|
|
1328
2970
|
var BROKER_LAUNCHER_PROCESS_NAME = "scout-broker-run";
|
|
1329
2971
|
var EDGE_PROCESS_NAME = "scout-edge";
|
|
@@ -1336,8 +2978,11 @@ var mdnsProcesses = [];
|
|
|
1336
2978
|
var brokerRestartDelayMs = RESTART_MIN_DELAY_MS;
|
|
1337
2979
|
var edgeRestartDelayMs = RESTART_MIN_DELAY_MS;
|
|
1338
2980
|
var supervisedWebPid = null;
|
|
1339
|
-
var
|
|
2981
|
+
var baseKeepAlive = null;
|
|
2982
|
+
var webStartRetryTimer = null;
|
|
2983
|
+
var webStartInFlight = false;
|
|
1340
2984
|
var config = resolveBrokerServiceConfig();
|
|
2985
|
+
var brokerControlUrl = buildLocalBrokerControlUrl(config.brokerHost, config.brokerPort);
|
|
1341
2986
|
function log(message, details) {
|
|
1342
2987
|
if (details === undefined) {
|
|
1343
2988
|
console.log(`[openscout-base] ${message}`);
|
|
@@ -1352,24 +2997,54 @@ function warn(message, details) {
|
|
|
1352
2997
|
}
|
|
1353
2998
|
console.warn(`[openscout-base] ${message}`, details);
|
|
1354
2999
|
}
|
|
3000
|
+
function splitCsv(value) {
|
|
3001
|
+
return (value ?? "").split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
3002
|
+
}
|
|
3003
|
+
function appendCsvValues(input, values) {
|
|
3004
|
+
const seen = new Set;
|
|
3005
|
+
const out = [];
|
|
3006
|
+
for (const value of [...splitCsv(input), ...values]) {
|
|
3007
|
+
const normalized = value.trim().toLowerCase();
|
|
3008
|
+
if (!normalized || seen.has(normalized)) {
|
|
3009
|
+
continue;
|
|
3010
|
+
}
|
|
3011
|
+
seen.add(normalized);
|
|
3012
|
+
out.push(normalized);
|
|
3013
|
+
}
|
|
3014
|
+
return out.length > 0 ? out.join(",") : undefined;
|
|
3015
|
+
}
|
|
3016
|
+
function resolveTailnetWebHosts() {
|
|
3017
|
+
return readTailscaleSelfWebHostsSync();
|
|
3018
|
+
}
|
|
3019
|
+
function resolveWebTrustedHostsEnv() {
|
|
3020
|
+
return appendCsvValues(process.env.OPENSCOUT_WEB_TRUSTED_HOSTS, resolveTailnetWebHosts());
|
|
3021
|
+
}
|
|
1355
3022
|
function ensureDirectory(path) {
|
|
1356
|
-
|
|
3023
|
+
mkdirSync2(path, { recursive: true });
|
|
1357
3024
|
}
|
|
1358
3025
|
function logFile(name) {
|
|
1359
|
-
const dir =
|
|
3026
|
+
const dir = join6(config.supportDirectory, "logs", "base");
|
|
1360
3027
|
ensureDirectory(dir);
|
|
1361
|
-
return openSync(
|
|
3028
|
+
return openSync(join6(dir, name), "a");
|
|
1362
3029
|
}
|
|
1363
3030
|
function runtimeEntrypoint(config2) {
|
|
1364
|
-
return
|
|
3031
|
+
return join6(config2.runtimePackageDir, "bin", "openscout-runtime.mjs");
|
|
1365
3032
|
}
|
|
1366
3033
|
function spawnBroker() {
|
|
1367
3034
|
if (shuttingDown || brokerProcess) {
|
|
1368
3035
|
return;
|
|
1369
3036
|
}
|
|
3037
|
+
if (!config.bunExecutable) {
|
|
3038
|
+
warn("broker supervisor cannot start broker without Bun", {
|
|
3039
|
+
hint: "Use the headless broker entrypoint under Node or install Bun for the macOS base supervisor."
|
|
3040
|
+
});
|
|
3041
|
+
scheduleBrokerRestart();
|
|
3042
|
+
return;
|
|
3043
|
+
}
|
|
3044
|
+
const webTrustedHostsEnv = resolveWebTrustedHostsEnv();
|
|
1370
3045
|
const stdout = logFile("broker.stdout.log");
|
|
1371
3046
|
const stderr = logFile("broker.stderr.log");
|
|
1372
|
-
brokerProcess =
|
|
3047
|
+
brokerProcess = spawn3(config.bunExecutable, [
|
|
1373
3048
|
"run",
|
|
1374
3049
|
runtimeEntrypoint(config),
|
|
1375
3050
|
"broker"
|
|
@@ -1384,7 +3059,9 @@ function spawnBroker() {
|
|
|
1384
3059
|
OPENSCOUT_BROKER_URL: config.brokerUrl,
|
|
1385
3060
|
OPENSCOUT_BROKER_SOCKET_PATH: config.brokerSocketPath,
|
|
1386
3061
|
OPENSCOUT_CONTROL_HOME: config.controlHome,
|
|
1387
|
-
OPENSCOUT_ADVERTISE_SCOPE: config.advertiseScope
|
|
3062
|
+
OPENSCOUT_ADVERTISE_SCOPE: config.advertiseScope,
|
|
3063
|
+
...openScoutNetworkServiceEnvironment(process.env),
|
|
3064
|
+
...webTrustedHostsEnv ? { OPENSCOUT_WEB_TRUSTED_HOSTS: webTrustedHostsEnv } : {}
|
|
1388
3065
|
},
|
|
1389
3066
|
stdio: ["ignore", stdout, stderr]
|
|
1390
3067
|
});
|
|
@@ -1412,7 +3089,7 @@ async function waitForBrokerHealth(timeoutMs = BROKER_HEALTH_TIMEOUT_MS) {
|
|
|
1412
3089
|
const deadline = Date.now() + timeoutMs;
|
|
1413
3090
|
while (Date.now() < deadline) {
|
|
1414
3091
|
try {
|
|
1415
|
-
const response = await fetch(new URL("/health",
|
|
3092
|
+
const response = await fetch(new URL("/health", brokerControlUrl), {
|
|
1416
3093
|
headers: { accept: "application/json" },
|
|
1417
3094
|
signal: AbortSignal.timeout(1000)
|
|
1418
3095
|
});
|
|
@@ -1424,7 +3101,7 @@ async function waitForBrokerHealth(timeoutMs = BROKER_HEALTH_TIMEOUT_MS) {
|
|
|
1424
3101
|
}
|
|
1425
3102
|
}
|
|
1426
3103
|
} catch {}
|
|
1427
|
-
await
|
|
3104
|
+
await sleep(BROKER_HEALTH_POLL_MS);
|
|
1428
3105
|
}
|
|
1429
3106
|
return false;
|
|
1430
3107
|
}
|
|
@@ -1446,19 +3123,25 @@ function resolveEdgeConfig() {
|
|
|
1446
3123
|
nodeHost,
|
|
1447
3124
|
scheme: resolveEdgeScheme(),
|
|
1448
3125
|
brokerPort: config.brokerPort,
|
|
1449
|
-
webPort: Number.parseInt(process.env.OPENSCOUT_WEB_PORT ?? "", 10) || resolveWebPort()
|
|
3126
|
+
webPort: Number.parseInt(process.env.OPENSCOUT_WEB_PORT ?? "", 10) || resolveWebPort(),
|
|
3127
|
+
viteDevUrl: process.env.OPENSCOUT_WEB_VITE_URL,
|
|
3128
|
+
viteHmrPath: process.env.OPENSCOUT_WEB_VITE_HMR_PATH,
|
|
3129
|
+
extraHosts: [
|
|
3130
|
+
...splitCsv(process.env.OPENSCOUT_WEB_TRUSTED_HOSTS),
|
|
3131
|
+
...resolveTailnetWebHosts()
|
|
3132
|
+
]
|
|
1450
3133
|
});
|
|
1451
3134
|
}
|
|
1452
3135
|
function resolveLocalEdgeCaddyfilePath() {
|
|
1453
|
-
const dir =
|
|
3136
|
+
const dir = join6(homedir6(), ".scout", "local-edge");
|
|
1454
3137
|
ensureDirectory(dir);
|
|
1455
|
-
return
|
|
3138
|
+
return join6(dir, "Caddyfile");
|
|
1456
3139
|
}
|
|
1457
3140
|
function resolveCaddyExecutable() {
|
|
1458
3141
|
return process.env.OPENSCOUT_CADDY_BIN?.trim() || "caddy";
|
|
1459
3142
|
}
|
|
1460
3143
|
function spawnMdnsProxy(input) {
|
|
1461
|
-
return
|
|
3144
|
+
return spawn3("/usr/bin/dns-sd", [
|
|
1462
3145
|
"-P",
|
|
1463
3146
|
input.name,
|
|
1464
3147
|
input.scheme === "https" ? "_https._tcp" : "_http._tcp",
|
|
@@ -1494,7 +3177,7 @@ function startLocalEdge() {
|
|
|
1494
3177
|
const edgeConfig = resolveEdgeConfig();
|
|
1495
3178
|
const schemes = edgeConfig.scheme === "both" ? ["http", "https"] : [edgeConfig.scheme];
|
|
1496
3179
|
const caddyfilePath = resolveLocalEdgeCaddyfilePath();
|
|
1497
|
-
|
|
3180
|
+
writeFileSync2(caddyfilePath, renderOpenScoutCaddyfile(edgeConfig), "utf8");
|
|
1498
3181
|
mdnsProcesses = schemes.flatMap((scheme) => {
|
|
1499
3182
|
const edgePort = scheme === "https" ? 443 : 80;
|
|
1500
3183
|
const suffix = scheme.toUpperCase();
|
|
@@ -1513,7 +3196,7 @@ function startLocalEdge() {
|
|
|
1513
3196
|
})
|
|
1514
3197
|
];
|
|
1515
3198
|
});
|
|
1516
|
-
caddyProcess =
|
|
3199
|
+
caddyProcess = spawn3(resolveCaddyExecutable(), [
|
|
1517
3200
|
"run",
|
|
1518
3201
|
"--config",
|
|
1519
3202
|
caddyfilePath,
|
|
@@ -1556,32 +3239,53 @@ async function startWebWhenBrokerIsReady() {
|
|
|
1556
3239
|
if (process.env.OPENSCOUT_BASE_START_WEB === "0") {
|
|
1557
3240
|
return;
|
|
1558
3241
|
}
|
|
1559
|
-
if (
|
|
1560
|
-
warn("broker did not become healthy before web startup timeout");
|
|
3242
|
+
if (webStartInFlight) {
|
|
1561
3243
|
return;
|
|
1562
3244
|
}
|
|
3245
|
+
webStartInFlight = true;
|
|
1563
3246
|
try {
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
method: "POST",
|
|
1568
|
-
headers: {
|
|
1569
|
-
accept: "application/json",
|
|
1570
|
-
"x-forwarded-host": edgeConfig.portalHost,
|
|
1571
|
-
"x-forwarded-proto": scheme
|
|
1572
|
-
},
|
|
1573
|
-
signal: AbortSignal.timeout(15000)
|
|
1574
|
-
});
|
|
1575
|
-
const body = await response.json();
|
|
1576
|
-
supervisedWebPid = typeof body.pid === "number" ? body.pid : resolvePortListenerPid(Number.parseInt(process.env.OPENSCOUT_WEB_PORT ?? "", 10) || resolveWebPort());
|
|
1577
|
-
if (!response.ok || body.ok !== true) {
|
|
1578
|
-
warn("web server did not report healthy", body.error ?? response.statusText);
|
|
3247
|
+
if (!await waitForBrokerHealth()) {
|
|
3248
|
+
warn("broker did not become healthy before web startup timeout");
|
|
3249
|
+
scheduleWebStartRetry();
|
|
1579
3250
|
return;
|
|
1580
3251
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
3252
|
+
try {
|
|
3253
|
+
const edgeConfig = resolveEdgeConfig();
|
|
3254
|
+
const scheme = forwardedProtoForEdgeScheme(edgeConfig.scheme);
|
|
3255
|
+
const response = await fetch(new URL("/v1/web/start", brokerControlUrl), {
|
|
3256
|
+
method: "POST",
|
|
3257
|
+
headers: {
|
|
3258
|
+
accept: "application/json",
|
|
3259
|
+
"x-forwarded-host": edgeConfig.portalHost,
|
|
3260
|
+
"x-forwarded-proto": scheme
|
|
3261
|
+
},
|
|
3262
|
+
signal: AbortSignal.timeout(15000)
|
|
3263
|
+
});
|
|
3264
|
+
const body = await response.json();
|
|
3265
|
+
supervisedWebPid = typeof body.pid === "number" ? body.pid : resolvePortListenerPid(Number.parseInt(process.env.OPENSCOUT_WEB_PORT ?? "", 10) || resolveWebPort());
|
|
3266
|
+
if (!response.ok || body.ok !== true) {
|
|
3267
|
+
warn("web server did not report healthy", body.error ?? response.statusText);
|
|
3268
|
+
scheduleWebStartRetry();
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
log("web server ready", { pid: supervisedWebPid });
|
|
3272
|
+
} catch (error) {
|
|
3273
|
+
warn("web server startup failed", error instanceof Error ? error.message : String(error));
|
|
3274
|
+
scheduleWebStartRetry();
|
|
3275
|
+
}
|
|
3276
|
+
} finally {
|
|
3277
|
+
webStartInFlight = false;
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
function scheduleWebStartRetry() {
|
|
3281
|
+
if (shuttingDown || webStartRetryTimer || process.env.OPENSCOUT_BASE_START_WEB === "0") {
|
|
3282
|
+
return;
|
|
1584
3283
|
}
|
|
3284
|
+
webStartRetryTimer = setTimeout(() => {
|
|
3285
|
+
webStartRetryTimer = null;
|
|
3286
|
+
startWebWhenBrokerIsReady();
|
|
3287
|
+
}, WEB_START_RETRY_MS);
|
|
3288
|
+
webStartRetryTimer.unref();
|
|
1585
3289
|
}
|
|
1586
3290
|
function resolvePortListenerPid(port) {
|
|
1587
3291
|
const result = spawnSync2("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], {
|
|
@@ -1596,8 +3300,8 @@ function resolvePortListenerPid(port) {
|
|
|
1596
3300
|
function findRepoMenuBundle() {
|
|
1597
3301
|
let current = dirname4(fileURLToPath2(import.meta.url));
|
|
1598
3302
|
while (true) {
|
|
1599
|
-
const candidate = resolve3(current, "apps", "macos", "dist", "
|
|
1600
|
-
if (
|
|
3303
|
+
const candidate = resolve3(current, "apps", "macos", "dist", "ScoutMenu.app");
|
|
3304
|
+
if (existsSync6(candidate)) {
|
|
1601
3305
|
return candidate;
|
|
1602
3306
|
}
|
|
1603
3307
|
const parent = dirname4(current);
|
|
@@ -1612,9 +3316,9 @@ function startMenuBarApp() {
|
|
|
1612
3316
|
return;
|
|
1613
3317
|
}
|
|
1614
3318
|
const explicitBundle = process.env.OPENSCOUT_MENU_BUNDLE_PATH?.trim();
|
|
1615
|
-
const repoBundle = explicitBundle &&
|
|
3319
|
+
const repoBundle = explicitBundle && existsSync6(explicitBundle) ? explicitBundle : findRepoMenuBundle();
|
|
1616
3320
|
const args = repoBundle ? [repoBundle] : ["-b", MENU_BUNDLE_ID];
|
|
1617
|
-
const child =
|
|
3321
|
+
const child = spawn3("open", args, {
|
|
1618
3322
|
stdio: ["ignore", logFile("menu.stdout.log"), logFile("menu.stderr.log")]
|
|
1619
3323
|
});
|
|
1620
3324
|
child.once("exit", (code) => {
|
|
@@ -1629,7 +3333,7 @@ function stopMenuBarApp() {
|
|
|
1629
3333
|
if (process.platform !== "darwin" || process.env.OPENSCOUT_BASE_MENU_ENABLED === "0") {
|
|
1630
3334
|
return;
|
|
1631
3335
|
}
|
|
1632
|
-
|
|
3336
|
+
spawn3("pkill", ["-x", MENU_PROCESS_NAME], { stdio: "ignore" }).unref();
|
|
1633
3337
|
}
|
|
1634
3338
|
function isChildExited(child) {
|
|
1635
3339
|
return child.exitCode !== null || child.signalCode !== null;
|
|
@@ -1682,9 +3386,13 @@ async function shutdown(exitCode = 0) {
|
|
|
1682
3386
|
return;
|
|
1683
3387
|
}
|
|
1684
3388
|
shuttingDown = true;
|
|
1685
|
-
if (
|
|
1686
|
-
clearInterval(
|
|
1687
|
-
|
|
3389
|
+
if (baseKeepAlive) {
|
|
3390
|
+
clearInterval(baseKeepAlive);
|
|
3391
|
+
baseKeepAlive = null;
|
|
3392
|
+
}
|
|
3393
|
+
if (webStartRetryTimer) {
|
|
3394
|
+
clearTimeout(webStartRetryTimer);
|
|
3395
|
+
webStartRetryTimer = null;
|
|
1688
3396
|
}
|
|
1689
3397
|
stopSupervisedWeb();
|
|
1690
3398
|
stopMenuBarApp();
|
|
@@ -1694,14 +3402,14 @@ async function shutdown(exitCode = 0) {
|
|
|
1694
3402
|
await terminateChildProcess(activeCaddyProcess, "local edge", 2000);
|
|
1695
3403
|
process.exit(exitCode);
|
|
1696
3404
|
}
|
|
1697
|
-
function
|
|
3405
|
+
function sleep(ms) {
|
|
1698
3406
|
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
1699
3407
|
}
|
|
1700
|
-
function
|
|
1701
|
-
if (
|
|
3408
|
+
function anchorBaseDaemonLifetime() {
|
|
3409
|
+
if (baseKeepAlive) {
|
|
1702
3410
|
return;
|
|
1703
3411
|
}
|
|
1704
|
-
|
|
3412
|
+
baseKeepAlive = setInterval(() => {
|
|
1705
3413
|
return;
|
|
1706
3414
|
}, 60000);
|
|
1707
3415
|
}
|
|
@@ -1718,7 +3426,7 @@ log("starting Scout base service", {
|
|
|
1718
3426
|
brokerUrl: config.brokerUrl,
|
|
1719
3427
|
bootout: `launchctl bootout ${config.serviceTarget}`
|
|
1720
3428
|
});
|
|
1721
|
-
|
|
3429
|
+
anchorBaseDaemonLifetime();
|
|
1722
3430
|
spawnBroker();
|
|
1723
3431
|
startLocalEdge();
|
|
1724
3432
|
startMenuBarApp();
|