@multiplayer-app/sandbox 7.0.1 → 7.0.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/README.md +7 -0
- package/dist/cli.js +504 -105
- package/dist/cli.js.map +4 -4
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/console-relays.d.ts +4 -4
- package/dist/console-relays.d.ts.map +1 -1
- package/dist/executor/index.d.ts +12 -1
- package/dist/executor/index.d.ts.map +1 -1
- package/dist/executor/lambda-microvm-executor.d.ts +13 -3
- package/dist/executor/lambda-microvm-executor.d.ts.map +1 -1
- package/dist/executor/types.d.ts +16 -2
- package/dist/executor/types.d.ts.map +1 -1
- package/dist/images.d.ts.map +1 -1
- package/dist/index.js +409 -53
- package/dist/index.js.map +4 -4
- package/dist/services/serial-relay.d.ts +11 -0
- package/dist/services/serial-relay.d.ts.map +1 -1
- package/dist/socket-client.d.ts.map +1 -1
- package/dist/vendor/builder-types/events.d.ts +10 -0
- package/dist/vendor/builder-types/events.d.ts.map +1 -1
- package/dist/vendor/builder-types/index.d.ts +1 -0
- package/dist/vendor/builder-types/index.d.ts.map +1 -1
- package/dist/vendor/builder-types/release.d.ts +5 -5
- package/dist/vendor/builder-types/release.d.ts.map +1 -1
- package/dist/vendor/builder-types/vm-cost.d.ts +88 -0
- package/dist/vendor/builder-types/vm-cost.d.ts.map +1 -0
- package/dist/vendor/builder-types/vm.d.ts +5 -0
- package/dist/vendor/builder-types/vm.d.ts.map +1 -1
- package/dist/worker-identity.d.ts +11 -0
- package/dist/worker-identity.d.ts.map +1 -0
- package/package.json +8 -4
package/dist/index.js
CHANGED
|
@@ -58,7 +58,7 @@ var import_os2 = __toESM(require("os"));
|
|
|
58
58
|
var import_path2 = __toESM(require("path"));
|
|
59
59
|
|
|
60
60
|
// package.json
|
|
61
|
-
var version = "7.0.
|
|
61
|
+
var version = "7.0.2";
|
|
62
62
|
|
|
63
63
|
// src/images.ts
|
|
64
64
|
var import_fs = __toESM(require("fs"));
|
|
@@ -77,6 +77,14 @@ var WorkerEvent = {
|
|
|
77
77
|
// worker → sandbox, fire-and-forget like HEARTBEAT — sent once when a 'job' VM's
|
|
78
78
|
// entrypoint exits on its own (the worker has already torn the resource down by then).
|
|
79
79
|
VM_RUN_EXITED: "vm:run-exited",
|
|
80
|
+
// worker → sandbox after an asynchronous VM_CREATE finishes. Socket.IO acknowledgements
|
|
81
|
+
// cannot be relied on across the detached Redis-adapter emitter used by the API.
|
|
82
|
+
VM_CREATE_RESULT: "vm:create-result",
|
|
83
|
+
// worker → sandbox, fire-and-forget — sent when periodic reconciliation finds the
|
|
84
|
+
// underlying resource gone without this worker ever issuing a destroy (e.g. AWS's own
|
|
85
|
+
// idle-suspend/max-duration policies terminating a Lambda MicroVM). Only engines whose
|
|
86
|
+
// VMs can disappear outside an explicit command from this process emit it.
|
|
87
|
+
VM_EXTERNALLY_TERMINATED: "vm:externally-terminated",
|
|
80
88
|
// sandbox → worker, generated by the control plane and reused by this worker process
|
|
81
89
|
// on Socket.IO reconnects. This keeps duplicate configured WORKER_ID values isolated.
|
|
82
90
|
ASSIGNED_ID: "worker:assigned-id",
|
|
@@ -104,6 +112,8 @@ var WorkerEvent = {
|
|
|
104
112
|
// sandbox → worker (browser input forwarded downstream)
|
|
105
113
|
VNC_MOUSE: "vnc:mouse",
|
|
106
114
|
VNC_KEYBOARD: "vnc:keyboard",
|
|
115
|
+
TERMINAL_ATTACH: "terminal:attach",
|
|
116
|
+
TERMINAL_DETACH: "terminal:detach",
|
|
107
117
|
TERMINAL_INPUT: "terminal:input"
|
|
108
118
|
};
|
|
109
119
|
var VM_ACTION_EVENTS = {
|
|
@@ -148,7 +158,20 @@ function s3Key(prefix, arch, stack, file) {
|
|
|
148
158
|
async function makeS3Client(endpoint) {
|
|
149
159
|
const { S3Client } = await import("@aws-sdk/client-s3");
|
|
150
160
|
return new S3Client({
|
|
151
|
-
...endpoint ? {
|
|
161
|
+
...endpoint ? {
|
|
162
|
+
endpoint,
|
|
163
|
+
forcePathStyle: true,
|
|
164
|
+
// A custom endpoint means a local/self-hosted store (MinIO, etc.), not real AWS.
|
|
165
|
+
// Use its own static credentials explicitly rather than the ambient default
|
|
166
|
+
// provider chain — otherwise, whenever AWS_PROFILE is also set (e.g. for the
|
|
167
|
+
// lambda-microvm engine's real AWS calls), the SDK unconditionally skips static env
|
|
168
|
+
// credentials process-wide and sends the real AWS SSO session to this local store
|
|
169
|
+
// instead, which rejects it since it only recognizes its own configured user.
|
|
170
|
+
credentials: {
|
|
171
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "minioadmin",
|
|
172
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "minioadmin"
|
|
173
|
+
}
|
|
174
|
+
} : {},
|
|
152
175
|
...process.env.AWS_REGION ? {} : { region: "us-east-1" },
|
|
153
176
|
// aws-chunked checksum streaming breaks older S3-compatible stores (MinIO)
|
|
154
177
|
requestChecksumCalculation: "WHEN_REQUIRED"
|
|
@@ -169,7 +192,10 @@ async function downloadFromHttp(opts, stack, file, dest) {
|
|
|
169
192
|
if (!response.ok || !response.body) {
|
|
170
193
|
throw new Error(`GET ${url} failed: ${response.status} ${response.statusText}`);
|
|
171
194
|
}
|
|
172
|
-
await (0, import_promises.pipeline)(
|
|
195
|
+
await (0, import_promises.pipeline)(
|
|
196
|
+
import_stream.Readable.fromWeb(response.body),
|
|
197
|
+
import_fs.default.createWriteStream(dest)
|
|
198
|
+
);
|
|
173
199
|
}
|
|
174
200
|
async function pullImages(opts, destDir) {
|
|
175
201
|
const isS3 = opts.location.startsWith("s3://");
|
|
@@ -204,11 +230,13 @@ async function pushImages(opts, srcDir) {
|
|
|
204
230
|
throw new Error(`${src} not found \u2014 run scripts/setup-vm-images.sh first`);
|
|
205
231
|
}
|
|
206
232
|
const key = s3Key(prefix, opts.arch, stack, file);
|
|
207
|
-
await client.send(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
233
|
+
await client.send(
|
|
234
|
+
new PutObjectCommand({
|
|
235
|
+
Bucket: bucket,
|
|
236
|
+
Key: key,
|
|
237
|
+
Body: import_fs.default.readFileSync(src)
|
|
238
|
+
})
|
|
239
|
+
);
|
|
212
240
|
uploaded.push(`s3://${bucket}/${key}`);
|
|
213
241
|
}
|
|
214
242
|
}
|
|
@@ -247,12 +275,14 @@ function resolveSandboxEndpoint(rawUrl) {
|
|
|
247
275
|
}
|
|
248
276
|
function loadWorkerConfigFromEnv(env = process.env) {
|
|
249
277
|
const workerId = env.WORKER_ID?.trim() || import_os2.default.hostname();
|
|
278
|
+
const workerIdFile = env.WORKER_ID_FILE?.trim() || import_path2.default.join(import_os2.default.tmpdir(), `multiplayer-sandbox-worker-${workerId}.id`);
|
|
250
279
|
const token = env.WORKER_TOKEN?.trim() || void 0;
|
|
251
280
|
const sandboxEndpoint = resolveSandboxEndpoint(
|
|
252
281
|
env.SANDBOX_URL ?? "https://api.sandbox.filingramp.com/v0/gapi"
|
|
253
282
|
);
|
|
254
283
|
return {
|
|
255
284
|
workerId,
|
|
285
|
+
workerIdFile,
|
|
256
286
|
token,
|
|
257
287
|
sandboxUrl: sandboxEndpoint.sandboxUrl,
|
|
258
288
|
workerSocketPath: sandboxEndpoint.workerSocketPath,
|
|
@@ -287,7 +317,10 @@ function loadWorkerConfigFromEnv(env = process.env) {
|
|
|
287
317
|
// 0 disables auto-suspend; the sandbox drives pause/resume explicitly by default
|
|
288
318
|
maxIdleSeconds: Number(env.MICROVM_MAX_IDLE_SECONDS ?? 900),
|
|
289
319
|
suspendedDurationSeconds: Number(env.MICROVM_SUSPENDED_DURATION_SECONDS ?? 28800),
|
|
290
|
-
maximumDurationInSeconds: Number(env.MICROVM_MAX_DURATION_SECONDS ?? 28800)
|
|
320
|
+
maximumDurationInSeconds: Number(env.MICROVM_MAX_DURATION_SECONDS ?? 28800),
|
|
321
|
+
// A real cold MicroVM boot can comfortably exceed the old 25s hardcoded default —
|
|
322
|
+
// must stay well under the api's own VM_CREATE ack timeout (see vm.service.ts).
|
|
323
|
+
createTimeoutMs: Number(env.MICROVM_CREATE_TIMEOUT_MS ?? 6e4)
|
|
291
324
|
},
|
|
292
325
|
fargate: {
|
|
293
326
|
region: env.FARGATE_AWS_REGION ?? env.AWS_REGION,
|
|
@@ -2492,8 +2525,8 @@ var RemoteExecutor = class {
|
|
|
2492
2525
|
body: result.body ? Buffer.from(result.body, "base64") : Buffer.alloc(0)
|
|
2493
2526
|
};
|
|
2494
2527
|
}
|
|
2495
|
-
request(method,
|
|
2496
|
-
const url = new URL(this.base +
|
|
2528
|
+
request(method, path8, body) {
|
|
2529
|
+
const url = new URL(this.base + path8);
|
|
2497
2530
|
const lib = url.protocol === "https:" ? import_https.default : import_http5.default;
|
|
2498
2531
|
const payload = body !== void 0 ? JSON.stringify(body) : void 0;
|
|
2499
2532
|
return new Promise((resolve, reject) => {
|
|
@@ -2514,7 +2547,7 @@ var RemoteExecutor = class {
|
|
|
2514
2547
|
res.on("end", () => {
|
|
2515
2548
|
const text = Buffer.concat(chunks).toString();
|
|
2516
2549
|
if (res.statusCode && res.statusCode >= 400) {
|
|
2517
|
-
reject(new Error(`vm-agent ${method} ${
|
|
2550
|
+
reject(new Error(`vm-agent ${method} ${path8} \u2192 ${res.statusCode}: ${text}`));
|
|
2518
2551
|
return;
|
|
2519
2552
|
}
|
|
2520
2553
|
if (res.statusCode === 204 || !text) {
|
|
@@ -2541,6 +2574,23 @@ var import_https2 = __toESM(require("https"));
|
|
|
2541
2574
|
var import_client_lambda_microvms = require("@aws-sdk/client-lambda-microvms");
|
|
2542
2575
|
var AUTH_TOKEN_TTL_MINUTES = 60;
|
|
2543
2576
|
var AUTH_TOKEN_REFRESH_MARGIN_MS = 2 * 6e4;
|
|
2577
|
+
var SHELL_PORT = 8022;
|
|
2578
|
+
function managedConnectorArn(imageIdentifier, name) {
|
|
2579
|
+
const [arn, partition, service, region] = imageIdentifier.split(":");
|
|
2580
|
+
if (arn !== "arn" || !partition || service !== "lambda" || !region) {
|
|
2581
|
+
throw new Error(`cannot derive Lambda MicroVM connector ARN from image "${imageIdentifier}"`);
|
|
2582
|
+
}
|
|
2583
|
+
return `arn:${partition}:lambda:${region}:aws:network-connector:aws-network-connector:${name}`;
|
|
2584
|
+
}
|
|
2585
|
+
function runtimeEnv(spec) {
|
|
2586
|
+
const env = {
|
|
2587
|
+
...spec.env,
|
|
2588
|
+
// A release URL is unique and short-lived, so it belongs in per-VM run data rather than
|
|
2589
|
+
// the image-level environment shared by every MicroVM launched from that image.
|
|
2590
|
+
...spec.appUrl ? { APP_URL: spec.appUrl } : {}
|
|
2591
|
+
};
|
|
2592
|
+
return Object.keys(env).length ? env : void 0;
|
|
2593
|
+
}
|
|
2544
2594
|
var LambdaMicrovmExecutor = class {
|
|
2545
2595
|
constructor(config) {
|
|
2546
2596
|
this.config = config;
|
|
@@ -2550,10 +2600,15 @@ var LambdaMicrovmExecutor = class {
|
|
|
2550
2600
|
async create(spec) {
|
|
2551
2601
|
const imageIdentifier = this.config.stackImageArns?.[spec.stack] ?? this.config.imageArn;
|
|
2552
2602
|
const maxIdle = this.config.maxIdleSeconds ?? 900;
|
|
2603
|
+
const shellConnectorArn = managedConnectorArn(imageIdentifier, "SHELL_INGRESS");
|
|
2604
|
+
const ingressNetworkConnectors = [
|
|
2605
|
+
.../* @__PURE__ */ new Set([...this.config.ingressConnectorArns ?? [], shellConnectorArn])
|
|
2606
|
+
];
|
|
2553
2607
|
const run = await this.client.send(
|
|
2554
2608
|
new import_client_lambda_microvms.RunMicrovmCommand({
|
|
2555
2609
|
imageIdentifier,
|
|
2556
|
-
|
|
2610
|
+
// Required by CreateMicrovmShellAuthToken and the /shell WebSocket used by vm-console.
|
|
2611
|
+
ingressNetworkConnectors,
|
|
2557
2612
|
egressNetworkConnectors: this.config.egressConnectorArns,
|
|
2558
2613
|
executionRoleArn: this.config.executionRoleArn,
|
|
2559
2614
|
maximumDurationInSeconds: this.config.maximumDurationInSeconds ?? 28800,
|
|
@@ -2570,9 +2625,11 @@ var LambdaMicrovmExecutor = class {
|
|
|
2570
2625
|
vmId: spec.vmId,
|
|
2571
2626
|
sessionId: spec.sessionId,
|
|
2572
2627
|
stack: spec.stack,
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2628
|
+
env: runtimeEnv(spec)
|
|
2629
|
+
}),
|
|
2630
|
+
// VM_CREATE may be delivered more than once by the Redis-backed control plane.
|
|
2631
|
+
// AWS returns the original request result instead of provisioning another MicroVM.
|
|
2632
|
+
clientToken: spec.vmId
|
|
2576
2633
|
})
|
|
2577
2634
|
);
|
|
2578
2635
|
if (!run.microvmId || !run.endpoint) {
|
|
@@ -2635,6 +2692,31 @@ var LambdaMicrovmExecutor = class {
|
|
|
2635
2692
|
list() {
|
|
2636
2693
|
return [...this.vms.values()].map((v) => v.handle);
|
|
2637
2694
|
}
|
|
2695
|
+
getSerialSource(vmId) {
|
|
2696
|
+
const vm = this.vms.get(vmId);
|
|
2697
|
+
if (!vm) return void 0;
|
|
2698
|
+
return {
|
|
2699
|
+
type: "websocket",
|
|
2700
|
+
url: `wss://${vm.endpoint}/shell`,
|
|
2701
|
+
protocols: async () => {
|
|
2702
|
+
const response = await this.client.send(
|
|
2703
|
+
new import_client_lambda_microvms.CreateMicrovmShellAuthTokenCommand({
|
|
2704
|
+
microvmIdentifier: vm.microvmId,
|
|
2705
|
+
expirationInMinutes: AUTH_TOKEN_TTL_MINUTES
|
|
2706
|
+
})
|
|
2707
|
+
);
|
|
2708
|
+
const token = Object.entries(response.authToken ?? {}).find(
|
|
2709
|
+
([key]) => key.toLowerCase() === "x-aws-proxy-auth"
|
|
2710
|
+
)?.[1];
|
|
2711
|
+
if (!token) throw new Error("create-microvm-shell-auth-token returned no auth token");
|
|
2712
|
+
return [
|
|
2713
|
+
"lambda-microvms",
|
|
2714
|
+
`lambda-microvms.authentication.${token}`,
|
|
2715
|
+
`lambda-microvms.port.${SHELL_PORT}`
|
|
2716
|
+
];
|
|
2717
|
+
}
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2638
2720
|
async proxyHttp(vmId, request) {
|
|
2639
2721
|
const vm = this.mustGet(vmId);
|
|
2640
2722
|
const auth = await this.getAuthHeaders(vm);
|
|
@@ -2702,6 +2784,47 @@ var LambdaMicrovmExecutor = class {
|
|
|
2702
2784
|
);
|
|
2703
2785
|
return handle;
|
|
2704
2786
|
}
|
|
2787
|
+
/**
|
|
2788
|
+
* AWS can suspend or terminate a MicroVM on its own (idle-suspend policy, the suspended-
|
|
2789
|
+
* duration cap, the hard maximumDurationInSeconds lifetime) without this process ever
|
|
2790
|
+
* issuing a command — nothing else here polls for that. Called periodically by the
|
|
2791
|
+
* worker: re-describes every tracked VM, quietly reconciles running/suspended drift, and
|
|
2792
|
+
* reports (then forgets) any that AWS has actually terminated so the control plane's
|
|
2793
|
+
* record doesn't stay stuck saying "running" forever.
|
|
2794
|
+
*/
|
|
2795
|
+
async reconcile() {
|
|
2796
|
+
const externallyTerminated = [];
|
|
2797
|
+
for (const [vmId, vm] of [...this.vms.entries()]) {
|
|
2798
|
+
try {
|
|
2799
|
+
const described = await this.client.send(
|
|
2800
|
+
new import_client_lambda_microvms.GetMicrovmCommand({ microvmIdentifier: vm.microvmId })
|
|
2801
|
+
);
|
|
2802
|
+
if (described.state === "TERMINATED" || described.state === "TERMINATING") {
|
|
2803
|
+
this.vms.delete(vmId);
|
|
2804
|
+
externallyTerminated.push(vmId);
|
|
2805
|
+
src_default.warn(
|
|
2806
|
+
{ vmId, microvmId: vm.microvmId, state: described.state },
|
|
2807
|
+
"[LambdaMicrovm] vm terminated outside our control"
|
|
2808
|
+
);
|
|
2809
|
+
} else if (typeof described.state === "string") {
|
|
2810
|
+
const running = described.state === "RUNNING";
|
|
2811
|
+
if (running !== vm.handle.running) vm.handle = { ...vm.handle, running };
|
|
2812
|
+
}
|
|
2813
|
+
} catch (error) {
|
|
2814
|
+
if (error instanceof Error && error.name === "ResourceNotFoundException") {
|
|
2815
|
+
this.vms.delete(vmId);
|
|
2816
|
+
externallyTerminated.push(vmId);
|
|
2817
|
+
src_default.warn(
|
|
2818
|
+
{ vmId, microvmId: vm.microvmId },
|
|
2819
|
+
"[LambdaMicrovm] vm no longer exists in AWS"
|
|
2820
|
+
);
|
|
2821
|
+
} else {
|
|
2822
|
+
src_default.error({ vmId, err: error }, "[LambdaMicrovm] reconcile check failed");
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
return externallyTerminated;
|
|
2827
|
+
}
|
|
2705
2828
|
mustGet(vmId) {
|
|
2706
2829
|
const vm = this.vms.get(vmId);
|
|
2707
2830
|
if (!vm) throw new Error(`vm ${vmId} not found`);
|
|
@@ -3274,6 +3397,8 @@ function detectEngines(config) {
|
|
|
3274
3397
|
}
|
|
3275
3398
|
|
|
3276
3399
|
// src/executor/index.ts
|
|
3400
|
+
var VmResourceGoneError = class extends Error {
|
|
3401
|
+
};
|
|
3277
3402
|
function findNotAttachedVms(executor, records) {
|
|
3278
3403
|
return records.filter((record) => !executor.get(record.vmId));
|
|
3279
3404
|
}
|
|
@@ -3303,11 +3428,25 @@ var MultiEngineExecutor = class {
|
|
|
3303
3428
|
constructor(executors, defaultEngine) {
|
|
3304
3429
|
this.executors = executors;
|
|
3305
3430
|
this.defaultEngine = defaultEngine;
|
|
3431
|
+
this.creations = /* @__PURE__ */ new Map();
|
|
3306
3432
|
}
|
|
3307
3433
|
get engines() {
|
|
3308
3434
|
return [...this.executors.keys()];
|
|
3309
3435
|
}
|
|
3310
3436
|
async create(spec) {
|
|
3437
|
+
const existing = this.get(spec.vmId);
|
|
3438
|
+
if (existing) return existing;
|
|
3439
|
+
const pending = this.creations.get(spec.vmId);
|
|
3440
|
+
if (pending) return pending;
|
|
3441
|
+
const creation = this.createOnce(spec);
|
|
3442
|
+
this.creations.set(spec.vmId, creation);
|
|
3443
|
+
try {
|
|
3444
|
+
return await creation;
|
|
3445
|
+
} finally {
|
|
3446
|
+
this.creations.delete(spec.vmId);
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
async createOnce(spec) {
|
|
3311
3450
|
const engine = spec.engine ?? this.defaultEngine;
|
|
3312
3451
|
const executor = this.executors.get(engine);
|
|
3313
3452
|
if (!executor) {
|
|
@@ -3318,22 +3457,27 @@ var MultiEngineExecutor = class {
|
|
|
3318
3457
|
const handle = await executor.create(spec);
|
|
3319
3458
|
return { ...handle, engine };
|
|
3320
3459
|
}
|
|
3321
|
-
|
|
3460
|
+
// async here isn't a no-op: route() throws synchronously when the vmId isn't tracked in any
|
|
3461
|
+
// executor's in-memory map (e.g. worker restarted and hasn't reattached this VM yet). Without
|
|
3462
|
+
// `async`, that throw escapes as a real exception instead of a rejected promise — which,
|
|
3463
|
+
// called from socket-client.ts's wrap(), skips its .catch() entirely and leaves the command
|
|
3464
|
+
// un-acked until the caller's timeout, looking like the action "does nothing".
|
|
3465
|
+
async pause(vmId) {
|
|
3322
3466
|
return this.route(vmId).pause(vmId);
|
|
3323
3467
|
}
|
|
3324
|
-
resume(vmId) {
|
|
3468
|
+
async resume(vmId) {
|
|
3325
3469
|
return this.route(vmId).resume(vmId);
|
|
3326
3470
|
}
|
|
3327
|
-
stop(vmId) {
|
|
3471
|
+
async stop(vmId) {
|
|
3328
3472
|
return this.route(vmId).stop(vmId);
|
|
3329
3473
|
}
|
|
3330
|
-
restore(vmId) {
|
|
3474
|
+
async restore(vmId) {
|
|
3331
3475
|
return this.route(vmId).restore(vmId);
|
|
3332
3476
|
}
|
|
3333
|
-
destroy(vmId) {
|
|
3477
|
+
async destroy(vmId) {
|
|
3334
3478
|
return this.route(vmId).destroy(vmId);
|
|
3335
3479
|
}
|
|
3336
|
-
proxyHttp(vmId, request) {
|
|
3480
|
+
async proxyHttp(vmId, request) {
|
|
3337
3481
|
return this.route(vmId).proxyHttp(vmId, request);
|
|
3338
3482
|
}
|
|
3339
3483
|
get(vmId) {
|
|
@@ -3352,15 +3496,14 @@ var MultiEngineExecutor = class {
|
|
|
3352
3496
|
}
|
|
3353
3497
|
return void 0;
|
|
3354
3498
|
}
|
|
3355
|
-
exec(vmId, request) {
|
|
3499
|
+
async exec(vmId, request) {
|
|
3356
3500
|
const executor = this.route(vmId);
|
|
3357
|
-
if (!executor.exec)
|
|
3501
|
+
if (!executor.exec) throw new Error("executor does not support guest exec");
|
|
3358
3502
|
return executor.exec(vmId, request);
|
|
3359
3503
|
}
|
|
3360
|
-
writeFiles(vmId, files) {
|
|
3504
|
+
async writeFiles(vmId, files) {
|
|
3361
3505
|
const executor = this.route(vmId);
|
|
3362
|
-
if (!executor.writeFiles)
|
|
3363
|
-
return Promise.reject(new Error("executor does not support guest file writes"));
|
|
3506
|
+
if (!executor.writeFiles) throw new Error("executor does not support guest file writes");
|
|
3364
3507
|
return executor.writeFiles(vmId, files);
|
|
3365
3508
|
}
|
|
3366
3509
|
onExit(vmId, handler) {
|
|
@@ -3368,6 +3511,15 @@ var MultiEngineExecutor = class {
|
|
|
3368
3511
|
if (!executor.onExit) throw new Error("executor does not support job run mode");
|
|
3369
3512
|
executor.onExit(vmId, handler);
|
|
3370
3513
|
}
|
|
3514
|
+
/** Fans out to every sub-executor that supports it (currently just lambda-microvm). */
|
|
3515
|
+
async reconcile() {
|
|
3516
|
+
const externallyTerminated = [];
|
|
3517
|
+
for (const executor of this.executors.values()) {
|
|
3518
|
+
if (!executor.reconcile) continue;
|
|
3519
|
+
externallyTerminated.push(...await executor.reconcile());
|
|
3520
|
+
}
|
|
3521
|
+
return externallyTerminated;
|
|
3522
|
+
}
|
|
3371
3523
|
/**
|
|
3372
3524
|
* Recover VMs sandbox says this workerId owns but that aren't currently tracked (a worker
|
|
3373
3525
|
* restart, most likely). Runs in parallel — probing many VMs serially would needlessly
|
|
@@ -3376,9 +3528,16 @@ var MultiEngineExecutor = class {
|
|
|
3376
3528
|
* never thrown — a VM that can't be reattached just stays unavailable until something
|
|
3377
3529
|
* touches it again (create/proxy/lifecycle), which already errors gracefully today.
|
|
3378
3530
|
*/
|
|
3531
|
+
/**
|
|
3532
|
+
* Returns vmIds found genuinely gone (executor.reattach resolved undefined — the
|
|
3533
|
+
* resource itself no longer exists, e.g. AWS already terminated it while this worker
|
|
3534
|
+
* was down) as distinct from any other reattach failure (engine unavailable, no reattach
|
|
3535
|
+
* support, a transient describe error, ...), which leave the VM merely unavailable
|
|
3536
|
+
* rather than confirmed gone.
|
|
3537
|
+
*/
|
|
3379
3538
|
async reattachAll(records) {
|
|
3380
3539
|
const pending = findNotAttachedVms(this, records);
|
|
3381
|
-
if (!pending.length) return;
|
|
3540
|
+
if (!pending.length) return [];
|
|
3382
3541
|
const results = await Promise.allSettled(
|
|
3383
3542
|
pending.map(async (record) => {
|
|
3384
3543
|
const executor = record.engine ? this.executors.get(record.engine) : void 0;
|
|
@@ -3387,18 +3546,25 @@ var MultiEngineExecutor = class {
|
|
|
3387
3546
|
if (!executor.reattach)
|
|
3388
3547
|
throw new Error(`engine "${record.engine}" does not support reattach`);
|
|
3389
3548
|
const handle = await executor.reattach(record);
|
|
3390
|
-
if (!handle) throw new
|
|
3549
|
+
if (!handle) throw new VmResourceGoneError();
|
|
3391
3550
|
})
|
|
3392
3551
|
);
|
|
3552
|
+
const externallyTerminated = [];
|
|
3393
3553
|
results.forEach((result, i) => {
|
|
3394
3554
|
const vmId = pending[i].vmId;
|
|
3395
|
-
if (result.status === "fulfilled")
|
|
3396
|
-
|
|
3555
|
+
if (result.status === "fulfilled") {
|
|
3556
|
+
src_default.info({ vmId }, "vm reattached");
|
|
3557
|
+
} else if (result.reason instanceof VmResourceGoneError) {
|
|
3558
|
+
externallyTerminated.push(vmId);
|
|
3559
|
+
src_default.warn({ vmId }, "vm reattach found resource already gone");
|
|
3560
|
+
} else {
|
|
3397
3561
|
src_default.warn(
|
|
3398
3562
|
{ vmId, err: result.reason },
|
|
3399
3563
|
"vm reattach failed \u2014 staying unavailable until next touch"
|
|
3400
3564
|
);
|
|
3565
|
+
}
|
|
3401
3566
|
});
|
|
3567
|
+
return externallyTerminated;
|
|
3402
3568
|
}
|
|
3403
3569
|
route(vmId) {
|
|
3404
3570
|
for (const executor of this.executors.values()) {
|
|
@@ -3420,8 +3586,36 @@ function createExecutor(config) {
|
|
|
3420
3586
|
// src/socket-client.ts
|
|
3421
3587
|
var import_os9 = __toESM(require("os"));
|
|
3422
3588
|
var import_socket = require("socket.io-client");
|
|
3589
|
+
|
|
3590
|
+
// src/worker-identity.ts
|
|
3591
|
+
var import_fs3 = __toESM(require("fs"));
|
|
3592
|
+
var import_path7 = __toESM(require("path"));
|
|
3593
|
+
function loadPersistedWorkerId(filePath) {
|
|
3594
|
+
try {
|
|
3595
|
+
const id = import_fs3.default.readFileSync(filePath, "utf8").trim();
|
|
3596
|
+
return id || void 0;
|
|
3597
|
+
} catch {
|
|
3598
|
+
return void 0;
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
function savePersistedWorkerId(filePath, workerId) {
|
|
3602
|
+
try {
|
|
3603
|
+
import_fs3.default.mkdirSync(import_path7.default.dirname(filePath), { recursive: true });
|
|
3604
|
+
import_fs3.default.writeFileSync(filePath, workerId, "utf8");
|
|
3605
|
+
} catch (error) {
|
|
3606
|
+
src_default.warn(
|
|
3607
|
+
{ err: error, filePath },
|
|
3608
|
+
"failed to persist assigned worker id \u2014 reattach after a restart may not find its VMs"
|
|
3609
|
+
);
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
// src/socket-client.ts
|
|
3423
3614
|
var HEARTBEAT_INTERVAL_MS = 1e4;
|
|
3615
|
+
var RECONCILE_INTERVAL_MS = 6e4;
|
|
3424
3616
|
var REATTACH_FALLBACK_MS = 5e3;
|
|
3617
|
+
var RECONNECTION_DELAY_MS = 1e3;
|
|
3618
|
+
var RECONNECTION_DELAY_MAX_MS = 1e4;
|
|
3425
3619
|
function primaryIp() {
|
|
3426
3620
|
for (const ifaces of Object.values(import_os9.default.networkInterfaces())) {
|
|
3427
3621
|
for (const iface of ifaces ?? []) {
|
|
@@ -3450,7 +3644,11 @@ function wrap(handler) {
|
|
|
3450
3644
|
return (payload, callback) => {
|
|
3451
3645
|
handler(payload).then((result) => callback({ ok: true, result })).catch((error) => {
|
|
3452
3646
|
src_default.error({ err: error }, "command failed");
|
|
3453
|
-
|
|
3647
|
+
try {
|
|
3648
|
+
callback({ ok: false, error: error.message });
|
|
3649
|
+
} catch (ackError) {
|
|
3650
|
+
src_default.error({ err: ackError }, "failed to send command failure ack");
|
|
3651
|
+
}
|
|
3454
3652
|
});
|
|
3455
3653
|
};
|
|
3456
3654
|
}
|
|
@@ -3458,20 +3656,32 @@ function registerCommandHandlers(socket, executor) {
|
|
|
3458
3656
|
socket.on(
|
|
3459
3657
|
WorkerEvent.VM_CREATE,
|
|
3460
3658
|
wrap(async (spec) => {
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3659
|
+
try {
|
|
3660
|
+
const handle = await executor.create(spec);
|
|
3661
|
+
if (spec.runMode === "job") {
|
|
3464
3662
|
executor.onExit(
|
|
3465
3663
|
spec.vmId,
|
|
3466
|
-
(
|
|
3664
|
+
(result2) => socket.emit(WorkerEvent.VM_RUN_EXITED, { vmId: spec.vmId, ...result2 })
|
|
3467
3665
|
);
|
|
3468
|
-
} catch (error) {
|
|
3469
|
-
await executor.destroy(spec.vmId).catch(() => {
|
|
3470
|
-
});
|
|
3471
|
-
throw error;
|
|
3472
3666
|
}
|
|
3667
|
+
const result = {
|
|
3668
|
+
vmId: spec.vmId,
|
|
3669
|
+
ok: true,
|
|
3670
|
+
metadata: handle.metadata
|
|
3671
|
+
};
|
|
3672
|
+
socket.emit(WorkerEvent.VM_CREATE_RESULT, result);
|
|
3673
|
+
return handle;
|
|
3674
|
+
} catch (error) {
|
|
3675
|
+
if (spec.runMode === "job") await executor.destroy(spec.vmId).catch(() => {
|
|
3676
|
+
});
|
|
3677
|
+
const result = {
|
|
3678
|
+
vmId: spec.vmId,
|
|
3679
|
+
ok: false,
|
|
3680
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3681
|
+
};
|
|
3682
|
+
socket.emit(WorkerEvent.VM_CREATE_RESULT, result);
|
|
3683
|
+
throw error;
|
|
3473
3684
|
}
|
|
3474
|
-
return handle;
|
|
3475
3685
|
})
|
|
3476
3686
|
);
|
|
3477
3687
|
socket.on(
|
|
@@ -3523,7 +3733,7 @@ function registerCommandHandlers(socket, executor) {
|
|
|
3523
3733
|
);
|
|
3524
3734
|
}
|
|
3525
3735
|
function connectToSandbox(config, executor) {
|
|
3526
|
-
let assignedWorkerId;
|
|
3736
|
+
let assignedWorkerId = loadPersistedWorkerId(config.workerIdFile);
|
|
3527
3737
|
const socket = (0, import_socket.io)(config.sandboxUrl, {
|
|
3528
3738
|
path: config.workerSocketPath,
|
|
3529
3739
|
auth: (callback) => callback({
|
|
@@ -3532,12 +3742,17 @@ function connectToSandbox(config, executor) {
|
|
|
3532
3742
|
...config.token ? { token: config.token } : {}
|
|
3533
3743
|
}),
|
|
3534
3744
|
transports: ["websocket"],
|
|
3535
|
-
reconnection: true
|
|
3745
|
+
reconnection: true,
|
|
3746
|
+
reconnectionAttempts: Infinity,
|
|
3747
|
+
reconnectionDelay: RECONNECTION_DELAY_MS,
|
|
3748
|
+
reconnectionDelayMax: RECONNECTION_DELAY_MAX_MS
|
|
3536
3749
|
});
|
|
3537
3750
|
let heartbeatTimer;
|
|
3751
|
+
let reconcileTimer;
|
|
3538
3752
|
let reattachFallbackTimer;
|
|
3539
3753
|
socket.on(WorkerEvent.ASSIGNED_ID, ({ workerId }) => {
|
|
3540
3754
|
assignedWorkerId = workerId;
|
|
3755
|
+
savePersistedWorkerId(config.workerIdFile, workerId);
|
|
3541
3756
|
src_default.info({ workerId: config.workerId, assignedWorkerId }, "worker runtime ID assigned");
|
|
3542
3757
|
});
|
|
3543
3758
|
const sendHeartbeat = () => socket.emit(WorkerEvent.HEARTBEAT, buildHeartbeat(config, executor));
|
|
@@ -3546,6 +3761,17 @@ function connectToSandbox(config, executor) {
|
|
|
3546
3761
|
sendHeartbeat();
|
|
3547
3762
|
heartbeatTimer = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL_MS);
|
|
3548
3763
|
}
|
|
3764
|
+
const runReconcile = () => {
|
|
3765
|
+
executor.reconcile().then((externallyTerminated) => {
|
|
3766
|
+
for (const vmId of externallyTerminated) {
|
|
3767
|
+
socket.emit(WorkerEvent.VM_EXTERNALLY_TERMINATED, { vmId });
|
|
3768
|
+
}
|
|
3769
|
+
}).catch((error) => src_default.error({ err: error }, "vm reconcile pass failed"));
|
|
3770
|
+
};
|
|
3771
|
+
function startReconcile() {
|
|
3772
|
+
if (reconcileTimer) return;
|
|
3773
|
+
reconcileTimer = setInterval(runReconcile, RECONCILE_INTERVAL_MS);
|
|
3774
|
+
}
|
|
3549
3775
|
socket.on("connect", () => {
|
|
3550
3776
|
src_default.info(
|
|
3551
3777
|
{ workerId: config.workerId, assignedWorkerId, url: config.sandboxUrl },
|
|
@@ -3558,10 +3784,26 @@ function connectToSandbox(config, executor) {
|
|
|
3558
3784
|
"no reattach handshake from sandbox \u2014 starting heartbeat anyway"
|
|
3559
3785
|
);
|
|
3560
3786
|
startHeartbeat();
|
|
3787
|
+
startReconcile();
|
|
3561
3788
|
}, REATTACH_FALLBACK_MS);
|
|
3562
3789
|
socket.once(WorkerEvent.REATTACH, (records) => {
|
|
3563
3790
|
clearTimeout(reattachFallbackTimer);
|
|
3564
|
-
executor.reattachAll(records).
|
|
3791
|
+
executor.reattachAll(records).then((externallyTerminated) => {
|
|
3792
|
+
for (const vmId of externallyTerminated) {
|
|
3793
|
+
socket.emit(WorkerEvent.VM_EXTERNALLY_TERMINATED, { vmId });
|
|
3794
|
+
}
|
|
3795
|
+
}).catch((error) => src_default.error({ err: error }, "reattach batch failed")).finally(() => {
|
|
3796
|
+
for (const record of records) {
|
|
3797
|
+
const handle = executor.get(record.vmId);
|
|
3798
|
+
if (record.state === "creating" && handle) {
|
|
3799
|
+
const result = {
|
|
3800
|
+
vmId: record.vmId,
|
|
3801
|
+
ok: true,
|
|
3802
|
+
metadata: handle.metadata
|
|
3803
|
+
};
|
|
3804
|
+
socket.emit(WorkerEvent.VM_CREATE_RESULT, result);
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3565
3807
|
for (const record of records) {
|
|
3566
3808
|
if (record.runMode !== "job" || !executor.get(record.vmId)) continue;
|
|
3567
3809
|
try {
|
|
@@ -3577,21 +3819,36 @@ function connectToSandbox(config, executor) {
|
|
|
3577
3819
|
}
|
|
3578
3820
|
}
|
|
3579
3821
|
startHeartbeat();
|
|
3822
|
+
startReconcile();
|
|
3580
3823
|
});
|
|
3581
3824
|
});
|
|
3582
3825
|
});
|
|
3583
3826
|
socket.on("disconnect", (reason) => {
|
|
3584
3827
|
src_default.warn({ workerId: config.workerId, reason }, "disconnected from sandbox");
|
|
3585
3828
|
clearInterval(heartbeatTimer);
|
|
3829
|
+
clearInterval(reconcileTimer);
|
|
3586
3830
|
clearTimeout(reattachFallbackTimer);
|
|
3587
3831
|
heartbeatTimer = void 0;
|
|
3832
|
+
reconcileTimer = void 0;
|
|
3588
3833
|
socket.off(WorkerEvent.REATTACH);
|
|
3589
3834
|
});
|
|
3590
3835
|
socket.on("connect_error", (error) => {
|
|
3591
|
-
src_default.error(
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3836
|
+
src_default.error(
|
|
3837
|
+
{
|
|
3838
|
+
err: error,
|
|
3839
|
+
sandboxUrl: config.sandboxUrl
|
|
3840
|
+
},
|
|
3841
|
+
"sandbox connection error"
|
|
3842
|
+
);
|
|
3843
|
+
});
|
|
3844
|
+
socket.io.on("reconnect_attempt", (attempt) => {
|
|
3845
|
+
src_default.warn({ workerId: config.workerId, attempt }, "reconnecting to sandbox");
|
|
3846
|
+
});
|
|
3847
|
+
socket.io.on("reconnect_error", (error) => {
|
|
3848
|
+
src_default.warn({ workerId: config.workerId, err: error }, "sandbox reconnect attempt failed");
|
|
3849
|
+
});
|
|
3850
|
+
socket.io.on("reconnect", (attempt) => {
|
|
3851
|
+
src_default.info({ workerId: config.workerId, attempt }, "reconnected to sandbox");
|
|
3595
3852
|
});
|
|
3596
3853
|
registerCommandHandlers(socket, executor);
|
|
3597
3854
|
return socket;
|
|
@@ -3701,19 +3958,42 @@ var VncRelay = class {
|
|
|
3701
3958
|
|
|
3702
3959
|
// src/services/serial-relay.ts
|
|
3703
3960
|
var import_net3 = __toESM(require("net"));
|
|
3961
|
+
var import_ws = __toESM(require("ws"));
|
|
3704
3962
|
var SerialRelay = class {
|
|
3705
3963
|
constructor(vmId, source, socket) {
|
|
3706
3964
|
this.vmId = vmId;
|
|
3707
3965
|
this.source = source;
|
|
3708
3966
|
this.socket = socket;
|
|
3709
3967
|
this.conn = null;
|
|
3968
|
+
this.webSocket = null;
|
|
3969
|
+
this.reconnectTimer = null;
|
|
3970
|
+
this.connecting = false;
|
|
3971
|
+
this.browserCount = 0;
|
|
3972
|
+
this.pendingInput = [];
|
|
3973
|
+
this.destroyed = false;
|
|
3710
3974
|
this.inputHandler = null;
|
|
3975
|
+
this.attachHandler = null;
|
|
3976
|
+
this.detachHandler = null;
|
|
3711
3977
|
}
|
|
3712
3978
|
connect() {
|
|
3979
|
+
this.destroyed = false;
|
|
3713
3980
|
if (this.source.type === "tcp") {
|
|
3714
3981
|
this.connectTcp();
|
|
3715
|
-
} else {
|
|
3982
|
+
} else if (this.source.type === "pipe") {
|
|
3716
3983
|
this.connectPipe();
|
|
3984
|
+
} else {
|
|
3985
|
+
this.attachHandler = ({ vmId }) => {
|
|
3986
|
+
if (vmId !== this.vmId) return;
|
|
3987
|
+
this.browserCount += 1;
|
|
3988
|
+
if (this.browserCount === 1) void this.connectWebSocket();
|
|
3989
|
+
};
|
|
3990
|
+
this.detachHandler = ({ vmId }) => {
|
|
3991
|
+
if (vmId !== this.vmId) return;
|
|
3992
|
+
this.browserCount = Math.max(0, this.browserCount - 1);
|
|
3993
|
+
if (this.browserCount === 0) this.disconnectWebSocket();
|
|
3994
|
+
};
|
|
3995
|
+
this.socket.on(WorkerEvent.TERMINAL_ATTACH, this.attachHandler);
|
|
3996
|
+
this.socket.on(WorkerEvent.TERMINAL_DETACH, this.detachHandler);
|
|
3717
3997
|
}
|
|
3718
3998
|
this.inputHandler = (e) => {
|
|
3719
3999
|
if (e.vmId !== this.vmId) return;
|
|
@@ -3722,6 +4002,15 @@ var SerialRelay = class {
|
|
|
3722
4002
|
} else if (this.source.type === "pipe") {
|
|
3723
4003
|
const proc = this.source.process;
|
|
3724
4004
|
proc.stdin?.write(e.data);
|
|
4005
|
+
} else if (this.source.type === "websocket") {
|
|
4006
|
+
if (this.webSocket?.readyState === import_ws.default.OPEN) {
|
|
4007
|
+
this.webSocket.send(e.data);
|
|
4008
|
+
} else {
|
|
4009
|
+
this.browserCount = Math.max(1, this.browserCount);
|
|
4010
|
+
this.pendingInput.push(e.data);
|
|
4011
|
+
if (this.pendingInput.length > 100) this.pendingInput.shift();
|
|
4012
|
+
void this.connectWebSocket();
|
|
4013
|
+
}
|
|
3725
4014
|
}
|
|
3726
4015
|
};
|
|
3727
4016
|
this.socket.on(WorkerEvent.TERMINAL_INPUT, this.inputHandler);
|
|
@@ -3740,7 +4029,9 @@ var SerialRelay = class {
|
|
|
3740
4029
|
sock.on("end", () => src_default.info({ vmId: this.vmId }, "[Serial] TCP ended"));
|
|
3741
4030
|
sock.on("error", (err) => src_default.error({ vmId: this.vmId, err }, "[Serial] TCP error"));
|
|
3742
4031
|
});
|
|
3743
|
-
sock.once("error", () =>
|
|
4032
|
+
sock.once("error", () => {
|
|
4033
|
+
if (!this.destroyed) setTimeout(attempt, 1e3);
|
|
4034
|
+
});
|
|
3744
4035
|
};
|
|
3745
4036
|
attempt();
|
|
3746
4037
|
}
|
|
@@ -3755,13 +4046,65 @@ var SerialRelay = class {
|
|
|
3755
4046
|
(d) => this.socket.emit(WorkerEvent.TERMINAL_DATA, { vmId: this.vmId, data: d })
|
|
3756
4047
|
);
|
|
3757
4048
|
}
|
|
4049
|
+
async connectWebSocket() {
|
|
4050
|
+
if (this.destroyed || this.browserCount === 0 || this.source.type !== "websocket" || this.webSocket || this.connecting) {
|
|
4051
|
+
return;
|
|
4052
|
+
}
|
|
4053
|
+
try {
|
|
4054
|
+
this.connecting = true;
|
|
4055
|
+
const protocols = await this.source.protocols();
|
|
4056
|
+
if (this.destroyed || this.browserCount === 0) return;
|
|
4057
|
+
const webSocket = new import_ws.default(this.source.url, protocols);
|
|
4058
|
+
this.webSocket = webSocket;
|
|
4059
|
+
webSocket.on("open", () => {
|
|
4060
|
+
for (const data of this.pendingInput.splice(0)) webSocket.send(data);
|
|
4061
|
+
src_default.info({ vmId: this.vmId }, "[Serial] Lambda shell connected");
|
|
4062
|
+
});
|
|
4063
|
+
webSocket.on("message", (data) => {
|
|
4064
|
+
const chunk = Array.isArray(data) ? Buffer.concat(data) : Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
4065
|
+
this.socket.emit(WorkerEvent.TERMINAL_DATA, { vmId: this.vmId, data: chunk });
|
|
4066
|
+
});
|
|
4067
|
+
webSocket.on("error", (error) => {
|
|
4068
|
+
src_default.error({ vmId: this.vmId, err: error }, "[Serial] Lambda shell error");
|
|
4069
|
+
});
|
|
4070
|
+
webSocket.on("close", () => {
|
|
4071
|
+
if (this.webSocket === webSocket) this.webSocket = null;
|
|
4072
|
+
src_default.info({ vmId: this.vmId }, "[Serial] Lambda shell disconnected");
|
|
4073
|
+
this.scheduleWebSocketReconnect();
|
|
4074
|
+
});
|
|
4075
|
+
} catch (error) {
|
|
4076
|
+
src_default.error({ vmId: this.vmId, err: error }, "[Serial] Lambda shell connect failed");
|
|
4077
|
+
this.scheduleWebSocketReconnect();
|
|
4078
|
+
} finally {
|
|
4079
|
+
this.connecting = false;
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
scheduleWebSocketReconnect() {
|
|
4083
|
+
if (this.destroyed || this.browserCount === 0 || this.reconnectTimer) return;
|
|
4084
|
+
this.reconnectTimer = setTimeout(() => {
|
|
4085
|
+
this.reconnectTimer = null;
|
|
4086
|
+
void this.connectWebSocket();
|
|
4087
|
+
}, 1e3);
|
|
4088
|
+
}
|
|
4089
|
+
disconnectWebSocket() {
|
|
4090
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
4091
|
+
this.reconnectTimer = null;
|
|
4092
|
+
this.pendingInput = [];
|
|
4093
|
+
const webSocket = this.webSocket;
|
|
4094
|
+
this.webSocket = null;
|
|
4095
|
+
webSocket?.close();
|
|
4096
|
+
}
|
|
3758
4097
|
destroy() {
|
|
4098
|
+
this.destroyed = true;
|
|
3759
4099
|
if (this.inputHandler) this.socket.off(WorkerEvent.TERMINAL_INPUT, this.inputHandler);
|
|
4100
|
+
if (this.attachHandler) this.socket.off(WorkerEvent.TERMINAL_ATTACH, this.attachHandler);
|
|
4101
|
+
if (this.detachHandler) this.socket.off(WorkerEvent.TERMINAL_DETACH, this.detachHandler);
|
|
3760
4102
|
try {
|
|
3761
4103
|
this.conn?.destroy();
|
|
3762
4104
|
} catch {
|
|
3763
4105
|
}
|
|
3764
4106
|
this.conn = null;
|
|
4107
|
+
this.disconnectWebSocket();
|
|
3765
4108
|
src_default.info({ vmId: this.vmId }, "[Serial] relay destroyed");
|
|
3766
4109
|
}
|
|
3767
4110
|
};
|
|
@@ -3769,9 +4112,8 @@ var SerialRelay = class {
|
|
|
3769
4112
|
// src/console-relays.ts
|
|
3770
4113
|
function attachConsoleRelays(executor, sandboxSocket) {
|
|
3771
4114
|
const relays = /* @__PURE__ */ new Map();
|
|
3772
|
-
const
|
|
3773
|
-
|
|
3774
|
-
const handle = await originalCreate(spec);
|
|
4115
|
+
const attach = (handle) => {
|
|
4116
|
+
if (relays.has(handle.vmId)) return;
|
|
3775
4117
|
const entry = {};
|
|
3776
4118
|
if (handle.vncPort) {
|
|
3777
4119
|
const vnc = new VncRelay(handle.vmId, "127.0.0.1", handle.vncPort, sandboxSocket);
|
|
@@ -3785,8 +4127,22 @@ function attachConsoleRelays(executor, sandboxSocket) {
|
|
|
3785
4127
|
entry.serial = serial;
|
|
3786
4128
|
}
|
|
3787
4129
|
if (entry.vnc || entry.serial) relays.set(handle.vmId, entry);
|
|
4130
|
+
};
|
|
4131
|
+
const originalCreate = executor.create.bind(executor);
|
|
4132
|
+
executor.create = async (spec) => {
|
|
4133
|
+
const handle = await originalCreate(spec);
|
|
4134
|
+
attach(handle);
|
|
3788
4135
|
return handle;
|
|
3789
4136
|
};
|
|
4137
|
+
const originalReattachAll = executor.reattachAll.bind(executor);
|
|
4138
|
+
executor.reattachAll = async (records) => {
|
|
4139
|
+
const externallyTerminated = await originalReattachAll(records);
|
|
4140
|
+
for (const record of records) {
|
|
4141
|
+
const handle = executor.get(record.vmId);
|
|
4142
|
+
if (handle) attach(handle);
|
|
4143
|
+
}
|
|
4144
|
+
return externallyTerminated;
|
|
4145
|
+
};
|
|
3790
4146
|
const originalDestroy = executor.destroy.bind(executor);
|
|
3791
4147
|
executor.destroy = async (vmId) => {
|
|
3792
4148
|
const entry = relays.get(vmId);
|