@rynx-ai/daemon 0.1.9 → 0.1.10-beta.2
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/dist/app-browser-host-supervisor.d.ts +97 -0
- package/dist/app-browser-host-supervisor.js +529 -0
- package/dist/browser-artifact-management.d.ts +20 -0
- package/dist/browser-artifact-management.js +61 -0
- package/dist/chrome-for-testing-store.js +1 -1
- package/dist/chrome-inspection-gateway.d.ts +58 -0
- package/dist/chrome-inspection-gateway.js +806 -0
- package/dist/chrome-inspection-manager.d.ts +94 -0
- package/dist/chrome-inspection-manager.js +573 -0
- package/dist/cli.js +13 -2
- package/dist/control-client.d.ts +9 -0
- package/dist/control-client.js +63 -0
- package/dist/daemon-build-status.d.ts +8 -0
- package/dist/daemon-build-status.js +18 -0
- package/dist/daemon-server.d.ts +19 -2
- package/dist/daemon-server.js +615 -59
- package/dist/db.js +56 -0
- package/dist/desktop-browser-host-client.d.ts +2 -1
- package/dist/desktop-browser-host-client.js +3 -1
- package/dist/direct-runtime-authenticator.js +11 -6
- package/dist/headless-browser-host.js +18 -1
- package/dist/index-daemon.js +28 -1
- package/dist/maintenance-management.d.ts +11 -0
- package/dist/maintenance-management.js +13 -0
- package/dist/plugin-installer.d.ts +35 -0
- package/dist/plugin-installer.js +195 -94
- package/dist/plugin-management-service.d.ts +58 -0
- package/dist/plugin-management-service.js +240 -0
- package/dist/plugin-package.d.ts +26 -3
- package/dist/plugin-package.js +235 -56
- package/dist/pm2.js +37 -5
- package/dist/remote-runtime-access-store.d.ts +1 -0
- package/dist/remote-runtime-access-store.js +7 -0
- package/dist/remote-runtime-admin.d.ts +3 -0
- package/dist/remote-runtime-admin.js +3 -0
- package/dist/remote-runtime-connection-manager.d.ts +12 -1
- package/dist/remote-runtime-connection-manager.js +170 -4
- package/dist/remote-runtime-target-control.d.ts +5 -1
- package/dist/remote-runtime-target-control.js +62 -7
- package/dist/remote-runtime-target-store.d.ts +4 -1
- package/dist/remote-runtime-target-store.js +21 -2
- package/dist/session-log-store.js +29 -0
- package/dist/session-meta-store.js +3 -1
- package/dist/session-pending-message-store.d.ts +2 -0
- package/dist/session-pending-message-store.js +41 -0
- package/dist/session-resource-store.d.ts +32 -0
- package/dist/session-resource-store.js +700 -0
- package/dist/setup.d.ts +16 -0
- package/dist/setup.js +136 -2
- package/package.json +14 -9
package/dist/daemon-server.js
CHANGED
|
@@ -7,15 +7,24 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
9
9
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
10
12
|
import { dirname, join } from "node:path";
|
|
13
|
+
import WebSocket from "ws";
|
|
11
14
|
import { loadConfig, rynxHome } from "@rynx-ai/core";
|
|
15
|
+
import { LOCAL_MANAGEMENT_PROTOCOL, } from "@rynx-ai/protocol/control";
|
|
12
16
|
import { REMOTE_RUNTIME_CORE_PROTOCOL } from "@rynx-ai/protocol/remote-runtime";
|
|
17
|
+
import { RUNTIME_BROWSER_INSPECT_MAX_MESSAGE_BYTES, RUNTIME_BROWSER_INSPECT_SEMANTIC_CAPABILITY, } from "@rynx-ai/protocol/runtime-browser-inspect";
|
|
13
18
|
import { RUNTIME_BROWSER_SURFACE_SEMANTIC_CAPABILITY } from "@rynx-ai/protocol/runtime-browser-surface";
|
|
14
19
|
import { RUNTIME_EMULATOR_SURFACE_SEMANTIC_CAPABILITY } from "@rynx-ai/protocol/runtime-emulator-surface";
|
|
15
|
-
import { createDaemonRuntimeHost, createDesktopFirstSessionBrowserHost, createRunnerSessionTerminalHost, createSessionRuntimeServices, DesktopBrowserHostRegistry, DirectRuntimeConnectionHub, MachineSessionService, RemoteRuntimeDispatcher, SessionBrowserService, SessionBrowserSurfaceCoordinator, SessionEmulatorService, startServer, startDirectRuntimeServer, } from "@rynx-ai/server";
|
|
20
|
+
import { createDaemonRuntimeHost, createDesktopFirstSessionBrowserHost, createRunnerSessionTerminalHost, createSessionRuntimeServices, DesktopBrowserHostAbsentError, DesktopBrowserHostRegistry, DirectRuntimeConnectionHub, DirectRuntimeBrowserInspectHostError, MachineSessionService, RemoteRuntimeDispatcher, SessionBrowserService, SessionBrowserServiceError, SessionBrowserSurfaceCoordinator, SessionEmulatorService, startServer, startDirectRuntimeServer, } from "@rynx-ai/server";
|
|
21
|
+
import { APP_BROWSER_HOST_BOOTSTRAP_TYPE, AppBrowserHostSupervisor, parseAppBrowserHostLaunchConfig, } from "./app-browser-host-supervisor.js";
|
|
16
22
|
import { buildControlDeps } from "./control-deps.js";
|
|
23
|
+
import { createBrowserArtifactManagementService } from "./browser-artifact-management.js";
|
|
24
|
+
import { createDaemonBuildStatus } from "./daemon-build-status.js";
|
|
17
25
|
import { BrowserRunnerSessionContextProvider } from "./browser-runner-session-context.js";
|
|
18
26
|
import { isChromeForTestingHostSupported } from "./chrome-for-testing-store.js";
|
|
27
|
+
import { ChromeInspectionManager, } from "./chrome-inspection-manager.js";
|
|
19
28
|
import { createHeadlessBrowserHost } from "./headless-browser-host.js";
|
|
20
29
|
import { acquireDaemonOwner } from "./daemon-owner.js";
|
|
21
30
|
import { DaemonDirectRuntimeAuthenticator } from "./direct-runtime-authenticator.js";
|
|
@@ -23,15 +32,19 @@ import { DurableEmbeddedBrowserProfileHost } from "./embedded-browser-profile-cl
|
|
|
23
32
|
import { loadDirectRuntimeConfig, } from "./direct-runtime-config.js";
|
|
24
33
|
import { isDaemonDirectRuntimeHostingSupported, loadOrCreateDaemonDirectRuntimeEncryptionIdentity, loadOrCreateDaemonInstallationIdentity, } from "./installation-id.js";
|
|
25
34
|
import { cleanupLegacySessions } from "./migrations/cleanup-legacy-sessions.js";
|
|
35
|
+
import { createMaintenanceManagementService } from "./maintenance-management.js";
|
|
36
|
+
import { PluginManagementService } from "./plugin-management-service.js";
|
|
26
37
|
import { PluginSupervisor } from "./plugin-supervisor.js";
|
|
27
38
|
import { pruneInactivePluginInstallations } from "./plugin-installer.js";
|
|
28
39
|
import { runPluginCliCommand } from "./plugin-cli.js";
|
|
29
40
|
import { getPlugin, isRunnablePluginRecord, listEnabledPlugins } from "./plugin-store.js";
|
|
30
41
|
import { loadPlugins, resolveChannelInstances } from "./registry.js";
|
|
31
42
|
import { SqliteSessionLogStore } from "./session-log-store.js";
|
|
43
|
+
import { SqliteSessionResourceStore } from "./session-resource-store.js";
|
|
32
44
|
import { SessionBrowserCapabilityStore } from "./session-browser-capability-store.js";
|
|
33
45
|
import { SqliteSessionEmulatorBindingStore } from "./session-emulator-binding-store.js";
|
|
34
46
|
import { sessionRegistry } from "./session-meta-store.js";
|
|
47
|
+
import { sessionPendingMessageStore } from "./session-pending-message-store.js";
|
|
35
48
|
import { RemoteRuntimeAccessStore } from "./remote-runtime-access-store.js";
|
|
36
49
|
import { DaemonRemoteRuntimeAdmin } from "./remote-runtime-admin.js";
|
|
37
50
|
import { FileRemoteRuntimeCredentialStore } from "./remote-runtime-credential-store.js";
|
|
@@ -40,7 +53,15 @@ import { DaemonRuntimeTargetControl } from "./remote-runtime-target-control.js";
|
|
|
40
53
|
import { RemoteRuntimeTargetStore } from "./remote-runtime-target-store.js";
|
|
41
54
|
import { ProviderCliService } from "./provider-cli-service.js";
|
|
42
55
|
const PLUGIN_CLI_OUTPUT_MAX_BYTES = 256 * 1024;
|
|
56
|
+
const BROWSER_INSPECT_OPEN_TIMEOUT_MS = 10_000;
|
|
57
|
+
const BROWSER_INSPECT_CLOSE_TIMEOUT_MS = 1_000;
|
|
58
|
+
const BROWSER_INSPECT_MAX_QUEUED_BYTES = 32 * 1024 * 1024;
|
|
43
59
|
const DIRECT_RUNTIME_HOSTING_UNSUPPORTED = "Direct Runtime hosting is supported only on macOS and Linux until protected server-key storage is available on other operating systems";
|
|
60
|
+
const require = createRequire(import.meta.url);
|
|
61
|
+
const DAEMON_PACKAGE_VERSION = readDaemonPackageVersion();
|
|
62
|
+
export function daemonActivityBlocksShutdown(activity) {
|
|
63
|
+
return activity.runningTurns > 0 || activity.pendingInteractions > 0;
|
|
64
|
+
}
|
|
44
65
|
function defaultWarn(message) {
|
|
45
66
|
console.warn(JSON.stringify({ level: "warn", type: "channel-registry", msg: message }));
|
|
46
67
|
}
|
|
@@ -53,6 +74,7 @@ function unsupportedRemoteRuntimeAdmin(reason) {
|
|
|
53
74
|
createPairingOffer: unsupported,
|
|
54
75
|
listClientGrants: unsupported,
|
|
55
76
|
revokeClientGrant: unsupported,
|
|
77
|
+
deleteRevokedClientGrant: unsupported,
|
|
56
78
|
};
|
|
57
79
|
}
|
|
58
80
|
function logLegacyCleanup(config) {
|
|
@@ -81,6 +103,14 @@ function logLegacyCleanup(config) {
|
|
|
81
103
|
function dialableHost(host) {
|
|
82
104
|
return host === "0.0.0.0" || host === "::" || host.trim() === "" ? "127.0.0.1" : host;
|
|
83
105
|
}
|
|
106
|
+
function daemonControlOrigin(config) {
|
|
107
|
+
const host = dialableHost(config.HOST);
|
|
108
|
+
const unwrappedHost = host.replace(/^\[|\]$/g, "");
|
|
109
|
+
const authorityHost = unwrappedHost.includes(":")
|
|
110
|
+
? `[${unwrappedHost}]`
|
|
111
|
+
: unwrappedHost;
|
|
112
|
+
return `http://${authorityHost}:${config.PORT}`;
|
|
113
|
+
}
|
|
84
114
|
function controlEndpointFile() {
|
|
85
115
|
return join(rynxHome(), "control.json");
|
|
86
116
|
}
|
|
@@ -90,11 +120,13 @@ async function writeControlEndpoint(config, managementToken) {
|
|
|
90
120
|
await mkdir(dirname(file), { recursive: true });
|
|
91
121
|
try {
|
|
92
122
|
await writeFile(temporary, `${JSON.stringify({
|
|
93
|
-
origin:
|
|
123
|
+
origin: daemonControlOrigin(config),
|
|
94
124
|
host: config.HOST,
|
|
95
125
|
port: config.PORT,
|
|
96
126
|
pid: process.pid,
|
|
97
127
|
managementToken,
|
|
128
|
+
managementProtocol: LOCAL_MANAGEMENT_PROTOCOL,
|
|
129
|
+
...controlEndpointDistributionMetadata(),
|
|
98
130
|
updatedAt: new Date().toISOString(),
|
|
99
131
|
}, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
100
132
|
await rename(temporary, file);
|
|
@@ -103,6 +135,38 @@ async function writeControlEndpoint(config, managementToken) {
|
|
|
103
135
|
await rm(temporary, { force: true });
|
|
104
136
|
}
|
|
105
137
|
}
|
|
138
|
+
function controlEndpointDistributionMetadata() {
|
|
139
|
+
const distribution = process.env.RYNX_DISTRIBUTION === "app"
|
|
140
|
+
? "app"
|
|
141
|
+
: "standalone";
|
|
142
|
+
const productVersion = boundedEnvironmentMetadata(process.env.RYNX_PRODUCT_VERSION, 128) ?? DAEMON_PACKAGE_VERSION;
|
|
143
|
+
const buildId = boundedEnvironmentMetadata(process.env.RYNX_BUILD_ID, 256) ?? `${distribution}:${productVersion}`;
|
|
144
|
+
const requestedLifecycle = process.env.RYNX_DAEMON_LIFECYCLE;
|
|
145
|
+
const daemonLifecycle = distribution === "standalone"
|
|
146
|
+
? "standalone"
|
|
147
|
+
: requestedLifecycle === "resident"
|
|
148
|
+
? "resident"
|
|
149
|
+
: "follow-app";
|
|
150
|
+
return { distribution, productVersion, buildId, daemonLifecycle };
|
|
151
|
+
}
|
|
152
|
+
function boundedEnvironmentMetadata(value, maxLength) {
|
|
153
|
+
if (!value ||
|
|
154
|
+
value.length > maxLength ||
|
|
155
|
+
value.trim() !== value ||
|
|
156
|
+
/[\u0000-\u001f\u007f]/.test(value)) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
function readDaemonPackageVersion() {
|
|
162
|
+
try {
|
|
163
|
+
const version = require("../package.json").version;
|
|
164
|
+
return boundedEnvironmentMetadata(typeof version === "string" ? version : undefined, 128) ?? "development";
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return "development";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
106
170
|
async function removeControlEndpointForCurrentProcess() {
|
|
107
171
|
try {
|
|
108
172
|
const file = controlEndpointFile();
|
|
@@ -115,13 +179,14 @@ async function removeControlEndpointForCurrentProcess() {
|
|
|
115
179
|
// Best effort only; stale endpoint files are ignored by the client fallback.
|
|
116
180
|
}
|
|
117
181
|
}
|
|
118
|
-
export async function startRynxDaemonServer({ config, warn = defaultWarn, writeEndpoint = writeControlEndpoint, acquireOwner = acquireDaemonOwner, loadDirectConfig = loadDirectRuntimeConfig, startDirectServer = startDirectRuntimeServer, } = {}) {
|
|
182
|
+
export async function startRynxDaemonServer({ config, warn = defaultWarn, writeEndpoint = writeControlEndpoint, acquireOwner = acquireDaemonOwner, loadDirectConfig = loadDirectRuntimeConfig, startDirectServer = startDirectRuntimeServer, createChromeInspectionManager = (options) => ChromeInspectionManager.create(options), createBrowserHostSupervisor = (options) => new AppBrowserHostSupervisor(options), } = {}) {
|
|
119
183
|
const owner = await acquireOwner();
|
|
120
184
|
let ownerManagedByServer = false;
|
|
121
185
|
try {
|
|
122
186
|
const appConfig = config ?? loadConfig();
|
|
187
|
+
const appBrowserHostLaunch = parseAppBrowserHostLaunchConfig();
|
|
123
188
|
const directHostingSupported = isDaemonDirectRuntimeHostingSupported();
|
|
124
|
-
const runtimeBrowserSupported = isChromeForTestingHostSupported();
|
|
189
|
+
const runtimeBrowserSupported = appBrowserHostLaunch !== undefined || isChromeForTestingHostSupported();
|
|
125
190
|
const directConfig = directHostingSupported
|
|
126
191
|
? await loadDirectConfig()
|
|
127
192
|
: undefined;
|
|
@@ -134,6 +199,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
134
199
|
warn(DIRECT_RUNTIME_HOSTING_UNSUPPORTED);
|
|
135
200
|
const daemonInstanceId = randomUUID();
|
|
136
201
|
const daemonStartedAt = new Date();
|
|
202
|
+
const daemonBuildStatus = createDaemonBuildStatus(fileURLToPath(import.meta.url), daemonStartedAt.getTime());
|
|
137
203
|
const remoteRuntime = createDaemonRuntimeHost({
|
|
138
204
|
daemonId: installationIdentity.installationId,
|
|
139
205
|
daemonInstanceId,
|
|
@@ -150,12 +216,17 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
150
216
|
"semantic.session.launch-options.v2",
|
|
151
217
|
"semantic.session.create.v1",
|
|
152
218
|
"semantic.session.message.v1",
|
|
219
|
+
"semantic.session.resources.v1",
|
|
220
|
+
"semantic.session.message-queue.v1",
|
|
153
221
|
"semantic.session.delete.v1",
|
|
154
222
|
"semantic.session.interaction.v1",
|
|
155
223
|
"semantic.provider-cli.status.v1",
|
|
156
224
|
...(runtimeBrowserSupported ? ["semantic.browser.v1"] : []),
|
|
157
225
|
...(runtimeBrowserSupported && directHostingSupported
|
|
158
|
-
? [
|
|
226
|
+
? [
|
|
227
|
+
RUNTIME_BROWSER_SURFACE_SEMANTIC_CAPABILITY,
|
|
228
|
+
RUNTIME_BROWSER_INSPECT_SEMANTIC_CAPABILITY,
|
|
229
|
+
]
|
|
159
230
|
: []),
|
|
160
231
|
...(control.emulator
|
|
161
232
|
? ["semantic.emulator.unary.v1", "semantic.session.emulator.v1"]
|
|
@@ -167,6 +238,19 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
167
238
|
startedAt: daemonStartedAt,
|
|
168
239
|
});
|
|
169
240
|
const managementToken = randomBytes(32).toString("base64url");
|
|
241
|
+
const desktopBrowserHosts = new DesktopBrowserHostRegistry();
|
|
242
|
+
const appBrowserHostSupervisor = appBrowserHostLaunch
|
|
243
|
+
? createBrowserHostSupervisor({
|
|
244
|
+
launch: appBrowserHostLaunch,
|
|
245
|
+
bootstrap: {
|
|
246
|
+
type: APP_BROWSER_HOST_BOOTSTRAP_TYPE,
|
|
247
|
+
schemaVersion: 1,
|
|
248
|
+
origin: daemonControlOrigin(appConfig),
|
|
249
|
+
managementToken,
|
|
250
|
+
},
|
|
251
|
+
isDesktopHostConnected: () => desktopBrowserHosts.directConnected,
|
|
252
|
+
})
|
|
253
|
+
: undefined;
|
|
170
254
|
try {
|
|
171
255
|
for (const removed of pruneInactivePluginInstallations()) {
|
|
172
256
|
console.log(JSON.stringify({ level: "info", type: "plugin-gc", removed }));
|
|
@@ -186,6 +270,11 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
186
270
|
output(JSON.stringify(entry));
|
|
187
271
|
},
|
|
188
272
|
});
|
|
273
|
+
const pluginManagement = new PluginManagementService();
|
|
274
|
+
const browserArtifactManagement = createBrowserArtifactManagementService();
|
|
275
|
+
const maintenanceManagement = createMaintenanceManagementService({
|
|
276
|
+
codexStorePath: appConfig.AGENT_SESSION_STORE_PATH,
|
|
277
|
+
});
|
|
189
278
|
await loadPlugins(pluginSupervisor, warn);
|
|
190
279
|
logLegacyCleanup(appConfig);
|
|
191
280
|
const reloadPluginRuntime = async (pluginId) => {
|
|
@@ -253,21 +342,12 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
253
342
|
statusSnapshot: pluginSupervisor.statusSnapshot.bind(pluginSupervisor),
|
|
254
343
|
dispose: pluginSupervisor.dispose.bind(pluginSupervisor),
|
|
255
344
|
};
|
|
256
|
-
const daemonManagement = {
|
|
257
|
-
managementToken,
|
|
258
|
-
identity: () => ({
|
|
259
|
-
installationId: installationIdentity.installationId,
|
|
260
|
-
algorithm: installationIdentity.algorithm,
|
|
261
|
-
publicKey: installationIdentity.publicKey,
|
|
262
|
-
}),
|
|
263
|
-
invokePluginCli,
|
|
264
|
-
};
|
|
265
345
|
const remoteRuntimeAccess = new RemoteRuntimeAccessStore();
|
|
266
346
|
const directConnections = new DirectRuntimeConnectionHub();
|
|
267
347
|
const sessionLog = new SqliteSessionLogStore();
|
|
348
|
+
const sessionResources = new SqliteSessionResourceStore();
|
|
268
349
|
const providerClis = new ProviderCliService();
|
|
269
350
|
const sessionBrowserCapabilities = new SessionBrowserCapabilityStore();
|
|
270
|
-
const desktopBrowserHosts = new DesktopBrowserHostRegistry();
|
|
271
351
|
const durableDesktopBrowserHost = new DurableEmbeddedBrowserProfileHost(desktopBrowserHosts, { warn });
|
|
272
352
|
const browserRunnerSessionContexts = new BrowserRunnerSessionContextProvider({
|
|
273
353
|
rootDir: join(rynxHome(), "run", "browser-session-contexts"),
|
|
@@ -280,7 +360,9 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
280
360
|
onMirrorError: (error, sessionId) => warn(`Session mirror failed for ${sessionId}: ${error instanceof Error ? error.message : String(error)}`),
|
|
281
361
|
});
|
|
282
362
|
const sessionBrowsers = new SessionBrowserService({
|
|
283
|
-
host:
|
|
363
|
+
host: appBrowserHostSupervisor
|
|
364
|
+
? createSupervisedAppSessionBrowserHost(durableDesktopBrowserHost, appBrowserHostSupervisor)
|
|
365
|
+
: createDesktopFirstSessionBrowserHost(durableDesktopBrowserHost, createHeadlessBrowserHost()),
|
|
284
366
|
sessions: {
|
|
285
367
|
exists: async (sessionId) => Boolean(sessionRegistry.get(sessionId) ??
|
|
286
368
|
await sessionRuntimeServices.sessionStore.get(sessionId)),
|
|
@@ -356,6 +438,8 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
356
438
|
agents: {
|
|
357
439
|
list: () => control.listAgents(),
|
|
358
440
|
},
|
|
441
|
+
pendingMessages: sessionPendingMessageStore,
|
|
442
|
+
resources: sessionResources,
|
|
359
443
|
execution: {
|
|
360
444
|
runtime: sessionRuntimeServices.conversationRuntime,
|
|
361
445
|
runner: sessionRuntimeServices.runnerManager,
|
|
@@ -377,6 +461,53 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
377
461
|
return { cwd };
|
|
378
462
|
},
|
|
379
463
|
});
|
|
464
|
+
let shutdownAccepted = false;
|
|
465
|
+
let requestServerShutdown;
|
|
466
|
+
const daemonManagement = {
|
|
467
|
+
managementToken,
|
|
468
|
+
identity: () => ({
|
|
469
|
+
installationId: installationIdentity.installationId,
|
|
470
|
+
algorithm: installationIdentity.algorithm,
|
|
471
|
+
publicKey: installationIdentity.publicKey,
|
|
472
|
+
}),
|
|
473
|
+
buildStatus: daemonBuildStatus,
|
|
474
|
+
pluginManagement: {
|
|
475
|
+
listPlugins: () => pluginManagement.list(),
|
|
476
|
+
setPluginEnabled: (pluginId, enabled) => pluginManagement.setEnabled(pluginId, enabled),
|
|
477
|
+
uninstallPlugin: (pluginId) => pluginManagement.uninstall(pluginId),
|
|
478
|
+
prepareInstallation: (input, context) => pluginManagement.prepareInstallation(input, context),
|
|
479
|
+
commitInstallation: (token, input, context) => pluginManagement.commitInstallation(token, input, context),
|
|
480
|
+
cancelPreparation: (token) => pluginManagement.cancelPreparation(token),
|
|
481
|
+
dispose: () => pluginManagement.dispose(),
|
|
482
|
+
},
|
|
483
|
+
browserArtifacts: browserArtifactManagement,
|
|
484
|
+
maintenance: maintenanceManagement,
|
|
485
|
+
shutdownIfIdle: () => {
|
|
486
|
+
if (shutdownAccepted)
|
|
487
|
+
return { outcome: "accepted" };
|
|
488
|
+
const activity = {
|
|
489
|
+
...sessionRuntimeServices.sessionRuntimeIndex.activitySnapshot(),
|
|
490
|
+
activeTerminals: sessionTerminals.activeTerminalCount(),
|
|
491
|
+
activeBrowsers: sessionBrowsers.activeBrowserCount(),
|
|
492
|
+
};
|
|
493
|
+
if (daemonActivityBlocksShutdown(activity)) {
|
|
494
|
+
return { outcome: "busy", activity };
|
|
495
|
+
}
|
|
496
|
+
shutdownAccepted = true;
|
|
497
|
+
const shutdown = requestServerShutdown;
|
|
498
|
+
if (!shutdown) {
|
|
499
|
+
shutdownAccepted = false;
|
|
500
|
+
throw new Error("idle daemon shutdown was requested before server lifecycle was ready");
|
|
501
|
+
}
|
|
502
|
+
// Start admission fences in this event-loop turn, before another
|
|
503
|
+
// request or Direct dispatch can be accepted.
|
|
504
|
+
void shutdown().catch((error) => {
|
|
505
|
+
warn(`idle daemon shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
506
|
+
});
|
|
507
|
+
return { outcome: "accepted" };
|
|
508
|
+
},
|
|
509
|
+
invokePluginCli,
|
|
510
|
+
};
|
|
380
511
|
const remoteRuntimeDispatcher = new RemoteRuntimeDispatcher({
|
|
381
512
|
runtime: remoteRuntime,
|
|
382
513
|
emulator: control.emulator,
|
|
@@ -401,39 +532,57 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
401
532
|
credentials: runtimeCredentials,
|
|
402
533
|
});
|
|
403
534
|
const runtimeTargetControl = new DaemonRuntimeTargetControl(remoteRuntime, runtimeTargets, runtimeCredentials, runtimeConnectionManager);
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
sessionLog,
|
|
409
|
-
sessionRegistry,
|
|
410
|
-
sessionRuntimeServices,
|
|
411
|
-
pluginRuntime,
|
|
412
|
-
daemonManagement,
|
|
413
|
-
remoteRuntime,
|
|
414
|
-
remoteRuntimeAdmin,
|
|
415
|
-
runtimeTargetControl,
|
|
416
|
-
runtimeConnectionResolver: runtimeConnectionManager,
|
|
417
|
-
providerClis,
|
|
418
|
-
sessionEmulators,
|
|
419
|
-
sessionLifecycle: sessionResourceLifecycle,
|
|
420
|
-
runtimeLocalBrowser: {
|
|
421
|
-
authorizeCredential: (credential) => sessionBrowserCapabilities.verify(credential.sessionId, credential.capability),
|
|
422
|
-
endpointForCredential: async (credential) => {
|
|
423
|
-
if (!sessionBrowserCapabilities.verify(credential.sessionId, credential.capability))
|
|
424
|
-
return undefined;
|
|
425
|
-
return sessionBrowsers.getRuntimeLocalEndpoint(credential.sessionId);
|
|
426
|
-
},
|
|
427
|
-
endpointForSession: (sessionId) => sessionBrowsers.getRuntimeLocalEndpoint(sessionId),
|
|
428
|
-
},
|
|
429
|
-
desktopBrowserHost: {
|
|
430
|
-
registry: desktopBrowserHosts,
|
|
431
|
-
daemonInstanceId,
|
|
432
|
-
},
|
|
433
|
-
});
|
|
434
|
-
const baseShutdown = server.shutdown.bind(server);
|
|
535
|
+
const directBrowserInspectHost = createDirectRuntimeBrowserInspectHost(sessionBrowsers);
|
|
536
|
+
let chromeInspectionManager;
|
|
537
|
+
let server;
|
|
538
|
+
let baseShutdown;
|
|
435
539
|
let directServer;
|
|
436
540
|
try {
|
|
541
|
+
const initializedChromeInspectionManager = await createChromeInspectionManager({
|
|
542
|
+
localDaemonInstanceId: daemonInstanceId,
|
|
543
|
+
browsers: sessionBrowsers,
|
|
544
|
+
runtimeTargets,
|
|
545
|
+
runtimeConnections: runtimeConnectionManager,
|
|
546
|
+
onError: (error, context) => warn(`Chrome inspection ${context} failed: ${error.message}`),
|
|
547
|
+
});
|
|
548
|
+
chromeInspectionManager = initializedChromeInspectionManager;
|
|
549
|
+
daemonManagement.chromeInspection = {
|
|
550
|
+
status: async () => asDaemonChromeInspectionStatus(await initializedChromeInspectionManager.status()),
|
|
551
|
+
configure: async (input) => asDaemonChromeInspectionStatus(await initializedChromeInspectionManager.configure(input)),
|
|
552
|
+
};
|
|
553
|
+
server = await startServer({
|
|
554
|
+
config: appConfig,
|
|
555
|
+
loadInstances: () => resolveChannelInstances(warn),
|
|
556
|
+
control,
|
|
557
|
+
sessionLog,
|
|
558
|
+
sessionRegistry,
|
|
559
|
+
sessionRuntimeServices,
|
|
560
|
+
pluginRuntime,
|
|
561
|
+
daemonManagement,
|
|
562
|
+
remoteRuntime,
|
|
563
|
+
remoteRuntimeAdmin,
|
|
564
|
+
runtimeTargetControl,
|
|
565
|
+
runtimeConnectionResolver: runtimeConnectionManager,
|
|
566
|
+
localSessionResources: machineSessions,
|
|
567
|
+
providerClis,
|
|
568
|
+
sessionEmulators,
|
|
569
|
+
sessionLifecycle: sessionResourceLifecycle,
|
|
570
|
+
sessionTerminals,
|
|
571
|
+
runtimeLocalBrowser: {
|
|
572
|
+
authorizeCredential: (credential) => sessionBrowserCapabilities.verify(credential.sessionId, credential.capability),
|
|
573
|
+
endpointForCredential: async (credential) => {
|
|
574
|
+
if (!sessionBrowserCapabilities.verify(credential.sessionId, credential.capability))
|
|
575
|
+
return undefined;
|
|
576
|
+
return sessionBrowsers.getRuntimeLocalEndpoint(credential.sessionId);
|
|
577
|
+
},
|
|
578
|
+
endpointForSession: (sessionId) => sessionBrowsers.getRuntimeLocalEndpoint(sessionId),
|
|
579
|
+
},
|
|
580
|
+
desktopBrowserHost: {
|
|
581
|
+
registry: desktopBrowserHosts,
|
|
582
|
+
daemonInstanceId,
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
baseShutdown = server.shutdown.bind(server);
|
|
437
586
|
if (directConfig && directEncryptionIdentity) {
|
|
438
587
|
const startedDirect = await startDirectListener(startDirectServer, directConfig, {
|
|
439
588
|
e2ee: {
|
|
@@ -445,6 +594,9 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
445
594
|
isPeerActive: (peer) => remoteRuntimeAccess.isClientGrantActive(peer.grantId, peer.clientId),
|
|
446
595
|
connectionHub: directConnections,
|
|
447
596
|
browserSurface: { host: sessionBrowserSurfaces },
|
|
597
|
+
...(runtimeBrowserSupported
|
|
598
|
+
? { browserInspect: { host: directBrowserInspectHost } }
|
|
599
|
+
: {}),
|
|
448
600
|
...(sessionEmulatorSurface
|
|
449
601
|
? { emulatorSurface: { host: sessionEmulatorSurface } }
|
|
450
602
|
: {}),
|
|
@@ -464,6 +616,15 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
464
616
|
}
|
|
465
617
|
catch (startupError) {
|
|
466
618
|
const rollbackErrors = [startupError];
|
|
619
|
+
server?.stopAccepting();
|
|
620
|
+
if (chromeInspectionManager) {
|
|
621
|
+
try {
|
|
622
|
+
await chromeInspectionManager.shutdown();
|
|
623
|
+
}
|
|
624
|
+
catch (shutdownError) {
|
|
625
|
+
rollbackErrors.push(shutdownError);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
467
628
|
if (directServer) {
|
|
468
629
|
try {
|
|
469
630
|
await directServer.shutdown();
|
|
@@ -473,7 +634,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
473
634
|
}
|
|
474
635
|
}
|
|
475
636
|
try {
|
|
476
|
-
await
|
|
637
|
+
await runtimeConnectionManager.close();
|
|
477
638
|
}
|
|
478
639
|
catch (shutdownError) {
|
|
479
640
|
rollbackErrors.push(shutdownError);
|
|
@@ -487,25 +648,47 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
487
648
|
finally {
|
|
488
649
|
durableDesktopBrowserHost.dispose();
|
|
489
650
|
}
|
|
490
|
-
|
|
491
|
-
|
|
651
|
+
if (appBrowserHostSupervisor) {
|
|
652
|
+
try {
|
|
653
|
+
await appBrowserHostSupervisor.stop();
|
|
654
|
+
}
|
|
655
|
+
catch (shutdownError) {
|
|
656
|
+
rollbackErrors.push(shutdownError);
|
|
657
|
+
}
|
|
492
658
|
}
|
|
493
|
-
|
|
494
|
-
|
|
659
|
+
if (baseShutdown) {
|
|
660
|
+
try {
|
|
661
|
+
await baseShutdown();
|
|
662
|
+
}
|
|
663
|
+
catch (shutdownError) {
|
|
664
|
+
rollbackErrors.push(shutdownError);
|
|
665
|
+
}
|
|
495
666
|
}
|
|
496
667
|
if (rollbackErrors.length > 1) {
|
|
497
|
-
throw new AggregateError(rollbackErrors, "failed to start
|
|
668
|
+
throw new AggregateError(rollbackErrors, "failed to start daemon resources and roll back the local server");
|
|
498
669
|
}
|
|
499
670
|
throw startupError;
|
|
500
671
|
}
|
|
672
|
+
if (!server || !baseShutdown || !chromeInspectionManager) {
|
|
673
|
+
throw new Error("daemon startup completed without all required resources");
|
|
674
|
+
}
|
|
501
675
|
let shutdownPromise;
|
|
502
676
|
server.shutdown = () => {
|
|
503
677
|
if (shutdownPromise)
|
|
504
678
|
return shutdownPromise;
|
|
679
|
+
// Fence Local admission without closing the Desktop Browser Host WS.
|
|
680
|
+
// Browser handles can therefore close gracefully before full teardown.
|
|
681
|
+
server.stopAccepting();
|
|
505
682
|
shutdownPromise = (async () => {
|
|
506
683
|
let shutdownComplete = false;
|
|
507
684
|
try {
|
|
508
685
|
const shutdownErrors = [];
|
|
686
|
+
try {
|
|
687
|
+
await chromeInspectionManager.shutdown();
|
|
688
|
+
}
|
|
689
|
+
catch (error) {
|
|
690
|
+
shutdownErrors.push(error);
|
|
691
|
+
}
|
|
509
692
|
if (directServer) {
|
|
510
693
|
try {
|
|
511
694
|
await directServer.shutdown();
|
|
@@ -514,12 +697,6 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
514
697
|
shutdownErrors.push(error);
|
|
515
698
|
}
|
|
516
699
|
}
|
|
517
|
-
try {
|
|
518
|
-
await baseShutdown();
|
|
519
|
-
}
|
|
520
|
-
catch (error) {
|
|
521
|
-
shutdownErrors.push(error);
|
|
522
|
-
}
|
|
523
700
|
try {
|
|
524
701
|
await runtimeConnectionManager.close();
|
|
525
702
|
}
|
|
@@ -535,6 +712,20 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
535
712
|
finally {
|
|
536
713
|
durableDesktopBrowserHost.dispose();
|
|
537
714
|
}
|
|
715
|
+
if (appBrowserHostSupervisor) {
|
|
716
|
+
try {
|
|
717
|
+
await appBrowserHostSupervisor.stop();
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
shutdownErrors.push(error);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
await baseShutdown();
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
shutdownErrors.push(error);
|
|
728
|
+
}
|
|
538
729
|
try {
|
|
539
730
|
browserRunnerSessionContexts.closeAll();
|
|
540
731
|
sessionBrowserCapabilities.revokeAll();
|
|
@@ -558,6 +749,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
558
749
|
})();
|
|
559
750
|
return shutdownPromise;
|
|
560
751
|
};
|
|
752
|
+
requestServerShutdown = server.shutdown.bind(server);
|
|
561
753
|
ownerManagedByServer = true;
|
|
562
754
|
try {
|
|
563
755
|
await writeEndpoint(appConfig, managementToken);
|
|
@@ -579,6 +771,370 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
579
771
|
throw error;
|
|
580
772
|
}
|
|
581
773
|
}
|
|
774
|
+
export function createSupervisedAppSessionBrowserHost(desktop, supervisor) {
|
|
775
|
+
const withOnDemandSidecar = async (operation) => {
|
|
776
|
+
try {
|
|
777
|
+
return await operation();
|
|
778
|
+
}
|
|
779
|
+
catch (error) {
|
|
780
|
+
if (!(error instanceof DesktopBrowserHostAbsentError))
|
|
781
|
+
throw error;
|
|
782
|
+
}
|
|
783
|
+
try {
|
|
784
|
+
await supervisor.ensureConnected();
|
|
785
|
+
}
|
|
786
|
+
catch (startupError) {
|
|
787
|
+
try {
|
|
788
|
+
return await operation();
|
|
789
|
+
}
|
|
790
|
+
catch (retryError) {
|
|
791
|
+
if (retryError instanceof DesktopBrowserHostAbsentError)
|
|
792
|
+
throw startupError;
|
|
793
|
+
throw retryError;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
try {
|
|
797
|
+
return await operation();
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
if (!(error instanceof DesktopBrowserHostAbsentError))
|
|
801
|
+
throw error;
|
|
802
|
+
await supervisor.restart();
|
|
803
|
+
return operation();
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
return {
|
|
807
|
+
createBrowser: (input) => withOnDemandSidecar(() => desktop.createBrowser(input)),
|
|
808
|
+
...(desktop.deleteSessionData
|
|
809
|
+
? {
|
|
810
|
+
deleteSessionData: (sessionId) => withOnDemandSidecar(() => desktop.deleteSessionData(sessionId)),
|
|
811
|
+
}
|
|
812
|
+
: {}),
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
function asDaemonChromeInspectionStatus(status) {
|
|
816
|
+
if (status.runtime.address !== "127.0.0.1:9333") {
|
|
817
|
+
throw new Error("resident Chrome inspection must use the fixed 127.0.0.1:9333 endpoint");
|
|
818
|
+
}
|
|
819
|
+
return {
|
|
820
|
+
...status,
|
|
821
|
+
runtime: {
|
|
822
|
+
...status.runtime,
|
|
823
|
+
address: "127.0.0.1:9333",
|
|
824
|
+
},
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Resolve the authenticated semantic target inside its owning Runtime and
|
|
829
|
+
* attach directly to that Page's loopback CDP WebSocket. No endpoint is ever
|
|
830
|
+
* projected into the Direct protocol.
|
|
831
|
+
*/
|
|
832
|
+
export function createDirectRuntimeBrowserInspectHost(browsers, options = {}) {
|
|
833
|
+
const openTimeoutMs = options.openTimeoutMs ??
|
|
834
|
+
BROWSER_INSPECT_OPEN_TIMEOUT_MS;
|
|
835
|
+
if (!Number.isSafeInteger(openTimeoutMs) || openTimeoutMs <= 0) {
|
|
836
|
+
throw new TypeError("Browser Inspect open timeout must be a positive integer");
|
|
837
|
+
}
|
|
838
|
+
return {
|
|
839
|
+
open: async (target, context) => {
|
|
840
|
+
const deadline = AbortSignal.timeout(openTimeoutMs);
|
|
841
|
+
const openingSignal = AbortSignal.any([context.signal, deadline]);
|
|
842
|
+
let socket;
|
|
843
|
+
try {
|
|
844
|
+
const endpoint = await waitForBrowserInspectValue(browsers.getInspectionPageEndpoint(target), openingSignal);
|
|
845
|
+
const url = parseRuntimeLocalPageEndpoint(endpoint.endpoint);
|
|
846
|
+
socket = new WebSocket(url, {
|
|
847
|
+
followRedirects: false,
|
|
848
|
+
handshakeTimeout: openTimeoutMs,
|
|
849
|
+
maxPayload: RUNTIME_BROWSER_INSPECT_MAX_MESSAGE_BYTES,
|
|
850
|
+
perMessageDeflate: false,
|
|
851
|
+
});
|
|
852
|
+
await waitForBrowserInspectSocketOpen(socket, openingSignal);
|
|
853
|
+
return browserInspectAttachment(socket, context.signal);
|
|
854
|
+
}
|
|
855
|
+
catch (error) {
|
|
856
|
+
if (socket?.readyState !== WebSocket.CLOSED)
|
|
857
|
+
socket?.terminate();
|
|
858
|
+
throw mapBrowserInspectOpenError(error, context.signal.aborted, deadline.aborted);
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
function browserInspectAttachment(socket, signal) {
|
|
864
|
+
const queue = new BrowserInspectMessageQueue(BROWSER_INSPECT_MAX_QUEUED_BYTES, () => socket.pause(), () => {
|
|
865
|
+
if (socket.readyState === WebSocket.OPEN)
|
|
866
|
+
socket.resume();
|
|
867
|
+
});
|
|
868
|
+
let closed = false;
|
|
869
|
+
const onMessage = (data, binary) => {
|
|
870
|
+
const message = binary
|
|
871
|
+
? { binary: true, data: copyBrowserInspectBytes(data) }
|
|
872
|
+
: { binary: false, data: browserInspectText(data) };
|
|
873
|
+
if (!queue.push(message)) {
|
|
874
|
+
const error = new Error("Browser Inspect local Page output queue is full");
|
|
875
|
+
queue.fail(error);
|
|
876
|
+
void closeBrowserInspectSocket(socket, 1_009, "Browser Inspect output capacity exceeded");
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
const onError = (error) => queue.fail(error);
|
|
880
|
+
const onClose = () => queue.end();
|
|
881
|
+
const onAbort = () => {
|
|
882
|
+
queue.fail(signal.reason instanceof Error
|
|
883
|
+
? signal.reason
|
|
884
|
+
: new Error("Browser Inspect attachment was cancelled"));
|
|
885
|
+
void closeBrowserInspectSocket(socket, 1_001, "Browser Inspect cancelled");
|
|
886
|
+
};
|
|
887
|
+
socket.on("message", onMessage);
|
|
888
|
+
socket.once("error", onError);
|
|
889
|
+
socket.once("close", onClose);
|
|
890
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
891
|
+
if (signal.aborted)
|
|
892
|
+
onAbort();
|
|
893
|
+
const close = async (reason) => {
|
|
894
|
+
if (closed)
|
|
895
|
+
return;
|
|
896
|
+
closed = true;
|
|
897
|
+
signal.removeEventListener("abort", onAbort);
|
|
898
|
+
socket.removeListener("message", onMessage);
|
|
899
|
+
socket.removeListener("error", onError);
|
|
900
|
+
socket.removeListener("close", onClose);
|
|
901
|
+
queue.end();
|
|
902
|
+
await closeBrowserInspectSocket(socket, reason === "client_closed" ? 1_000 : 1_001, `Browser Inspect ${reason.replaceAll("_", " ")}`);
|
|
903
|
+
};
|
|
904
|
+
return {
|
|
905
|
+
messages: queue,
|
|
906
|
+
send: async (message) => {
|
|
907
|
+
if (closed || socket.readyState !== WebSocket.OPEN) {
|
|
908
|
+
throw new Error("Browser Inspect local Page is closed");
|
|
909
|
+
}
|
|
910
|
+
const byteLength = message.binary
|
|
911
|
+
? message.data.byteLength
|
|
912
|
+
: Buffer.byteLength(message.data, "utf8");
|
|
913
|
+
if (byteLength > RUNTIME_BROWSER_INSPECT_MAX_MESSAGE_BYTES ||
|
|
914
|
+
socket.bufferedAmount + byteLength >
|
|
915
|
+
BROWSER_INSPECT_MAX_QUEUED_BYTES) {
|
|
916
|
+
throw new Error("Browser Inspect local Page input capacity exceeded");
|
|
917
|
+
}
|
|
918
|
+
await new Promise((resolve, reject) => {
|
|
919
|
+
socket.send(message.data, { binary: message.binary, compress: false }, (error) => error ? reject(error) : resolve());
|
|
920
|
+
});
|
|
921
|
+
},
|
|
922
|
+
close,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
class BrowserInspectMessageQueue {
|
|
926
|
+
maxBytes;
|
|
927
|
+
onBuffered;
|
|
928
|
+
onDrained;
|
|
929
|
+
values = [];
|
|
930
|
+
waiters = [];
|
|
931
|
+
queuedBytes = 0;
|
|
932
|
+
ended = false;
|
|
933
|
+
failure;
|
|
934
|
+
constructor(maxBytes, onBuffered, onDrained) {
|
|
935
|
+
this.maxBytes = maxBytes;
|
|
936
|
+
this.onBuffered = onBuffered;
|
|
937
|
+
this.onDrained = onDrained;
|
|
938
|
+
}
|
|
939
|
+
push(message) {
|
|
940
|
+
if (this.ended || this.failure !== undefined)
|
|
941
|
+
return false;
|
|
942
|
+
const byteLength = message.binary
|
|
943
|
+
? message.data.byteLength
|
|
944
|
+
: Buffer.byteLength(message.data, "utf8");
|
|
945
|
+
if (byteLength > RUNTIME_BROWSER_INSPECT_MAX_MESSAGE_BYTES)
|
|
946
|
+
return false;
|
|
947
|
+
const waiter = this.waiters.shift();
|
|
948
|
+
if (waiter) {
|
|
949
|
+
waiter.resolve({ value: message, done: false });
|
|
950
|
+
return true;
|
|
951
|
+
}
|
|
952
|
+
if (this.queuedBytes + byteLength > this.maxBytes)
|
|
953
|
+
return false;
|
|
954
|
+
this.values.push({ message, byteLength });
|
|
955
|
+
this.queuedBytes += byteLength;
|
|
956
|
+
if (this.values.length === 1)
|
|
957
|
+
this.onBuffered();
|
|
958
|
+
return true;
|
|
959
|
+
}
|
|
960
|
+
end() {
|
|
961
|
+
if (this.ended || this.failure !== undefined)
|
|
962
|
+
return;
|
|
963
|
+
this.ended = true;
|
|
964
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
965
|
+
waiter.resolve({ value: undefined, done: true });
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
fail(error) {
|
|
969
|
+
if (this.ended || this.failure !== undefined)
|
|
970
|
+
return;
|
|
971
|
+
this.failure = error;
|
|
972
|
+
this.values.length = 0;
|
|
973
|
+
this.queuedBytes = 0;
|
|
974
|
+
this.onDrained();
|
|
975
|
+
for (const waiter of this.waiters.splice(0))
|
|
976
|
+
waiter.reject(error);
|
|
977
|
+
}
|
|
978
|
+
[Symbol.asyncIterator]() {
|
|
979
|
+
return {
|
|
980
|
+
next: () => {
|
|
981
|
+
if (this.failure !== undefined)
|
|
982
|
+
return Promise.reject(this.failure);
|
|
983
|
+
const entry = this.values.shift();
|
|
984
|
+
if (entry) {
|
|
985
|
+
this.queuedBytes -= entry.byteLength;
|
|
986
|
+
if (this.values.length === 0)
|
|
987
|
+
this.onDrained();
|
|
988
|
+
return Promise.resolve({ value: entry.message, done: false });
|
|
989
|
+
}
|
|
990
|
+
if (this.ended) {
|
|
991
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
992
|
+
}
|
|
993
|
+
return new Promise((resolve, reject) => this.waiters.push({ resolve, reject }));
|
|
994
|
+
},
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
function parseRuntimeLocalPageEndpoint(endpoint) {
|
|
999
|
+
let url;
|
|
1000
|
+
try {
|
|
1001
|
+
url = new URL(endpoint);
|
|
1002
|
+
}
|
|
1003
|
+
catch {
|
|
1004
|
+
throw new Error("Browser Inspect local Page endpoint is invalid");
|
|
1005
|
+
}
|
|
1006
|
+
if (url.protocol !== "ws:" ||
|
|
1007
|
+
url.hostname !== "127.0.0.1" ||
|
|
1008
|
+
!url.port ||
|
|
1009
|
+
url.username ||
|
|
1010
|
+
url.password ||
|
|
1011
|
+
url.search ||
|
|
1012
|
+
url.hash ||
|
|
1013
|
+
!/^\/devtools\/page\/[A-Za-z0-9._~%-]+$/.test(url.pathname)) {
|
|
1014
|
+
throw new Error("Browser Inspect local Page endpoint must be an exact loopback Page WebSocket");
|
|
1015
|
+
}
|
|
1016
|
+
return url.href;
|
|
1017
|
+
}
|
|
1018
|
+
function waitForBrowserInspectValue(promise, signal) {
|
|
1019
|
+
if (signal.aborted) {
|
|
1020
|
+
return Promise.reject(signal.reason ?? new Error("Browser Inspect opening was cancelled"));
|
|
1021
|
+
}
|
|
1022
|
+
return new Promise((resolve, reject) => {
|
|
1023
|
+
let settled = false;
|
|
1024
|
+
const finish = (callback) => {
|
|
1025
|
+
if (settled)
|
|
1026
|
+
return;
|
|
1027
|
+
settled = true;
|
|
1028
|
+
signal.removeEventListener("abort", onAbort);
|
|
1029
|
+
callback();
|
|
1030
|
+
};
|
|
1031
|
+
const onAbort = () => finish(() => reject(signal.reason ?? new Error("Browser Inspect opening was cancelled")));
|
|
1032
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1033
|
+
promise.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
function waitForBrowserInspectSocketOpen(socket, signal) {
|
|
1037
|
+
if (socket.readyState === WebSocket.OPEN)
|
|
1038
|
+
return Promise.resolve();
|
|
1039
|
+
return new Promise((resolve, reject) => {
|
|
1040
|
+
const cleanup = () => {
|
|
1041
|
+
signal.removeEventListener("abort", onAbort);
|
|
1042
|
+
socket.removeListener("open", onOpen);
|
|
1043
|
+
socket.removeListener("error", onError);
|
|
1044
|
+
socket.removeListener("unexpected-response", onUnexpectedResponse);
|
|
1045
|
+
};
|
|
1046
|
+
const onOpen = () => {
|
|
1047
|
+
cleanup();
|
|
1048
|
+
resolve();
|
|
1049
|
+
};
|
|
1050
|
+
const onError = (error) => {
|
|
1051
|
+
cleanup();
|
|
1052
|
+
reject(error);
|
|
1053
|
+
};
|
|
1054
|
+
const onUnexpectedResponse = (_request, response) => {
|
|
1055
|
+
response.resume();
|
|
1056
|
+
cleanup();
|
|
1057
|
+
reject(new Error("Browser Inspect local Page rejected the connection"));
|
|
1058
|
+
};
|
|
1059
|
+
const onAbort = () => {
|
|
1060
|
+
cleanup();
|
|
1061
|
+
socket.terminate();
|
|
1062
|
+
reject(signal.reason ?? new Error("Browser Inspect opening was cancelled"));
|
|
1063
|
+
};
|
|
1064
|
+
if (signal.aborted)
|
|
1065
|
+
return onAbort();
|
|
1066
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1067
|
+
socket.once("open", onOpen);
|
|
1068
|
+
socket.once("error", onError);
|
|
1069
|
+
socket.once("unexpected-response", onUnexpectedResponse);
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
function closeBrowserInspectSocket(socket, code, reason) {
|
|
1073
|
+
if (socket.readyState === WebSocket.CLOSED)
|
|
1074
|
+
return Promise.resolve();
|
|
1075
|
+
return new Promise((resolve) => {
|
|
1076
|
+
let settled = false;
|
|
1077
|
+
const finish = () => {
|
|
1078
|
+
if (settled)
|
|
1079
|
+
return;
|
|
1080
|
+
settled = true;
|
|
1081
|
+
clearTimeout(timer);
|
|
1082
|
+
socket.removeListener("close", finish);
|
|
1083
|
+
resolve();
|
|
1084
|
+
};
|
|
1085
|
+
const timer = setTimeout(() => {
|
|
1086
|
+
if (socket.readyState !== WebSocket.CLOSED)
|
|
1087
|
+
socket.terminate();
|
|
1088
|
+
finish();
|
|
1089
|
+
}, BROWSER_INSPECT_CLOSE_TIMEOUT_MS);
|
|
1090
|
+
timer.unref?.();
|
|
1091
|
+
socket.once("close", finish);
|
|
1092
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
1093
|
+
socket.close(code, reason.slice(0, 123));
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
socket.terminate();
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
function copyBrowserInspectBytes(data) {
|
|
1101
|
+
if (data instanceof ArrayBuffer)
|
|
1102
|
+
return new Uint8Array(data.slice(0));
|
|
1103
|
+
if (Array.isArray(data))
|
|
1104
|
+
return new Uint8Array(Buffer.concat(data));
|
|
1105
|
+
return new Uint8Array(Buffer.from(data));
|
|
1106
|
+
}
|
|
1107
|
+
function browserInspectText(data) {
|
|
1108
|
+
return Buffer.from(copyBrowserInspectBytes(data)).toString("utf8");
|
|
1109
|
+
}
|
|
1110
|
+
function mapBrowserInspectOpenError(error, callerAborted, deadlineAborted) {
|
|
1111
|
+
if (error instanceof DirectRuntimeBrowserInspectHostError)
|
|
1112
|
+
return error;
|
|
1113
|
+
if (callerAborted) {
|
|
1114
|
+
return new DirectRuntimeBrowserInspectHostError("cancelled", "Browser Inspect attachment was cancelled", { cause: error });
|
|
1115
|
+
}
|
|
1116
|
+
if (deadlineAborted) {
|
|
1117
|
+
return new DirectRuntimeBrowserInspectHostError("internal_error", "Browser Inspect local Page opening timed out", { cause: error });
|
|
1118
|
+
}
|
|
1119
|
+
if (error instanceof SessionBrowserServiceError) {
|
|
1120
|
+
const reason = error.code === "generation_mismatch"
|
|
1121
|
+
? "generation_changed"
|
|
1122
|
+
: error.code === "not_found" || error.code === "invalid_request"
|
|
1123
|
+
? "not_found"
|
|
1124
|
+
: error.code === "capacity"
|
|
1125
|
+
? "capacity"
|
|
1126
|
+
: error.code === "unavailable" &&
|
|
1127
|
+
error.message.includes("does not support")
|
|
1128
|
+
? "unsupported"
|
|
1129
|
+
: error.code === "unavailable" || error.code === "shutting_down"
|
|
1130
|
+
? "browser_closed"
|
|
1131
|
+
: "internal_error";
|
|
1132
|
+
return new DirectRuntimeBrowserInspectHostError(reason, error.message, {
|
|
1133
|
+
cause: error,
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
return new DirectRuntimeBrowserInspectHostError("internal_error", "Browser Inspect local Page is unavailable", { cause: error });
|
|
1137
|
+
}
|
|
582
1138
|
async function startDirectListener(start, config, options) {
|
|
583
1139
|
const candidates = [...new Set([config.port, ...(config.fallbackPorts ?? [])])];
|
|
584
1140
|
for (let index = 0; index < candidates.length; index += 1) {
|