@openscout/scout 0.2.72 → 0.2.73
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/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/RepoDiffViewer-D8gg36W3.js +56 -0
- package/dist/client/assets/{addon-fit-C5ueiijZ.js → addon-fit-D4KdY-pj.js} +1 -1
- package/dist/client/assets/{addon-webgl-Cm7sl8RO.js → addon-webgl-Bo_tmKBp.js} +1 -1
- package/dist/client/assets/arc.es-D0gujIKy.js +188 -0
- package/dist/client/assets/embed-entry-CUVNwyB_.js +1 -0
- package/dist/client/assets/index-CRkjiso5.js +257 -0
- package/dist/client/assets/index-D_RX2cRb.css +1 -0
- package/dist/client/assets/{xterm-FFux-PdF.js → xterm-RfflDFod.js} +1 -1
- 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 +72583 -60970
- package/dist/node/main.mjs +7607 -0
- package/dist/openscout-terminal-relay.mjs +3819 -158
- package/dist/{pair-supervisor.mjs → pairing-runtime-controller.mjs} +51525 -46801
- package/dist/runtime/base-daemon.mjs +2153 -502
- package/dist/runtime/broker-daemon.mjs +34730 -27036
- package/dist/runtime/broker-process-manager.mjs +2038 -421
- package/dist/runtime/mesh-discover.mjs +2002 -420
- package/dist/scout-control-plane-web.mjs +54522 -39921
- package/dist/scout-web-server.mjs +54522 -39921
- 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,12 +1,13 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/runtime/src/broker-process-manager.ts
|
|
3
|
-
import { spawnSync } from "child_process";
|
|
4
|
-
import {
|
|
5
|
-
import { homedir as
|
|
6
|
-
import { dirname as
|
|
3
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
4
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
5
|
+
import { homedir as homedir5 } from "os";
|
|
6
|
+
import { dirname as dirname3, join as join5, resolve as resolve2 } from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
|
|
9
9
|
// packages/runtime/src/broker-api.ts
|
|
10
|
+
import { existsSync } from "fs";
|
|
10
11
|
import { request as httpRequest } from "http";
|
|
11
12
|
var DEFAULT_BROKER_JSON_REQUEST_TIMEOUT_MS = 30000;
|
|
12
13
|
function normalizeBaseUrl(baseUrl) {
|
|
@@ -55,13 +56,17 @@ async function requestBrokerOverHttp(baseUrl, path, options) {
|
|
|
55
56
|
}
|
|
56
57
|
function shouldFallbackFromUnixSocket(error) {
|
|
57
58
|
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
58
|
-
return code === "ENOENT" || code === "ECONNREFUSED" || code === "ENOTSOCK" || code === "EACCES" || code === "FailedToOpenSocket";
|
|
59
|
+
return code === "ENOENT" || code === "ECONNREFUSED" || code === "ENOTSOCK" || code === "EACCES" || code === "EINVAL" || code === "FailedToOpenSocket";
|
|
59
60
|
}
|
|
60
61
|
function errorMessage(error) {
|
|
61
62
|
return error instanceof Error ? error.message : String(error);
|
|
62
63
|
}
|
|
63
64
|
function requestBrokerOverUnixSocket(socketPath, baseUrl, path, options) {
|
|
64
65
|
return new Promise((resolve, reject) => {
|
|
66
|
+
if (!existsSync(socketPath)) {
|
|
67
|
+
reject(Object.assign(new Error(`Scout broker socket does not exist: ${socketPath}`), { code: "ENOENT" }));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
65
70
|
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
66
71
|
const url = new URL(path, normalizedBaseUrl);
|
|
67
72
|
const body = requestBody(options);
|
|
@@ -142,100 +147,1569 @@ async function requestBrokerWire(baseUrl, path, options) {
|
|
|
142
147
|
}
|
|
143
148
|
}
|
|
144
149
|
return {
|
|
145
|
-
response: await requestBrokerOverHttp(baseUrl, path, requestOptions),
|
|
146
|
-
trace: {
|
|
147
|
-
transport: "http"
|
|
150
|
+
response: await requestBrokerOverHttp(baseUrl, path, requestOptions),
|
|
151
|
+
trace: {
|
|
152
|
+
transport: "http"
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
async function requestScoutBrokerJsonWithTrace(baseUrl, path, options = {}) {
|
|
157
|
+
const { response, trace } = await requestBrokerWire(baseUrl, path, options);
|
|
158
|
+
let parsed;
|
|
159
|
+
let parsedJson = false;
|
|
160
|
+
if (response.text.length > 0) {
|
|
161
|
+
try {
|
|
162
|
+
parsed = JSON.parse(response.text);
|
|
163
|
+
parsedJson = true;
|
|
164
|
+
} catch {
|
|
165
|
+
parsedJson = false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
if (parsedJson && options.acceptErrorJson?.(parsed)) {
|
|
170
|
+
return { value: parsed, trace };
|
|
171
|
+
}
|
|
172
|
+
throw new Error(`${path} returned ${response.status}: ${response.text}`);
|
|
173
|
+
}
|
|
174
|
+
if (parsedJson) {
|
|
175
|
+
return { value: parsed, trace };
|
|
176
|
+
}
|
|
177
|
+
return { value: undefined, trace };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// packages/runtime/src/support-paths.ts
|
|
181
|
+
import { homedir } from "os";
|
|
182
|
+
import { join } from "path";
|
|
183
|
+
var OPENSCOUT_RPC_CUTOVER_MARKER = "rpc-runtime-cutover-v1";
|
|
184
|
+
function resolveOpenScoutSupportPaths() {
|
|
185
|
+
const home = homedir();
|
|
186
|
+
const supportDirectory = process.env.OPENSCOUT_SUPPORT_DIRECTORY ?? join(home, "Library", "Application Support", "OpenScout");
|
|
187
|
+
const logsDirectory = join(supportDirectory, "logs");
|
|
188
|
+
const runtimeDirectory = join(supportDirectory, "runtime");
|
|
189
|
+
const catalogDirectory = join(supportDirectory, "catalog");
|
|
190
|
+
const controlHome = process.env.OPENSCOUT_CONTROL_HOME ?? join(home, ".openscout", "control-plane");
|
|
191
|
+
const knowledgeDirectory = join(controlHome, "knowledge");
|
|
192
|
+
return {
|
|
193
|
+
supportDirectory,
|
|
194
|
+
logsDirectory,
|
|
195
|
+
appLogsDirectory: join(logsDirectory, "app"),
|
|
196
|
+
brokerLogsDirectory: join(logsDirectory, "broker"),
|
|
197
|
+
runtimeDirectory,
|
|
198
|
+
catalogDirectory,
|
|
199
|
+
relayAgentsDirectory: join(runtimeDirectory, "agents"),
|
|
200
|
+
settingsPath: join(supportDirectory, "settings.json"),
|
|
201
|
+
harnessCatalogPath: join(catalogDirectory, "harness-catalog.json"),
|
|
202
|
+
relayAgentsRegistryPath: join(supportDirectory, "relay-agents.json"),
|
|
203
|
+
managedInstallsPath: join(supportDirectory, "managed-installs.json"),
|
|
204
|
+
hostInfoPath: join(supportDirectory, ".host-info"),
|
|
205
|
+
relayHubDirectory: process.env.OPENSCOUT_RELAY_HUB ?? join(home, ".openscout", "relay"),
|
|
206
|
+
controlHome,
|
|
207
|
+
knowledgeDirectory,
|
|
208
|
+
knowledgeQmdDirectory: join(knowledgeDirectory, "qmd"),
|
|
209
|
+
knowledgeSqlitePath: join(knowledgeDirectory, "knowledge.sqlite"),
|
|
210
|
+
desktopStatusPath: join(supportDirectory, "agent-status.json"),
|
|
211
|
+
workspaceStatePath: join(supportDirectory, "workspace-state.json"),
|
|
212
|
+
cutoverMarkerPath: join(supportDirectory, OPENSCOUT_RPC_CUTOVER_MARKER)
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// packages/runtime/src/open-scout-network.ts
|
|
217
|
+
import { readFileSync } from "fs";
|
|
218
|
+
var DEFAULT_OPENSCOUT_NETWORK_RENDEZVOUS_URL = "https://mesh.oscout.net";
|
|
219
|
+
var DEFAULT_OPENSCOUT_NETWORK_PAIRING_RELAY_URL = "wss://mesh.oscout.net/v1/relay";
|
|
220
|
+
function defaultOpenScoutNetworkSettings() {
|
|
221
|
+
return {
|
|
222
|
+
discoveryEnabled: false,
|
|
223
|
+
rendezvousUrl: DEFAULT_OPENSCOUT_NETWORK_RENDEZVOUS_URL,
|
|
224
|
+
pairingRelayUrl: DEFAULT_OPENSCOUT_NETWORK_PAIRING_RELAY_URL,
|
|
225
|
+
keepPairingRelayRunning: true
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function normalizeOpenScoutNetworkSettings(input) {
|
|
229
|
+
const base = defaultOpenScoutNetworkSettings();
|
|
230
|
+
const record = isRecord(input) ? input : {};
|
|
231
|
+
return {
|
|
232
|
+
discoveryEnabled: typeof record.discoveryEnabled === "boolean" ? record.discoveryEnabled : base.discoveryEnabled,
|
|
233
|
+
rendezvousUrl: normalizeUrlString(record.rendezvousUrl, base.rendezvousUrl),
|
|
234
|
+
pairingRelayUrl: normalizeUrlString(record.pairingRelayUrl, base.pairingRelayUrl),
|
|
235
|
+
keepPairingRelayRunning: typeof record.keepPairingRelayRunning === "boolean" ? record.keepPairingRelayRunning : base.keepPairingRelayRunning
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function readOpenScoutNetworkSettingsSync() {
|
|
239
|
+
try {
|
|
240
|
+
const raw = readFileSync(resolveOpenScoutSupportPaths().settingsPath, "utf8");
|
|
241
|
+
const parsed = JSON.parse(raw);
|
|
242
|
+
const network = isRecord(parsed) && isRecord(parsed.network) ? parsed.network : {};
|
|
243
|
+
return normalizeOpenScoutNetworkSettings(isRecord(network.openScoutNetwork) ? network.openScoutNetwork : {});
|
|
244
|
+
} catch {
|
|
245
|
+
return defaultOpenScoutNetworkSettings();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function openScoutNetworkDiscoveryEnabled(env = process.env) {
|
|
249
|
+
const explicit = readBooleanEnv(env.OPENSCOUT_NETWORK_DISCOVERY_ENABLED) ?? readBooleanEnv(env.OPENSCOUT_OSN_DISCOVERY_ENABLED);
|
|
250
|
+
if (explicit !== undefined) {
|
|
251
|
+
return explicit;
|
|
252
|
+
}
|
|
253
|
+
return readOpenScoutNetworkSettingsSync().discoveryEnabled;
|
|
254
|
+
}
|
|
255
|
+
function normalizeUrlString(value, fallback) {
|
|
256
|
+
if (typeof value !== "string") {
|
|
257
|
+
return fallback;
|
|
258
|
+
}
|
|
259
|
+
const trimmed = value.trim().replace(/\/$/, "");
|
|
260
|
+
return trimmed || fallback;
|
|
261
|
+
}
|
|
262
|
+
function readBooleanEnv(value) {
|
|
263
|
+
const normalized = value?.trim().toLowerCase();
|
|
264
|
+
if (!normalized)
|
|
265
|
+
return;
|
|
266
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
267
|
+
return true;
|
|
268
|
+
if (["0", "false", "no", "off"].includes(normalized))
|
|
269
|
+
return false;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
function isRecord(value) {
|
|
273
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// packages/runtime/src/tailscale.ts
|
|
277
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
278
|
+
import { execFileSync } from "child_process";
|
|
279
|
+
|
|
280
|
+
// packages/runtime/src/system-probes/tailscale-status.ts
|
|
281
|
+
import { readFile } from "fs/promises";
|
|
282
|
+
|
|
283
|
+
// packages/runtime/src/system-probes/registry.ts
|
|
284
|
+
var DEFAULT_MIN_MAX_STALE_MS = 2 * 60000;
|
|
285
|
+
var PROBE_RUN_OUTPUT = Symbol("openscout.probeRunOutput");
|
|
286
|
+
var registeredProbes = [];
|
|
287
|
+
|
|
288
|
+
class ProbeBackendError extends Error {
|
|
289
|
+
backend;
|
|
290
|
+
fallbackSince;
|
|
291
|
+
fallbackReason;
|
|
292
|
+
code;
|
|
293
|
+
timedOut;
|
|
294
|
+
constructor(message, metadata, cause) {
|
|
295
|
+
super(message);
|
|
296
|
+
this.name = "ProbeBackendError";
|
|
297
|
+
this.backend = metadata.backend;
|
|
298
|
+
this.fallbackSince = metadata.fallbackSince;
|
|
299
|
+
this.fallbackReason = metadata.fallbackReason;
|
|
300
|
+
const details = probeErrorDetails(cause);
|
|
301
|
+
if (details.code) {
|
|
302
|
+
this.code = details.code;
|
|
303
|
+
}
|
|
304
|
+
if (details.timedOut !== undefined) {
|
|
305
|
+
this.timedOut = details.timedOut;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function probeRunOutput(value, metadata) {
|
|
310
|
+
return {
|
|
311
|
+
[PROBE_RUN_OUTPUT]: true,
|
|
312
|
+
value,
|
|
313
|
+
...metadata
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function isProbeRunOutput(value) {
|
|
317
|
+
return typeof value === "object" && value !== null && value[PROBE_RUN_OUTPUT] === true;
|
|
318
|
+
}
|
|
319
|
+
function backendMetadataFromError(error) {
|
|
320
|
+
if (error instanceof ProbeBackendError) {
|
|
321
|
+
return {
|
|
322
|
+
backend: error.backend,
|
|
323
|
+
fallbackSince: error.fallbackSince,
|
|
324
|
+
fallbackReason: error.fallbackReason
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (typeof error === "object" && error !== null) {
|
|
328
|
+
const record = error;
|
|
329
|
+
if (record.backend === "local" || record.backend === "scoutd" || record.backend === "local-fallback") {
|
|
330
|
+
return {
|
|
331
|
+
backend: record.backend,
|
|
332
|
+
fallbackSince: typeof record.fallbackSince === "number" ? record.fallbackSince : undefined,
|
|
333
|
+
fallbackReason: typeof record.fallbackReason === "string" ? record.fallbackReason : undefined
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
function probeErrorDetails(error) {
|
|
340
|
+
if (typeof error !== "object" || error === null) {
|
|
341
|
+
return {};
|
|
342
|
+
}
|
|
343
|
+
const record = error;
|
|
344
|
+
const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : undefined;
|
|
345
|
+
const timedOut = record.timedOut === true || code === "timeout" ? true : record.timedOut === false ? false : undefined;
|
|
346
|
+
return { code, timedOut };
|
|
347
|
+
}
|
|
348
|
+
function assertProbeSpec(spec) {
|
|
349
|
+
if (!spec.id.trim()) {
|
|
350
|
+
throw new Error("Probe id is required");
|
|
351
|
+
}
|
|
352
|
+
if (!Number.isFinite(spec.ttlMs) || spec.ttlMs <= 0) {
|
|
353
|
+
throw new Error(`Probe ${spec.id} must declare a positive ttlMs`);
|
|
354
|
+
}
|
|
355
|
+
if (!Number.isFinite(spec.timeoutMs) || spec.timeoutMs <= 0) {
|
|
356
|
+
throw new Error(`Probe ${spec.id} must declare a positive timeoutMs`);
|
|
357
|
+
}
|
|
358
|
+
if (spec.maxStaleMs !== undefined && (!Number.isFinite(spec.maxStaleMs) || spec.maxStaleMs <= 0)) {
|
|
359
|
+
throw new Error(`Probe ${spec.id} maxStaleMs must be positive when set`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function maxStaleMsFor(spec) {
|
|
363
|
+
return spec.maxStaleMs ?? Math.max(DEFAULT_MIN_MAX_STALE_MS, spec.ttlMs * 10);
|
|
364
|
+
}
|
|
365
|
+
function failureBackoffMs(consecutiveFailures) {
|
|
366
|
+
if (consecutiveFailures <= 0) {
|
|
367
|
+
return 0;
|
|
368
|
+
}
|
|
369
|
+
return Math.min(30000, 1000 * 2 ** Math.min(consecutiveFailures - 1, 5));
|
|
370
|
+
}
|
|
371
|
+
function probeErrorFromUnknown(error, at, timedOut = false) {
|
|
372
|
+
if (typeof error === "object" && error !== null) {
|
|
373
|
+
const record = error;
|
|
374
|
+
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";
|
|
375
|
+
const message = typeof record.message === "string" && record.message.trim() ? record.message : String(error);
|
|
376
|
+
return { code, message, at, ...timedOut ? { timedOut: true } : {} };
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
code: timedOut ? "timeout" : "error",
|
|
380
|
+
message: String(error),
|
|
381
|
+
at,
|
|
382
|
+
...timedOut ? { timedOut: true } : {}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function timeoutError(probeId, timeoutMs) {
|
|
386
|
+
return {
|
|
387
|
+
code: "timeout",
|
|
388
|
+
message: `Probe ${probeId} timed out after ${timeoutMs}ms`,
|
|
389
|
+
at: Date.now(),
|
|
390
|
+
timedOut: true
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function staleTooLongError(probeId, ageMs, maxStaleMs) {
|
|
394
|
+
return {
|
|
395
|
+
code: "max_stale_exceeded",
|
|
396
|
+
message: `Probe ${probeId} last good snapshot is ${ageMs}ms old, exceeding maxStaleMs ${maxStaleMs}`,
|
|
397
|
+
at: Date.now()
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function isFreshEnough(state, maxAgeMs) {
|
|
401
|
+
if (state.at === null || state.invalidated) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
return Date.now() - state.at <= maxAgeMs;
|
|
405
|
+
}
|
|
406
|
+
function logBackendTransition(input) {
|
|
407
|
+
if (input.from === input.to) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const keySuffix = input.key === undefined ? "" : ` key=${JSON.stringify(input.key)}`;
|
|
411
|
+
const reasonSuffix = input.reason ? ` (${input.reason})` : "";
|
|
412
|
+
console.warn(`[openscout] system probe ${input.id}${keySuffix} backend ${input.from} -> ${input.to}${reasonSuffix}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
class ProbeInstance {
|
|
416
|
+
spec;
|
|
417
|
+
key;
|
|
418
|
+
runProbe;
|
|
419
|
+
scheduleRun;
|
|
420
|
+
state = {
|
|
421
|
+
value: null,
|
|
422
|
+
at: null,
|
|
423
|
+
error: null,
|
|
424
|
+
consecutiveFailures: 0,
|
|
425
|
+
inFlight: null,
|
|
426
|
+
invalidated: false,
|
|
427
|
+
invalidationReason: null,
|
|
428
|
+
invalidationSerial: 0,
|
|
429
|
+
nextRetryAt: 0,
|
|
430
|
+
backend: "local",
|
|
431
|
+
fallbackSince: null,
|
|
432
|
+
fallbackReason: null
|
|
433
|
+
};
|
|
434
|
+
metricState;
|
|
435
|
+
lastAccessAt = Date.now();
|
|
436
|
+
constructor(options) {
|
|
437
|
+
this.spec = options.spec;
|
|
438
|
+
this.key = options.key;
|
|
439
|
+
this.runProbe = options.run;
|
|
440
|
+
this.scheduleRun = options.scheduleRun ?? ((task) => task());
|
|
441
|
+
this.metricState = {
|
|
442
|
+
id: options.spec.id,
|
|
443
|
+
...options.key !== undefined ? { key: options.key } : {},
|
|
444
|
+
backend: "local",
|
|
445
|
+
runCount: 0,
|
|
446
|
+
failureCount: 0,
|
|
447
|
+
timeoutCount: 0,
|
|
448
|
+
staleServedCount: 0,
|
|
449
|
+
lastRunAt: null,
|
|
450
|
+
lastDurationMs: null,
|
|
451
|
+
lastSuccessAt: null
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
read() {
|
|
455
|
+
this.touch();
|
|
456
|
+
if (this.shouldRefreshForRead(Date.now())) {
|
|
457
|
+
this.ensureRefresh(false, this.spec.ttlMs);
|
|
458
|
+
}
|
|
459
|
+
const snap = this.snapshot();
|
|
460
|
+
if (snap.status === "stale" || snap.status === "failed" && snap.at !== null) {
|
|
461
|
+
this.metricState.staleServedCount += 1;
|
|
462
|
+
}
|
|
463
|
+
return snap;
|
|
464
|
+
}
|
|
465
|
+
async fresh(options = {}) {
|
|
466
|
+
this.touch();
|
|
467
|
+
const maxAgeMs = options.maxAgeMs ?? this.spec.ttlMs;
|
|
468
|
+
for (let attempt = 0;attempt < 3; attempt += 1) {
|
|
469
|
+
if (isFreshEnough(this.state, maxAgeMs)) {
|
|
470
|
+
return this.snapshot();
|
|
471
|
+
}
|
|
472
|
+
await this.ensureRefresh(true, maxAgeMs);
|
|
473
|
+
if (!this.state.invalidated) {
|
|
474
|
+
return this.snapshot();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return this.snapshot();
|
|
478
|
+
}
|
|
479
|
+
snapshot() {
|
|
480
|
+
const now = Date.now();
|
|
481
|
+
const ageMs = this.state.at === null ? null : Math.max(0, now - this.state.at);
|
|
482
|
+
const maxStaleMs = maxStaleMsFor(this.spec);
|
|
483
|
+
const exceededMaxStale = ageMs !== null && ageMs > maxStaleMs;
|
|
484
|
+
const fresh = ageMs !== null && !this.state.invalidated && ageMs <= this.spec.ttlMs;
|
|
485
|
+
const status = this.state.at === null ? this.state.error ? "failed" : "empty" : exceededMaxStale ? "failed" : fresh ? "fresh" : "stale";
|
|
486
|
+
const error = exceededMaxStale ? this.state.error ?? staleTooLongError(this.spec.id, ageMs ?? 0, maxStaleMs) : this.state.error;
|
|
487
|
+
return {
|
|
488
|
+
id: this.spec.id,
|
|
489
|
+
...this.key !== undefined ? { key: this.key } : {},
|
|
490
|
+
value: status === "failed" && exceededMaxStale ? null : this.state.value,
|
|
491
|
+
at: this.state.at,
|
|
492
|
+
ageMs,
|
|
493
|
+
stale: status === "stale" || status === "failed" && this.state.at !== null,
|
|
494
|
+
refreshing: this.state.inFlight !== null,
|
|
495
|
+
status,
|
|
496
|
+
error,
|
|
497
|
+
consecutiveFailures: this.state.consecutiveFailures,
|
|
498
|
+
backend: this.state.backend,
|
|
499
|
+
...this.state.fallbackSince !== null ? { fallbackSince: this.state.fallbackSince } : {},
|
|
500
|
+
...this.state.fallbackReason !== null ? { fallbackReason: this.state.fallbackReason } : {}
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
invalidate(reason) {
|
|
504
|
+
this.touch();
|
|
505
|
+
this.state.invalidated = true;
|
|
506
|
+
this.state.invalidationReason = reason ?? null;
|
|
507
|
+
this.state.invalidationSerial += 1;
|
|
508
|
+
this.state.nextRetryAt = 0;
|
|
509
|
+
}
|
|
510
|
+
metrics() {
|
|
511
|
+
return {
|
|
512
|
+
...this.metricState,
|
|
513
|
+
consecutiveFailures: this.state.consecutiveFailures,
|
|
514
|
+
inFlight: this.state.inFlight !== null,
|
|
515
|
+
...this.state.fallbackSince !== null ? { fallbackSince: this.state.fallbackSince } : {},
|
|
516
|
+
...this.state.fallbackReason !== null ? { fallbackReason: this.state.fallbackReason } : {}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
isRefreshing() {
|
|
520
|
+
return this.state.inFlight !== null;
|
|
521
|
+
}
|
|
522
|
+
touch() {
|
|
523
|
+
this.lastAccessAt = Date.now();
|
|
524
|
+
}
|
|
525
|
+
shouldRefreshForRead(now) {
|
|
526
|
+
if (this.state.inFlight !== null) {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
if (this.state.nextRetryAt > now) {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
if (this.state.at === null) {
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
if (this.state.invalidated) {
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
return now - this.state.at > this.spec.ttlMs;
|
|
539
|
+
}
|
|
540
|
+
ensureRefresh(force, maxAgeMs) {
|
|
541
|
+
if (this.state.inFlight) {
|
|
542
|
+
return this.state.inFlight;
|
|
543
|
+
}
|
|
544
|
+
const now = Date.now();
|
|
545
|
+
if (!force && this.state.nextRetryAt > now) {
|
|
546
|
+
return Promise.resolve();
|
|
547
|
+
}
|
|
548
|
+
const inFlight = this.scheduleRun(() => this.executeRun(maxAgeMs)).finally(() => {
|
|
549
|
+
if (this.state.inFlight === inFlight) {
|
|
550
|
+
this.state.inFlight = null;
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
this.state.inFlight = inFlight;
|
|
554
|
+
return inFlight;
|
|
555
|
+
}
|
|
556
|
+
async executeRun(maxAgeMs) {
|
|
557
|
+
const startedAt = Date.now();
|
|
558
|
+
const previousBackend = this.state.backend;
|
|
559
|
+
const invalidationSerialAtStart = this.state.invalidationSerial;
|
|
560
|
+
const controller = new AbortController;
|
|
561
|
+
let timeout = null;
|
|
562
|
+
this.metricState.runCount += 1;
|
|
563
|
+
this.metricState.lastRunAt = startedAt;
|
|
564
|
+
const ctx = {
|
|
565
|
+
probeId: this.spec.id,
|
|
566
|
+
...this.key !== undefined ? { key: this.key } : {},
|
|
567
|
+
signal: controller.signal,
|
|
568
|
+
timeoutMs: this.spec.timeoutMs,
|
|
569
|
+
maxAgeMs,
|
|
570
|
+
startedAt
|
|
571
|
+
};
|
|
572
|
+
const timeoutProbeError = timeoutError(this.spec.id, this.spec.timeoutMs);
|
|
573
|
+
const runPromise = this.runProbe(ctx);
|
|
574
|
+
runPromise.catch(() => {
|
|
575
|
+
return;
|
|
576
|
+
});
|
|
577
|
+
try {
|
|
578
|
+
const result = await new Promise((resolve, reject) => {
|
|
579
|
+
timeout = setTimeout(() => {
|
|
580
|
+
controller.abort(timeoutProbeError);
|
|
581
|
+
reject(timeoutProbeError);
|
|
582
|
+
}, this.spec.timeoutMs);
|
|
583
|
+
runPromise.then(resolve, reject);
|
|
584
|
+
});
|
|
585
|
+
const output = isProbeRunOutput(result) ? result : probeRunOutput(result, { backend: "local" });
|
|
586
|
+
this.state.value = output.value;
|
|
587
|
+
this.state.at = output.generatedAt ?? Date.now();
|
|
588
|
+
this.state.error = null;
|
|
589
|
+
this.state.consecutiveFailures = 0;
|
|
590
|
+
if (this.state.invalidationSerial === invalidationSerialAtStart) {
|
|
591
|
+
this.state.invalidated = false;
|
|
592
|
+
this.state.invalidationReason = null;
|
|
593
|
+
}
|
|
594
|
+
this.state.nextRetryAt = 0;
|
|
595
|
+
this.state.backend = output.backend;
|
|
596
|
+
this.state.fallbackSince = output.fallbackSince ?? null;
|
|
597
|
+
this.state.fallbackReason = output.fallbackReason ?? null;
|
|
598
|
+
this.metricState.backend = output.backend;
|
|
599
|
+
this.metricState.lastSuccessAt = this.state.at;
|
|
600
|
+
logBackendTransition({
|
|
601
|
+
id: this.spec.id,
|
|
602
|
+
key: this.key,
|
|
603
|
+
from: previousBackend,
|
|
604
|
+
to: output.backend,
|
|
605
|
+
reason: output.fallbackReason
|
|
606
|
+
});
|
|
607
|
+
} catch (error) {
|
|
608
|
+
const timedOut = error === timeoutProbeError || typeof error === "object" && error !== null && (error.code === "timeout" || error.timedOut === true);
|
|
609
|
+
const at = Date.now();
|
|
610
|
+
this.state.error = error === timeoutProbeError ? timeoutProbeError : probeErrorFromUnknown(error, at, timedOut);
|
|
611
|
+
this.state.consecutiveFailures += 1;
|
|
612
|
+
this.state.nextRetryAt = at + failureBackoffMs(this.state.consecutiveFailures);
|
|
613
|
+
const backendMetadata = backendMetadataFromError(error);
|
|
614
|
+
if (backendMetadata) {
|
|
615
|
+
this.state.backend = backendMetadata.backend;
|
|
616
|
+
this.state.fallbackSince = backendMetadata.fallbackSince ?? null;
|
|
617
|
+
this.state.fallbackReason = backendMetadata.fallbackReason ?? null;
|
|
618
|
+
this.metricState.backend = backendMetadata.backend;
|
|
619
|
+
logBackendTransition({
|
|
620
|
+
id: this.spec.id,
|
|
621
|
+
key: this.key,
|
|
622
|
+
from: previousBackend,
|
|
623
|
+
to: backendMetadata.backend,
|
|
624
|
+
reason: backendMetadata.fallbackReason
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
this.metricState.failureCount += 1;
|
|
628
|
+
if (this.state.error.timedOut || this.state.error.code === "timeout") {
|
|
629
|
+
this.metricState.timeoutCount += 1;
|
|
630
|
+
}
|
|
631
|
+
} finally {
|
|
632
|
+
if (timeout) {
|
|
633
|
+
clearTimeout(timeout);
|
|
634
|
+
}
|
|
635
|
+
this.metricState.lastDurationMs = Date.now() - startedAt;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
class FamilyLimiter {
|
|
641
|
+
maxConcurrent;
|
|
642
|
+
active = 0;
|
|
643
|
+
queue = [];
|
|
644
|
+
constructor(maxConcurrent) {
|
|
645
|
+
this.maxConcurrent = maxConcurrent;
|
|
646
|
+
}
|
|
647
|
+
queued() {
|
|
648
|
+
return this.queue.length;
|
|
649
|
+
}
|
|
650
|
+
async run(task) {
|
|
651
|
+
if (this.active >= this.maxConcurrent) {
|
|
652
|
+
await new Promise((resolve) => this.queue.push(resolve));
|
|
653
|
+
}
|
|
654
|
+
this.active += 1;
|
|
655
|
+
try {
|
|
656
|
+
return await task();
|
|
657
|
+
} finally {
|
|
658
|
+
this.active -= 1;
|
|
659
|
+
const next = this.queue.shift();
|
|
660
|
+
if (next) {
|
|
661
|
+
next();
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
class ProbeFamily {
|
|
668
|
+
spec;
|
|
669
|
+
entries = new Map;
|
|
670
|
+
limiter;
|
|
671
|
+
constructor(spec) {
|
|
672
|
+
this.spec = spec;
|
|
673
|
+
assertProbeSpec(spec);
|
|
674
|
+
if (!Number.isInteger(spec.maxKeys) || spec.maxKeys <= 0) {
|
|
675
|
+
throw new Error(`Probe family ${spec.id} must declare a positive maxKeys`);
|
|
676
|
+
}
|
|
677
|
+
if (!Number.isFinite(spec.idleKeyTtlMs) || spec.idleKeyTtlMs <= 0) {
|
|
678
|
+
throw new Error(`Probe family ${spec.id} must declare a positive idleKeyTtlMs`);
|
|
679
|
+
}
|
|
680
|
+
if (!Number.isInteger(spec.maxConcurrentKeys) || spec.maxConcurrentKeys <= 0) {
|
|
681
|
+
throw new Error(`Probe family ${spec.id} must declare a positive maxConcurrentKeys`);
|
|
682
|
+
}
|
|
683
|
+
this.limiter = new FamilyLimiter(spec.maxConcurrentKeys);
|
|
684
|
+
}
|
|
685
|
+
for(rawKey) {
|
|
686
|
+
const key = this.normalize(rawKey);
|
|
687
|
+
this.cleanupIdle(Date.now());
|
|
688
|
+
let entry = this.entries.get(key);
|
|
689
|
+
if (!entry) {
|
|
690
|
+
entry = new ProbeInstance({
|
|
691
|
+
spec: this.spec,
|
|
692
|
+
key,
|
|
693
|
+
run: (ctx) => this.spec.run(key, ctx),
|
|
694
|
+
scheduleRun: (task) => this.limiter.run(task)
|
|
695
|
+
});
|
|
696
|
+
this.entries.set(key, entry);
|
|
697
|
+
}
|
|
698
|
+
entry.lastAccessAt = Date.now();
|
|
699
|
+
this.evictLruIfNeeded();
|
|
700
|
+
return entry;
|
|
701
|
+
}
|
|
702
|
+
snapshot(key) {
|
|
703
|
+
return this.for(key).snapshot();
|
|
704
|
+
}
|
|
705
|
+
invalidate(key, reason) {
|
|
706
|
+
this.for(key).invalidate(reason);
|
|
707
|
+
}
|
|
708
|
+
metrics() {
|
|
709
|
+
this.cleanupIdle(Date.now());
|
|
710
|
+
return {
|
|
711
|
+
id: this.spec.id,
|
|
712
|
+
keyCount: this.entries.size,
|
|
713
|
+
maxKeys: this.spec.maxKeys,
|
|
714
|
+
activeRuns: this.limiter.active,
|
|
715
|
+
queuedRuns: this.limiter.queued(),
|
|
716
|
+
keys: Array.from(this.entries.values(), (entry) => entry.metrics())
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
keys() {
|
|
720
|
+
this.cleanupIdle(Date.now());
|
|
721
|
+
return Array.from(this.entries.keys());
|
|
722
|
+
}
|
|
723
|
+
normalize(rawKey) {
|
|
724
|
+
const key = this.spec.normalizeKey(rawKey).trim();
|
|
725
|
+
if (!key) {
|
|
726
|
+
throw new Error(`Probe family ${this.spec.id} normalized an empty key`);
|
|
727
|
+
}
|
|
728
|
+
return key;
|
|
729
|
+
}
|
|
730
|
+
cleanupIdle(now) {
|
|
731
|
+
for (const [key, entry] of this.entries) {
|
|
732
|
+
if (!entry.isRefreshing() && now - entry.lastAccessAt > this.spec.idleKeyTtlMs) {
|
|
733
|
+
this.entries.delete(key);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
evictLruIfNeeded() {
|
|
738
|
+
while (this.entries.size > this.spec.maxKeys) {
|
|
739
|
+
let oldestKey = null;
|
|
740
|
+
let oldestAccess = Number.POSITIVE_INFINITY;
|
|
741
|
+
for (const [key, entry] of this.entries) {
|
|
742
|
+
if (entry.isRefreshing()) {
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (entry.lastAccessAt < oldestAccess) {
|
|
746
|
+
oldestAccess = entry.lastAccessAt;
|
|
747
|
+
oldestKey = key;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (!oldestKey) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
this.entries.delete(oldestKey);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
function defineProbe(spec) {
|
|
758
|
+
assertProbeSpec(spec);
|
|
759
|
+
const handle = new ProbeInstance({
|
|
760
|
+
spec,
|
|
761
|
+
run: spec.run
|
|
762
|
+
});
|
|
763
|
+
registeredProbes.push({
|
|
764
|
+
kind: "probe",
|
|
765
|
+
id: spec.id,
|
|
766
|
+
handle
|
|
767
|
+
});
|
|
768
|
+
return handle;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// packages/runtime/src/system-probes/exec.ts
|
|
772
|
+
import { spawn } from "child_process";
|
|
773
|
+
|
|
774
|
+
// packages/runtime/src/system-probes/scoutd-client.ts
|
|
775
|
+
import { existsSync as existsSync2 } from "fs";
|
|
776
|
+
import { homedir as homedir2 } from "os";
|
|
777
|
+
import { join as join2 } from "path";
|
|
778
|
+
import { Socket } from "net";
|
|
779
|
+
|
|
780
|
+
// packages/runtime/src/system-probes/scout-host-catalog.ts
|
|
781
|
+
var SCOUT_HOST_PROBE_SCHEMA_VERSIONS = {
|
|
782
|
+
"tailscale.status": 1,
|
|
783
|
+
"git.buildInfo": 1,
|
|
784
|
+
"git.revParse": 1,
|
|
785
|
+
"git.diffShortstat": 1,
|
|
786
|
+
"git.statusPorcelain": 1,
|
|
787
|
+
"git.mergeBase": 1,
|
|
788
|
+
"git.logLastCommitUnix": 1,
|
|
789
|
+
"git.worktreeListPorcelain": 1,
|
|
790
|
+
"tmux.sessions": 1,
|
|
791
|
+
"tmux.panes": 1,
|
|
792
|
+
"zellij.sessions": 1,
|
|
793
|
+
"ps.runtime": 1,
|
|
794
|
+
"ps.discovery": 1,
|
|
795
|
+
"ps.cwd": 1,
|
|
796
|
+
"net.listeners": 1,
|
|
797
|
+
"repo.scan": 1,
|
|
798
|
+
"repo.diff": 1
|
|
799
|
+
};
|
|
800
|
+
var SCOUT_HOST_EXEC_VERB_SCHEMA_VERSIONS = {
|
|
801
|
+
"tmux.sendKeys": 1,
|
|
802
|
+
"tmux.sendKeysLiteral": 1,
|
|
803
|
+
"tmux.loadBuffer": 1,
|
|
804
|
+
"tmux.pasteBuffer": 1,
|
|
805
|
+
"tmux.deleteBuffer": 1,
|
|
806
|
+
"tmux.killSession": 1,
|
|
807
|
+
"tmux.newSession": 1,
|
|
808
|
+
"tmux.detachClient": 1,
|
|
809
|
+
"tailscale.cert": 1,
|
|
810
|
+
"reveal.open": 1
|
|
811
|
+
};
|
|
812
|
+
function expectedScoutHostProbeSchemaVersion(probeId) {
|
|
813
|
+
return Object.hasOwn(SCOUT_HOST_PROBE_SCHEMA_VERSIONS, probeId) ? SCOUT_HOST_PROBE_SCHEMA_VERSIONS[probeId] : null;
|
|
814
|
+
}
|
|
815
|
+
function expectedScoutHostExecVerbSchemaVersion(verb) {
|
|
816
|
+
return Object.hasOwn(SCOUT_HOST_EXEC_VERB_SCHEMA_VERSIONS, verb) ? SCOUT_HOST_EXEC_VERB_SCHEMA_VERSIONS[verb] : null;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// packages/runtime/src/system-probes/scoutd-client.ts
|
|
820
|
+
var CAPABILITIES_SCHEMA = "openscout.probe.capabilities/v1";
|
|
821
|
+
var REQUEST_SCHEMA = "openscout.probe.request/v1";
|
|
822
|
+
var SNAPSHOT_SCHEMA = "openscout.probe.snapshot/v1";
|
|
823
|
+
var ERROR_SCHEMA = "openscout.probe.error/v1";
|
|
824
|
+
var EXEC_REQUEST_SCHEMA = "openscout.exec.request/v1";
|
|
825
|
+
var EXEC_RESPONSE_SCHEMA = "openscout.exec.response/v1";
|
|
826
|
+
var CAPABILITY_RECHECK_MS = 1e4;
|
|
827
|
+
var SOCKET_TIMEOUT_MS = 900;
|
|
828
|
+
var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
829
|
+
function readString(value) {
|
|
830
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
831
|
+
}
|
|
832
|
+
function readNumber(value) {
|
|
833
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
834
|
+
}
|
|
835
|
+
function isRecord2(value) {
|
|
836
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
837
|
+
}
|
|
838
|
+
function fallbackMessage(error) {
|
|
839
|
+
if (error instanceof Error && error.message.trim()) {
|
|
840
|
+
return error.message;
|
|
841
|
+
}
|
|
842
|
+
return String(error);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
class ScoutdExecResponseError extends Error {
|
|
846
|
+
code;
|
|
847
|
+
timedOut;
|
|
848
|
+
constructor(message, options = {}) {
|
|
849
|
+
super(message);
|
|
850
|
+
this.name = "ScoutdExecResponseError";
|
|
851
|
+
this.code = options.code ?? "exec_error";
|
|
852
|
+
this.timedOut = options.timedOut ?? false;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
class ScoutdProbeResponseError extends Error {
|
|
857
|
+
code;
|
|
858
|
+
timedOut;
|
|
859
|
+
constructor(message, options = {}) {
|
|
860
|
+
super(message);
|
|
861
|
+
this.name = "ScoutdProbeResponseError";
|
|
862
|
+
this.code = options.code ?? "probe_error";
|
|
863
|
+
this.timedOut = options.timedOut ?? false;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
function resolveScoutdProbesSocketPath(env = process.env) {
|
|
867
|
+
const explicit = env.OPENSCOUT_PROBES_SOCKET?.trim();
|
|
868
|
+
if (explicit) {
|
|
869
|
+
return explicit;
|
|
870
|
+
}
|
|
871
|
+
const openScoutHome = env.OPENSCOUT_HOME?.trim() || join2(homedir2(), ".openscout");
|
|
872
|
+
return join2(openScoutHome, "run", "scoutd-probes.sock");
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
class ScoutdProbeClient {
|
|
876
|
+
env;
|
|
877
|
+
capabilities = null;
|
|
878
|
+
lastCapabilityCheckAt = null;
|
|
879
|
+
lastError = null;
|
|
880
|
+
daemonObserved = false;
|
|
881
|
+
fallbackByProbe = new Map;
|
|
882
|
+
fallbackByExec = new Map;
|
|
883
|
+
constructor(env = process.env) {
|
|
884
|
+
this.env = env;
|
|
885
|
+
}
|
|
886
|
+
async requestProbe(input) {
|
|
887
|
+
const socketPath = resolveScoutdProbesSocketPath(this.env);
|
|
888
|
+
const socketTimeoutMs = probeSocketTimeoutMs(input.timeoutMs);
|
|
889
|
+
const socketExists = existsSync2(socketPath);
|
|
890
|
+
if (!socketExists) {
|
|
891
|
+
this.capabilities = null;
|
|
892
|
+
this.lastError = null;
|
|
893
|
+
return this.daemonObserved ? this.fallback(input.probeId, input.key, `probe socket is missing: ${socketPath}`) : { state: "local" };
|
|
894
|
+
}
|
|
895
|
+
this.daemonObserved = true;
|
|
896
|
+
const capabilities = await this.ensureCapabilities(socketPath, socketTimeoutMs);
|
|
897
|
+
if (!capabilities) {
|
|
898
|
+
return this.fallback(input.probeId, input.key, this.lastError ?? "probe capabilities unavailable");
|
|
899
|
+
}
|
|
900
|
+
const capability = capabilities.families.get(input.probeId);
|
|
901
|
+
if (!capability) {
|
|
902
|
+
return this.fallback(input.probeId, input.key, `scoutd does not serve ${input.probeId}`);
|
|
903
|
+
}
|
|
904
|
+
const expectedSchemaVersion = expectedScoutHostProbeSchemaVersion(input.probeId);
|
|
905
|
+
if (expectedSchemaVersion === null) {
|
|
906
|
+
return this.fallback(input.probeId, input.key, `client has no compiled schema version for ${input.probeId}`);
|
|
907
|
+
}
|
|
908
|
+
if (capability.schemaVersion !== expectedSchemaVersion) {
|
|
909
|
+
return this.fallback(input.probeId, input.key, `scoutd serves ${input.probeId} schema v${capability.schemaVersion}, expected v${expectedSchemaVersion}`);
|
|
910
|
+
}
|
|
911
|
+
try {
|
|
912
|
+
const response = await requestJson(socketPath, {
|
|
913
|
+
schema: REQUEST_SCHEMA,
|
|
914
|
+
schemaVersion: expectedSchemaVersion,
|
|
915
|
+
probeId: input.probeId,
|
|
916
|
+
key: input.key ?? null,
|
|
917
|
+
maxAgeMs: input.maxAgeMs,
|
|
918
|
+
opTimeoutMs: requestOpTimeoutMs(input.timeoutMs)
|
|
919
|
+
}, { timeoutMs: socketTimeoutMs });
|
|
920
|
+
if (!isRecord2(response) || response.schema !== SNAPSHOT_SCHEMA) {
|
|
921
|
+
throw new Error("scoutd returned an invalid probe snapshot envelope");
|
|
922
|
+
}
|
|
923
|
+
if (response.error) {
|
|
924
|
+
const error = isRecord2(response.error) ? response.error : {};
|
|
925
|
+
const code = readString(error.code) ?? "probe_error";
|
|
926
|
+
const message = readString(error.message) ?? code;
|
|
927
|
+
const timedOut = error.timedOut === true || error.timed_out === true;
|
|
928
|
+
throw new ScoutdProbeResponseError(message, { code, timedOut });
|
|
929
|
+
}
|
|
930
|
+
this.fallbackByProbe.delete(fallbackKey(input.probeId, input.key));
|
|
931
|
+
this.lastError = null;
|
|
932
|
+
return {
|
|
933
|
+
state: "scoutd",
|
|
934
|
+
value: response.value,
|
|
935
|
+
generatedAt: readNumber(response.generatedAt) ?? Date.now(),
|
|
936
|
+
daemonVersion: readString(response.daemonVersion) ?? capabilities.daemonVersion
|
|
937
|
+
};
|
|
938
|
+
} catch (error) {
|
|
939
|
+
if (!(error instanceof ScoutdProbeResponseError)) {
|
|
940
|
+
this.capabilities = null;
|
|
941
|
+
this.lastCapabilityCheckAt = null;
|
|
942
|
+
}
|
|
943
|
+
this.lastError = fallbackMessage(error);
|
|
944
|
+
return this.fallback(input.probeId, input.key, this.lastError);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
async requestExecVerb(input) {
|
|
948
|
+
const socketPath = resolveScoutdProbesSocketPath(this.env);
|
|
949
|
+
const socketTimeoutMs = execSocketTimeoutMs(input.args);
|
|
950
|
+
const socketExists = existsSync2(socketPath);
|
|
951
|
+
if (!socketExists) {
|
|
952
|
+
this.capabilities = null;
|
|
953
|
+
this.lastError = null;
|
|
954
|
+
return this.daemonObserved ? this.execFallback(input.verb, `probe socket is missing: ${socketPath}`) : { state: "local" };
|
|
955
|
+
}
|
|
956
|
+
this.daemonObserved = true;
|
|
957
|
+
const capabilities = await this.ensureCapabilities(socketPath, socketTimeoutMs);
|
|
958
|
+
if (!capabilities) {
|
|
959
|
+
return this.execFallback(input.verb, this.lastError ?? "probe capabilities unavailable");
|
|
960
|
+
}
|
|
961
|
+
const capability = capabilities.verbs.get(input.verb);
|
|
962
|
+
if (!capability) {
|
|
963
|
+
return this.execFallback(input.verb, `scoutd does not serve ${input.verb}`);
|
|
964
|
+
}
|
|
965
|
+
const expectedSchemaVersion = expectedScoutHostExecVerbSchemaVersion(input.verb);
|
|
966
|
+
if (expectedSchemaVersion === null) {
|
|
967
|
+
return this.execFallback(input.verb, `client has no compiled schema version for ${input.verb}`);
|
|
968
|
+
}
|
|
969
|
+
if (capability.schemaVersion !== expectedSchemaVersion) {
|
|
970
|
+
return this.execFallback(input.verb, `scoutd serves ${input.verb} schema v${capability.schemaVersion}, expected v${expectedSchemaVersion}`);
|
|
971
|
+
}
|
|
972
|
+
try {
|
|
973
|
+
const response = await requestJson(socketPath, {
|
|
974
|
+
schema: EXEC_REQUEST_SCHEMA,
|
|
975
|
+
schemaVersion: expectedSchemaVersion,
|
|
976
|
+
verb: input.verb,
|
|
977
|
+
args: input.args
|
|
978
|
+
}, { timeoutMs: socketTimeoutMs });
|
|
979
|
+
if (!isRecord2(response) || response.schema !== EXEC_RESPONSE_SCHEMA) {
|
|
980
|
+
throw new Error("scoutd returned an invalid exec response envelope");
|
|
981
|
+
}
|
|
982
|
+
if (!response.ok) {
|
|
983
|
+
const error = isRecord2(response.error) ? response.error : {};
|
|
984
|
+
const code = readString(error.code) ?? "exec_error";
|
|
985
|
+
const message = readString(error.message) ?? code;
|
|
986
|
+
const timedOut = error.timedOut === true || error.timed_out === true;
|
|
987
|
+
throw new ScoutdExecResponseError(message, { code, timedOut });
|
|
988
|
+
}
|
|
989
|
+
this.fallbackByExec.delete(input.verb);
|
|
990
|
+
this.lastError = null;
|
|
991
|
+
return {
|
|
992
|
+
state: "scoutd",
|
|
993
|
+
value: response.value,
|
|
994
|
+
daemonVersion: readString(response.daemonVersion) ?? capabilities.daemonVersion
|
|
995
|
+
};
|
|
996
|
+
} catch (error) {
|
|
997
|
+
if (error instanceof ScoutdExecResponseError) {
|
|
998
|
+
throw error;
|
|
999
|
+
}
|
|
1000
|
+
this.capabilities = null;
|
|
1001
|
+
this.lastCapabilityCheckAt = null;
|
|
1002
|
+
this.lastError = fallbackMessage(error);
|
|
1003
|
+
return this.execFallback(input.verb, this.lastError);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
diagnostics() {
|
|
1007
|
+
const socketPath = resolveScoutdProbesSocketPath(this.env);
|
|
1008
|
+
return {
|
|
1009
|
+
socketPath,
|
|
1010
|
+
socketExists: existsSync2(socketPath),
|
|
1011
|
+
daemonObserved: this.daemonObserved,
|
|
1012
|
+
daemonVersion: this.capabilities?.daemonVersion ?? null,
|
|
1013
|
+
supportedProbeIds: this.capabilities ? [...this.capabilities.families.keys()].sort() : [],
|
|
1014
|
+
supportedExecVerbs: this.capabilities ? [...this.capabilities.verbs.keys()].sort() : [],
|
|
1015
|
+
lastCapabilityCheckAt: this.lastCapabilityCheckAt,
|
|
1016
|
+
lastError: this.lastError
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
resetForTests() {
|
|
1020
|
+
this.capabilities = null;
|
|
1021
|
+
this.lastCapabilityCheckAt = null;
|
|
1022
|
+
this.lastError = null;
|
|
1023
|
+
this.daemonObserved = false;
|
|
1024
|
+
this.fallbackByProbe.clear();
|
|
1025
|
+
this.fallbackByExec.clear();
|
|
1026
|
+
}
|
|
1027
|
+
async ensureCapabilities(socketPath, timeoutMs = SOCKET_TIMEOUT_MS) {
|
|
1028
|
+
const now = Date.now();
|
|
1029
|
+
if (this.capabilities && this.lastCapabilityCheckAt !== null && now - this.lastCapabilityCheckAt < CAPABILITY_RECHECK_MS) {
|
|
1030
|
+
return this.capabilities;
|
|
1031
|
+
}
|
|
1032
|
+
this.lastCapabilityCheckAt = now;
|
|
1033
|
+
try {
|
|
1034
|
+
const response = await requestJson(socketPath, { schema: CAPABILITIES_SCHEMA }, { timeoutMs });
|
|
1035
|
+
if (!isRecord2(response) || response.schema !== CAPABILITIES_SCHEMA) {
|
|
1036
|
+
throw new Error("scoutd returned an invalid capabilities envelope");
|
|
1037
|
+
}
|
|
1038
|
+
const daemonVersion = readString(response.daemonVersion) ?? "unknown";
|
|
1039
|
+
const families = new Map;
|
|
1040
|
+
if (Array.isArray(response.families)) {
|
|
1041
|
+
for (const entry of response.families) {
|
|
1042
|
+
if (!isRecord2(entry))
|
|
1043
|
+
continue;
|
|
1044
|
+
const probeId = readString(entry.probeId);
|
|
1045
|
+
const schemaVersion = readNumber(entry.schemaVersion);
|
|
1046
|
+
const ttlMs = readNumber(entry.ttlMs);
|
|
1047
|
+
if (probeId && schemaVersion !== null && ttlMs !== null) {
|
|
1048
|
+
families.set(probeId, { probeId, schemaVersion, ttlMs });
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const verbs = new Map;
|
|
1053
|
+
if (Array.isArray(response.verbs)) {
|
|
1054
|
+
for (const entry of response.verbs) {
|
|
1055
|
+
if (!isRecord2(entry))
|
|
1056
|
+
continue;
|
|
1057
|
+
const verb = readString(entry.verb);
|
|
1058
|
+
const schemaVersion = readNumber(entry.schemaVersion);
|
|
1059
|
+
if (verb && schemaVersion !== null) {
|
|
1060
|
+
verbs.set(verb, { verb, schemaVersion });
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
this.capabilities = { daemonVersion, families, verbs };
|
|
1065
|
+
this.daemonObserved = true;
|
|
1066
|
+
this.lastError = null;
|
|
1067
|
+
return this.capabilities;
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
this.capabilities = null;
|
|
1070
|
+
this.lastError = fallbackMessage(error);
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
fallback(probeId, key, reason) {
|
|
1075
|
+
const id = fallbackKey(probeId, key);
|
|
1076
|
+
let state = this.fallbackByProbe.get(id);
|
|
1077
|
+
if (!state || state.reason !== reason) {
|
|
1078
|
+
state = { since: Date.now(), reason };
|
|
1079
|
+
this.fallbackByProbe.set(id, state);
|
|
1080
|
+
}
|
|
1081
|
+
return {
|
|
1082
|
+
state: "local",
|
|
1083
|
+
fallbackSince: state.since,
|
|
1084
|
+
fallbackReason: state.reason
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
execFallback(verb, reason) {
|
|
1088
|
+
let state = this.fallbackByExec.get(verb);
|
|
1089
|
+
if (!state || state.reason !== reason) {
|
|
1090
|
+
state = { since: Date.now(), reason };
|
|
1091
|
+
this.fallbackByExec.set(verb, state);
|
|
1092
|
+
}
|
|
1093
|
+
return {
|
|
1094
|
+
state: "local",
|
|
1095
|
+
fallbackSince: state.since,
|
|
1096
|
+
fallbackReason: state.reason
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
var singletonClient = null;
|
|
1101
|
+
function getScoutdProbeClient() {
|
|
1102
|
+
singletonClient ??= new ScoutdProbeClient;
|
|
1103
|
+
return singletonClient;
|
|
1104
|
+
}
|
|
1105
|
+
async function runWithScoutdFallback(input) {
|
|
1106
|
+
const client = getScoutdProbeClient();
|
|
1107
|
+
const scoutd = await client.requestProbe({
|
|
1108
|
+
probeId: input.probeId,
|
|
1109
|
+
key: input.key,
|
|
1110
|
+
maxAgeMs: input.ctx.maxAgeMs,
|
|
1111
|
+
timeoutMs: input.ctx.timeoutMs
|
|
1112
|
+
});
|
|
1113
|
+
if (scoutd.state === "scoutd") {
|
|
1114
|
+
return probeRunOutput(scoutd.value, {
|
|
1115
|
+
backend: "scoutd",
|
|
1116
|
+
generatedAt: scoutd.generatedAt
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
const metadata = scoutd.fallbackSince ? {
|
|
1120
|
+
backend: "local-fallback",
|
|
1121
|
+
fallbackSince: scoutd.fallbackSince,
|
|
1122
|
+
fallbackReason: scoutd.fallbackReason ?? "scoutd unavailable"
|
|
1123
|
+
} : { backend: "local" };
|
|
1124
|
+
try {
|
|
1125
|
+
const local = await input.local();
|
|
1126
|
+
return probeRunOutput(local, metadata);
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
throw new ProbeBackendError(error instanceof Error ? error.message : String(error), metadata, error);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
function probeSocketTimeoutMs(timeoutMs) {
|
|
1132
|
+
if (timeoutMs === undefined || !Number.isFinite(timeoutMs)) {
|
|
1133
|
+
return SOCKET_TIMEOUT_MS;
|
|
1134
|
+
}
|
|
1135
|
+
return Math.max(SOCKET_TIMEOUT_MS, Math.min(Math.max(0, timeoutMs) + 1000, 31000));
|
|
1136
|
+
}
|
|
1137
|
+
function requestOpTimeoutMs(timeoutMs) {
|
|
1138
|
+
if (timeoutMs === undefined || !Number.isFinite(timeoutMs)) {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
return Math.max(0, Math.floor(timeoutMs));
|
|
1142
|
+
}
|
|
1143
|
+
function execSocketTimeoutMs(args) {
|
|
1144
|
+
const timeoutMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) ? Math.max(0, args.timeoutMs) : 5000;
|
|
1145
|
+
return Math.max(SOCKET_TIMEOUT_MS, Math.min(timeoutMs + 1000, 31000));
|
|
1146
|
+
}
|
|
1147
|
+
async function requestJson(socketPath, payload, options = {}) {
|
|
1148
|
+
const bun = globalThis.Bun;
|
|
1149
|
+
if (bun?.connect) {
|
|
1150
|
+
return await requestJsonWithBun(bun.connect, socketPath, payload, options);
|
|
1151
|
+
}
|
|
1152
|
+
return await new Promise((resolve, reject) => {
|
|
1153
|
+
const socket = new Socket;
|
|
1154
|
+
let response = "";
|
|
1155
|
+
let settled = false;
|
|
1156
|
+
const finish = (error, value) => {
|
|
1157
|
+
if (settled)
|
|
1158
|
+
return;
|
|
1159
|
+
settled = true;
|
|
1160
|
+
clearTimeout(timer);
|
|
1161
|
+
socket.removeAllListeners();
|
|
1162
|
+
socket.destroy();
|
|
1163
|
+
if (error) {
|
|
1164
|
+
reject(error);
|
|
1165
|
+
} else {
|
|
1166
|
+
resolve(value);
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
const timeoutMs = options.timeoutMs ?? SOCKET_TIMEOUT_MS;
|
|
1170
|
+
const timer = setTimeout(() => {
|
|
1171
|
+
finish(new Error(`scoutd probe socket timed out after ${timeoutMs}ms`));
|
|
1172
|
+
}, timeoutMs);
|
|
1173
|
+
timer.unref?.();
|
|
1174
|
+
const finishFromResponse = () => {
|
|
1175
|
+
if (settled)
|
|
1176
|
+
return;
|
|
1177
|
+
try {
|
|
1178
|
+
const parsed = JSON.parse(response);
|
|
1179
|
+
if (isRecord2(parsed) && parsed.schema === ERROR_SCHEMA) {
|
|
1180
|
+
const error = isRecord2(parsed.error) ? parsed.error : {};
|
|
1181
|
+
const message = readString(error.message) ?? readString(error.code) ?? "scoutd probe request failed";
|
|
1182
|
+
finish(new Error(message));
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
finish(null, parsed);
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
finish(new Error(`scoutd probe response was not JSON: ${fallbackMessage(error)}`));
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
socket.setEncoding("utf8");
|
|
1191
|
+
socket.on("connect", () => {
|
|
1192
|
+
socket.write(`${JSON.stringify(payload)}
|
|
1193
|
+
`);
|
|
1194
|
+
});
|
|
1195
|
+
socket.on("data", (chunk) => {
|
|
1196
|
+
response += chunk;
|
|
1197
|
+
if (Buffer.byteLength(response, "utf8") > MAX_RESPONSE_BYTES) {
|
|
1198
|
+
finish(new Error("scoutd probe response exceeded output limit"));
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
socket.on("error", (error) => finish(error));
|
|
1202
|
+
socket.on("end", finishFromResponse);
|
|
1203
|
+
socket.on("close", () => {
|
|
1204
|
+
if (!settled) {
|
|
1205
|
+
if (response.length === 0) {
|
|
1206
|
+
finish(new Error("scoutd probe socket closed without a response"));
|
|
1207
|
+
} else {
|
|
1208
|
+
finishFromResponse();
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
socket.connect(socketPath);
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
async function requestJsonWithBun(connect, socketPath, payload, options = {}) {
|
|
1216
|
+
return await new Promise((resolve, reject) => {
|
|
1217
|
+
let response = "";
|
|
1218
|
+
let settled = false;
|
|
1219
|
+
let socketRef = null;
|
|
1220
|
+
const decoder = new TextDecoder;
|
|
1221
|
+
const finish = (error, value) => {
|
|
1222
|
+
if (settled)
|
|
1223
|
+
return;
|
|
1224
|
+
settled = true;
|
|
1225
|
+
clearTimeout(timer);
|
|
1226
|
+
try {
|
|
1227
|
+
socketRef?.end?.();
|
|
1228
|
+
} catch {}
|
|
1229
|
+
if (error) {
|
|
1230
|
+
reject(error);
|
|
1231
|
+
} else {
|
|
1232
|
+
resolve(value);
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
const finishFromResponse = () => {
|
|
1236
|
+
if (settled)
|
|
1237
|
+
return;
|
|
1238
|
+
try {
|
|
1239
|
+
const parsed = JSON.parse(response);
|
|
1240
|
+
if (isRecord2(parsed) && parsed.schema === ERROR_SCHEMA) {
|
|
1241
|
+
const error = isRecord2(parsed.error) ? parsed.error : {};
|
|
1242
|
+
const message = readString(error.message) ?? readString(error.code) ?? "scoutd probe request failed";
|
|
1243
|
+
finish(new Error(message));
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
finish(null, parsed);
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
finish(new Error(`scoutd probe response was not JSON: ${fallbackMessage(error)}`));
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
const timeoutMs = options.timeoutMs ?? SOCKET_TIMEOUT_MS;
|
|
1252
|
+
const timer = setTimeout(() => {
|
|
1253
|
+
finish(new Error(`scoutd probe socket timed out after ${timeoutMs}ms`));
|
|
1254
|
+
}, timeoutMs);
|
|
1255
|
+
timer.unref?.();
|
|
1256
|
+
connect({
|
|
1257
|
+
unix: socketPath,
|
|
1258
|
+
socket: {
|
|
1259
|
+
open(socket) {
|
|
1260
|
+
socketRef = socket;
|
|
1261
|
+
socket.write(`${JSON.stringify(payload)}
|
|
1262
|
+
`);
|
|
1263
|
+
},
|
|
1264
|
+
data(_socket, data) {
|
|
1265
|
+
response += decoder.decode(data, { stream: true });
|
|
1266
|
+
if (Buffer.byteLength(response, "utf8") > MAX_RESPONSE_BYTES) {
|
|
1267
|
+
finish(new Error("scoutd probe response exceeded output limit"));
|
|
1268
|
+
}
|
|
1269
|
+
},
|
|
1270
|
+
close() {
|
|
1271
|
+
if (response.length === 0) {
|
|
1272
|
+
finish(new Error("scoutd probe socket closed without a response"));
|
|
1273
|
+
} else {
|
|
1274
|
+
response += decoder.decode();
|
|
1275
|
+
finishFromResponse();
|
|
1276
|
+
}
|
|
1277
|
+
},
|
|
1278
|
+
error(_socket, error) {
|
|
1279
|
+
finish(error);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}).then((socket) => {
|
|
1283
|
+
socketRef = socket;
|
|
1284
|
+
}, (error) => {
|
|
1285
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
1286
|
+
});
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
function fallbackKey(probeId, key) {
|
|
1290
|
+
return `${probeId}\x00${key ?? ""}`;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// packages/runtime/src/system-probes/exec.ts
|
|
1294
|
+
class ProbeCommandError extends Error {
|
|
1295
|
+
code;
|
|
1296
|
+
exitCode;
|
|
1297
|
+
signal;
|
|
1298
|
+
constructor(message, options) {
|
|
1299
|
+
super(message);
|
|
1300
|
+
this.name = "ProbeCommandError";
|
|
1301
|
+
this.code = options.code;
|
|
1302
|
+
this.exitCode = options.exitCode;
|
|
1303
|
+
this.signal = options.signal;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
var DEFAULT_STDOUT_CAP_BYTES = 1024 * 1024;
|
|
1307
|
+
var DEFAULT_STDERR_CAP_BYTES = 128 * 1024;
|
|
1308
|
+
var SIGKILL_DELAY_MS = 500;
|
|
1309
|
+
var EXEC_SYSTEM_TRANSPORT_METADATA = Symbol.for("openscout.execSystem.transport");
|
|
1310
|
+
var spawnProcess = spawn;
|
|
1311
|
+
function bufferByteLength(chunks) {
|
|
1312
|
+
return chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
1313
|
+
}
|
|
1314
|
+
function abortMessage(ctx) {
|
|
1315
|
+
const reason = ctx.signal.reason;
|
|
1316
|
+
if (typeof reason === "object" && reason !== null && "message" in reason) {
|
|
1317
|
+
const message = reason.message;
|
|
1318
|
+
if (typeof message === "string" && message.trim()) {
|
|
1319
|
+
return message;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
return `Probe ${ctx.probeId} aborted`;
|
|
1323
|
+
}
|
|
1324
|
+
async function execProbeFile(ctx, file, args, options = {}) {
|
|
1325
|
+
const maxStdoutBytes = options.maxStdoutBytes ?? DEFAULT_STDOUT_CAP_BYTES;
|
|
1326
|
+
const maxStderrBytes = options.maxStderrBytes ?? DEFAULT_STDERR_CAP_BYTES;
|
|
1327
|
+
return await new Promise((resolve, reject) => {
|
|
1328
|
+
if (ctx.signal.aborted) {
|
|
1329
|
+
reject(new ProbeCommandError(abortMessage(ctx), { code: "aborted" }));
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
const stdoutChunks = [];
|
|
1333
|
+
const stderrChunks = [];
|
|
1334
|
+
let settled = false;
|
|
1335
|
+
let killTimer = null;
|
|
1336
|
+
const child = spawnProcess(file, [...args], {
|
|
1337
|
+
cwd: options.cwd,
|
|
1338
|
+
env: options.env,
|
|
1339
|
+
stdio: [options.input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
1340
|
+
windowsHide: true
|
|
1341
|
+
});
|
|
1342
|
+
const cleanup = () => {
|
|
1343
|
+
ctx.signal.removeEventListener("abort", onAbort);
|
|
1344
|
+
if (killTimer) {
|
|
1345
|
+
clearTimeout(killTimer);
|
|
1346
|
+
killTimer = null;
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
const fail = (error) => {
|
|
1350
|
+
if (settled) {
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
settled = true;
|
|
1354
|
+
cleanup();
|
|
1355
|
+
if (!child.killed) {
|
|
1356
|
+
child.kill("SIGTERM");
|
|
1357
|
+
}
|
|
1358
|
+
reject(error);
|
|
1359
|
+
};
|
|
1360
|
+
const onAbort = () => {
|
|
1361
|
+
if (settled) {
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
settled = true;
|
|
1365
|
+
ctx.signal.removeEventListener("abort", onAbort);
|
|
1366
|
+
child.kill("SIGTERM");
|
|
1367
|
+
killTimer = setTimeout(() => {
|
|
1368
|
+
child.kill("SIGKILL");
|
|
1369
|
+
}, SIGKILL_DELAY_MS);
|
|
1370
|
+
reject(new ProbeCommandError(abortMessage(ctx), { code: "timeout" }));
|
|
1371
|
+
};
|
|
1372
|
+
ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
1373
|
+
if (options.input !== undefined) {
|
|
1374
|
+
child.stdin?.end(options.input);
|
|
1375
|
+
}
|
|
1376
|
+
child.stdout?.on("data", (chunk) => {
|
|
1377
|
+
if (settled) {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
stdoutChunks.push(chunk);
|
|
1381
|
+
if (bufferByteLength(stdoutChunks) > maxStdoutBytes) {
|
|
1382
|
+
fail(new ProbeCommandError(`Probe ${ctx.probeId} stdout exceeded ${maxStdoutBytes} bytes`, { code: "output_cap" }));
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
child.stderr?.on("data", (chunk) => {
|
|
1386
|
+
if (settled) {
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
stderrChunks.push(chunk);
|
|
1390
|
+
if (bufferByteLength(stderrChunks) > maxStderrBytes) {
|
|
1391
|
+
fail(new ProbeCommandError(`Probe ${ctx.probeId} stderr exceeded ${maxStderrBytes} bytes`, { code: "output_cap" }));
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
child.once("error", (error) => {
|
|
1395
|
+
if (settled) {
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
settled = true;
|
|
1399
|
+
cleanup();
|
|
1400
|
+
const code = typeof error.code === "string" ? String(error.code) : "spawn";
|
|
1401
|
+
reject(new ProbeCommandError(error.message, { code }));
|
|
1402
|
+
});
|
|
1403
|
+
child.once("close", (exitCode, signal) => {
|
|
1404
|
+
if (settled) {
|
|
1405
|
+
cleanup();
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
settled = true;
|
|
1409
|
+
cleanup();
|
|
1410
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
1411
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
|
1412
|
+
if (exitCode === 0) {
|
|
1413
|
+
resolve({ stdout, stderr, exitCode });
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
reject(new ProbeCommandError(`${file} exited with ${exitCode ?? signal ?? "unknown status"}`, { code: "exit", exitCode, signal }));
|
|
1417
|
+
});
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// packages/runtime/src/system-probes/tailscale-status.ts
|
|
1422
|
+
var DEFAULT_TAILSCALE_STATUS_TIMEOUT_MS = 1500;
|
|
1423
|
+
function parseTailscaleStatusJson(raw) {
|
|
1424
|
+
return JSON.parse(raw);
|
|
1425
|
+
}
|
|
1426
|
+
function parsePeers(status) {
|
|
1427
|
+
const peers = Object.entries(status.Peer ?? {});
|
|
1428
|
+
return peers.map(([fallbackId, peer]) => ({
|
|
1429
|
+
id: peer.ID ?? fallbackId,
|
|
1430
|
+
name: peer.HostName ?? peer.DNSName ?? fallbackId,
|
|
1431
|
+
dnsName: peer.DNSName,
|
|
1432
|
+
addresses: peer.TailscaleIPs ?? [],
|
|
1433
|
+
online: peer.Online ?? false,
|
|
1434
|
+
hostName: peer.HostName,
|
|
1435
|
+
os: peer.OS,
|
|
1436
|
+
tags: peer.Tags ?? []
|
|
1437
|
+
}));
|
|
1438
|
+
}
|
|
1439
|
+
function parseSelf(status) {
|
|
1440
|
+
const self = status.Self;
|
|
1441
|
+
if (!self) {
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1444
|
+
return {
|
|
1445
|
+
id: self.ID ?? self.DNSName ?? self.HostName ?? "self",
|
|
1446
|
+
name: self.HostName ?? self.DNSName ?? "self",
|
|
1447
|
+
dnsName: self.DNSName,
|
|
1448
|
+
addresses: self.TailscaleIPs ?? [],
|
|
1449
|
+
online: self.Online ?? true,
|
|
1450
|
+
hostName: self.HostName,
|
|
1451
|
+
os: self.OS,
|
|
1452
|
+
tailnetName: status.CurrentTailnet?.Name,
|
|
1453
|
+
magicDnsSuffix: status.CurrentTailnet?.MagicDNSSuffix
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
function isTailscaleBackendRunning(status) {
|
|
1457
|
+
return (status.BackendState ?? "").trim().toLowerCase() === "running";
|
|
1458
|
+
}
|
|
1459
|
+
function normalizeHost(value) {
|
|
1460
|
+
const normalized = value?.trim().replace(/\.$/, "").toLowerCase();
|
|
1461
|
+
if (!normalized || normalized.includes("/") || /\s/.test(normalized)) {
|
|
1462
|
+
return null;
|
|
1463
|
+
}
|
|
1464
|
+
return normalized;
|
|
1465
|
+
}
|
|
1466
|
+
function uniq(values) {
|
|
1467
|
+
const seen = new Set;
|
|
1468
|
+
const out = [];
|
|
1469
|
+
for (const value of values) {
|
|
1470
|
+
const normalized = normalizeHost(value ?? undefined);
|
|
1471
|
+
if (!normalized || seen.has(normalized)) {
|
|
1472
|
+
continue;
|
|
148
1473
|
}
|
|
1474
|
+
seen.add(normalized);
|
|
1475
|
+
out.push(normalized);
|
|
1476
|
+
}
|
|
1477
|
+
return out;
|
|
1478
|
+
}
|
|
1479
|
+
function tailscaleSelfWebHosts(self) {
|
|
1480
|
+
if (!self) {
|
|
1481
|
+
return [];
|
|
1482
|
+
}
|
|
1483
|
+
return uniq([
|
|
1484
|
+
self.dnsName,
|
|
1485
|
+
self.hostName && self.magicDnsSuffix ? `${self.hostName}.${self.magicDnsSuffix}` : undefined,
|
|
1486
|
+
...self.addresses
|
|
1487
|
+
]);
|
|
1488
|
+
}
|
|
1489
|
+
function summarizeTailscaleStatus(status) {
|
|
1490
|
+
return {
|
|
1491
|
+
backendState: status.BackendState ?? null,
|
|
1492
|
+
running: isTailscaleBackendRunning(status),
|
|
1493
|
+
health: status.Health ?? [],
|
|
1494
|
+
peers: parsePeers(status),
|
|
1495
|
+
self: parseSelf(status)
|
|
149
1496
|
};
|
|
150
1497
|
}
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
1498
|
+
function statusTimeoutMs(env) {
|
|
1499
|
+
const parsed = Number.parseInt(env.OPENSCOUT_TAILSCALE_STATUS_TIMEOUT_MS ?? "", 10);
|
|
1500
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TAILSCALE_STATUS_TIMEOUT_MS;
|
|
1501
|
+
}
|
|
1502
|
+
async function readStatusJsonFromFile(filePath) {
|
|
1503
|
+
try {
|
|
1504
|
+
const raw = await readFile(filePath, "utf8");
|
|
1505
|
+
return parseTailscaleStatusJson(raw);
|
|
1506
|
+
} catch {
|
|
1507
|
+
return null;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function isDomainUnavailableError(error) {
|
|
1511
|
+
if (!(error instanceof ProbeCommandError)) {
|
|
1512
|
+
return false;
|
|
1513
|
+
}
|
|
1514
|
+
return error.code === "ENOENT" || error.code === "spawn" || error.code === "exit";
|
|
1515
|
+
}
|
|
1516
|
+
async function readTailscaleStatusSummaryLocal(ctx) {
|
|
1517
|
+
const fixturePath = process.env.OPENSCOUT_TAILSCALE_STATUS_JSON;
|
|
1518
|
+
if (fixturePath) {
|
|
1519
|
+
const status = await readStatusJsonFromFile(fixturePath);
|
|
1520
|
+
return status ? summarizeTailscaleStatus(status) : null;
|
|
1521
|
+
}
|
|
1522
|
+
const tailscaleBin = process.env.OPENSCOUT_TAILSCALE_BIN ?? "tailscale";
|
|
1523
|
+
try {
|
|
1524
|
+
const { stdout } = await execProbeFile(ctx, tailscaleBin, ["status", "--json"], {
|
|
1525
|
+
maxStdoutBytes: 4 * 1024 * 1024,
|
|
1526
|
+
maxStderrBytes: 256 * 1024
|
|
1527
|
+
});
|
|
1528
|
+
return summarizeTailscaleStatus(parseTailscaleStatusJson(stdout));
|
|
1529
|
+
} catch (error) {
|
|
1530
|
+
if (isDomainUnavailableError(error)) {
|
|
1531
|
+
return null;
|
|
1532
|
+
}
|
|
1533
|
+
throw error;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
var tailscaleStatusProbe = defineProbe({
|
|
1537
|
+
id: "tailscale.status",
|
|
1538
|
+
ttlMs: 30000,
|
|
1539
|
+
timeoutMs: statusTimeoutMs(process.env),
|
|
1540
|
+
run: (ctx) => runWithScoutdFallback({
|
|
1541
|
+
probeId: "tailscale.status",
|
|
1542
|
+
ctx,
|
|
1543
|
+
local: () => readTailscaleStatusSummaryLocal(ctx)
|
|
1544
|
+
})
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
// packages/runtime/src/tailscale.ts
|
|
1548
|
+
var DEFAULT_TAILSCALE_STATUS_TIMEOUT_MS2 = 1500;
|
|
1549
|
+
function readStatusJsonFromFileSync(filePath) {
|
|
1550
|
+
const raw = readFileSync2(filePath, "utf8");
|
|
1551
|
+
return parseTailscaleStatusJson(raw);
|
|
1552
|
+
}
|
|
1553
|
+
function statusTimeoutMs2(env) {
|
|
1554
|
+
const parsed = Number.parseInt(env.OPENSCOUT_TAILSCALE_STATUS_TIMEOUT_MS ?? "", 10);
|
|
1555
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TAILSCALE_STATUS_TIMEOUT_MS2;
|
|
1556
|
+
}
|
|
1557
|
+
function readStatusJsonSync(env = process.env) {
|
|
1558
|
+
if (env.OPENSCOUT_TAILSCALE_AUTO_HOSTS === "0") {
|
|
1559
|
+
return null;
|
|
1560
|
+
}
|
|
1561
|
+
const fixturePath = env.OPENSCOUT_TAILSCALE_STATUS_JSON;
|
|
1562
|
+
if (fixturePath) {
|
|
156
1563
|
try {
|
|
157
|
-
|
|
158
|
-
parsedJson = true;
|
|
1564
|
+
return readStatusJsonFromFileSync(fixturePath);
|
|
159
1565
|
} catch {
|
|
160
|
-
|
|
1566
|
+
return null;
|
|
161
1567
|
}
|
|
162
1568
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
throw new Error(`${path} returned ${response.status}: ${response.text}`);
|
|
1569
|
+
const tailscaleBin = env.OPENSCOUT_TAILSCALE_BIN ?? (env === process.env ? "tailscale" : undefined);
|
|
1570
|
+
if (!tailscaleBin) {
|
|
1571
|
+
return null;
|
|
168
1572
|
}
|
|
169
|
-
|
|
170
|
-
|
|
1573
|
+
try {
|
|
1574
|
+
const stdout = execFileSync(tailscaleBin, ["status", "--json"], {
|
|
1575
|
+
encoding: "utf8",
|
|
1576
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1577
|
+
timeout: statusTimeoutMs2(env),
|
|
1578
|
+
windowsHide: true
|
|
1579
|
+
});
|
|
1580
|
+
return parseTailscaleStatusJson(stdout);
|
|
1581
|
+
} catch {
|
|
1582
|
+
return null;
|
|
171
1583
|
}
|
|
172
|
-
|
|
1584
|
+
}
|
|
1585
|
+
function readTailscaleSelfWebHostsSync(env = process.env) {
|
|
1586
|
+
const status = readStatusJsonSync(env);
|
|
1587
|
+
if (!status || !isTailscaleBackendRunning(status)) {
|
|
1588
|
+
return [];
|
|
1589
|
+
}
|
|
1590
|
+
return tailscaleSelfWebHosts(summarizeTailscaleStatus(status).self);
|
|
173
1591
|
}
|
|
174
1592
|
|
|
175
|
-
// packages/runtime/src/
|
|
176
|
-
import {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
1593
|
+
// packages/runtime/src/local-config.ts
|
|
1594
|
+
import {
|
|
1595
|
+
existsSync as existsSync3,
|
|
1596
|
+
mkdirSync,
|
|
1597
|
+
readFileSync as readFileSync3,
|
|
1598
|
+
renameSync,
|
|
1599
|
+
writeFileSync
|
|
1600
|
+
} from "fs";
|
|
1601
|
+
import { homedir as homedir3, hostname as osHostname } from "os";
|
|
1602
|
+
import { dirname, join as join3 } from "path";
|
|
1603
|
+
var LOCAL_CONFIG_VERSION = 1;
|
|
1604
|
+
var DEFAULT_SCOUT_WEB_PORTAL_HOST = "scout.local";
|
|
1605
|
+
var DEFAULT_SCOUT_WEB_DEV_HOST = `dev.${DEFAULT_SCOUT_WEB_PORTAL_HOST}`;
|
|
1606
|
+
var OPENSCOUT_PORTS = {
|
|
1607
|
+
broker: 43110,
|
|
1608
|
+
web: 43120,
|
|
1609
|
+
webTerminalRelay: 43121,
|
|
1610
|
+
vite: 43122,
|
|
1611
|
+
pairingBridge: 43130,
|
|
1612
|
+
pairingRelay: 43131,
|
|
1613
|
+
pairingFileServer: 43132,
|
|
1614
|
+
studio: 43140
|
|
1615
|
+
};
|
|
1616
|
+
var DEFAULT_LOCAL_CONFIG = {
|
|
1617
|
+
version: LOCAL_CONFIG_VERSION,
|
|
1618
|
+
host: "127.0.0.1",
|
|
1619
|
+
webLocalName: undefined,
|
|
1620
|
+
ports: {
|
|
1621
|
+
broker: OPENSCOUT_PORTS.broker,
|
|
1622
|
+
web: OPENSCOUT_PORTS.web,
|
|
1623
|
+
pairing: OPENSCOUT_PORTS.pairingBridge
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
function localConfigHome() {
|
|
1627
|
+
return process.env.OPENSCOUT_HOME ?? join3(homedir3(), ".openscout");
|
|
1628
|
+
}
|
|
1629
|
+
function localConfigPath() {
|
|
1630
|
+
return join3(localConfigHome(), "config.json");
|
|
1631
|
+
}
|
|
1632
|
+
function loadLocalConfig() {
|
|
1633
|
+
const configPath = localConfigPath();
|
|
1634
|
+
if (!existsSync3(configPath))
|
|
1635
|
+
return { version: LOCAL_CONFIG_VERSION };
|
|
1636
|
+
try {
|
|
1637
|
+
return validateLocalConfig(JSON.parse(readFileSync3(configPath, "utf8")));
|
|
1638
|
+
} catch {
|
|
1639
|
+
return { version: LOCAL_CONFIG_VERSION };
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
function validateLocalConfig(input) {
|
|
1643
|
+
if (!input || typeof input !== "object")
|
|
1644
|
+
return { version: LOCAL_CONFIG_VERSION };
|
|
1645
|
+
const raw = input;
|
|
1646
|
+
const out = { version: LOCAL_CONFIG_VERSION };
|
|
1647
|
+
if (typeof raw.host === "string" && raw.host.trim().length > 0) {
|
|
1648
|
+
out.host = raw.host.trim();
|
|
1649
|
+
}
|
|
1650
|
+
if (typeof raw.webLocalName === "string" && raw.webLocalName.trim().length > 0) {
|
|
1651
|
+
out.webLocalName = raw.webLocalName.trim();
|
|
1652
|
+
}
|
|
1653
|
+
if (raw.ports && typeof raw.ports === "object") {
|
|
1654
|
+
const ports = raw.ports;
|
|
1655
|
+
const p = {};
|
|
1656
|
+
if (isValidPort(ports.broker))
|
|
1657
|
+
p.broker = ports.broker;
|
|
1658
|
+
if (isValidPort(ports.web))
|
|
1659
|
+
p.web = ports.web;
|
|
1660
|
+
if (isValidPort(ports.pairing))
|
|
1661
|
+
p.pairing = ports.pairing;
|
|
1662
|
+
if (Object.keys(p).length > 0)
|
|
1663
|
+
out.ports = p;
|
|
1664
|
+
}
|
|
1665
|
+
return out;
|
|
1666
|
+
}
|
|
1667
|
+
function isValidPort(value) {
|
|
1668
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 && value < 65536;
|
|
209
1669
|
}
|
|
210
|
-
function
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
1670
|
+
function parseEnvPort(name) {
|
|
1671
|
+
const raw = process.env[name]?.trim();
|
|
1672
|
+
if (!raw) {
|
|
213
1673
|
return;
|
|
214
1674
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
1675
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1676
|
+
return isValidPort(parsed) ? parsed : undefined;
|
|
1677
|
+
}
|
|
1678
|
+
function resolveEffectiveLocalConfig() {
|
|
1679
|
+
const file = loadLocalConfig();
|
|
1680
|
+
const host = process.env.OPENSCOUT_BROKER_HOST?.trim() || process.env.OPENSCOUT_HOST?.trim() || file.host;
|
|
1681
|
+
const ports = {
|
|
1682
|
+
broker: parseEnvPort("OPENSCOUT_BROKER_PORT") ?? file.ports?.broker,
|
|
1683
|
+
web: parseEnvPort("OPENSCOUT_WEB_PORT") ?? parseEnvPort("SCOUT_WEB_PORT") ?? file.ports?.web,
|
|
1684
|
+
pairing: parseEnvPort("OPENSCOUT_PAIRING_PORT") ?? file.ports?.pairing
|
|
1685
|
+
};
|
|
1686
|
+
return {
|
|
1687
|
+
version: LOCAL_CONFIG_VERSION,
|
|
1688
|
+
...host ? { host } : {},
|
|
1689
|
+
...file.webLocalName ? { webLocalName: file.webLocalName } : {},
|
|
1690
|
+
...ports.broker || ports.web || ports.pairing ? { ports } : {}
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
function resolveBrokerPort() {
|
|
1694
|
+
return resolveEffectiveLocalConfig().ports?.broker ?? DEFAULT_LOCAL_CONFIG.ports.broker;
|
|
221
1695
|
}
|
|
222
1696
|
|
|
223
1697
|
// packages/runtime/src/tool-resolution.ts
|
|
224
|
-
import { accessSync, constants, existsSync as
|
|
225
|
-
import { homedir as
|
|
226
|
-
import { basename, dirname, join as
|
|
1698
|
+
import { accessSync, constants, existsSync as existsSync4 } from "fs";
|
|
1699
|
+
import { homedir as homedir4 } from "os";
|
|
1700
|
+
import { basename, dirname as dirname2, join as join4, resolve } from "path";
|
|
227
1701
|
var DEFAULT_COMMON_EXECUTABLE_DIRECTORIES = [
|
|
228
|
-
|
|
1702
|
+
join4(homedir4(), ".bun", "bin"),
|
|
229
1703
|
"/opt/homebrew/bin",
|
|
230
1704
|
"/usr/local/bin"
|
|
231
1705
|
];
|
|
232
1706
|
function expandHomePath(value, env = process.env) {
|
|
233
|
-
const home = env.HOME ??
|
|
1707
|
+
const home = env.HOME ?? homedir4();
|
|
234
1708
|
if (value === "~") {
|
|
235
1709
|
return home;
|
|
236
1710
|
}
|
|
237
1711
|
if (value.startsWith("~/")) {
|
|
238
|
-
return
|
|
1712
|
+
return join4(home, value.slice(2));
|
|
239
1713
|
}
|
|
240
1714
|
return value;
|
|
241
1715
|
}
|
|
@@ -292,7 +1766,7 @@ function resolveExecutableFromSearch(options) {
|
|
|
292
1766
|
], env);
|
|
293
1767
|
for (const directory of searchDirectories) {
|
|
294
1768
|
for (const name of options.names) {
|
|
295
|
-
const candidate =
|
|
1769
|
+
const candidate = join4(directory, name);
|
|
296
1770
|
if (isExecutablePath(candidate)) {
|
|
297
1771
|
return {
|
|
298
1772
|
path: candidate,
|
|
@@ -316,7 +1790,7 @@ function findExecutableOnSearchPath(name, env) {
|
|
|
316
1790
|
...DEFAULT_COMMON_EXECUTABLE_DIRECTORIES
|
|
317
1791
|
], env);
|
|
318
1792
|
for (const directory of searchDirectories) {
|
|
319
|
-
const candidate =
|
|
1793
|
+
const candidate = join4(directory, name);
|
|
320
1794
|
if (isExecutablePath(candidate)) {
|
|
321
1795
|
return { path: candidate, source: "path" };
|
|
322
1796
|
}
|
|
@@ -324,37 +1798,106 @@ function findExecutableOnSearchPath(name, env) {
|
|
|
324
1798
|
return null;
|
|
325
1799
|
}
|
|
326
1800
|
|
|
1801
|
+
// packages/runtime/src/runtime-adapters.ts
|
|
1802
|
+
function normalizeRuntimeHost(value) {
|
|
1803
|
+
const normalized = value?.trim().toLowerCase();
|
|
1804
|
+
if (normalized === "bun")
|
|
1805
|
+
return "bun";
|
|
1806
|
+
if (normalized === "node")
|
|
1807
|
+
return "node";
|
|
1808
|
+
return null;
|
|
1809
|
+
}
|
|
1810
|
+
function normalizeRuntimeServiceAdapter(value) {
|
|
1811
|
+
const normalized = value?.trim().toLowerCase().replaceAll("_", "-");
|
|
1812
|
+
if (normalized === "macos-scoutd" || normalized === "macos-launchd")
|
|
1813
|
+
return "macos-scoutd";
|
|
1814
|
+
if (normalized === "linux-systemd-user" || normalized === "systemd-user")
|
|
1815
|
+
return "linux-systemd-user";
|
|
1816
|
+
if (normalized === "headless-foreground" || normalized === "foreground" || normalized === "headless") {
|
|
1817
|
+
return "headless-foreground";
|
|
1818
|
+
}
|
|
1819
|
+
if (normalized === "windows-service")
|
|
1820
|
+
return "windows-service";
|
|
1821
|
+
return null;
|
|
1822
|
+
}
|
|
1823
|
+
function defaultServiceAdapterForPlatform(platform = process.platform, env = process.env) {
|
|
1824
|
+
const explicit = normalizeRuntimeServiceAdapter(env.OPENSCOUT_SERVICE_ADAPTER);
|
|
1825
|
+
if (explicit)
|
|
1826
|
+
return explicit;
|
|
1827
|
+
if (normalizeRuntimeHost(env.OPENSCOUT_RUNTIME_HOST) === "node") {
|
|
1828
|
+
return "headless-foreground";
|
|
1829
|
+
}
|
|
1830
|
+
if (platform === "darwin")
|
|
1831
|
+
return "macos-scoutd";
|
|
1832
|
+
if (platform === "linux")
|
|
1833
|
+
return "headless-foreground";
|
|
1834
|
+
if (platform === "win32")
|
|
1835
|
+
return "windows-service";
|
|
1836
|
+
return "headless-foreground";
|
|
1837
|
+
}
|
|
1838
|
+
|
|
327
1839
|
// packages/runtime/src/broker-process-manager.ts
|
|
328
1840
|
function isTmpPath(p) {
|
|
329
1841
|
return /^\/(?:private\/)?tmp\//.test(p);
|
|
330
1842
|
}
|
|
331
1843
|
var DEFAULT_BROKER_HOST = "127.0.0.1";
|
|
332
1844
|
var DEFAULT_BROKER_HOST_MESH = "0.0.0.0";
|
|
333
|
-
var DEFAULT_BROKER_PORT =
|
|
1845
|
+
var DEFAULT_BROKER_PORT = OPENSCOUT_PORTS.broker;
|
|
334
1846
|
var DEFAULT_ADVERTISE_SCOPE = "local";
|
|
335
|
-
function resolveAdvertiseScope() {
|
|
336
|
-
|
|
1847
|
+
function resolveAdvertiseScope(env = process.env) {
|
|
1848
|
+
if (openScoutNetworkDiscoveryEnabled(env))
|
|
1849
|
+
return "mesh";
|
|
1850
|
+
const raw = (env.OPENSCOUT_ADVERTISE_SCOPE ?? "").trim().toLowerCase();
|
|
337
1851
|
if (raw === "mesh")
|
|
338
1852
|
return "mesh";
|
|
339
1853
|
if (raw === "local")
|
|
340
1854
|
return "local";
|
|
341
1855
|
return DEFAULT_ADVERTISE_SCOPE;
|
|
342
1856
|
}
|
|
343
|
-
function resolveBrokerHost(scope = resolveAdvertiseScope()) {
|
|
344
|
-
const explicit =
|
|
345
|
-
if (
|
|
1857
|
+
function resolveBrokerHost(scope = resolveAdvertiseScope(), env = process.env) {
|
|
1858
|
+
const explicit = env.OPENSCOUT_BROKER_HOST?.trim();
|
|
1859
|
+
if (explicit) {
|
|
1860
|
+
if (scope === "mesh" && isLoopbackHost(explicit)) {
|
|
1861
|
+
return DEFAULT_BROKER_HOST_MESH;
|
|
1862
|
+
}
|
|
346
1863
|
return explicit;
|
|
347
1864
|
}
|
|
348
1865
|
return scope === "mesh" ? DEFAULT_BROKER_HOST_MESH : DEFAULT_BROKER_HOST;
|
|
349
1866
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
1867
|
+
function isLoopbackHost(host) {
|
|
1868
|
+
const trimmed = host.trim();
|
|
1869
|
+
return trimmed === "127.0.0.1" || trimmed === "::1" || trimmed === "localhost";
|
|
1870
|
+
}
|
|
353
1871
|
function buildDefaultBrokerUrl(host = DEFAULT_BROKER_HOST, port = DEFAULT_BROKER_PORT) {
|
|
354
1872
|
return `http://${host}:${port}`;
|
|
355
1873
|
}
|
|
1874
|
+
function resolveBrokerUrl(host, port, scope, env = process.env) {
|
|
1875
|
+
const explicit = env.OPENSCOUT_BROKER_URL?.trim();
|
|
1876
|
+
if (explicit && !(scope === "mesh" && isUnreachableMeshBrokerUrl(explicit))) {
|
|
1877
|
+
return explicit;
|
|
1878
|
+
}
|
|
1879
|
+
if (scope === "mesh") {
|
|
1880
|
+
const tailnetHost = readTailscaleSelfWebHostsSync(env)[0];
|
|
1881
|
+
if (tailnetHost) {
|
|
1882
|
+
return buildDefaultBrokerUrl(tailnetHost, port);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
return buildDefaultBrokerUrl(host, port);
|
|
1886
|
+
}
|
|
1887
|
+
function isUnreachableMeshBrokerUrl(value) {
|
|
1888
|
+
try {
|
|
1889
|
+
const host = new URL(value).hostname;
|
|
1890
|
+
return isLoopbackHost(host) || isWildcardHost(host);
|
|
1891
|
+
} catch {
|
|
1892
|
+
return false;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
function isWildcardHost(host) {
|
|
1896
|
+
const trimmed = host.trim();
|
|
1897
|
+
return trimmed === "0.0.0.0" || trimmed === "::" || trimmed === "[::]";
|
|
1898
|
+
}
|
|
356
1899
|
function buildDefaultBrokerSocketPath(runtimeDirectory) {
|
|
357
|
-
return
|
|
1900
|
+
return join5(runtimeDirectory, "broker.sock");
|
|
358
1901
|
}
|
|
359
1902
|
var DEFAULT_BROKER_URL = buildDefaultBrokerUrl();
|
|
360
1903
|
function runtimePackageDir() {
|
|
@@ -370,17 +1913,17 @@ function runtimePackageDir() {
|
|
|
370
1913
|
const fromGlobal = findGlobalRuntimeDir();
|
|
371
1914
|
if (fromGlobal)
|
|
372
1915
|
return fromGlobal;
|
|
373
|
-
const moduleDir =
|
|
1916
|
+
const moduleDir = dirname3(fileURLToPath(import.meta.url));
|
|
374
1917
|
return resolve2(moduleDir, "..");
|
|
375
1918
|
}
|
|
376
1919
|
function isInstalledRuntimePackageDir(candidate) {
|
|
377
|
-
return
|
|
1920
|
+
return existsSync5(join5(candidate, "package.json")) && existsSync5(join5(candidate, "bin", "openscout-runtime.mjs"));
|
|
378
1921
|
}
|
|
379
1922
|
function findGlobalRuntimeDir() {
|
|
380
1923
|
const candidates = [
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
1924
|
+
join5(homedir5(), ".bun", "node_modules", "@openscout", "runtime"),
|
|
1925
|
+
join5(homedir5(), ".bun", "install", "global", "node_modules", "@openscout", "runtime"),
|
|
1926
|
+
join5(homedir5(), ".bun", "install", "global", "node_modules", "@openscout", "scout", "node_modules", "@openscout", "runtime")
|
|
384
1927
|
];
|
|
385
1928
|
for (const c of candidates) {
|
|
386
1929
|
if (isInstalledRuntimePackageDir(c))
|
|
@@ -393,7 +1936,7 @@ function findGlobalRuntimeDir() {
|
|
|
393
1936
|
const scoutPkg = resolve2(scoutBin, "..", "..");
|
|
394
1937
|
if (isInstalledRuntimePackageDir(scoutPkg))
|
|
395
1938
|
return scoutPkg;
|
|
396
|
-
const nested =
|
|
1939
|
+
const nested = join5(scoutPkg, "node_modules", "@openscout", "runtime");
|
|
397
1940
|
if (isInstalledRuntimePackageDir(nested))
|
|
398
1941
|
return nested;
|
|
399
1942
|
const sibling = resolve2(scoutPkg, "..", "runtime");
|
|
@@ -404,30 +1947,43 @@ function findGlobalRuntimeDir() {
|
|
|
404
1947
|
return null;
|
|
405
1948
|
}
|
|
406
1949
|
function findBundledRuntimeDir() {
|
|
407
|
-
const moduleDir =
|
|
408
|
-
|
|
409
|
-
|
|
1950
|
+
const moduleDir = dirname3(fileURLToPath(import.meta.url));
|
|
1951
|
+
return resolveBundledRuntimeDirFromModuleDir(moduleDir);
|
|
1952
|
+
}
|
|
1953
|
+
function resolveBundledRuntimeDirFromModuleDir(moduleDir) {
|
|
1954
|
+
const candidates = [
|
|
1955
|
+
resolve2(moduleDir, ".."),
|
|
1956
|
+
resolve2(moduleDir, "..", "..")
|
|
1957
|
+
];
|
|
1958
|
+
for (const candidate of candidates) {
|
|
1959
|
+
if (isInstalledRuntimePackageDir(candidate))
|
|
1960
|
+
return candidate;
|
|
1961
|
+
}
|
|
1962
|
+
return null;
|
|
410
1963
|
}
|
|
411
1964
|
function findWorkspaceRuntimeDir(startDir) {
|
|
412
1965
|
let current = resolve2(startDir);
|
|
413
1966
|
while (true) {
|
|
414
|
-
const candidate =
|
|
415
|
-
if (
|
|
1967
|
+
const candidate = join5(current, "packages", "runtime");
|
|
1968
|
+
if (existsSync5(join5(candidate, "package.json")) && existsSync5(join5(candidate, "src"))) {
|
|
416
1969
|
return candidate;
|
|
417
1970
|
}
|
|
418
|
-
const parent =
|
|
1971
|
+
const parent = dirname3(current);
|
|
419
1972
|
if (parent === current)
|
|
420
1973
|
return null;
|
|
421
1974
|
current = parent;
|
|
422
1975
|
}
|
|
423
1976
|
}
|
|
424
1977
|
function resolveBunExecutable2() {
|
|
425
|
-
const bun =
|
|
1978
|
+
const bun = resolveOptionalBunExecutable();
|
|
426
1979
|
if (bun) {
|
|
427
|
-
return bun
|
|
1980
|
+
return bun;
|
|
428
1981
|
}
|
|
429
1982
|
throw new Error("Unable to locate Bun for broker service management. Install Bun or set OPENSCOUT_BUN_BIN.");
|
|
430
1983
|
}
|
|
1984
|
+
function resolveOptionalBunExecutable() {
|
|
1985
|
+
return resolveBunExecutable(process.env)?.path ?? null;
|
|
1986
|
+
}
|
|
431
1987
|
function resolveBrokerServiceMode() {
|
|
432
1988
|
const explicit = (process.env.OPENSCOUT_BROKER_SERVICE_MODE ?? "").trim().toLowerCase();
|
|
433
1989
|
if (explicit === "prod" || explicit === "production") {
|
|
@@ -438,20 +1994,6 @@ function resolveBrokerServiceMode() {
|
|
|
438
1994
|
}
|
|
439
1995
|
return "dev";
|
|
440
1996
|
}
|
|
441
|
-
function resolveBrokerStartTimeoutMs() {
|
|
442
|
-
const explicit = Number.parseInt(process.env.OPENSCOUT_BROKER_START_TIMEOUT_MS ?? "", 10);
|
|
443
|
-
if (Number.isFinite(explicit) && explicit > 0) {
|
|
444
|
-
return Math.max(explicit, BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
445
|
-
}
|
|
446
|
-
return DEFAULT_BROKER_START_TIMEOUT_MS;
|
|
447
|
-
}
|
|
448
|
-
function resolveBrokerStopTimeoutMs() {
|
|
449
|
-
const explicit = Number.parseInt(process.env.OPENSCOUT_BROKER_STOP_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_STOP_TIMEOUT_MS;
|
|
454
|
-
}
|
|
455
1997
|
function resolveBrokerServiceLabel(mode) {
|
|
456
1998
|
const explicit = process.env.OPENSCOUT_SERVICE_LABEL?.trim() || process.env.OPENSCOUT_BROKER_SERVICE_LABEL?.trim();
|
|
457
1999
|
if (explicit) {
|
|
@@ -467,33 +2009,23 @@ function resolveBrokerServiceLabel(mode) {
|
|
|
467
2009
|
return "dev.openscout";
|
|
468
2010
|
}
|
|
469
2011
|
}
|
|
470
|
-
function legacyBrokerServiceLabel(mode) {
|
|
471
|
-
switch (mode) {
|
|
472
|
-
case "prod":
|
|
473
|
-
return "com.openscout.broker";
|
|
474
|
-
case "custom":
|
|
475
|
-
return "com.openscout.broker.custom";
|
|
476
|
-
case "dev":
|
|
477
|
-
default:
|
|
478
|
-
return "dev.openscout.broker";
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
2012
|
function resolveBrokerServiceConfig() {
|
|
482
2013
|
const mode = resolveBrokerServiceMode();
|
|
483
2014
|
const label = resolveBrokerServiceLabel(mode);
|
|
2015
|
+
const serviceAdapter = resolveBrokerServiceAdapter();
|
|
484
2016
|
const uid = typeof process.getuid === "function" ? process.getuid() : Number.parseInt(process.env.UID ?? "0", 10);
|
|
485
2017
|
const supportPaths = resolveOpenScoutSupportPaths();
|
|
486
|
-
const defaultSupportDir =
|
|
2018
|
+
const defaultSupportDir = join5(homedir5(), "Library", "Application Support", "OpenScout");
|
|
487
2019
|
const supportDirectory = isTmpPath(supportPaths.supportDirectory) ? defaultSupportDir : supportPaths.supportDirectory;
|
|
488
|
-
const runtimeDirectory =
|
|
489
|
-
const logsDirectory =
|
|
490
|
-
const controlHome = isTmpPath(supportPaths.controlHome) ?
|
|
2020
|
+
const runtimeDirectory = join5(supportDirectory, "runtime");
|
|
2021
|
+
const logsDirectory = join5(supportDirectory, "logs", "broker");
|
|
2022
|
+
const controlHome = isTmpPath(supportPaths.controlHome) ? join5(homedir5(), ".openscout", "control-plane") : supportPaths.controlHome;
|
|
491
2023
|
const advertiseScope = resolveAdvertiseScope();
|
|
492
2024
|
const brokerHost = resolveBrokerHost(advertiseScope);
|
|
493
|
-
const brokerPort = Number.parseInt(process.env.OPENSCOUT_BROKER_PORT ?? String(
|
|
494
|
-
const brokerUrl =
|
|
2025
|
+
const brokerPort = Number.parseInt(process.env.OPENSCOUT_BROKER_PORT ?? String(resolveBrokerPort()), 10);
|
|
2026
|
+
const brokerUrl = resolveBrokerUrl(brokerHost, brokerPort, advertiseScope);
|
|
495
2027
|
const brokerSocketPath = process.env.OPENSCOUT_BROKER_SOCKET_PATH ?? buildDefaultBrokerSocketPath(runtimeDirectory);
|
|
496
|
-
const launchAgentPath =
|
|
2028
|
+
const launchAgentPath = join5(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
|
|
497
2029
|
return {
|
|
498
2030
|
label,
|
|
499
2031
|
mode,
|
|
@@ -504,11 +2036,11 @@ function resolveBrokerServiceConfig() {
|
|
|
504
2036
|
supportDirectory,
|
|
505
2037
|
runtimeDirectory,
|
|
506
2038
|
logsDirectory,
|
|
507
|
-
stdoutLogPath:
|
|
508
|
-
stderrLogPath:
|
|
2039
|
+
stdoutLogPath: join5(logsDirectory, "stdout.log"),
|
|
2040
|
+
stderrLogPath: join5(logsDirectory, "stderr.log"),
|
|
509
2041
|
controlHome,
|
|
510
2042
|
runtimePackageDir: runtimePackageDir(),
|
|
511
|
-
bunExecutable: resolveBunExecutable2(),
|
|
2043
|
+
bunExecutable: serviceAdapter === "macos-scoutd" ? resolveBunExecutable2() : resolveOptionalBunExecutable(),
|
|
512
2044
|
brokerHost,
|
|
513
2045
|
brokerPort,
|
|
514
2046
|
brokerUrl,
|
|
@@ -517,86 +2049,259 @@ function resolveBrokerServiceConfig() {
|
|
|
517
2049
|
coreAgents: readCoreAgentsSync()
|
|
518
2050
|
};
|
|
519
2051
|
}
|
|
520
|
-
function
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
2052
|
+
function resolveBrokerServiceAdapter(env = process.env, platform = process.platform) {
|
|
2053
|
+
return defaultServiceAdapterForPlatform(platform, env);
|
|
2054
|
+
}
|
|
2055
|
+
function executableCandidate(path) {
|
|
2056
|
+
return isExecutablePath(path) ? path : null;
|
|
2057
|
+
}
|
|
2058
|
+
function resolveExecutableName(name) {
|
|
2059
|
+
return resolveExecutableFromSearch({ names: [name] })?.path ?? null;
|
|
2060
|
+
}
|
|
2061
|
+
function resolveEnvExecutable(value) {
|
|
2062
|
+
const trimmed = value?.trim();
|
|
2063
|
+
if (!trimmed)
|
|
2064
|
+
return null;
|
|
2065
|
+
if (trimmed.includes("/") || trimmed.startsWith(".")) {
|
|
2066
|
+
const expanded = resolve2(expandHomePath(trimmed));
|
|
2067
|
+
return executableCandidate(expanded);
|
|
2068
|
+
}
|
|
2069
|
+
return resolveExecutableName(trimmed);
|
|
2070
|
+
}
|
|
2071
|
+
function findWorkspaceRootFromRuntimeDir(runtimePackageDir2) {
|
|
2072
|
+
let current = resolve2(runtimePackageDir2);
|
|
2073
|
+
while (true) {
|
|
2074
|
+
if (existsSync5(join5(current, "Cargo.toml")) && existsSync5(join5(current, "crates", "scoutd", "Cargo.toml"))) {
|
|
2075
|
+
return current;
|
|
2076
|
+
}
|
|
2077
|
+
const parent = dirname3(current);
|
|
2078
|
+
if (parent === current)
|
|
2079
|
+
return null;
|
|
2080
|
+
current = parent;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
function workspaceScoutdAllowed() {
|
|
2084
|
+
const raw = process.env.OPENSCOUT_ALLOW_WORKSPACE_SCOUTD?.trim().toLowerCase();
|
|
2085
|
+
return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
|
|
2086
|
+
}
|
|
2087
|
+
function resolveScoutdCommand(config = resolveBrokerServiceConfig()) {
|
|
2088
|
+
const explicit = resolveEnvExecutable(process.env.OPENSCOUT_SCOUTD_BIN);
|
|
2089
|
+
if (explicit) {
|
|
2090
|
+
return { path: explicit, source: "env" };
|
|
2091
|
+
}
|
|
2092
|
+
const workspaceRoot = findWorkspaceRootFromRuntimeDir(config.runtimePackageDir);
|
|
2093
|
+
const packageCandidates = [
|
|
2094
|
+
join5(config.runtimePackageDir, "bin", "scoutd"),
|
|
2095
|
+
join5(config.runtimePackageDir, "native", "scoutd"),
|
|
2096
|
+
join5(config.runtimePackageDir, "scoutd"),
|
|
2097
|
+
workspaceRoot ? join5(workspaceRoot, "packages", "cli", "bin", "scoutd") : null,
|
|
2098
|
+
workspaceRoot ? join5(workspaceRoot, "packages", "runtime", "bin", "scoutd") : null,
|
|
2099
|
+
join5(config.runtimeDirectory, "scoutd"),
|
|
2100
|
+
join5(dirname3(config.runtimePackageDir), "scout", "bin", "scoutd")
|
|
2101
|
+
];
|
|
2102
|
+
for (const candidate of packageCandidates) {
|
|
2103
|
+
const resolved = executableCandidate(candidate);
|
|
2104
|
+
if (resolved) {
|
|
2105
|
+
return { path: resolved, source: "package" };
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
const fromPath = resolveExecutableName("scoutd");
|
|
2109
|
+
if (fromPath) {
|
|
2110
|
+
return { path: fromPath, source: "path" };
|
|
2111
|
+
}
|
|
2112
|
+
if (workspaceRoot && workspaceScoutdAllowed()) {
|
|
2113
|
+
for (const candidate of [
|
|
2114
|
+
join5(workspaceRoot, "target", "release", "scoutd"),
|
|
2115
|
+
join5(workspaceRoot, "target", "debug", "scoutd")
|
|
2116
|
+
]) {
|
|
2117
|
+
const resolved = executableCandidate(candidate);
|
|
2118
|
+
if (resolved) {
|
|
2119
|
+
return { path: resolved, source: "workspace" };
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
return null;
|
|
2124
|
+
}
|
|
2125
|
+
function nativeServiceEnvironment(config, scoutdPath) {
|
|
2126
|
+
const env = {
|
|
2127
|
+
...process.env,
|
|
2128
|
+
OPENSCOUT_SCOUTD_BIN: scoutdPath,
|
|
2129
|
+
OPENSCOUT_RUNTIME_PACKAGE_DIR: config.runtimePackageDir,
|
|
2130
|
+
OPENSCOUT_SUPPORT_DIRECTORY: config.supportDirectory,
|
|
2131
|
+
OPENSCOUT_CONTROL_HOME: config.controlHome,
|
|
524
2132
|
OPENSCOUT_BROKER_HOST: config.brokerHost,
|
|
525
2133
|
OPENSCOUT_BROKER_PORT: String(config.brokerPort),
|
|
526
2134
|
OPENSCOUT_BROKER_URL: config.brokerUrl,
|
|
527
2135
|
OPENSCOUT_BROKER_SOCKET_PATH: config.brokerSocketPath,
|
|
528
|
-
OPENSCOUT_CONTROL_HOME: config.controlHome,
|
|
529
2136
|
OPENSCOUT_BROKER_SERVICE_MODE: config.mode,
|
|
530
2137
|
OPENSCOUT_BROKER_SERVICE_LABEL: config.label,
|
|
531
2138
|
OPENSCOUT_SERVICE_LABEL: config.label,
|
|
532
|
-
OPENSCOUT_ADVERTISE_SCOPE: config.advertiseScope
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
2139
|
+
OPENSCOUT_ADVERTISE_SCOPE: config.advertiseScope
|
|
2140
|
+
};
|
|
2141
|
+
if (config.bunExecutable) {
|
|
2142
|
+
env.OPENSCOUT_BUN_BIN = config.bunExecutable;
|
|
2143
|
+
}
|
|
2144
|
+
if (config.coreAgents.length > 0) {
|
|
2145
|
+
env.OPENSCOUT_CORE_AGENTS = config.coreAgents.join(",");
|
|
2146
|
+
}
|
|
2147
|
+
return env;
|
|
2148
|
+
}
|
|
2149
|
+
function isRecord3(value) {
|
|
2150
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2151
|
+
}
|
|
2152
|
+
function readString2(value) {
|
|
2153
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
2154
|
+
}
|
|
2155
|
+
function readBoolean(value) {
|
|
2156
|
+
return typeof value === "boolean" ? value : undefined;
|
|
2157
|
+
}
|
|
2158
|
+
function readNumber2(value) {
|
|
2159
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
2160
|
+
}
|
|
2161
|
+
function readHealthTransport(value) {
|
|
2162
|
+
return value === "unix_socket" || value === "http" || value === "in_process" ? value : undefined;
|
|
2163
|
+
}
|
|
2164
|
+
function normalizeNativeServiceStatus(input, config) {
|
|
2165
|
+
const healthRecord = isRecord3(input.health) ? input.health : {};
|
|
2166
|
+
const healthReachable = readBoolean(healthRecord.reachable) ?? readBoolean(input.reachable) ?? false;
|
|
2167
|
+
const healthOk = readBoolean(healthRecord.ok) ?? (typeof input.health === "boolean" ? input.health : undefined) ?? false;
|
|
2168
|
+
const healthError = readString2(healthRecord.error) ?? readString2(input.healthError);
|
|
2169
|
+
const healthTransport = readHealthTransport(healthRecord.transport) ?? readHealthTransport(input.healthTransport);
|
|
2170
|
+
const healthNodeId = readString2(healthRecord.nodeId);
|
|
2171
|
+
const healthMeshId = readString2(healthRecord.meshId);
|
|
2172
|
+
const healthSocketFallbackError = readString2(healthRecord.socketFallbackError);
|
|
2173
|
+
const healthCounts = isRecord3(healthRecord.counts) ? healthRecord.counts : undefined;
|
|
2174
|
+
const installed = readBoolean(input.installed) ?? existsSync5(config.launchAgentPath);
|
|
2175
|
+
const loaded = readBoolean(input.loaded) ?? false;
|
|
2176
|
+
const stdoutLogPath = readString2(input.stdoutLogPath) ?? config.stdoutLogPath;
|
|
2177
|
+
const stderrLogPath = readString2(input.stderrLogPath) ?? config.stderrLogPath;
|
|
2178
|
+
const lastLogLine = readString2(input.lastLogLine) ?? (healthReachable ? readLastLogLine([stdoutLogPath, stderrLogPath]) : readLastLogLine([stderrLogPath, stdoutLogPath]));
|
|
2179
|
+
return {
|
|
2180
|
+
label: readString2(input.label) ?? config.label,
|
|
2181
|
+
mode: readString2(input.mode) ?? config.mode,
|
|
2182
|
+
launchAgentPath: readString2(input.launchAgentPath) ?? config.launchAgentPath,
|
|
2183
|
+
bootoutCommand: readString2(input.bootoutCommand) ?? bootoutCommand(config),
|
|
2184
|
+
brokerUrl: readString2(input.brokerUrl) ?? config.brokerUrl,
|
|
2185
|
+
brokerSocketPath: readString2(input.brokerSocketPath) ?? config.brokerSocketPath,
|
|
2186
|
+
supportDirectory: readString2(input.supportDirectory) ?? config.supportDirectory,
|
|
2187
|
+
runtimeDirectory: readString2(input.runtimeDirectory) ?? config.runtimeDirectory,
|
|
2188
|
+
controlHome: readString2(input.controlHome) ?? config.controlHome,
|
|
2189
|
+
stdoutLogPath,
|
|
2190
|
+
stderrLogPath,
|
|
2191
|
+
installed,
|
|
2192
|
+
loaded,
|
|
2193
|
+
pid: readNumber2(input.pid) ?? null,
|
|
2194
|
+
launchdState: readString2(input.launchdState) ?? null,
|
|
2195
|
+
lastExitStatus: readNumber2(input.lastExitStatus) ?? null,
|
|
2196
|
+
usesLaunchAgent: readBoolean(input.usesLaunchAgent) ?? (installed || loaded),
|
|
2197
|
+
reachable: healthReachable,
|
|
2198
|
+
serviceAdapter: "macos-scoutd",
|
|
2199
|
+
health: {
|
|
2200
|
+
reachable: healthReachable,
|
|
2201
|
+
ok: healthOk,
|
|
2202
|
+
checkedAt: readNumber2(healthRecord.checkedAt) ?? Date.now(),
|
|
2203
|
+
transport: healthTransport,
|
|
2204
|
+
socketPath: config.brokerSocketPath,
|
|
2205
|
+
...healthSocketFallbackError ? { socketFallbackError: healthSocketFallbackError } : {},
|
|
2206
|
+
...healthNodeId ? { nodeId: healthNodeId } : {},
|
|
2207
|
+
...healthMeshId ? { meshId: healthMeshId } : {},
|
|
2208
|
+
...healthCounts ? { counts: healthCounts } : {},
|
|
2209
|
+
...isRecord3(healthRecord.build) ? { build: healthRecord.build } : {},
|
|
2210
|
+
...isRecord3(healthRecord.services) ? { services: healthRecord.services } : {},
|
|
2211
|
+
error: healthError
|
|
2212
|
+
},
|
|
2213
|
+
lastLogLine
|
|
557
2214
|
};
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
2215
|
+
}
|
|
2216
|
+
var SCOUTD_MAX_BUFFER = 2 * 1024 * 1024;
|
|
2217
|
+
var SCOUTD_DEFAULT_TIMEOUT_MS = 45000;
|
|
2218
|
+
var SCOUTD_KILL_GRACE_MS = 250;
|
|
2219
|
+
function spawnScoutdJson(scoutdPath, command, env, timeoutMs) {
|
|
2220
|
+
return new Promise((resolvePromise, reject) => {
|
|
2221
|
+
const child = spawn2(scoutdPath, [command, "--json"], {
|
|
2222
|
+
env,
|
|
2223
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2224
|
+
});
|
|
2225
|
+
let stdout = "";
|
|
2226
|
+
let stderr = "";
|
|
2227
|
+
let settled = false;
|
|
2228
|
+
let timedOut = false;
|
|
2229
|
+
const killTimer = setTimeout(() => {
|
|
2230
|
+
timedOut = true;
|
|
2231
|
+
terminate();
|
|
2232
|
+
fail(new Error(`scoutd ${command} timed out after ${timeoutMs}ms`));
|
|
2233
|
+
}, timeoutMs);
|
|
2234
|
+
killTimer.unref?.();
|
|
2235
|
+
function terminate() {
|
|
2236
|
+
child.kill("SIGTERM");
|
|
2237
|
+
const hardKillTimer = setTimeout(() => child.kill("SIGKILL"), SCOUTD_KILL_GRACE_MS);
|
|
2238
|
+
hardKillTimer.unref?.();
|
|
2239
|
+
}
|
|
2240
|
+
function cleanup() {
|
|
2241
|
+
clearTimeout(killTimer);
|
|
2242
|
+
}
|
|
2243
|
+
function fail(error) {
|
|
2244
|
+
if (settled)
|
|
2245
|
+
return;
|
|
2246
|
+
settled = true;
|
|
2247
|
+
cleanup();
|
|
2248
|
+
reject(error);
|
|
2249
|
+
}
|
|
2250
|
+
function succeed(output) {
|
|
2251
|
+
if (settled)
|
|
2252
|
+
return;
|
|
2253
|
+
settled = true;
|
|
2254
|
+
cleanup();
|
|
2255
|
+
resolvePromise(output);
|
|
2256
|
+
}
|
|
2257
|
+
function append(kind, chunk) {
|
|
2258
|
+
const text = typeof chunk === "string" ? chunk : String(chunk);
|
|
2259
|
+
if (kind === "stdout")
|
|
2260
|
+
stdout += text;
|
|
2261
|
+
else
|
|
2262
|
+
stderr += text;
|
|
2263
|
+
if (stdout.length + stderr.length > SCOUTD_MAX_BUFFER) {
|
|
2264
|
+
terminate();
|
|
2265
|
+
fail(new Error(`scoutd ${command} exceeded output limit`));
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
child.stdout.setEncoding("utf8");
|
|
2269
|
+
child.stderr.setEncoding("utf8");
|
|
2270
|
+
child.stdout.on("data", (chunk) => append("stdout", chunk));
|
|
2271
|
+
child.stderr.on("data", (chunk) => append("stderr", chunk));
|
|
2272
|
+
child.on("error", (error) => fail(new Error(`scoutd ${command} failed: ${error.message}`)));
|
|
2273
|
+
child.on("close", (code, signal) => {
|
|
2274
|
+
if (settled || timedOut)
|
|
2275
|
+
return;
|
|
2276
|
+
const trimmedStdout = stdout.trim();
|
|
2277
|
+
const trimmedStderr = stderr.trim();
|
|
2278
|
+
if ((code ?? 1) !== 0) {
|
|
2279
|
+
const detail = trimmedStderr || trimmedStdout || `exit ${signal ?? code ?? "unknown status"}`;
|
|
2280
|
+
fail(new Error(`scoutd ${command} failed: ${detail}`));
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
succeed(trimmedStdout);
|
|
2284
|
+
});
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
async function runScoutdServiceCommand(command, config, timeoutMs = SCOUTD_DEFAULT_TIMEOUT_MS, runScoutdJson = spawnScoutdJson) {
|
|
2288
|
+
const scoutd = resolveScoutdCommand(config);
|
|
2289
|
+
if (!scoutd) {
|
|
2290
|
+
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.");
|
|
2291
|
+
}
|
|
2292
|
+
const stdout = await runScoutdJson(scoutd.path, command, nativeServiceEnvironment(config, scoutd.path), timeoutMs);
|
|
2293
|
+
let parsed;
|
|
2294
|
+
try {
|
|
2295
|
+
parsed = JSON.parse(stdout);
|
|
2296
|
+
} catch {
|
|
2297
|
+
throw new Error(`scoutd ${command} returned non-JSON stdout: ${stdout.slice(0, 400)}`);
|
|
2298
|
+
}
|
|
2299
|
+
return normalizeNativeServiceStatus(parsed, config);
|
|
595
2300
|
}
|
|
596
2301
|
function readCoreAgentsSync() {
|
|
597
2302
|
try {
|
|
598
2303
|
const settingsPath = resolveOpenScoutSupportPaths().settingsPath;
|
|
599
|
-
const raw =
|
|
2304
|
+
const raw = readFileSync4(settingsPath, "utf8");
|
|
600
2305
|
const settings = JSON.parse(raw);
|
|
601
2306
|
const raw_agents = settings?.agents?.coreAgents;
|
|
602
2307
|
if (Array.isArray(raw_agents)) {
|
|
@@ -605,88 +2310,14 @@ function readCoreAgentsSync() {
|
|
|
605
2310
|
} catch {}
|
|
606
2311
|
return [];
|
|
607
2312
|
}
|
|
608
|
-
function collectOptionalEnvVars(keys) {
|
|
609
|
-
const entries = {};
|
|
610
|
-
for (const key of keys) {
|
|
611
|
-
const value = process.env[key];
|
|
612
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
613
|
-
entries[key] = value;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
return entries;
|
|
617
|
-
}
|
|
618
|
-
function resolveLaunchAgentPATH() {
|
|
619
|
-
const entries = [
|
|
620
|
-
join3(homedir3(), ".bun", "bin"),
|
|
621
|
-
...(process.env.PATH ?? "").split(":").filter(Boolean),
|
|
622
|
-
"/opt/homebrew/bin",
|
|
623
|
-
"/usr/local/bin",
|
|
624
|
-
"/usr/bin",
|
|
625
|
-
"/bin",
|
|
626
|
-
"/usr/sbin",
|
|
627
|
-
"/sbin"
|
|
628
|
-
];
|
|
629
|
-
return Array.from(new Set(entries)).filter((e) => !isTmpPath(e)).join(":");
|
|
630
|
-
}
|
|
631
|
-
function xmlEscape(value) {
|
|
632
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
633
|
-
}
|
|
634
|
-
function ensureParentDirectory(filePath) {
|
|
635
|
-
mkdirSync2(dirname2(filePath), { recursive: true });
|
|
636
|
-
}
|
|
637
|
-
function ensureServiceDirectories(config) {
|
|
638
|
-
ensureOpenScoutCleanSlateSync();
|
|
639
|
-
mkdirSync2(config.supportDirectory, { recursive: true });
|
|
640
|
-
mkdirSync2(config.runtimeDirectory, { recursive: true, mode: 448 });
|
|
641
|
-
chmodSync(config.runtimeDirectory, 448);
|
|
642
|
-
mkdirSync2(config.logsDirectory, { recursive: true });
|
|
643
|
-
mkdirSync2(config.controlHome, { recursive: true });
|
|
644
|
-
ensureParentDirectory(config.launchAgentPath);
|
|
645
|
-
}
|
|
646
|
-
function writeLaunchAgentPlist(config) {
|
|
647
|
-
ensureServiceDirectories(config);
|
|
648
|
-
writeFileSync2(config.launchAgentPath, renderLaunchAgentPlist(config), "utf8");
|
|
649
|
-
}
|
|
650
2313
|
function bootoutCommand(config) {
|
|
651
|
-
return
|
|
652
|
-
}
|
|
653
|
-
function legacyLaunchAgentPath(config) {
|
|
654
|
-
return join3(homedir3(), "Library", "LaunchAgents", `${legacyBrokerServiceLabel(config.mode)}.plist`);
|
|
655
|
-
}
|
|
656
|
-
function legacyServiceTarget(config) {
|
|
657
|
-
return `${config.domainTarget}/${legacyBrokerServiceLabel(config.mode)}`;
|
|
658
|
-
}
|
|
659
|
-
function bootoutLegacyBrokerService(config) {
|
|
660
|
-
const legacyTarget = legacyServiceTarget(config);
|
|
661
|
-
if (legacyTarget === config.serviceTarget) {
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
runCommand(launchctlPath(), ["bootout", legacyTarget], { allowFailure: true });
|
|
665
|
-
}
|
|
666
|
-
function runCommand(command, args, options) {
|
|
667
|
-
const result = spawnSync(command, args, {
|
|
668
|
-
env: {
|
|
669
|
-
...process.env,
|
|
670
|
-
...options?.env ?? {}
|
|
671
|
-
},
|
|
672
|
-
encoding: "utf8"
|
|
673
|
-
});
|
|
674
|
-
const stdout = (result.stdout ?? "").trim();
|
|
675
|
-
const stderr = (result.stderr ?? "").trim();
|
|
676
|
-
const exitCode = result.status ?? 1;
|
|
677
|
-
if (exitCode !== 0 && !options?.allowFailure) {
|
|
678
|
-
throw new Error(stderr || stdout || `${command} exited with status ${exitCode}`);
|
|
679
|
-
}
|
|
680
|
-
return { exitCode, stdout, stderr };
|
|
681
|
-
}
|
|
682
|
-
function launchctlPath() {
|
|
683
|
-
return "/bin/launchctl";
|
|
2314
|
+
return `/bin/launchctl bootout ${config.serviceTarget}`;
|
|
684
2315
|
}
|
|
685
2316
|
function readLogLines(path) {
|
|
686
|
-
if (!
|
|
2317
|
+
if (!existsSync5(path)) {
|
|
687
2318
|
return [];
|
|
688
2319
|
}
|
|
689
|
-
return
|
|
2320
|
+
return readFileSync4(path, "utf8").split(`
|
|
690
2321
|
`).map((line) => line.trim()).filter(Boolean);
|
|
691
2322
|
}
|
|
692
2323
|
function isPackageScriptBanner(line) {
|
|
@@ -719,170 +2350,120 @@ function readLastLogLine(paths) {
|
|
|
719
2350
|
}
|
|
720
2351
|
return fallback;
|
|
721
2352
|
}
|
|
722
|
-
function
|
|
723
|
-
const
|
|
724
|
-
const stateMatch = output.match(/\bstate = ([^\n]+)/);
|
|
725
|
-
const lastExitMatch = output.match(/\blast exit code = (-?\d+)/i) || output.match(/\blast exit status = (-?\d+)/i);
|
|
2353
|
+
async function readHeadlessBrokerHealth(config) {
|
|
2354
|
+
const result = await requestScoutBrokerJsonWithTrace(config.brokerUrl, "/health", { socketPath: config.brokerSocketPath });
|
|
726
2355
|
return {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
lastExitStatus: lastExitMatch ? Number.parseInt(lastExitMatch[1] ?? "0", 10) : null
|
|
2356
|
+
health: result.value,
|
|
2357
|
+
trace: result.trace
|
|
730
2358
|
};
|
|
731
2359
|
}
|
|
732
|
-
function
|
|
733
|
-
|
|
734
|
-
if (printResult.exitCode !== 0) {
|
|
735
|
-
return {
|
|
736
|
-
loaded: false,
|
|
737
|
-
pid: null,
|
|
738
|
-
launchdState: null,
|
|
739
|
-
lastExitStatus: null,
|
|
740
|
-
raw: printResult.stderr || printResult.stdout
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
return {
|
|
744
|
-
loaded: true,
|
|
745
|
-
raw: printResult.stdout,
|
|
746
|
-
...parseLaunchctlPrint(printResult.stdout)
|
|
747
|
-
};
|
|
2360
|
+
function headlessLifecycleError(command) {
|
|
2361
|
+
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.`);
|
|
748
2362
|
}
|
|
749
|
-
async function
|
|
750
|
-
|
|
751
|
-
|
|
2363
|
+
async function runHeadlessForegroundServiceCommand(command, config, readHealth = readHeadlessBrokerHealth) {
|
|
2364
|
+
if (command !== "status") {
|
|
2365
|
+
throw headlessLifecycleError(command);
|
|
2366
|
+
}
|
|
752
2367
|
const checkedAt = Date.now();
|
|
753
2368
|
try {
|
|
754
|
-
const {
|
|
755
|
-
|
|
756
|
-
socketPath: config.brokerSocketPath
|
|
757
|
-
});
|
|
2369
|
+
const { health, trace } = await readHealth(config);
|
|
2370
|
+
const reachable = true;
|
|
758
2371
|
return {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
2372
|
+
serviceAdapter: "headless-foreground",
|
|
2373
|
+
label: config.label,
|
|
2374
|
+
mode: config.mode,
|
|
2375
|
+
launchAgentPath: config.launchAgentPath,
|
|
2376
|
+
bootoutCommand: "headless-foreground does not use launchd",
|
|
2377
|
+
brokerUrl: config.brokerUrl,
|
|
2378
|
+
brokerSocketPath: config.brokerSocketPath,
|
|
2379
|
+
supportDirectory: config.supportDirectory,
|
|
2380
|
+
runtimeDirectory: config.runtimeDirectory,
|
|
2381
|
+
controlHome: config.controlHome,
|
|
2382
|
+
stdoutLogPath: config.stdoutLogPath,
|
|
2383
|
+
stderrLogPath: config.stderrLogPath,
|
|
2384
|
+
installed: false,
|
|
2385
|
+
loaded: reachable,
|
|
2386
|
+
pid: null,
|
|
2387
|
+
launchdState: null,
|
|
2388
|
+
lastExitStatus: null,
|
|
2389
|
+
usesLaunchAgent: false,
|
|
2390
|
+
reachable,
|
|
2391
|
+
health: {
|
|
2392
|
+
reachable,
|
|
2393
|
+
ok: Boolean(health.ok),
|
|
2394
|
+
checkedAt,
|
|
2395
|
+
transport: trace.transport,
|
|
2396
|
+
socketPath: trace.socketPath,
|
|
2397
|
+
socketFallbackError: trace.socketFallbackError,
|
|
2398
|
+
nodeId: health.nodeId ?? undefined,
|
|
2399
|
+
meshId: health.meshId ?? undefined,
|
|
2400
|
+
counts: health.counts ?? undefined,
|
|
2401
|
+
build: health.build,
|
|
2402
|
+
services: health.services
|
|
2403
|
+
},
|
|
2404
|
+
lastLogLine: readLastLogLine([config.stderrLogPath, config.stdoutLogPath])
|
|
787
2405
|
};
|
|
788
2406
|
} catch (error) {
|
|
789
2407
|
return {
|
|
2408
|
+
serviceAdapter: "headless-foreground",
|
|
2409
|
+
label: config.label,
|
|
2410
|
+
mode: config.mode,
|
|
2411
|
+
launchAgentPath: config.launchAgentPath,
|
|
2412
|
+
bootoutCommand: "headless-foreground does not use launchd",
|
|
2413
|
+
brokerUrl: config.brokerUrl,
|
|
2414
|
+
brokerSocketPath: config.brokerSocketPath,
|
|
2415
|
+
supportDirectory: config.supportDirectory,
|
|
2416
|
+
runtimeDirectory: config.runtimeDirectory,
|
|
2417
|
+
controlHome: config.controlHome,
|
|
2418
|
+
stdoutLogPath: config.stdoutLogPath,
|
|
2419
|
+
stderrLogPath: config.stderrLogPath,
|
|
2420
|
+
installed: false,
|
|
2421
|
+
loaded: false,
|
|
2422
|
+
pid: null,
|
|
2423
|
+
launchdState: null,
|
|
2424
|
+
lastExitStatus: null,
|
|
2425
|
+
usesLaunchAgent: false,
|
|
790
2426
|
reachable: false,
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
2427
|
+
health: {
|
|
2428
|
+
reachable: false,
|
|
2429
|
+
ok: false,
|
|
2430
|
+
checkedAt,
|
|
2431
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2432
|
+
},
|
|
2433
|
+
lastLogLine: readLastLogLine([config.stderrLogPath, config.stdoutLogPath])
|
|
795
2434
|
};
|
|
796
|
-
}
|
|
797
|
-
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
async function runBrokerServiceCommand(command, config) {
|
|
2438
|
+
const adapter = resolveBrokerServiceAdapter();
|
|
2439
|
+
switch (adapter) {
|
|
2440
|
+
case "macos-scoutd":
|
|
2441
|
+
return await runScoutdServiceCommand(command, config);
|
|
2442
|
+
case "headless-foreground":
|
|
2443
|
+
return await runHeadlessForegroundServiceCommand(command, config);
|
|
2444
|
+
case "linux-systemd-user":
|
|
2445
|
+
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.");
|
|
2446
|
+
case "windows-service":
|
|
2447
|
+
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.");
|
|
798
2448
|
}
|
|
799
2449
|
}
|
|
800
2450
|
async function brokerServiceStatus(config = resolveBrokerServiceConfig()) {
|
|
801
|
-
|
|
802
|
-
const launchctl = inspectLaunchctl(config);
|
|
803
|
-
const health = await fetchHealthSnapshot(config);
|
|
804
|
-
const installed = existsSync3(config.launchAgentPath);
|
|
805
|
-
const lastLogLine = health.reachable ? readLastLogLine([config.stdoutLogPath, config.stderrLogPath]) : readLastLogLine([config.stderrLogPath, config.stdoutLogPath]);
|
|
806
|
-
return {
|
|
807
|
-
label: config.label,
|
|
808
|
-
mode: config.mode,
|
|
809
|
-
launchAgentPath: config.launchAgentPath,
|
|
810
|
-
bootoutCommand: bootoutCommand(config),
|
|
811
|
-
brokerUrl: config.brokerUrl,
|
|
812
|
-
brokerSocketPath: config.brokerSocketPath,
|
|
813
|
-
supportDirectory: config.supportDirectory,
|
|
814
|
-
runtimeDirectory: config.runtimeDirectory,
|
|
815
|
-
controlHome: config.controlHome,
|
|
816
|
-
stdoutLogPath: config.stdoutLogPath,
|
|
817
|
-
stderrLogPath: config.stderrLogPath,
|
|
818
|
-
installed,
|
|
819
|
-
loaded: launchctl.loaded,
|
|
820
|
-
pid: launchctl.pid,
|
|
821
|
-
launchdState: launchctl.launchdState,
|
|
822
|
-
lastExitStatus: launchctl.lastExitStatus,
|
|
823
|
-
usesLaunchAgent: installed || launchctl.loaded,
|
|
824
|
-
reachable: health.reachable,
|
|
825
|
-
health,
|
|
826
|
-
lastLogLine
|
|
827
|
-
};
|
|
2451
|
+
return runBrokerServiceCommand("status", config);
|
|
828
2452
|
}
|
|
829
2453
|
async function installBrokerService(config = resolveBrokerServiceConfig()) {
|
|
830
|
-
|
|
831
|
-
writeLaunchAgentPlist(config);
|
|
832
|
-
return brokerServiceStatus(config);
|
|
2454
|
+
return runBrokerServiceCommand("install", config);
|
|
833
2455
|
}
|
|
834
2456
|
async function startBrokerService(config = resolveBrokerServiceConfig()) {
|
|
835
|
-
|
|
836
|
-
writeLaunchAgentPlist(config);
|
|
837
|
-
runCommand(launchctlPath(), ["bootout", config.serviceTarget], { allowFailure: true });
|
|
838
|
-
await waitForBrokerServiceStopped(config);
|
|
839
|
-
runCommand(launchctlPath(), ["bootstrap", config.domainTarget, config.launchAgentPath], { allowFailure: true });
|
|
840
|
-
runCommand(launchctlPath(), ["kickstart", "-k", config.serviceTarget], { allowFailure: true });
|
|
841
|
-
const attempts = Math.ceil(resolveBrokerStartTimeoutMs() / BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
842
|
-
for (let attempt = 0;attempt < attempts; attempt += 1) {
|
|
843
|
-
const status2 = await brokerServiceStatus(config);
|
|
844
|
-
if (status2.health.reachable) {
|
|
845
|
-
return status2;
|
|
846
|
-
}
|
|
847
|
-
await sleep(BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
848
|
-
}
|
|
849
|
-
const status = await brokerServiceStatus(config);
|
|
850
|
-
throw new Error(status.lastLogLine ?? status.health.error ?? "Broker service did not become healthy.");
|
|
851
|
-
}
|
|
852
|
-
function sleep(ms) {
|
|
853
|
-
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
854
|
-
}
|
|
855
|
-
async function waitForBrokerServiceStopped(config) {
|
|
856
|
-
let status = await brokerServiceStatus(config);
|
|
857
|
-
const attempts = Math.ceil(resolveBrokerStopTimeoutMs() / BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
858
|
-
for (let attempt = 0;attempt < attempts; attempt += 1) {
|
|
859
|
-
status = await brokerServiceStatus(config);
|
|
860
|
-
if (!status.loaded && !status.health.reachable) {
|
|
861
|
-
return status;
|
|
862
|
-
}
|
|
863
|
-
await sleep(BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
864
|
-
}
|
|
865
|
-
return status;
|
|
2457
|
+
return runBrokerServiceCommand("start", config);
|
|
866
2458
|
}
|
|
867
2459
|
async function stopBrokerService(config = resolveBrokerServiceConfig()) {
|
|
868
|
-
|
|
869
|
-
return waitForBrokerServiceStopped(config);
|
|
2460
|
+
return runBrokerServiceCommand("stop", config);
|
|
870
2461
|
}
|
|
871
2462
|
async function restartBrokerService(config = resolveBrokerServiceConfig()) {
|
|
872
|
-
|
|
873
|
-
return startBrokerService(config);
|
|
2463
|
+
return runBrokerServiceCommand("restart", config);
|
|
874
2464
|
}
|
|
875
2465
|
async function uninstallBrokerService(config = resolveBrokerServiceConfig()) {
|
|
876
|
-
|
|
877
|
-
if (existsSync3(config.launchAgentPath)) {
|
|
878
|
-
rmSync2(config.launchAgentPath, { force: true });
|
|
879
|
-
}
|
|
880
|
-
const legacyPath = legacyLaunchAgentPath(config);
|
|
881
|
-
bootoutLegacyBrokerService(config);
|
|
882
|
-
if (existsSync3(legacyPath)) {
|
|
883
|
-
rmSync2(legacyPath, { force: true });
|
|
884
|
-
}
|
|
885
|
-
return brokerServiceStatus(config);
|
|
2466
|
+
return runBrokerServiceCommand("uninstall", config);
|
|
886
2467
|
}
|
|
887
2468
|
async function main() {
|
|
888
2469
|
const command = process.argv[2] ?? "status";
|
|
@@ -920,6 +2501,7 @@ async function main() {
|
|
|
920
2501
|
}
|
|
921
2502
|
function formatBrokerServiceStatus(status) {
|
|
922
2503
|
const lines = [
|
|
2504
|
+
`service adapter: ${status.serviceAdapter ?? "unknown"}`,
|
|
923
2505
|
`label: ${status.label}`,
|
|
924
2506
|
`mode: ${status.mode}`,
|
|
925
2507
|
`launch agent: ${status.installed ? status.launchAgentPath : "not installed"}`,
|
|
@@ -951,7 +2533,7 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1] && !pr
|
|
|
951
2533
|
}
|
|
952
2534
|
|
|
953
2535
|
// packages/runtime/src/mesh-discover.ts
|
|
954
|
-
function
|
|
2536
|
+
function resolveBrokerUrl2() {
|
|
955
2537
|
return (process.env.OPENSCOUT_BROKER_URL ?? DEFAULT_BROKER_URL).replace(/\/$/, "");
|
|
956
2538
|
}
|
|
957
2539
|
function collectSeeds(argv) {
|
|
@@ -985,7 +2567,7 @@ ${detail}` : ""}`);
|
|
|
985
2567
|
return await response.json();
|
|
986
2568
|
}
|
|
987
2569
|
async function main2() {
|
|
988
|
-
const brokerUrl =
|
|
2570
|
+
const brokerUrl = resolveBrokerUrl2();
|
|
989
2571
|
const seeds = collectSeeds(process.argv.slice(2));
|
|
990
2572
|
const health = await getJson(`${brokerUrl}/health`);
|
|
991
2573
|
console.log(`Broker: ${brokerUrl}`);
|