@rynx-ai/daemon 0.1.0 → 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/browser-cli-args.d.ts +20 -0
- package/dist/browser-cli-args.js +100 -0
- package/dist/browser-runner-session-context.d.ts +24 -0
- package/dist/browser-runner-session-context.js +113 -0
- package/dist/cdp-keyboard-input.d.ts +6 -0
- package/dist/cdp-keyboard-input.js +209 -0
- package/dist/channel-store.d.ts +0 -6
- package/dist/channel-store.js +2 -18
- package/dist/chrome-for-testing-release-resolver.d.ts +34 -0
- package/dist/chrome-for-testing-release-resolver.js +214 -0
- package/dist/chrome-for-testing-store.d.ts +85 -0
- package/dist/chrome-for-testing-store.js +836 -0
- 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 +478 -30
- package/dist/control-client.d.ts +122 -0
- package/dist/control-client.js +855 -0
- package/dist/control-deps.js +37 -6
- package/dist/control-endpoint.d.ts +14 -0
- package/dist/control-endpoint.js +124 -0
- package/dist/control-link-store.d.ts +16 -0
- package/dist/control-link-store.js +53 -0
- package/dist/daemon-build-status.d.ts +8 -0
- package/dist/daemon-build-status.js +18 -0
- package/dist/daemon-owner.d.ts +14 -0
- package/dist/daemon-owner.js +104 -0
- package/dist/daemon-server.d.ts +29 -2
- package/dist/daemon-server.js +1113 -26
- package/dist/db.js +196 -10
- package/dist/desktop-browser-host-client.d.ts +35 -0
- package/dist/desktop-browser-host-client.js +415 -0
- package/dist/direct-runtime-authenticator.d.ts +13 -0
- package/dist/direct-runtime-authenticator.js +132 -0
- package/dist/direct-runtime-config.d.ts +20 -0
- package/dist/direct-runtime-config.js +101 -0
- package/dist/embedded-browser-profile-cleanup.d.ts +58 -0
- package/dist/embedded-browser-profile-cleanup.js +338 -0
- package/dist/headless-browser-host.d.ts +14 -0
- package/dist/headless-browser-host.js +2173 -0
- package/dist/index-daemon.js +55 -5
- package/dist/installation-id.d.ts +33 -0
- package/dist/installation-id.js +692 -0
- package/dist/maintenance-management.d.ts +11 -0
- package/dist/maintenance-management.js +13 -0
- package/dist/migrations/cleanup-legacy-sessions.d.ts +0 -3
- package/dist/migrations/cleanup-legacy-sessions.js +10 -28
- package/dist/plugin-cli.d.ts +30 -0
- package/dist/plugin-cli.js +270 -0
- package/dist/plugin-host-rpc.d.ts +85 -0
- package/dist/plugin-host-rpc.js +878 -0
- package/dist/plugin-install-restart.d.ts +13 -0
- package/dist/plugin-install-restart.js +24 -0
- package/dist/plugin-installer.d.ts +71 -15
- package/dist/plugin-installer.js +521 -155
- package/dist/plugin-management-service.d.ts +58 -0
- package/dist/plugin-management-service.js +240 -0
- package/dist/plugin-package.d.ts +61 -0
- package/dist/plugin-package.js +732 -0
- package/dist/plugin-runtime-control.d.ts +2 -0
- package/dist/plugin-runtime-control.js +37 -0
- package/dist/plugin-store.d.ts +37 -8
- package/dist/plugin-store.js +80 -15
- package/dist/plugin-supervisor.d.ts +160 -0
- package/dist/plugin-supervisor.js +1143 -0
- package/dist/pm2.d.ts +3 -1
- package/dist/pm2.js +92 -18
- package/dist/provider-cli-service.d.ts +46 -0
- package/dist/provider-cli-service.js +510 -0
- package/dist/registry.d.ts +6 -19
- package/dist/registry.js +22 -87
- package/dist/remote-runtime-access-store.d.ts +62 -0
- package/dist/remote-runtime-access-store.js +393 -0
- package/dist/remote-runtime-admin.d.ts +35 -0
- package/dist/remote-runtime-admin.js +110 -0
- package/dist/remote-runtime-connection-manager.d.ts +103 -0
- package/dist/remote-runtime-connection-manager.js +1575 -0
- package/dist/remote-runtime-credential-store.d.ts +45 -0
- package/dist/remote-runtime-credential-store.js +639 -0
- package/dist/remote-runtime-target-control.d.ts +51 -0
- package/dist/remote-runtime-target-control.js +488 -0
- package/dist/remote-runtime-target-store.d.ts +56 -0
- package/dist/remote-runtime-target-store.js +326 -0
- package/dist/runtime-share-addresses.d.ts +21 -0
- package/dist/runtime-share-addresses.js +50 -0
- package/dist/session-browser-capability-store.d.ts +13 -0
- package/dist/session-browser-capability-store.js +58 -0
- package/dist/session-emulator-binding-store.d.ts +10 -0
- package/dist/session-emulator-binding-store.js +61 -0
- package/dist/session-launch-operations.d.ts +41 -0
- package/dist/session-launch-operations.js +124 -0
- package/dist/session-log-store.js +29 -0
- package/dist/session-meta-store.d.ts +3 -2
- package/dist/session-meta-store.js +59 -7
- 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 +144 -5
- package/dist/skills-catalog.js +8 -4
- package/dist/update.d.ts +11 -2
- package/dist/update.js +0 -27
- package/package.json +23 -10
package/dist/daemon-server.js
CHANGED
|
@@ -5,19 +5,78 @@
|
|
|
5
5
|
* channel env hydration, plugin catalog loading, control deps, session log, and
|
|
6
6
|
* session registry. Keeping that wiring here avoids a second app-specific fork.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
9
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
9
12
|
import { dirname, join } from "node:path";
|
|
13
|
+
import WebSocket from "ws";
|
|
10
14
|
import { loadConfig, rynxHome } from "@rynx-ai/core";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
15
|
+
import { LOCAL_MANAGEMENT_PROTOCOL, } from "@rynx-ai/protocol/control";
|
|
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";
|
|
18
|
+
import { RUNTIME_BROWSER_SURFACE_SEMANTIC_CAPABILITY } from "@rynx-ai/protocol/runtime-browser-surface";
|
|
19
|
+
import { RUNTIME_EMULATOR_SURFACE_SEMANTIC_CAPABILITY } from "@rynx-ai/protocol/runtime-emulator-surface";
|
|
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";
|
|
13
22
|
import { buildControlDeps } from "./control-deps.js";
|
|
23
|
+
import { createBrowserArtifactManagementService } from "./browser-artifact-management.js";
|
|
24
|
+
import { createDaemonBuildStatus } from "./daemon-build-status.js";
|
|
25
|
+
import { BrowserRunnerSessionContextProvider } from "./browser-runner-session-context.js";
|
|
26
|
+
import { isChromeForTestingHostSupported } from "./chrome-for-testing-store.js";
|
|
27
|
+
import { ChromeInspectionManager, } from "./chrome-inspection-manager.js";
|
|
28
|
+
import { createHeadlessBrowserHost } from "./headless-browser-host.js";
|
|
29
|
+
import { acquireDaemonOwner } from "./daemon-owner.js";
|
|
30
|
+
import { DaemonDirectRuntimeAuthenticator } from "./direct-runtime-authenticator.js";
|
|
31
|
+
import { DurableEmbeddedBrowserProfileHost } from "./embedded-browser-profile-cleanup.js";
|
|
32
|
+
import { loadDirectRuntimeConfig, } from "./direct-runtime-config.js";
|
|
33
|
+
import { isDaemonDirectRuntimeHostingSupported, loadOrCreateDaemonDirectRuntimeEncryptionIdentity, loadOrCreateDaemonInstallationIdentity, } from "./installation-id.js";
|
|
14
34
|
import { cleanupLegacySessions } from "./migrations/cleanup-legacy-sessions.js";
|
|
35
|
+
import { createMaintenanceManagementService } from "./maintenance-management.js";
|
|
36
|
+
import { PluginManagementService } from "./plugin-management-service.js";
|
|
37
|
+
import { PluginSupervisor } from "./plugin-supervisor.js";
|
|
38
|
+
import { pruneInactivePluginInstallations } from "./plugin-installer.js";
|
|
39
|
+
import { runPluginCliCommand } from "./plugin-cli.js";
|
|
40
|
+
import { getPlugin, isRunnablePluginRecord, listEnabledPlugins } from "./plugin-store.js";
|
|
15
41
|
import { loadPlugins, resolveChannelInstances } from "./registry.js";
|
|
16
42
|
import { SqliteSessionLogStore } from "./session-log-store.js";
|
|
43
|
+
import { SqliteSessionResourceStore } from "./session-resource-store.js";
|
|
44
|
+
import { SessionBrowserCapabilityStore } from "./session-browser-capability-store.js";
|
|
45
|
+
import { SqliteSessionEmulatorBindingStore } from "./session-emulator-binding-store.js";
|
|
17
46
|
import { sessionRegistry } from "./session-meta-store.js";
|
|
47
|
+
import { sessionPendingMessageStore } from "./session-pending-message-store.js";
|
|
48
|
+
import { RemoteRuntimeAccessStore } from "./remote-runtime-access-store.js";
|
|
49
|
+
import { DaemonRemoteRuntimeAdmin } from "./remote-runtime-admin.js";
|
|
50
|
+
import { FileRemoteRuntimeCredentialStore } from "./remote-runtime-credential-store.js";
|
|
51
|
+
import { DaemonRuntimeConnectionManager } from "./remote-runtime-connection-manager.js";
|
|
52
|
+
import { DaemonRuntimeTargetControl } from "./remote-runtime-target-control.js";
|
|
53
|
+
import { RemoteRuntimeTargetStore } from "./remote-runtime-target-store.js";
|
|
54
|
+
import { ProviderCliService } from "./provider-cli-service.js";
|
|
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;
|
|
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
|
+
}
|
|
18
65
|
function defaultWarn(message) {
|
|
19
66
|
console.warn(JSON.stringify({ level: "warn", type: "channel-registry", msg: message }));
|
|
20
67
|
}
|
|
68
|
+
function unsupportedRemoteRuntimeAdmin(reason) {
|
|
69
|
+
const unsupported = () => {
|
|
70
|
+
throw new Error(reason);
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
listShareAddresses: unsupported,
|
|
74
|
+
createPairingOffer: unsupported,
|
|
75
|
+
listClientGrants: unsupported,
|
|
76
|
+
revokeClientGrant: unsupported,
|
|
77
|
+
deleteRevokedClientGrant: unsupported,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
21
80
|
function logLegacyCleanup(config) {
|
|
22
81
|
try {
|
|
23
82
|
const result = cleanupLegacySessions({ codexStorePath: config.AGENT_SESSION_STORE_PATH });
|
|
@@ -44,19 +103,69 @@ function logLegacyCleanup(config) {
|
|
|
44
103
|
function dialableHost(host) {
|
|
45
104
|
return host === "0.0.0.0" || host === "::" || host.trim() === "" ? "127.0.0.1" : host;
|
|
46
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
|
+
}
|
|
47
114
|
function controlEndpointFile() {
|
|
48
115
|
return join(rynxHome(), "control.json");
|
|
49
116
|
}
|
|
50
|
-
async function writeControlEndpoint(config) {
|
|
117
|
+
async function writeControlEndpoint(config, managementToken) {
|
|
51
118
|
const file = controlEndpointFile();
|
|
119
|
+
const temporary = `${file}.tmp-${process.pid}-${randomUUID()}`;
|
|
52
120
|
await mkdir(dirname(file), { recursive: true });
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
121
|
+
try {
|
|
122
|
+
await writeFile(temporary, `${JSON.stringify({
|
|
123
|
+
origin: daemonControlOrigin(config),
|
|
124
|
+
host: config.HOST,
|
|
125
|
+
port: config.PORT,
|
|
126
|
+
pid: process.pid,
|
|
127
|
+
managementToken,
|
|
128
|
+
managementProtocol: LOCAL_MANAGEMENT_PROTOCOL,
|
|
129
|
+
...controlEndpointDistributionMetadata(),
|
|
130
|
+
updatedAt: new Date().toISOString(),
|
|
131
|
+
}, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
132
|
+
await rename(temporary, file);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
await rm(temporary, { force: true });
|
|
136
|
+
}
|
|
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
|
+
}
|
|
60
169
|
}
|
|
61
170
|
async function removeControlEndpointForCurrentProcess() {
|
|
62
171
|
try {
|
|
@@ -70,21 +179,999 @@ async function removeControlEndpointForCurrentProcess() {
|
|
|
70
179
|
// Best effort only; stale endpoint files are ignored by the client fallback.
|
|
71
180
|
}
|
|
72
181
|
}
|
|
73
|
-
export async function startRynxDaemonServer({ config, warn = defaultWarn, } = {}) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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), } = {}) {
|
|
183
|
+
const owner = await acquireOwner();
|
|
184
|
+
let ownerManagedByServer = false;
|
|
185
|
+
try {
|
|
186
|
+
const appConfig = config ?? loadConfig();
|
|
187
|
+
const appBrowserHostLaunch = parseAppBrowserHostLaunchConfig();
|
|
188
|
+
const directHostingSupported = isDaemonDirectRuntimeHostingSupported();
|
|
189
|
+
const runtimeBrowserSupported = appBrowserHostLaunch !== undefined || isChromeForTestingHostSupported();
|
|
190
|
+
const directConfig = directHostingSupported
|
|
191
|
+
? await loadDirectConfig()
|
|
192
|
+
: undefined;
|
|
193
|
+
const control = buildControlDeps();
|
|
194
|
+
const installationIdentity = await loadOrCreateDaemonInstallationIdentity();
|
|
195
|
+
const directEncryptionIdentity = directHostingSupported
|
|
196
|
+
? await loadOrCreateDaemonDirectRuntimeEncryptionIdentity()
|
|
197
|
+
: undefined;
|
|
198
|
+
if (!directHostingSupported)
|
|
199
|
+
warn(DIRECT_RUNTIME_HOSTING_UNSUPPORTED);
|
|
200
|
+
const daemonInstanceId = randomUUID();
|
|
201
|
+
const daemonStartedAt = new Date();
|
|
202
|
+
const daemonBuildStatus = createDaemonBuildStatus(fileURLToPath(import.meta.url), daemonStartedAt.getTime());
|
|
203
|
+
const remoteRuntime = createDaemonRuntimeHost({
|
|
204
|
+
daemonId: installationIdentity.installationId,
|
|
205
|
+
daemonInstanceId,
|
|
206
|
+
coreProtocolVersion: REMOTE_RUNTIME_CORE_PROTOCOL.currentVersion,
|
|
207
|
+
minimumCompatibleCoreVersion: REMOTE_RUNTIME_CORE_PROTOCOL.minimumCompatibleVersion,
|
|
208
|
+
semanticCapabilities: [
|
|
209
|
+
"semantic.session.list.v1",
|
|
210
|
+
"semantic.session.snapshot.v1",
|
|
211
|
+
"semantic.session.events.v1",
|
|
212
|
+
"semantic.session.terminal.v1",
|
|
213
|
+
"semantic.session.terminal-start.v1",
|
|
214
|
+
"semantic.session.interrupt.v1",
|
|
215
|
+
"semantic.session.agent-options.v1",
|
|
216
|
+
"semantic.session.launch-options.v2",
|
|
217
|
+
"semantic.session.create.v1",
|
|
218
|
+
"semantic.session.message.v1",
|
|
219
|
+
"semantic.session.resources.v1",
|
|
220
|
+
"semantic.session.message-queue.v1",
|
|
221
|
+
"semantic.session.delete.v1",
|
|
222
|
+
"semantic.session.interaction.v1",
|
|
223
|
+
"semantic.provider-cli.status.v1",
|
|
224
|
+
...(runtimeBrowserSupported ? ["semantic.browser.v1"] : []),
|
|
225
|
+
...(runtimeBrowserSupported && directHostingSupported
|
|
226
|
+
? [
|
|
227
|
+
RUNTIME_BROWSER_SURFACE_SEMANTIC_CAPABILITY,
|
|
228
|
+
RUNTIME_BROWSER_INSPECT_SEMANTIC_CAPABILITY,
|
|
229
|
+
]
|
|
230
|
+
: []),
|
|
231
|
+
...(control.emulator
|
|
232
|
+
? ["semantic.emulator.unary.v1", "semantic.session.emulator.v1"]
|
|
233
|
+
: []),
|
|
234
|
+
...(control.emulator && directHostingSupported
|
|
235
|
+
? [RUNTIME_EMULATOR_SURFACE_SEMANTIC_CAPABILITY]
|
|
236
|
+
: []),
|
|
237
|
+
],
|
|
238
|
+
startedAt: daemonStartedAt,
|
|
239
|
+
});
|
|
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;
|
|
254
|
+
try {
|
|
255
|
+
for (const removed of pruneInactivePluginInstallations()) {
|
|
256
|
+
console.log(JSON.stringify({ level: "info", type: "plugin-gc", removed }));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
warn(`plugin package cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
261
|
+
}
|
|
262
|
+
const pluginSupervisor = new PluginSupervisor(listEnabledPlugins(), {
|
|
263
|
+
daemonInstanceId,
|
|
264
|
+
log: (entry) => {
|
|
265
|
+
const output = entry.level === "error"
|
|
266
|
+
? console.error
|
|
267
|
+
: entry.level === "warn"
|
|
268
|
+
? console.warn
|
|
269
|
+
: console.log;
|
|
270
|
+
output(JSON.stringify(entry));
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
const pluginManagement = new PluginManagementService();
|
|
274
|
+
const browserArtifactManagement = createBrowserArtifactManagementService();
|
|
275
|
+
const maintenanceManagement = createMaintenanceManagementService({
|
|
276
|
+
codexStorePath: appConfig.AGENT_SESSION_STORE_PATH,
|
|
277
|
+
});
|
|
278
|
+
await loadPlugins(pluginSupervisor, warn);
|
|
279
|
+
logLegacyCleanup(appConfig);
|
|
280
|
+
const reloadPluginRuntime = async (pluginId) => {
|
|
281
|
+
const record = getPlugin(pluginId);
|
|
282
|
+
if (!record)
|
|
283
|
+
throw new Error(`plugin "${pluginId}" is not installed`);
|
|
284
|
+
if (!record.enabled)
|
|
285
|
+
throw new Error(`plugin "${pluginId}" is disabled`);
|
|
286
|
+
if (!isRunnablePluginRecord(record)) {
|
|
287
|
+
throw new Error(`plugin "${pluginId}" must be reinstalled before its runtime can start`);
|
|
288
|
+
}
|
|
289
|
+
const status = await pluginSupervisor.reloadPluginRuntime(record);
|
|
290
|
+
await loadPlugins(pluginSupervisor, warn);
|
|
291
|
+
return { pluginId, status };
|
|
292
|
+
};
|
|
293
|
+
const ensurePluginRuntime = async (pluginId) => {
|
|
294
|
+
const record = getPlugin(pluginId);
|
|
295
|
+
if (!record)
|
|
296
|
+
throw new Error(`plugin "${pluginId}" is not installed`);
|
|
297
|
+
if (!record.enabled)
|
|
298
|
+
throw new Error(`plugin "${pluginId}" is disabled`);
|
|
299
|
+
if (!isRunnablePluginRecord(record)) {
|
|
300
|
+
throw new Error(`plugin "${pluginId}" must be reinstalled before its runtime can start`);
|
|
301
|
+
}
|
|
302
|
+
const status = await pluginSupervisor.ensurePluginRuntime(record);
|
|
303
|
+
await loadPlugins(pluginSupervisor, warn);
|
|
304
|
+
return { pluginId, status };
|
|
305
|
+
};
|
|
306
|
+
const invokePluginCli = async ({ pluginId, args, stdin }, { signal }) => {
|
|
307
|
+
const result = await runPluginCliCommand(pluginId, args, {
|
|
308
|
+
stdio: "pipe",
|
|
309
|
+
...(stdin !== undefined ? { stdin } : {}),
|
|
310
|
+
captureOutputBytes: PLUGIN_CLI_OUTPUT_MAX_BYTES,
|
|
311
|
+
signal,
|
|
312
|
+
});
|
|
313
|
+
if (result.code === 0) {
|
|
314
|
+
const reload = result.effects.some((effect) => effect.effect === "runtime.reload");
|
|
315
|
+
const ensure = result.effects.some((effect) => effect.effect === "runtime.ensure");
|
|
316
|
+
if (reload)
|
|
317
|
+
await reloadPluginRuntime(pluginId);
|
|
318
|
+
else if (ensure)
|
|
319
|
+
await ensurePluginRuntime(pluginId);
|
|
320
|
+
}
|
|
321
|
+
const output = result.output;
|
|
322
|
+
return {
|
|
323
|
+
code: result.code,
|
|
324
|
+
stdout: output?.stdout ?? "",
|
|
325
|
+
stderr: output?.stderr ?? "",
|
|
326
|
+
truncated: Boolean(output?.stdoutTruncated || output?.stderrTruncated),
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
const pluginRuntime = {
|
|
330
|
+
bindHostServices: (services) => pluginSupervisor.bindHostServices({
|
|
331
|
+
...services,
|
|
332
|
+
installationId: installationIdentity.installationId,
|
|
333
|
+
installationProof: {
|
|
334
|
+
algorithm: installationIdentity.algorithm,
|
|
335
|
+
publicKey: installationIdentity.publicKey,
|
|
336
|
+
sign: installationIdentity.sign,
|
|
337
|
+
},
|
|
338
|
+
}),
|
|
339
|
+
startPluginRuntimes: pluginSupervisor.startPluginRuntimes.bind(pluginSupervisor),
|
|
340
|
+
reloadPluginRuntime,
|
|
341
|
+
ensurePluginRuntime,
|
|
342
|
+
statusSnapshot: pluginSupervisor.statusSnapshot.bind(pluginSupervisor),
|
|
343
|
+
dispose: pluginSupervisor.dispose.bind(pluginSupervisor),
|
|
344
|
+
};
|
|
345
|
+
const remoteRuntimeAccess = new RemoteRuntimeAccessStore();
|
|
346
|
+
const directConnections = new DirectRuntimeConnectionHub();
|
|
347
|
+
const sessionLog = new SqliteSessionLogStore();
|
|
348
|
+
const sessionResources = new SqliteSessionResourceStore();
|
|
349
|
+
const providerClis = new ProviderCliService();
|
|
350
|
+
const sessionBrowserCapabilities = new SessionBrowserCapabilityStore();
|
|
351
|
+
const durableDesktopBrowserHost = new DurableEmbeddedBrowserProfileHost(desktopBrowserHosts, { warn });
|
|
352
|
+
const browserRunnerSessionContexts = new BrowserRunnerSessionContextProvider({
|
|
353
|
+
rootDir: join(rynxHome(), "run", "browser-session-contexts"),
|
|
354
|
+
capabilities: sessionBrowserCapabilities,
|
|
355
|
+
});
|
|
356
|
+
const sessionRuntimeServices = createSessionRuntimeServices({
|
|
357
|
+
config: appConfig,
|
|
358
|
+
sessionLog,
|
|
359
|
+
sessionContextProvider: browserRunnerSessionContexts,
|
|
360
|
+
onMirrorError: (error, sessionId) => warn(`Session mirror failed for ${sessionId}: ${error instanceof Error ? error.message : String(error)}`),
|
|
361
|
+
});
|
|
362
|
+
const sessionBrowsers = new SessionBrowserService({
|
|
363
|
+
host: appBrowserHostSupervisor
|
|
364
|
+
? createSupervisedAppSessionBrowserHost(durableDesktopBrowserHost, appBrowserHostSupervisor)
|
|
365
|
+
: createDesktopFirstSessionBrowserHost(durableDesktopBrowserHost, createHeadlessBrowserHost()),
|
|
366
|
+
sessions: {
|
|
367
|
+
exists: async (sessionId) => Boolean(sessionRegistry.get(sessionId) ??
|
|
368
|
+
await sessionRuntimeServices.sessionStore.get(sessionId)),
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
const sessionEmulators = control.emulator
|
|
372
|
+
? new SessionEmulatorService({
|
|
373
|
+
emulator: control.emulator,
|
|
374
|
+
store: new SqliteSessionEmulatorBindingStore(),
|
|
375
|
+
sessions: {
|
|
376
|
+
exists: async (sessionId) => Boolean(sessionRegistry.get(sessionId) ??
|
|
377
|
+
await sessionRuntimeServices.sessionStore.get(sessionId)),
|
|
378
|
+
},
|
|
379
|
+
})
|
|
380
|
+
: undefined;
|
|
381
|
+
const sessionBrowserSurfaces = new SessionBrowserSurfaceCoordinator(sessionBrowsers);
|
|
382
|
+
const sessionEmulatorSurface = sessionEmulators
|
|
383
|
+
? {
|
|
384
|
+
open: async (frame, context) => {
|
|
385
|
+
const surface = await sessionEmulators.openSurface({
|
|
386
|
+
sessionId: frame.sessionId,
|
|
387
|
+
bindingId: frame.bindingId,
|
|
388
|
+
signal: context.signal,
|
|
389
|
+
});
|
|
390
|
+
let closed = false;
|
|
391
|
+
const close = () => {
|
|
392
|
+
if (closed)
|
|
393
|
+
return;
|
|
394
|
+
closed = true;
|
|
395
|
+
context.signal.removeEventListener("abort", close);
|
|
396
|
+
surface.close();
|
|
397
|
+
};
|
|
398
|
+
context.signal.addEventListener("abort", close, { once: true });
|
|
399
|
+
if (context.signal.aborted)
|
|
400
|
+
close();
|
|
401
|
+
return {
|
|
402
|
+
ready: {
|
|
403
|
+
type: "emulator.surface.ready",
|
|
404
|
+
surfaceId: frame.surfaceId,
|
|
405
|
+
sessionId: frame.sessionId,
|
|
406
|
+
bindingId: frame.bindingId,
|
|
407
|
+
backend: surface.backend,
|
|
408
|
+
deviceId: surface.deviceId,
|
|
409
|
+
format: surface.format,
|
|
410
|
+
},
|
|
411
|
+
capture: () => surface.capture(),
|
|
412
|
+
handle: (input) => surface.touch({ type: input.phase, x: input.x, y: input.y }),
|
|
413
|
+
close,
|
|
414
|
+
};
|
|
415
|
+
},
|
|
416
|
+
}
|
|
417
|
+
: undefined;
|
|
418
|
+
const sessionResourceLifecycle = {
|
|
419
|
+
beforeDelete: async (sessionId) => {
|
|
420
|
+
await Promise.all([
|
|
421
|
+
sessionBrowsers.cleanupSession(sessionId),
|
|
422
|
+
sessionEmulators?.cleanupSession(sessionId),
|
|
423
|
+
]);
|
|
424
|
+
},
|
|
425
|
+
afterDelete: (sessionId) => {
|
|
426
|
+
sessionBrowserCapabilities.revoke(sessionId);
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
const machineSessions = new MachineSessionService({
|
|
430
|
+
registry: sessionRegistry,
|
|
431
|
+
log: sessionLog,
|
|
432
|
+
events: sessionRuntimeServices.sessionBus,
|
|
433
|
+
runtimeState: sessionRuntimeServices.sessionRuntimeIndex,
|
|
434
|
+
interruption: {
|
|
435
|
+
interrupt: (sessionId) => sessionRuntimeServices.runnerManager.interruptLiveSession(sessionId),
|
|
436
|
+
},
|
|
437
|
+
lifecycle: sessionResourceLifecycle,
|
|
438
|
+
agents: {
|
|
439
|
+
list: () => control.listAgents(),
|
|
440
|
+
},
|
|
441
|
+
pendingMessages: sessionPendingMessageStore,
|
|
442
|
+
resources: sessionResources,
|
|
443
|
+
execution: {
|
|
444
|
+
runtime: sessionRuntimeServices.conversationRuntime,
|
|
445
|
+
runner: sessionRuntimeServices.runnerManager,
|
|
446
|
+
sessionStore: sessionRuntimeServices.sessionStore,
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
const directAuthenticator = new DaemonDirectRuntimeAuthenticator(installationIdentity, remoteRuntime, remoteRuntimeAccess);
|
|
450
|
+
const sessionTerminals = createRunnerSessionTerminalHost({
|
|
451
|
+
runnerManager: sessionRuntimeServices.runnerManager,
|
|
452
|
+
resolveLiveSession: async (sessionId) => {
|
|
453
|
+
const bound = await sessionRuntimeServices.sessionStore.get(sessionId);
|
|
454
|
+
const registered = sessionRegistry.get(sessionId);
|
|
455
|
+
const cwd = bound?.cwd ?? registered?.config?.osEnv?.cwd;
|
|
456
|
+
if ((!bound && !registered) ||
|
|
457
|
+
!cwd ||
|
|
458
|
+
!sessionRuntimeServices.runnerManager.hasLiveSession(sessionId)) {
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
return { cwd };
|
|
462
|
+
},
|
|
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
|
+
};
|
|
511
|
+
const remoteRuntimeDispatcher = new RemoteRuntimeDispatcher({
|
|
512
|
+
runtime: remoteRuntime,
|
|
513
|
+
emulator: control.emulator,
|
|
514
|
+
sessionEmulators,
|
|
515
|
+
sessions: machineSessions,
|
|
516
|
+
terminals: sessionTerminals,
|
|
517
|
+
browsers: sessionBrowsers,
|
|
518
|
+
providerClis,
|
|
519
|
+
onError: (error) => warn(`Remote Runtime dispatch failed: ${error instanceof Error ? error.message : String(error)}`),
|
|
520
|
+
});
|
|
521
|
+
const remoteRuntimeAdmin = directConfig && directEncryptionIdentity
|
|
522
|
+
? new DaemonRemoteRuntimeAdmin(installationIdentity, directEncryptionIdentity, remoteRuntimeAccess, directConfig, directConnections)
|
|
523
|
+
: unsupportedRemoteRuntimeAdmin(DIRECT_RUNTIME_HOSTING_UNSUPPORTED);
|
|
524
|
+
const runtimeTargets = new RemoteRuntimeTargetStore();
|
|
525
|
+
const runtimeCredentials = new FileRemoteRuntimeCredentialStore();
|
|
526
|
+
const runtimeConnectionManager = new DaemonRuntimeConnectionManager({
|
|
527
|
+
localDaemonId: remoteRuntime.status().daemonId,
|
|
528
|
+
dispatcher: remoteRuntimeDispatcher,
|
|
529
|
+
localBrowserSurface: sessionBrowserSurfaces,
|
|
530
|
+
localEmulatorSurface: sessionEmulatorSurface,
|
|
531
|
+
targets: runtimeTargets,
|
|
532
|
+
credentials: runtimeCredentials,
|
|
533
|
+
});
|
|
534
|
+
const runtimeTargetControl = new DaemonRuntimeTargetControl(remoteRuntime, runtimeTargets, runtimeCredentials, runtimeConnectionManager);
|
|
535
|
+
const directBrowserInspectHost = createDirectRuntimeBrowserInspectHost(sessionBrowsers);
|
|
536
|
+
let chromeInspectionManager;
|
|
537
|
+
let server;
|
|
538
|
+
let baseShutdown;
|
|
539
|
+
let directServer;
|
|
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);
|
|
586
|
+
if (directConfig && directEncryptionIdentity) {
|
|
587
|
+
const startedDirect = await startDirectListener(startDirectServer, directConfig, {
|
|
588
|
+
e2ee: {
|
|
589
|
+
serverPublicKey: directEncryptionIdentity.publicKey,
|
|
590
|
+
deriveSharedSecret: (clientPublicKey) => directEncryptionIdentity.deriveSharedSecret(clientPublicKey),
|
|
591
|
+
},
|
|
592
|
+
authenticator: directAuthenticator,
|
|
593
|
+
dispatcher: remoteRuntimeDispatcher,
|
|
594
|
+
isPeerActive: (peer) => remoteRuntimeAccess.isClientGrantActive(peer.grantId, peer.clientId),
|
|
595
|
+
connectionHub: directConnections,
|
|
596
|
+
browserSurface: { host: sessionBrowserSurfaces },
|
|
597
|
+
...(runtimeBrowserSupported
|
|
598
|
+
? { browserInspect: { host: directBrowserInspectHost } }
|
|
599
|
+
: {}),
|
|
600
|
+
...(sessionEmulatorSurface
|
|
601
|
+
? { emulatorSurface: { host: sessionEmulatorSurface } }
|
|
602
|
+
: {}),
|
|
603
|
+
onError: (error, context) => warn(`Direct Runtime ${context} failed: ${error instanceof Error ? error.message : String(error)}`),
|
|
604
|
+
});
|
|
605
|
+
directServer = startedDirect.server;
|
|
606
|
+
// The admin object retains this config reference, so pairing links use
|
|
607
|
+
// the actual OS-selected fallback port instead of the requested zero.
|
|
608
|
+
directConfig.port = startedDirect.port;
|
|
609
|
+
try {
|
|
610
|
+
await directConfig.recordListeningPort?.(startedDirect.port);
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
warn(`Direct Runtime fallback port could not be persisted: ${error instanceof Error ? error.message : String(error)}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
catch (startupError) {
|
|
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
|
+
}
|
|
628
|
+
if (directServer) {
|
|
629
|
+
try {
|
|
630
|
+
await directServer.shutdown();
|
|
631
|
+
}
|
|
632
|
+
catch (shutdownError) {
|
|
633
|
+
rollbackErrors.push(shutdownError);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
try {
|
|
637
|
+
await runtimeConnectionManager.close();
|
|
638
|
+
}
|
|
639
|
+
catch (shutdownError) {
|
|
640
|
+
rollbackErrors.push(shutdownError);
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
643
|
+
await sessionBrowsers.closeAll();
|
|
644
|
+
}
|
|
645
|
+
catch (shutdownError) {
|
|
646
|
+
rollbackErrors.push(shutdownError);
|
|
647
|
+
}
|
|
648
|
+
finally {
|
|
649
|
+
durableDesktopBrowserHost.dispose();
|
|
650
|
+
}
|
|
651
|
+
if (appBrowserHostSupervisor) {
|
|
652
|
+
try {
|
|
653
|
+
await appBrowserHostSupervisor.stop();
|
|
654
|
+
}
|
|
655
|
+
catch (shutdownError) {
|
|
656
|
+
rollbackErrors.push(shutdownError);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (baseShutdown) {
|
|
660
|
+
try {
|
|
661
|
+
await baseShutdown();
|
|
662
|
+
}
|
|
663
|
+
catch (shutdownError) {
|
|
664
|
+
rollbackErrors.push(shutdownError);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (rollbackErrors.length > 1) {
|
|
668
|
+
throw new AggregateError(rollbackErrors, "failed to start daemon resources and roll back the local server");
|
|
669
|
+
}
|
|
670
|
+
throw startupError;
|
|
671
|
+
}
|
|
672
|
+
if (!server || !baseShutdown || !chromeInspectionManager) {
|
|
673
|
+
throw new Error("daemon startup completed without all required resources");
|
|
674
|
+
}
|
|
675
|
+
let shutdownPromise;
|
|
676
|
+
server.shutdown = () => {
|
|
677
|
+
if (shutdownPromise)
|
|
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();
|
|
682
|
+
shutdownPromise = (async () => {
|
|
683
|
+
let shutdownComplete = false;
|
|
684
|
+
try {
|
|
685
|
+
const shutdownErrors = [];
|
|
686
|
+
try {
|
|
687
|
+
await chromeInspectionManager.shutdown();
|
|
688
|
+
}
|
|
689
|
+
catch (error) {
|
|
690
|
+
shutdownErrors.push(error);
|
|
691
|
+
}
|
|
692
|
+
if (directServer) {
|
|
693
|
+
try {
|
|
694
|
+
await directServer.shutdown();
|
|
695
|
+
}
|
|
696
|
+
catch (error) {
|
|
697
|
+
shutdownErrors.push(error);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
await runtimeConnectionManager.close();
|
|
702
|
+
}
|
|
703
|
+
catch (error) {
|
|
704
|
+
shutdownErrors.push(error);
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
await sessionBrowsers.closeAll();
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
shutdownErrors.push(error);
|
|
711
|
+
}
|
|
712
|
+
finally {
|
|
713
|
+
durableDesktopBrowserHost.dispose();
|
|
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
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
browserRunnerSessionContexts.closeAll();
|
|
731
|
+
sessionBrowserCapabilities.revokeAll();
|
|
732
|
+
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
shutdownErrors.push(error);
|
|
735
|
+
}
|
|
736
|
+
if (shutdownErrors.length > 0) {
|
|
737
|
+
throw new AggregateError(shutdownErrors, "failed to shut down all daemon listeners");
|
|
738
|
+
}
|
|
739
|
+
shutdownComplete = true;
|
|
740
|
+
}
|
|
741
|
+
finally {
|
|
742
|
+
await removeControlEndpointForCurrentProcess();
|
|
743
|
+
// A failed resource shutdown may leave plugin transports alive. Keep
|
|
744
|
+
// the process-scoped owner lease in that fail-closed state; it will be
|
|
745
|
+
// released by the OS when the process exits.
|
|
746
|
+
if (shutdownComplete)
|
|
747
|
+
await owner.release();
|
|
748
|
+
}
|
|
749
|
+
})();
|
|
750
|
+
return shutdownPromise;
|
|
751
|
+
};
|
|
752
|
+
requestServerShutdown = server.shutdown.bind(server);
|
|
753
|
+
ownerManagedByServer = true;
|
|
754
|
+
try {
|
|
755
|
+
await writeEndpoint(appConfig, managementToken);
|
|
756
|
+
}
|
|
757
|
+
catch (startupError) {
|
|
758
|
+
try {
|
|
759
|
+
await server.shutdown();
|
|
760
|
+
}
|
|
761
|
+
catch (shutdownError) {
|
|
762
|
+
throw new AggregateError([startupError, shutdownError], "failed to publish the daemon endpoint and roll back the server");
|
|
763
|
+
}
|
|
764
|
+
throw startupError;
|
|
765
|
+
}
|
|
766
|
+
return server;
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
if (!ownerManagedByServer)
|
|
770
|
+
await owner.release();
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
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)));
|
|
84
1034
|
});
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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);
|
|
88
1070
|
});
|
|
89
|
-
|
|
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
|
+
}
|
|
1138
|
+
async function startDirectListener(start, config, options) {
|
|
1139
|
+
const candidates = [...new Set([config.port, ...(config.fallbackPorts ?? [])])];
|
|
1140
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
1141
|
+
const requestedPort = candidates[index];
|
|
1142
|
+
try {
|
|
1143
|
+
const server = await start({
|
|
1144
|
+
...options,
|
|
1145
|
+
host: config.bindHost,
|
|
1146
|
+
port: requestedPort,
|
|
1147
|
+
});
|
|
1148
|
+
const address = server.address();
|
|
1149
|
+
if (address && typeof address !== "string" && address.port > 0) {
|
|
1150
|
+
return { server, port: address.port };
|
|
1151
|
+
}
|
|
1152
|
+
if (requestedPort > 0)
|
|
1153
|
+
return { server, port: requestedPort };
|
|
1154
|
+
try {
|
|
1155
|
+
await server.shutdown();
|
|
1156
|
+
}
|
|
1157
|
+
catch (shutdownError) {
|
|
1158
|
+
throw new AggregateError([new Error("Direct Runtime did not report its OS-selected port"), shutdownError], "failed to resolve Direct Runtime listening port");
|
|
1159
|
+
}
|
|
1160
|
+
throw new Error("Direct Runtime did not report its OS-selected port");
|
|
1161
|
+
}
|
|
1162
|
+
catch (error) {
|
|
1163
|
+
const hasAnotherCandidate = index + 1 < candidates.length;
|
|
1164
|
+
const failedPersistedPreference = index === 0 && config.preferredPortIsPersistedFallback === true;
|
|
1165
|
+
if (!hasAnotherCandidate || (!failedPersistedPreference && !isAddressInUse(error))) {
|
|
1166
|
+
throw error;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
throw new Error("Direct Runtime has no listener port candidate");
|
|
1171
|
+
}
|
|
1172
|
+
function isAddressInUse(error) {
|
|
1173
|
+
return Boolean(error &&
|
|
1174
|
+
typeof error === "object" &&
|
|
1175
|
+
"code" in error &&
|
|
1176
|
+
error.code === "EADDRINUSE");
|
|
90
1177
|
}
|