@openscout/scout 0.2.70 → 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 +59 -3
- package/bin/openscout-runtime.mjs +23 -2
- 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-BNV7JPhj.js → addon-fit-D4KdY-pj.js} +1 -1
- package/dist/client/assets/{addon-webgl-B-_Y5L3F.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-Cx1oABPt.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 +42 -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 +77080 -60152
- package/dist/node/main.mjs +7607 -0
- package/dist/openscout-terminal-relay.mjs +3830 -152
- package/dist/{pair-supervisor.mjs → pairing-runtime-controller.mjs} +50837 -41212
- package/dist/runtime/base-daemon.mjs +2219 -488
- package/dist/runtime/broker-daemon.mjs +39082 -25118
- package/dist/runtime/broker-process-manager.mjs +2048 -392
- package/dist/runtime/mesh-discover.mjs +2012 -391
- package/dist/scout-control-plane-web.mjs +58128 -34736
- package/dist/scout-web-server.mjs +58128 -34736
- package/dist/statusline.mjs +287 -0
- package/package.json +8 -4
- package/dist/client/assets/index-BJri_z5a.css +0 -1
- package/dist/client/assets/index-Ccjo5BZz.js +0 -199
|
@@ -1,13 +1,15 @@
|
|
|
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";
|
|
12
|
+
var DEFAULT_BROKER_JSON_REQUEST_TIMEOUT_MS = 30000;
|
|
11
13
|
function normalizeBaseUrl(baseUrl) {
|
|
12
14
|
const trimmed = baseUrl.trim();
|
|
13
15
|
if (!trimmed) {
|
|
@@ -54,13 +56,17 @@ async function requestBrokerOverHttp(baseUrl, path, options) {
|
|
|
54
56
|
}
|
|
55
57
|
function shouldFallbackFromUnixSocket(error) {
|
|
56
58
|
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
57
|
-
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";
|
|
58
60
|
}
|
|
59
61
|
function errorMessage(error) {
|
|
60
62
|
return error instanceof Error ? error.message : String(error);
|
|
61
63
|
}
|
|
62
64
|
function requestBrokerOverUnixSocket(socketPath, baseUrl, path, options) {
|
|
63
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
|
+
}
|
|
64
70
|
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
65
71
|
const url = new URL(path, normalizedBaseUrl);
|
|
66
72
|
const body = requestBody(options);
|
|
@@ -108,12 +114,19 @@ function requestBrokerOverUnixSocket(socketPath, baseUrl, path, options) {
|
|
|
108
114
|
request.end();
|
|
109
115
|
});
|
|
110
116
|
}
|
|
117
|
+
function defaultBrokerRequestSignal(options) {
|
|
118
|
+
return options.signal ?? AbortSignal.timeout(DEFAULT_BROKER_JSON_REQUEST_TIMEOUT_MS);
|
|
119
|
+
}
|
|
111
120
|
async function requestBrokerWire(baseUrl, path, options) {
|
|
121
|
+
const requestOptions = {
|
|
122
|
+
...options,
|
|
123
|
+
signal: defaultBrokerRequestSignal(options)
|
|
124
|
+
};
|
|
112
125
|
const socketPath = options.socketPath?.trim();
|
|
113
126
|
if (socketPath) {
|
|
114
127
|
try {
|
|
115
128
|
return {
|
|
116
|
-
response: await requestBrokerOverUnixSocket(socketPath, baseUrl, path,
|
|
129
|
+
response: await requestBrokerOverUnixSocket(socketPath, baseUrl, path, requestOptions),
|
|
117
130
|
trace: {
|
|
118
131
|
transport: "unix_socket",
|
|
119
132
|
socketPath
|
|
@@ -124,7 +137,7 @@ async function requestBrokerWire(baseUrl, path, options) {
|
|
|
124
137
|
throw error;
|
|
125
138
|
}
|
|
126
139
|
return {
|
|
127
|
-
response: await requestBrokerOverHttp(baseUrl, path,
|
|
140
|
+
response: await requestBrokerOverHttp(baseUrl, path, requestOptions),
|
|
128
141
|
trace: {
|
|
129
142
|
transport: "http",
|
|
130
143
|
socketPath,
|
|
@@ -134,94 +147,1585 @@ async function requestBrokerWire(baseUrl, path, options) {
|
|
|
134
147
|
}
|
|
135
148
|
}
|
|
136
149
|
return {
|
|
137
|
-
response: await requestBrokerOverHttp(baseUrl, path,
|
|
138
|
-
trace: {
|
|
139
|
-
transport: "http"
|
|
140
|
-
}
|
|
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;
|
|
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)
|
|
141
1496
|
};
|
|
142
1497
|
}
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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) {
|
|
148
1563
|
try {
|
|
149
|
-
|
|
150
|
-
parsedJson = true;
|
|
1564
|
+
return readStatusJsonFromFileSync(fixturePath);
|
|
151
1565
|
} catch {
|
|
152
|
-
|
|
1566
|
+
return null;
|
|
153
1567
|
}
|
|
154
1568
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
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;
|
|
160
1572
|
}
|
|
161
|
-
|
|
162
|
-
|
|
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;
|
|
163
1583
|
}
|
|
164
|
-
|
|
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);
|
|
165
1591
|
}
|
|
166
1592
|
|
|
167
|
-
// packages/runtime/src/
|
|
168
|
-
import {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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;
|
|
1669
|
+
}
|
|
1670
|
+
function parseEnvPort(name) {
|
|
1671
|
+
const raw = process.env[name]?.trim();
|
|
1672
|
+
if (!raw) {
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
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
|
+
};
|
|
178
1686
|
return {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
runtimeDirectory,
|
|
184
|
-
catalogDirectory,
|
|
185
|
-
relayAgentsDirectory: join(runtimeDirectory, "agents"),
|
|
186
|
-
settingsPath: join(supportDirectory, "settings.json"),
|
|
187
|
-
harnessCatalogPath: join(catalogDirectory, "harness-catalog.json"),
|
|
188
|
-
relayAgentsRegistryPath: join(supportDirectory, "relay-agents.json"),
|
|
189
|
-
relayHubDirectory: process.env.OPENSCOUT_RELAY_HUB ?? join(home, ".openscout", "relay"),
|
|
190
|
-
controlHome: process.env.OPENSCOUT_CONTROL_HOME ?? join(home, ".openscout", "control-plane"),
|
|
191
|
-
desktopStatusPath: join(supportDirectory, "agent-status.json"),
|
|
192
|
-
workspaceStatePath: join(supportDirectory, "workspace-state.json"),
|
|
193
|
-
cutoverMarkerPath: join(supportDirectory, OPENSCOUT_RPC_CUTOVER_MARKER)
|
|
1687
|
+
version: LOCAL_CONFIG_VERSION,
|
|
1688
|
+
...host ? { host } : {},
|
|
1689
|
+
...file.webLocalName ? { webLocalName: file.webLocalName } : {},
|
|
1690
|
+
...ports.broker || ports.web || ports.pairing ? { ports } : {}
|
|
194
1691
|
};
|
|
195
1692
|
}
|
|
196
|
-
function
|
|
197
|
-
const
|
|
198
|
-
if (
|
|
199
|
-
return;
|
|
1693
|
+
function localBrokerControlHost(host) {
|
|
1694
|
+
const trimmed = host.trim();
|
|
1695
|
+
if (!trimmed || trimmed === "0.0.0.0" || trimmed === "::" || trimmed === "[::]") {
|
|
1696
|
+
return DEFAULT_LOCAL_CONFIG.host;
|
|
1697
|
+
}
|
|
1698
|
+
return trimmed;
|
|
1699
|
+
}
|
|
1700
|
+
function resolveBrokerControlUrl(config = resolveEffectiveLocalConfig()) {
|
|
1701
|
+
const injected = process.env.OPENSCOUT_BROKER_INTERNAL_URL?.trim();
|
|
1702
|
+
if (injected) {
|
|
1703
|
+
return injected;
|
|
200
1704
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
1705
|
+
const host = config.host ?? DEFAULT_LOCAL_CONFIG.host;
|
|
1706
|
+
const port = config.ports?.broker ?? DEFAULT_LOCAL_CONFIG.ports.broker;
|
|
1707
|
+
return `http://${localBrokerControlHost(host)}:${port}`;
|
|
1708
|
+
}
|
|
1709
|
+
function resolveBrokerPort() {
|
|
1710
|
+
return resolveEffectiveLocalConfig().ports?.broker ?? DEFAULT_LOCAL_CONFIG.ports.broker;
|
|
207
1711
|
}
|
|
208
1712
|
|
|
209
1713
|
// packages/runtime/src/tool-resolution.ts
|
|
210
|
-
import { accessSync, constants, existsSync as
|
|
211
|
-
import { homedir as
|
|
212
|
-
import { basename, dirname, join as
|
|
1714
|
+
import { accessSync, constants, existsSync as existsSync4 } from "fs";
|
|
1715
|
+
import { homedir as homedir4 } from "os";
|
|
1716
|
+
import { basename, dirname as dirname2, join as join4, resolve } from "path";
|
|
213
1717
|
var DEFAULT_COMMON_EXECUTABLE_DIRECTORIES = [
|
|
214
|
-
|
|
1718
|
+
join4(homedir4(), ".bun", "bin"),
|
|
215
1719
|
"/opt/homebrew/bin",
|
|
216
1720
|
"/usr/local/bin"
|
|
217
1721
|
];
|
|
218
1722
|
function expandHomePath(value, env = process.env) {
|
|
219
|
-
const home = env.HOME ??
|
|
1723
|
+
const home = env.HOME ?? homedir4();
|
|
220
1724
|
if (value === "~") {
|
|
221
1725
|
return home;
|
|
222
1726
|
}
|
|
223
1727
|
if (value.startsWith("~/")) {
|
|
224
|
-
return
|
|
1728
|
+
return join4(home, value.slice(2));
|
|
225
1729
|
}
|
|
226
1730
|
return value;
|
|
227
1731
|
}
|
|
@@ -278,7 +1782,7 @@ function resolveExecutableFromSearch(options) {
|
|
|
278
1782
|
], env);
|
|
279
1783
|
for (const directory of searchDirectories) {
|
|
280
1784
|
for (const name of options.names) {
|
|
281
|
-
const candidate =
|
|
1785
|
+
const candidate = join4(directory, name);
|
|
282
1786
|
if (isExecutablePath(candidate)) {
|
|
283
1787
|
return {
|
|
284
1788
|
path: candidate,
|
|
@@ -302,7 +1806,7 @@ function findExecutableOnSearchPath(name, env) {
|
|
|
302
1806
|
...DEFAULT_COMMON_EXECUTABLE_DIRECTORIES
|
|
303
1807
|
], env);
|
|
304
1808
|
for (const directory of searchDirectories) {
|
|
305
|
-
const candidate =
|
|
1809
|
+
const candidate = join4(directory, name);
|
|
306
1810
|
if (isExecutablePath(candidate)) {
|
|
307
1811
|
return { path: candidate, source: "path" };
|
|
308
1812
|
}
|
|
@@ -310,25 +1814,68 @@ function findExecutableOnSearchPath(name, env) {
|
|
|
310
1814
|
return null;
|
|
311
1815
|
}
|
|
312
1816
|
|
|
1817
|
+
// packages/runtime/src/runtime-adapters.ts
|
|
1818
|
+
function normalizeRuntimeHost(value) {
|
|
1819
|
+
const normalized = value?.trim().toLowerCase();
|
|
1820
|
+
if (normalized === "bun")
|
|
1821
|
+
return "bun";
|
|
1822
|
+
if (normalized === "node")
|
|
1823
|
+
return "node";
|
|
1824
|
+
return null;
|
|
1825
|
+
}
|
|
1826
|
+
function normalizeRuntimeServiceAdapter(value) {
|
|
1827
|
+
const normalized = value?.trim().toLowerCase().replaceAll("_", "-");
|
|
1828
|
+
if (normalized === "macos-scoutd" || normalized === "macos-launchd")
|
|
1829
|
+
return "macos-scoutd";
|
|
1830
|
+
if (normalized === "linux-systemd-user" || normalized === "systemd-user")
|
|
1831
|
+
return "linux-systemd-user";
|
|
1832
|
+
if (normalized === "headless-foreground" || normalized === "foreground" || normalized === "headless") {
|
|
1833
|
+
return "headless-foreground";
|
|
1834
|
+
}
|
|
1835
|
+
if (normalized === "windows-service")
|
|
1836
|
+
return "windows-service";
|
|
1837
|
+
return null;
|
|
1838
|
+
}
|
|
1839
|
+
function defaultServiceAdapterForPlatform(platform = process.platform, env = process.env) {
|
|
1840
|
+
const explicit = normalizeRuntimeServiceAdapter(env.OPENSCOUT_SERVICE_ADAPTER);
|
|
1841
|
+
if (explicit)
|
|
1842
|
+
return explicit;
|
|
1843
|
+
if (normalizeRuntimeHost(env.OPENSCOUT_RUNTIME_HOST) === "node") {
|
|
1844
|
+
return "headless-foreground";
|
|
1845
|
+
}
|
|
1846
|
+
if (platform === "darwin")
|
|
1847
|
+
return "macos-scoutd";
|
|
1848
|
+
if (platform === "linux")
|
|
1849
|
+
return "headless-foreground";
|
|
1850
|
+
if (platform === "win32")
|
|
1851
|
+
return "windows-service";
|
|
1852
|
+
return "headless-foreground";
|
|
1853
|
+
}
|
|
1854
|
+
|
|
313
1855
|
// packages/runtime/src/broker-process-manager.ts
|
|
314
1856
|
function isTmpPath(p) {
|
|
315
1857
|
return /^\/(?:private\/)?tmp\//.test(p);
|
|
316
1858
|
}
|
|
317
1859
|
var DEFAULT_BROKER_HOST = "127.0.0.1";
|
|
318
1860
|
var DEFAULT_BROKER_HOST_MESH = "0.0.0.0";
|
|
319
|
-
var DEFAULT_BROKER_PORT =
|
|
1861
|
+
var DEFAULT_BROKER_PORT = OPENSCOUT_PORTS.broker;
|
|
320
1862
|
var DEFAULT_ADVERTISE_SCOPE = "local";
|
|
321
|
-
function resolveAdvertiseScope() {
|
|
322
|
-
|
|
1863
|
+
function resolveAdvertiseScope(env = process.env) {
|
|
1864
|
+
if (openScoutNetworkDiscoveryEnabled(env))
|
|
1865
|
+
return "mesh";
|
|
1866
|
+
const raw = (env.OPENSCOUT_ADVERTISE_SCOPE ?? "").trim().toLowerCase();
|
|
323
1867
|
if (raw === "mesh")
|
|
324
1868
|
return "mesh";
|
|
325
1869
|
if (raw === "local")
|
|
326
1870
|
return "local";
|
|
327
1871
|
return DEFAULT_ADVERTISE_SCOPE;
|
|
328
1872
|
}
|
|
329
|
-
function resolveBrokerHost(scope = resolveAdvertiseScope()) {
|
|
330
|
-
const explicit =
|
|
331
|
-
if (
|
|
1873
|
+
function resolveBrokerHost(scope = resolveAdvertiseScope(), env = process.env) {
|
|
1874
|
+
const explicit = env.OPENSCOUT_BROKER_HOST?.trim();
|
|
1875
|
+
if (explicit) {
|
|
1876
|
+
if (scope === "mesh" && isLoopbackHost(explicit)) {
|
|
1877
|
+
return DEFAULT_BROKER_HOST_MESH;
|
|
1878
|
+
}
|
|
332
1879
|
return explicit;
|
|
333
1880
|
}
|
|
334
1881
|
return scope === "mesh" ? DEFAULT_BROKER_HOST_MESH : DEFAULT_BROKER_HOST;
|
|
@@ -337,13 +1884,46 @@ function isLoopbackHost(host) {
|
|
|
337
1884
|
const trimmed = host.trim();
|
|
338
1885
|
return trimmed === "127.0.0.1" || trimmed === "::1" || trimmed === "localhost";
|
|
339
1886
|
}
|
|
340
|
-
|
|
341
|
-
|
|
1887
|
+
function localBrokerControlHost2(host) {
|
|
1888
|
+
const trimmed = host.trim();
|
|
1889
|
+
if (!trimmed || isWildcardHost(trimmed)) {
|
|
1890
|
+
return DEFAULT_BROKER_HOST;
|
|
1891
|
+
}
|
|
1892
|
+
return trimmed;
|
|
1893
|
+
}
|
|
342
1894
|
function buildDefaultBrokerUrl(host = DEFAULT_BROKER_HOST, port = DEFAULT_BROKER_PORT) {
|
|
343
1895
|
return `http://${host}:${port}`;
|
|
344
1896
|
}
|
|
1897
|
+
function resolveBrokerUrl(host, port, scope, env = process.env) {
|
|
1898
|
+
const explicit = env.OPENSCOUT_BROKER_URL?.trim();
|
|
1899
|
+
if (explicit && !(scope === "mesh" && isUnreachableMeshBrokerUrl(explicit))) {
|
|
1900
|
+
return explicit;
|
|
1901
|
+
}
|
|
1902
|
+
if (scope === "mesh") {
|
|
1903
|
+
const tailnetHost = readTailscaleSelfWebHostsSync(env)[0];
|
|
1904
|
+
if (tailnetHost) {
|
|
1905
|
+
return buildDefaultBrokerUrl(tailnetHost, port);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
return buildDefaultBrokerUrl(host, port);
|
|
1909
|
+
}
|
|
1910
|
+
function isUnreachableMeshBrokerUrl(value) {
|
|
1911
|
+
try {
|
|
1912
|
+
const host = new URL(value).hostname;
|
|
1913
|
+
return isLoopbackHost(host) || isWildcardHost(host);
|
|
1914
|
+
} catch {
|
|
1915
|
+
return false;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
function isWildcardHost(host) {
|
|
1919
|
+
const trimmed = host.trim();
|
|
1920
|
+
return trimmed === "0.0.0.0" || trimmed === "::" || trimmed === "[::]";
|
|
1921
|
+
}
|
|
1922
|
+
function buildLocalBrokerControlUrl(host = DEFAULT_BROKER_HOST, port = DEFAULT_BROKER_PORT) {
|
|
1923
|
+
return buildDefaultBrokerUrl(localBrokerControlHost2(host), port);
|
|
1924
|
+
}
|
|
345
1925
|
function buildDefaultBrokerSocketPath(runtimeDirectory) {
|
|
346
|
-
return
|
|
1926
|
+
return join5(runtimeDirectory, "broker.sock");
|
|
347
1927
|
}
|
|
348
1928
|
var DEFAULT_BROKER_URL = buildDefaultBrokerUrl();
|
|
349
1929
|
function normalizeBrokerUrl(value) {
|
|
@@ -353,8 +1933,15 @@ function normalizeBrokerUrl(value) {
|
|
|
353
1933
|
return value.trim();
|
|
354
1934
|
}
|
|
355
1935
|
}
|
|
1936
|
+
function resolveScoutBrokerControlUrl(_config = resolveBrokerServiceConfig()) {
|
|
1937
|
+
return resolveBrokerControlUrl();
|
|
1938
|
+
}
|
|
356
1939
|
function resolveBrokerSocketPathForBaseUrl(baseUrl, config = resolveBrokerServiceConfig()) {
|
|
357
|
-
|
|
1940
|
+
const normalized = normalizeBrokerUrl(baseUrl);
|
|
1941
|
+
if (normalized === normalizeBrokerUrl(config.brokerUrl) || normalized === normalizeBrokerUrl(resolveScoutBrokerControlUrl(config))) {
|
|
1942
|
+
return config.brokerSocketPath;
|
|
1943
|
+
}
|
|
1944
|
+
return null;
|
|
358
1945
|
}
|
|
359
1946
|
function runtimePackageDir() {
|
|
360
1947
|
const explicit = process.env.OPENSCOUT_RUNTIME_PACKAGE_DIR?.trim();
|
|
@@ -369,17 +1956,17 @@ function runtimePackageDir() {
|
|
|
369
1956
|
const fromGlobal = findGlobalRuntimeDir();
|
|
370
1957
|
if (fromGlobal)
|
|
371
1958
|
return fromGlobal;
|
|
372
|
-
const moduleDir =
|
|
1959
|
+
const moduleDir = dirname3(fileURLToPath(import.meta.url));
|
|
373
1960
|
return resolve2(moduleDir, "..");
|
|
374
1961
|
}
|
|
375
1962
|
function isInstalledRuntimePackageDir(candidate) {
|
|
376
|
-
return
|
|
1963
|
+
return existsSync5(join5(candidate, "package.json")) && existsSync5(join5(candidate, "bin", "openscout-runtime.mjs"));
|
|
377
1964
|
}
|
|
378
1965
|
function findGlobalRuntimeDir() {
|
|
379
1966
|
const candidates = [
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
1967
|
+
join5(homedir5(), ".bun", "node_modules", "@openscout", "runtime"),
|
|
1968
|
+
join5(homedir5(), ".bun", "install", "global", "node_modules", "@openscout", "runtime"),
|
|
1969
|
+
join5(homedir5(), ".bun", "install", "global", "node_modules", "@openscout", "scout", "node_modules", "@openscout", "runtime")
|
|
383
1970
|
];
|
|
384
1971
|
for (const c of candidates) {
|
|
385
1972
|
if (isInstalledRuntimePackageDir(c))
|
|
@@ -392,7 +1979,7 @@ function findGlobalRuntimeDir() {
|
|
|
392
1979
|
const scoutPkg = resolve2(scoutBin, "..", "..");
|
|
393
1980
|
if (isInstalledRuntimePackageDir(scoutPkg))
|
|
394
1981
|
return scoutPkg;
|
|
395
|
-
const nested =
|
|
1982
|
+
const nested = join5(scoutPkg, "node_modules", "@openscout", "runtime");
|
|
396
1983
|
if (isInstalledRuntimePackageDir(nested))
|
|
397
1984
|
return nested;
|
|
398
1985
|
const sibling = resolve2(scoutPkg, "..", "runtime");
|
|
@@ -403,30 +1990,43 @@ function findGlobalRuntimeDir() {
|
|
|
403
1990
|
return null;
|
|
404
1991
|
}
|
|
405
1992
|
function findBundledRuntimeDir() {
|
|
406
|
-
const moduleDir =
|
|
407
|
-
|
|
408
|
-
|
|
1993
|
+
const moduleDir = dirname3(fileURLToPath(import.meta.url));
|
|
1994
|
+
return resolveBundledRuntimeDirFromModuleDir(moduleDir);
|
|
1995
|
+
}
|
|
1996
|
+
function resolveBundledRuntimeDirFromModuleDir(moduleDir) {
|
|
1997
|
+
const candidates = [
|
|
1998
|
+
resolve2(moduleDir, ".."),
|
|
1999
|
+
resolve2(moduleDir, "..", "..")
|
|
2000
|
+
];
|
|
2001
|
+
for (const candidate of candidates) {
|
|
2002
|
+
if (isInstalledRuntimePackageDir(candidate))
|
|
2003
|
+
return candidate;
|
|
2004
|
+
}
|
|
2005
|
+
return null;
|
|
409
2006
|
}
|
|
410
2007
|
function findWorkspaceRuntimeDir(startDir) {
|
|
411
2008
|
let current = resolve2(startDir);
|
|
412
2009
|
while (true) {
|
|
413
|
-
const candidate =
|
|
414
|
-
if (
|
|
2010
|
+
const candidate = join5(current, "packages", "runtime");
|
|
2011
|
+
if (existsSync5(join5(candidate, "package.json")) && existsSync5(join5(candidate, "src"))) {
|
|
415
2012
|
return candidate;
|
|
416
2013
|
}
|
|
417
|
-
const parent =
|
|
2014
|
+
const parent = dirname3(current);
|
|
418
2015
|
if (parent === current)
|
|
419
2016
|
return null;
|
|
420
2017
|
current = parent;
|
|
421
2018
|
}
|
|
422
2019
|
}
|
|
423
2020
|
function resolveBunExecutable2() {
|
|
424
|
-
const bun =
|
|
2021
|
+
const bun = resolveOptionalBunExecutable();
|
|
425
2022
|
if (bun) {
|
|
426
|
-
return bun
|
|
2023
|
+
return bun;
|
|
427
2024
|
}
|
|
428
2025
|
throw new Error("Unable to locate Bun for broker service management. Install Bun or set OPENSCOUT_BUN_BIN.");
|
|
429
2026
|
}
|
|
2027
|
+
function resolveOptionalBunExecutable() {
|
|
2028
|
+
return resolveBunExecutable(process.env)?.path ?? null;
|
|
2029
|
+
}
|
|
430
2030
|
function resolveBrokerServiceMode() {
|
|
431
2031
|
const explicit = (process.env.OPENSCOUT_BROKER_SERVICE_MODE ?? "").trim().toLowerCase();
|
|
432
2032
|
if (explicit === "prod" || explicit === "production") {
|
|
@@ -437,13 +2037,6 @@ function resolveBrokerServiceMode() {
|
|
|
437
2037
|
}
|
|
438
2038
|
return "dev";
|
|
439
2039
|
}
|
|
440
|
-
function resolveBrokerStartTimeoutMs() {
|
|
441
|
-
const explicit = Number.parseInt(process.env.OPENSCOUT_BROKER_START_TIMEOUT_MS ?? "", 10);
|
|
442
|
-
if (Number.isFinite(explicit) && explicit > 0) {
|
|
443
|
-
return Math.max(explicit, BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
444
|
-
}
|
|
445
|
-
return DEFAULT_BROKER_START_TIMEOUT_MS;
|
|
446
|
-
}
|
|
447
2040
|
function resolveBrokerServiceLabel(mode) {
|
|
448
2041
|
const explicit = process.env.OPENSCOUT_SERVICE_LABEL?.trim() || process.env.OPENSCOUT_BROKER_SERVICE_LABEL?.trim();
|
|
449
2042
|
if (explicit) {
|
|
@@ -459,33 +2052,23 @@ function resolveBrokerServiceLabel(mode) {
|
|
|
459
2052
|
return "dev.openscout";
|
|
460
2053
|
}
|
|
461
2054
|
}
|
|
462
|
-
function legacyBrokerServiceLabel(mode) {
|
|
463
|
-
switch (mode) {
|
|
464
|
-
case "prod":
|
|
465
|
-
return "com.openscout.broker";
|
|
466
|
-
case "custom":
|
|
467
|
-
return "com.openscout.broker.custom";
|
|
468
|
-
case "dev":
|
|
469
|
-
default:
|
|
470
|
-
return "dev.openscout.broker";
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
2055
|
function resolveBrokerServiceConfig() {
|
|
474
2056
|
const mode = resolveBrokerServiceMode();
|
|
475
2057
|
const label = resolveBrokerServiceLabel(mode);
|
|
2058
|
+
const serviceAdapter = resolveBrokerServiceAdapter();
|
|
476
2059
|
const uid = typeof process.getuid === "function" ? process.getuid() : Number.parseInt(process.env.UID ?? "0", 10);
|
|
477
2060
|
const supportPaths = resolveOpenScoutSupportPaths();
|
|
478
|
-
const defaultSupportDir =
|
|
2061
|
+
const defaultSupportDir = join5(homedir5(), "Library", "Application Support", "OpenScout");
|
|
479
2062
|
const supportDirectory = isTmpPath(supportPaths.supportDirectory) ? defaultSupportDir : supportPaths.supportDirectory;
|
|
480
|
-
const runtimeDirectory =
|
|
481
|
-
const logsDirectory =
|
|
482
|
-
const controlHome = isTmpPath(supportPaths.controlHome) ?
|
|
2063
|
+
const runtimeDirectory = join5(supportDirectory, "runtime");
|
|
2064
|
+
const logsDirectory = join5(supportDirectory, "logs", "broker");
|
|
2065
|
+
const controlHome = isTmpPath(supportPaths.controlHome) ? join5(homedir5(), ".openscout", "control-plane") : supportPaths.controlHome;
|
|
483
2066
|
const advertiseScope = resolveAdvertiseScope();
|
|
484
2067
|
const brokerHost = resolveBrokerHost(advertiseScope);
|
|
485
|
-
const brokerPort = Number.parseInt(process.env.OPENSCOUT_BROKER_PORT ?? String(
|
|
486
|
-
const brokerUrl =
|
|
2068
|
+
const brokerPort = Number.parseInt(process.env.OPENSCOUT_BROKER_PORT ?? String(resolveBrokerPort()), 10);
|
|
2069
|
+
const brokerUrl = resolveBrokerUrl(brokerHost, brokerPort, advertiseScope);
|
|
487
2070
|
const brokerSocketPath = process.env.OPENSCOUT_BROKER_SOCKET_PATH ?? buildDefaultBrokerSocketPath(runtimeDirectory);
|
|
488
|
-
const launchAgentPath =
|
|
2071
|
+
const launchAgentPath = join5(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
|
|
489
2072
|
return {
|
|
490
2073
|
label,
|
|
491
2074
|
mode,
|
|
@@ -496,11 +2079,11 @@ function resolveBrokerServiceConfig() {
|
|
|
496
2079
|
supportDirectory,
|
|
497
2080
|
runtimeDirectory,
|
|
498
2081
|
logsDirectory,
|
|
499
|
-
stdoutLogPath:
|
|
500
|
-
stderrLogPath:
|
|
2082
|
+
stdoutLogPath: join5(logsDirectory, "stdout.log"),
|
|
2083
|
+
stderrLogPath: join5(logsDirectory, "stderr.log"),
|
|
501
2084
|
controlHome,
|
|
502
2085
|
runtimePackageDir: runtimePackageDir(),
|
|
503
|
-
bunExecutable: resolveBunExecutable2(),
|
|
2086
|
+
bunExecutable: serviceAdapter === "macos-scoutd" ? resolveBunExecutable2() : resolveOptionalBunExecutable(),
|
|
504
2087
|
brokerHost,
|
|
505
2088
|
brokerPort,
|
|
506
2089
|
brokerUrl,
|
|
@@ -509,75 +2092,259 @@ function resolveBrokerServiceConfig() {
|
|
|
509
2092
|
coreAgents: readCoreAgentsSync()
|
|
510
2093
|
};
|
|
511
2094
|
}
|
|
512
|
-
function
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
2095
|
+
function resolveBrokerServiceAdapter(env = process.env, platform = process.platform) {
|
|
2096
|
+
return defaultServiceAdapterForPlatform(platform, env);
|
|
2097
|
+
}
|
|
2098
|
+
function executableCandidate(path) {
|
|
2099
|
+
return isExecutablePath(path) ? path : null;
|
|
2100
|
+
}
|
|
2101
|
+
function resolveExecutableName(name) {
|
|
2102
|
+
return resolveExecutableFromSearch({ names: [name] })?.path ?? null;
|
|
2103
|
+
}
|
|
2104
|
+
function resolveEnvExecutable(value) {
|
|
2105
|
+
const trimmed = value?.trim();
|
|
2106
|
+
if (!trimmed)
|
|
2107
|
+
return null;
|
|
2108
|
+
if (trimmed.includes("/") || trimmed.startsWith(".")) {
|
|
2109
|
+
const expanded = resolve2(expandHomePath(trimmed));
|
|
2110
|
+
return executableCandidate(expanded);
|
|
2111
|
+
}
|
|
2112
|
+
return resolveExecutableName(trimmed);
|
|
2113
|
+
}
|
|
2114
|
+
function findWorkspaceRootFromRuntimeDir(runtimePackageDir2) {
|
|
2115
|
+
let current = resolve2(runtimePackageDir2);
|
|
2116
|
+
while (true) {
|
|
2117
|
+
if (existsSync5(join5(current, "Cargo.toml")) && existsSync5(join5(current, "crates", "scoutd", "Cargo.toml"))) {
|
|
2118
|
+
return current;
|
|
2119
|
+
}
|
|
2120
|
+
const parent = dirname3(current);
|
|
2121
|
+
if (parent === current)
|
|
2122
|
+
return null;
|
|
2123
|
+
current = parent;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
function workspaceScoutdAllowed() {
|
|
2127
|
+
const raw = process.env.OPENSCOUT_ALLOW_WORKSPACE_SCOUTD?.trim().toLowerCase();
|
|
2128
|
+
return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
|
|
2129
|
+
}
|
|
2130
|
+
function resolveScoutdCommand(config = resolveBrokerServiceConfig()) {
|
|
2131
|
+
const explicit = resolveEnvExecutable(process.env.OPENSCOUT_SCOUTD_BIN);
|
|
2132
|
+
if (explicit) {
|
|
2133
|
+
return { path: explicit, source: "env" };
|
|
2134
|
+
}
|
|
2135
|
+
const workspaceRoot = findWorkspaceRootFromRuntimeDir(config.runtimePackageDir);
|
|
2136
|
+
const packageCandidates = [
|
|
2137
|
+
join5(config.runtimePackageDir, "bin", "scoutd"),
|
|
2138
|
+
join5(config.runtimePackageDir, "native", "scoutd"),
|
|
2139
|
+
join5(config.runtimePackageDir, "scoutd"),
|
|
2140
|
+
workspaceRoot ? join5(workspaceRoot, "packages", "cli", "bin", "scoutd") : null,
|
|
2141
|
+
workspaceRoot ? join5(workspaceRoot, "packages", "runtime", "bin", "scoutd") : null,
|
|
2142
|
+
join5(config.runtimeDirectory, "scoutd"),
|
|
2143
|
+
join5(dirname3(config.runtimePackageDir), "scout", "bin", "scoutd")
|
|
2144
|
+
];
|
|
2145
|
+
for (const candidate of packageCandidates) {
|
|
2146
|
+
const resolved = executableCandidate(candidate);
|
|
2147
|
+
if (resolved) {
|
|
2148
|
+
return { path: resolved, source: "package" };
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
const fromPath = resolveExecutableName("scoutd");
|
|
2152
|
+
if (fromPath) {
|
|
2153
|
+
return { path: fromPath, source: "path" };
|
|
2154
|
+
}
|
|
2155
|
+
if (workspaceRoot && workspaceScoutdAllowed()) {
|
|
2156
|
+
for (const candidate of [
|
|
2157
|
+
join5(workspaceRoot, "target", "release", "scoutd"),
|
|
2158
|
+
join5(workspaceRoot, "target", "debug", "scoutd")
|
|
2159
|
+
]) {
|
|
2160
|
+
const resolved = executableCandidate(candidate);
|
|
2161
|
+
if (resolved) {
|
|
2162
|
+
return { path: resolved, source: "workspace" };
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
return null;
|
|
2167
|
+
}
|
|
2168
|
+
function nativeServiceEnvironment(config, scoutdPath) {
|
|
2169
|
+
const env = {
|
|
2170
|
+
...process.env,
|
|
2171
|
+
OPENSCOUT_SCOUTD_BIN: scoutdPath,
|
|
2172
|
+
OPENSCOUT_RUNTIME_PACKAGE_DIR: config.runtimePackageDir,
|
|
2173
|
+
OPENSCOUT_SUPPORT_DIRECTORY: config.supportDirectory,
|
|
2174
|
+
OPENSCOUT_CONTROL_HOME: config.controlHome,
|
|
516
2175
|
OPENSCOUT_BROKER_HOST: config.brokerHost,
|
|
517
2176
|
OPENSCOUT_BROKER_PORT: String(config.brokerPort),
|
|
518
2177
|
OPENSCOUT_BROKER_URL: config.brokerUrl,
|
|
519
2178
|
OPENSCOUT_BROKER_SOCKET_PATH: config.brokerSocketPath,
|
|
520
|
-
OPENSCOUT_CONTROL_HOME: config.controlHome,
|
|
521
2179
|
OPENSCOUT_BROKER_SERVICE_MODE: config.mode,
|
|
522
2180
|
OPENSCOUT_BROKER_SERVICE_LABEL: config.label,
|
|
523
2181
|
OPENSCOUT_SERVICE_LABEL: config.label,
|
|
524
|
-
OPENSCOUT_ADVERTISE_SCOPE: config.advertiseScope
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
2182
|
+
OPENSCOUT_ADVERTISE_SCOPE: config.advertiseScope
|
|
2183
|
+
};
|
|
2184
|
+
if (config.bunExecutable) {
|
|
2185
|
+
env.OPENSCOUT_BUN_BIN = config.bunExecutable;
|
|
2186
|
+
}
|
|
2187
|
+
if (config.coreAgents.length > 0) {
|
|
2188
|
+
env.OPENSCOUT_CORE_AGENTS = config.coreAgents.join(",");
|
|
2189
|
+
}
|
|
2190
|
+
return env;
|
|
2191
|
+
}
|
|
2192
|
+
function isRecord3(value) {
|
|
2193
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2194
|
+
}
|
|
2195
|
+
function readString2(value) {
|
|
2196
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
2197
|
+
}
|
|
2198
|
+
function readBoolean(value) {
|
|
2199
|
+
return typeof value === "boolean" ? value : undefined;
|
|
2200
|
+
}
|
|
2201
|
+
function readNumber2(value) {
|
|
2202
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
2203
|
+
}
|
|
2204
|
+
function readHealthTransport(value) {
|
|
2205
|
+
return value === "unix_socket" || value === "http" || value === "in_process" ? value : undefined;
|
|
2206
|
+
}
|
|
2207
|
+
function normalizeNativeServiceStatus(input, config) {
|
|
2208
|
+
const healthRecord = isRecord3(input.health) ? input.health : {};
|
|
2209
|
+
const healthReachable = readBoolean(healthRecord.reachable) ?? readBoolean(input.reachable) ?? false;
|
|
2210
|
+
const healthOk = readBoolean(healthRecord.ok) ?? (typeof input.health === "boolean" ? input.health : undefined) ?? false;
|
|
2211
|
+
const healthError = readString2(healthRecord.error) ?? readString2(input.healthError);
|
|
2212
|
+
const healthTransport = readHealthTransport(healthRecord.transport) ?? readHealthTransport(input.healthTransport);
|
|
2213
|
+
const healthNodeId = readString2(healthRecord.nodeId);
|
|
2214
|
+
const healthMeshId = readString2(healthRecord.meshId);
|
|
2215
|
+
const healthSocketFallbackError = readString2(healthRecord.socketFallbackError);
|
|
2216
|
+
const healthCounts = isRecord3(healthRecord.counts) ? healthRecord.counts : undefined;
|
|
2217
|
+
const installed = readBoolean(input.installed) ?? existsSync5(config.launchAgentPath);
|
|
2218
|
+
const loaded = readBoolean(input.loaded) ?? false;
|
|
2219
|
+
const stdoutLogPath = readString2(input.stdoutLogPath) ?? config.stdoutLogPath;
|
|
2220
|
+
const stderrLogPath = readString2(input.stderrLogPath) ?? config.stderrLogPath;
|
|
2221
|
+
const lastLogLine = readString2(input.lastLogLine) ?? (healthReachable ? readLastLogLine([stdoutLogPath, stderrLogPath]) : readLastLogLine([stderrLogPath, stdoutLogPath]));
|
|
2222
|
+
return {
|
|
2223
|
+
label: readString2(input.label) ?? config.label,
|
|
2224
|
+
mode: readString2(input.mode) ?? config.mode,
|
|
2225
|
+
launchAgentPath: readString2(input.launchAgentPath) ?? config.launchAgentPath,
|
|
2226
|
+
bootoutCommand: readString2(input.bootoutCommand) ?? bootoutCommand(config),
|
|
2227
|
+
brokerUrl: readString2(input.brokerUrl) ?? config.brokerUrl,
|
|
2228
|
+
brokerSocketPath: readString2(input.brokerSocketPath) ?? config.brokerSocketPath,
|
|
2229
|
+
supportDirectory: readString2(input.supportDirectory) ?? config.supportDirectory,
|
|
2230
|
+
runtimeDirectory: readString2(input.runtimeDirectory) ?? config.runtimeDirectory,
|
|
2231
|
+
controlHome: readString2(input.controlHome) ?? config.controlHome,
|
|
2232
|
+
stdoutLogPath,
|
|
2233
|
+
stderrLogPath,
|
|
2234
|
+
installed,
|
|
2235
|
+
loaded,
|
|
2236
|
+
pid: readNumber2(input.pid) ?? null,
|
|
2237
|
+
launchdState: readString2(input.launchdState) ?? null,
|
|
2238
|
+
lastExitStatus: readNumber2(input.lastExitStatus) ?? null,
|
|
2239
|
+
usesLaunchAgent: readBoolean(input.usesLaunchAgent) ?? (installed || loaded),
|
|
2240
|
+
reachable: healthReachable,
|
|
2241
|
+
serviceAdapter: "macos-scoutd",
|
|
2242
|
+
health: {
|
|
2243
|
+
reachable: healthReachable,
|
|
2244
|
+
ok: healthOk,
|
|
2245
|
+
checkedAt: readNumber2(healthRecord.checkedAt) ?? Date.now(),
|
|
2246
|
+
transport: healthTransport,
|
|
2247
|
+
socketPath: config.brokerSocketPath,
|
|
2248
|
+
...healthSocketFallbackError ? { socketFallbackError: healthSocketFallbackError } : {},
|
|
2249
|
+
...healthNodeId ? { nodeId: healthNodeId } : {},
|
|
2250
|
+
...healthMeshId ? { meshId: healthMeshId } : {},
|
|
2251
|
+
...healthCounts ? { counts: healthCounts } : {},
|
|
2252
|
+
...isRecord3(healthRecord.build) ? { build: healthRecord.build } : {},
|
|
2253
|
+
...isRecord3(healthRecord.services) ? { services: healthRecord.services } : {},
|
|
2254
|
+
error: healthError
|
|
2255
|
+
},
|
|
2256
|
+
lastLogLine
|
|
538
2257
|
};
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
2258
|
+
}
|
|
2259
|
+
var SCOUTD_MAX_BUFFER = 2 * 1024 * 1024;
|
|
2260
|
+
var SCOUTD_DEFAULT_TIMEOUT_MS = 45000;
|
|
2261
|
+
var SCOUTD_KILL_GRACE_MS = 250;
|
|
2262
|
+
function spawnScoutdJson(scoutdPath, command, env, timeoutMs) {
|
|
2263
|
+
return new Promise((resolvePromise, reject) => {
|
|
2264
|
+
const child = spawn2(scoutdPath, [command, "--json"], {
|
|
2265
|
+
env,
|
|
2266
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2267
|
+
});
|
|
2268
|
+
let stdout = "";
|
|
2269
|
+
let stderr = "";
|
|
2270
|
+
let settled = false;
|
|
2271
|
+
let timedOut = false;
|
|
2272
|
+
const killTimer = setTimeout(() => {
|
|
2273
|
+
timedOut = true;
|
|
2274
|
+
terminate();
|
|
2275
|
+
fail(new Error(`scoutd ${command} timed out after ${timeoutMs}ms`));
|
|
2276
|
+
}, timeoutMs);
|
|
2277
|
+
killTimer.unref?.();
|
|
2278
|
+
function terminate() {
|
|
2279
|
+
child.kill("SIGTERM");
|
|
2280
|
+
const hardKillTimer = setTimeout(() => child.kill("SIGKILL"), SCOUTD_KILL_GRACE_MS);
|
|
2281
|
+
hardKillTimer.unref?.();
|
|
2282
|
+
}
|
|
2283
|
+
function cleanup() {
|
|
2284
|
+
clearTimeout(killTimer);
|
|
2285
|
+
}
|
|
2286
|
+
function fail(error) {
|
|
2287
|
+
if (settled)
|
|
2288
|
+
return;
|
|
2289
|
+
settled = true;
|
|
2290
|
+
cleanup();
|
|
2291
|
+
reject(error);
|
|
2292
|
+
}
|
|
2293
|
+
function succeed(output) {
|
|
2294
|
+
if (settled)
|
|
2295
|
+
return;
|
|
2296
|
+
settled = true;
|
|
2297
|
+
cleanup();
|
|
2298
|
+
resolvePromise(output);
|
|
2299
|
+
}
|
|
2300
|
+
function append(kind, chunk) {
|
|
2301
|
+
const text = typeof chunk === "string" ? chunk : String(chunk);
|
|
2302
|
+
if (kind === "stdout")
|
|
2303
|
+
stdout += text;
|
|
2304
|
+
else
|
|
2305
|
+
stderr += text;
|
|
2306
|
+
if (stdout.length + stderr.length > SCOUTD_MAX_BUFFER) {
|
|
2307
|
+
terminate();
|
|
2308
|
+
fail(new Error(`scoutd ${command} exceeded output limit`));
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
child.stdout.setEncoding("utf8");
|
|
2312
|
+
child.stderr.setEncoding("utf8");
|
|
2313
|
+
child.stdout.on("data", (chunk) => append("stdout", chunk));
|
|
2314
|
+
child.stderr.on("data", (chunk) => append("stderr", chunk));
|
|
2315
|
+
child.on("error", (error) => fail(new Error(`scoutd ${command} failed: ${error.message}`)));
|
|
2316
|
+
child.on("close", (code, signal) => {
|
|
2317
|
+
if (settled || timedOut)
|
|
2318
|
+
return;
|
|
2319
|
+
const trimmedStdout = stdout.trim();
|
|
2320
|
+
const trimmedStderr = stderr.trim();
|
|
2321
|
+
if ((code ?? 1) !== 0) {
|
|
2322
|
+
const detail = trimmedStderr || trimmedStdout || `exit ${signal ?? code ?? "unknown status"}`;
|
|
2323
|
+
fail(new Error(`scoutd ${command} failed: ${detail}`));
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
succeed(trimmedStdout);
|
|
2327
|
+
});
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
async function runScoutdServiceCommand(command, config, timeoutMs = SCOUTD_DEFAULT_TIMEOUT_MS, runScoutdJson = spawnScoutdJson) {
|
|
2331
|
+
const scoutd = resolveScoutdCommand(config);
|
|
2332
|
+
if (!scoutd) {
|
|
2333
|
+
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.");
|
|
2334
|
+
}
|
|
2335
|
+
const stdout = await runScoutdJson(scoutd.path, command, nativeServiceEnvironment(config, scoutd.path), timeoutMs);
|
|
2336
|
+
let parsed;
|
|
2337
|
+
try {
|
|
2338
|
+
parsed = JSON.parse(stdout);
|
|
2339
|
+
} catch {
|
|
2340
|
+
throw new Error(`scoutd ${command} returned non-JSON stdout: ${stdout.slice(0, 400)}`);
|
|
2341
|
+
}
|
|
2342
|
+
return normalizeNativeServiceStatus(parsed, config);
|
|
576
2343
|
}
|
|
577
2344
|
function readCoreAgentsSync() {
|
|
578
2345
|
try {
|
|
579
2346
|
const settingsPath = resolveOpenScoutSupportPaths().settingsPath;
|
|
580
|
-
const raw =
|
|
2347
|
+
const raw = readFileSync4(settingsPath, "utf8");
|
|
581
2348
|
const settings = JSON.parse(raw);
|
|
582
2349
|
const raw_agents = settings?.agents?.coreAgents;
|
|
583
2350
|
if (Array.isArray(raw_agents)) {
|
|
@@ -586,88 +2353,14 @@ function readCoreAgentsSync() {
|
|
|
586
2353
|
} catch {}
|
|
587
2354
|
return [];
|
|
588
2355
|
}
|
|
589
|
-
function collectOptionalEnvVars(keys) {
|
|
590
|
-
const entries = {};
|
|
591
|
-
for (const key of keys) {
|
|
592
|
-
const value = process.env[key];
|
|
593
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
594
|
-
entries[key] = value;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
return entries;
|
|
598
|
-
}
|
|
599
|
-
function resolveLaunchAgentPATH() {
|
|
600
|
-
const entries = [
|
|
601
|
-
join3(homedir3(), ".bun", "bin"),
|
|
602
|
-
...(process.env.PATH ?? "").split(":").filter(Boolean),
|
|
603
|
-
"/opt/homebrew/bin",
|
|
604
|
-
"/usr/local/bin",
|
|
605
|
-
"/usr/bin",
|
|
606
|
-
"/bin",
|
|
607
|
-
"/usr/sbin",
|
|
608
|
-
"/sbin"
|
|
609
|
-
];
|
|
610
|
-
return Array.from(new Set(entries)).filter((e) => !isTmpPath(e)).join(":");
|
|
611
|
-
}
|
|
612
|
-
function xmlEscape(value) {
|
|
613
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
614
|
-
}
|
|
615
|
-
function ensureParentDirectory(filePath) {
|
|
616
|
-
mkdirSync2(dirname2(filePath), { recursive: true });
|
|
617
|
-
}
|
|
618
|
-
function ensureServiceDirectories(config) {
|
|
619
|
-
ensureOpenScoutCleanSlateSync();
|
|
620
|
-
mkdirSync2(config.supportDirectory, { recursive: true });
|
|
621
|
-
mkdirSync2(config.runtimeDirectory, { recursive: true, mode: 448 });
|
|
622
|
-
chmodSync(config.runtimeDirectory, 448);
|
|
623
|
-
mkdirSync2(config.logsDirectory, { recursive: true });
|
|
624
|
-
mkdirSync2(config.controlHome, { recursive: true });
|
|
625
|
-
ensureParentDirectory(config.launchAgentPath);
|
|
626
|
-
}
|
|
627
|
-
function writeLaunchAgentPlist(config) {
|
|
628
|
-
ensureServiceDirectories(config);
|
|
629
|
-
writeFileSync2(config.launchAgentPath, renderLaunchAgentPlist(config), "utf8");
|
|
630
|
-
}
|
|
631
2356
|
function bootoutCommand(config) {
|
|
632
|
-
return
|
|
633
|
-
}
|
|
634
|
-
function legacyLaunchAgentPath(config) {
|
|
635
|
-
return join3(homedir3(), "Library", "LaunchAgents", `${legacyBrokerServiceLabel(config.mode)}.plist`);
|
|
636
|
-
}
|
|
637
|
-
function legacyServiceTarget(config) {
|
|
638
|
-
return `${config.domainTarget}/${legacyBrokerServiceLabel(config.mode)}`;
|
|
639
|
-
}
|
|
640
|
-
function bootoutLegacyBrokerService(config) {
|
|
641
|
-
const legacyTarget = legacyServiceTarget(config);
|
|
642
|
-
if (legacyTarget === config.serviceTarget) {
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
runCommand(launchctlPath(), ["bootout", legacyTarget], { allowFailure: true });
|
|
646
|
-
}
|
|
647
|
-
function runCommand(command, args, options) {
|
|
648
|
-
const result = spawnSync(command, args, {
|
|
649
|
-
env: {
|
|
650
|
-
...process.env,
|
|
651
|
-
...options?.env ?? {}
|
|
652
|
-
},
|
|
653
|
-
encoding: "utf8"
|
|
654
|
-
});
|
|
655
|
-
const stdout = (result.stdout ?? "").trim();
|
|
656
|
-
const stderr = (result.stderr ?? "").trim();
|
|
657
|
-
const exitCode = result.status ?? 1;
|
|
658
|
-
if (exitCode !== 0 && !options?.allowFailure) {
|
|
659
|
-
throw new Error(stderr || stdout || `${command} exited with status ${exitCode}`);
|
|
660
|
-
}
|
|
661
|
-
return { exitCode, stdout, stderr };
|
|
662
|
-
}
|
|
663
|
-
function launchctlPath() {
|
|
664
|
-
return "/bin/launchctl";
|
|
2357
|
+
return `/bin/launchctl bootout ${config.serviceTarget}`;
|
|
665
2358
|
}
|
|
666
2359
|
function readLogLines(path) {
|
|
667
|
-
if (!
|
|
2360
|
+
if (!existsSync5(path)) {
|
|
668
2361
|
return [];
|
|
669
2362
|
}
|
|
670
|
-
return
|
|
2363
|
+
return readFileSync4(path, "utf8").split(`
|
|
671
2364
|
`).map((line) => line.trim()).filter(Boolean);
|
|
672
2365
|
}
|
|
673
2366
|
function isPackageScriptBanner(line) {
|
|
@@ -700,164 +2393,120 @@ function readLastLogLine(paths) {
|
|
|
700
2393
|
}
|
|
701
2394
|
return fallback;
|
|
702
2395
|
}
|
|
703
|
-
function
|
|
704
|
-
const
|
|
705
|
-
const stateMatch = output.match(/\bstate = ([^\n]+)/);
|
|
706
|
-
const lastExitMatch = output.match(/\blast exit code = (-?\d+)/i) || output.match(/\blast exit status = (-?\d+)/i);
|
|
2396
|
+
async function readHeadlessBrokerHealth(config) {
|
|
2397
|
+
const result = await requestScoutBrokerJsonWithTrace(config.brokerUrl, "/health", { socketPath: config.brokerSocketPath });
|
|
707
2398
|
return {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
lastExitStatus: lastExitMatch ? Number.parseInt(lastExitMatch[1] ?? "0", 10) : null
|
|
2399
|
+
health: result.value,
|
|
2400
|
+
trace: result.trace
|
|
711
2401
|
};
|
|
712
2402
|
}
|
|
713
|
-
function
|
|
714
|
-
|
|
715
|
-
if (printResult.exitCode !== 0) {
|
|
716
|
-
return {
|
|
717
|
-
loaded: false,
|
|
718
|
-
pid: null,
|
|
719
|
-
launchdState: null,
|
|
720
|
-
lastExitStatus: null,
|
|
721
|
-
raw: printResult.stderr || printResult.stdout
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
return {
|
|
725
|
-
loaded: true,
|
|
726
|
-
raw: printResult.stdout,
|
|
727
|
-
...parseLaunchctlPrint(printResult.stdout)
|
|
728
|
-
};
|
|
2403
|
+
function headlessLifecycleError(command) {
|
|
2404
|
+
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.`);
|
|
729
2405
|
}
|
|
730
|
-
async function
|
|
731
|
-
|
|
732
|
-
|
|
2406
|
+
async function runHeadlessForegroundServiceCommand(command, config, readHealth = readHeadlessBrokerHealth) {
|
|
2407
|
+
if (command !== "status") {
|
|
2408
|
+
throw headlessLifecycleError(command);
|
|
2409
|
+
}
|
|
733
2410
|
const checkedAt = Date.now();
|
|
734
2411
|
try {
|
|
735
|
-
const {
|
|
736
|
-
|
|
737
|
-
socketPath: config.brokerSocketPath
|
|
738
|
-
});
|
|
2412
|
+
const { health, trace } = await readHealth(config);
|
|
2413
|
+
const reachable = true;
|
|
739
2414
|
return {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
2415
|
+
serviceAdapter: "headless-foreground",
|
|
2416
|
+
label: config.label,
|
|
2417
|
+
mode: config.mode,
|
|
2418
|
+
launchAgentPath: config.launchAgentPath,
|
|
2419
|
+
bootoutCommand: "headless-foreground does not use launchd",
|
|
2420
|
+
brokerUrl: config.brokerUrl,
|
|
2421
|
+
brokerSocketPath: config.brokerSocketPath,
|
|
2422
|
+
supportDirectory: config.supportDirectory,
|
|
2423
|
+
runtimeDirectory: config.runtimeDirectory,
|
|
2424
|
+
controlHome: config.controlHome,
|
|
2425
|
+
stdoutLogPath: config.stdoutLogPath,
|
|
2426
|
+
stderrLogPath: config.stderrLogPath,
|
|
2427
|
+
installed: false,
|
|
2428
|
+
loaded: reachable,
|
|
2429
|
+
pid: null,
|
|
2430
|
+
launchdState: null,
|
|
2431
|
+
lastExitStatus: null,
|
|
2432
|
+
usesLaunchAgent: false,
|
|
2433
|
+
reachable,
|
|
2434
|
+
health: {
|
|
2435
|
+
reachable,
|
|
2436
|
+
ok: Boolean(health.ok),
|
|
2437
|
+
checkedAt,
|
|
2438
|
+
transport: trace.transport,
|
|
2439
|
+
socketPath: trace.socketPath,
|
|
2440
|
+
socketFallbackError: trace.socketFallbackError,
|
|
2441
|
+
nodeId: health.nodeId ?? undefined,
|
|
2442
|
+
meshId: health.meshId ?? undefined,
|
|
2443
|
+
counts: health.counts ?? undefined,
|
|
2444
|
+
build: health.build,
|
|
2445
|
+
services: health.services
|
|
2446
|
+
},
|
|
2447
|
+
lastLogLine: readLastLogLine([config.stderrLogPath, config.stdoutLogPath])
|
|
768
2448
|
};
|
|
769
2449
|
} catch (error) {
|
|
770
2450
|
return {
|
|
2451
|
+
serviceAdapter: "headless-foreground",
|
|
2452
|
+
label: config.label,
|
|
2453
|
+
mode: config.mode,
|
|
2454
|
+
launchAgentPath: config.launchAgentPath,
|
|
2455
|
+
bootoutCommand: "headless-foreground does not use launchd",
|
|
2456
|
+
brokerUrl: config.brokerUrl,
|
|
2457
|
+
brokerSocketPath: config.brokerSocketPath,
|
|
2458
|
+
supportDirectory: config.supportDirectory,
|
|
2459
|
+
runtimeDirectory: config.runtimeDirectory,
|
|
2460
|
+
controlHome: config.controlHome,
|
|
2461
|
+
stdoutLogPath: config.stdoutLogPath,
|
|
2462
|
+
stderrLogPath: config.stderrLogPath,
|
|
2463
|
+
installed: false,
|
|
2464
|
+
loaded: false,
|
|
2465
|
+
pid: null,
|
|
2466
|
+
launchdState: null,
|
|
2467
|
+
lastExitStatus: null,
|
|
2468
|
+
usesLaunchAgent: false,
|
|
771
2469
|
reachable: false,
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
2470
|
+
health: {
|
|
2471
|
+
reachable: false,
|
|
2472
|
+
ok: false,
|
|
2473
|
+
checkedAt,
|
|
2474
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2475
|
+
},
|
|
2476
|
+
lastLogLine: readLastLogLine([config.stderrLogPath, config.stdoutLogPath])
|
|
776
2477
|
};
|
|
777
|
-
}
|
|
778
|
-
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
async function runBrokerServiceCommand(command, config) {
|
|
2481
|
+
const adapter = resolveBrokerServiceAdapter();
|
|
2482
|
+
switch (adapter) {
|
|
2483
|
+
case "macos-scoutd":
|
|
2484
|
+
return await runScoutdServiceCommand(command, config);
|
|
2485
|
+
case "headless-foreground":
|
|
2486
|
+
return await runHeadlessForegroundServiceCommand(command, config);
|
|
2487
|
+
case "linux-systemd-user":
|
|
2488
|
+
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.");
|
|
2489
|
+
case "windows-service":
|
|
2490
|
+
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.");
|
|
779
2491
|
}
|
|
780
2492
|
}
|
|
781
2493
|
async function brokerServiceStatus(config = resolveBrokerServiceConfig()) {
|
|
782
|
-
|
|
783
|
-
const launchctl = inspectLaunchctl(config);
|
|
784
|
-
const health = await fetchHealthSnapshot(config);
|
|
785
|
-
const installed = existsSync3(config.launchAgentPath);
|
|
786
|
-
const lastLogLine = health.reachable ? readLastLogLine([config.stdoutLogPath, config.stderrLogPath]) : readLastLogLine([config.stderrLogPath, config.stdoutLogPath]);
|
|
787
|
-
return {
|
|
788
|
-
label: config.label,
|
|
789
|
-
mode: config.mode,
|
|
790
|
-
launchAgentPath: config.launchAgentPath,
|
|
791
|
-
bootoutCommand: bootoutCommand(config),
|
|
792
|
-
brokerUrl: config.brokerUrl,
|
|
793
|
-
brokerSocketPath: config.brokerSocketPath,
|
|
794
|
-
supportDirectory: config.supportDirectory,
|
|
795
|
-
runtimeDirectory: config.runtimeDirectory,
|
|
796
|
-
controlHome: config.controlHome,
|
|
797
|
-
stdoutLogPath: config.stdoutLogPath,
|
|
798
|
-
stderrLogPath: config.stderrLogPath,
|
|
799
|
-
installed,
|
|
800
|
-
loaded: launchctl.loaded,
|
|
801
|
-
pid: launchctl.pid,
|
|
802
|
-
launchdState: launchctl.launchdState,
|
|
803
|
-
lastExitStatus: launchctl.lastExitStatus,
|
|
804
|
-
usesLaunchAgent: installed || launchctl.loaded,
|
|
805
|
-
reachable: health.reachable,
|
|
806
|
-
health,
|
|
807
|
-
lastLogLine
|
|
808
|
-
};
|
|
2494
|
+
return runBrokerServiceCommand("status", config);
|
|
809
2495
|
}
|
|
810
2496
|
async function installBrokerService(config = resolveBrokerServiceConfig()) {
|
|
811
|
-
|
|
812
|
-
writeLaunchAgentPlist(config);
|
|
813
|
-
return brokerServiceStatus(config);
|
|
2497
|
+
return runBrokerServiceCommand("install", config);
|
|
814
2498
|
}
|
|
815
2499
|
async function startBrokerService(config = resolveBrokerServiceConfig()) {
|
|
816
|
-
|
|
817
|
-
writeLaunchAgentPlist(config);
|
|
818
|
-
runCommand(launchctlPath(), ["bootout", config.serviceTarget], { allowFailure: true });
|
|
819
|
-
runCommand(launchctlPath(), ["bootstrap", config.domainTarget, config.launchAgentPath], { allowFailure: true });
|
|
820
|
-
runCommand(launchctlPath(), ["kickstart", "-k", config.serviceTarget], { allowFailure: true });
|
|
821
|
-
const attempts = Math.ceil(resolveBrokerStartTimeoutMs() / BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
822
|
-
for (let attempt = 0;attempt < attempts; attempt += 1) {
|
|
823
|
-
const status2 = await brokerServiceStatus(config);
|
|
824
|
-
if (status2.health.reachable) {
|
|
825
|
-
return status2;
|
|
826
|
-
}
|
|
827
|
-
await sleep(BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
828
|
-
}
|
|
829
|
-
const status = await brokerServiceStatus(config);
|
|
830
|
-
throw new Error(status.lastLogLine ?? status.health.error ?? "Broker service did not become healthy.");
|
|
831
|
-
}
|
|
832
|
-
function sleep(ms) {
|
|
833
|
-
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
2500
|
+
return runBrokerServiceCommand("start", config);
|
|
834
2501
|
}
|
|
835
2502
|
async function stopBrokerService(config = resolveBrokerServiceConfig()) {
|
|
836
|
-
|
|
837
|
-
for (let attempt = 0;attempt < 30; attempt += 1) {
|
|
838
|
-
const status = await brokerServiceStatus(config);
|
|
839
|
-
if (!status.health.reachable) {
|
|
840
|
-
return status;
|
|
841
|
-
}
|
|
842
|
-
await sleep(BROKER_SERVICE_POLL_INTERVAL_MS);
|
|
843
|
-
}
|
|
844
|
-
return brokerServiceStatus(config);
|
|
2503
|
+
return runBrokerServiceCommand("stop", config);
|
|
845
2504
|
}
|
|
846
2505
|
async function restartBrokerService(config = resolveBrokerServiceConfig()) {
|
|
847
|
-
|
|
848
|
-
return startBrokerService(config);
|
|
2506
|
+
return runBrokerServiceCommand("restart", config);
|
|
849
2507
|
}
|
|
850
2508
|
async function uninstallBrokerService(config = resolveBrokerServiceConfig()) {
|
|
851
|
-
|
|
852
|
-
if (existsSync3(config.launchAgentPath)) {
|
|
853
|
-
rmSync2(config.launchAgentPath, { force: true });
|
|
854
|
-
}
|
|
855
|
-
const legacyPath = legacyLaunchAgentPath(config);
|
|
856
|
-
bootoutLegacyBrokerService(config);
|
|
857
|
-
if (existsSync3(legacyPath)) {
|
|
858
|
-
rmSync2(legacyPath, { force: true });
|
|
859
|
-
}
|
|
860
|
-
return brokerServiceStatus(config);
|
|
2509
|
+
return runBrokerServiceCommand("uninstall", config);
|
|
861
2510
|
}
|
|
862
2511
|
async function main() {
|
|
863
2512
|
const command = process.argv[2] ?? "status";
|
|
@@ -895,6 +2544,7 @@ async function main() {
|
|
|
895
2544
|
}
|
|
896
2545
|
function formatBrokerServiceStatus(status) {
|
|
897
2546
|
const lines = [
|
|
2547
|
+
`service adapter: ${status.serviceAdapter ?? "unknown"}`,
|
|
898
2548
|
`label: ${status.label}`,
|
|
899
2549
|
`mode: ${status.mode}`,
|
|
900
2550
|
`launch agent: ${status.installed ? status.launchAgentPath : "not installed"}`,
|
|
@@ -929,15 +2579,21 @@ export {
|
|
|
929
2579
|
stopBrokerService,
|
|
930
2580
|
startBrokerService,
|
|
931
2581
|
selectLastRelevantLogLine,
|
|
2582
|
+
runScoutdServiceCommand,
|
|
2583
|
+
runHeadlessForegroundServiceCommand,
|
|
932
2584
|
restartBrokerService,
|
|
2585
|
+
resolveScoutdCommand,
|
|
2586
|
+
resolveScoutBrokerControlUrl,
|
|
2587
|
+
resolveBundledRuntimeDirFromModuleDir,
|
|
2588
|
+
resolveBrokerUrl,
|
|
933
2589
|
resolveBrokerSocketPathForBaseUrl,
|
|
934
2590
|
resolveBrokerServiceConfig,
|
|
2591
|
+
resolveBrokerServiceAdapter,
|
|
935
2592
|
resolveBrokerHost,
|
|
936
2593
|
resolveAdvertiseScope,
|
|
937
|
-
renderLaunchAgentPlist,
|
|
938
|
-
parseLaunchctlPrint,
|
|
939
2594
|
isLoopbackHost,
|
|
940
2595
|
installBrokerService,
|
|
2596
|
+
buildLocalBrokerControlUrl,
|
|
941
2597
|
buildDefaultBrokerUrl,
|
|
942
2598
|
buildDefaultBrokerSocketPath,
|
|
943
2599
|
brokerServiceStatus,
|