@multiplayer-app/sandbox 7.0.0 → 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 +21 -11
- package/dist/cli.js +511 -111
- 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 +416 -59
- 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 +6 -2
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
|
}
|
|
@@ -233,25 +261,28 @@ function parseArnList(raw) {
|
|
|
233
261
|
const arns = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
234
262
|
return arns.length ? arns : void 0;
|
|
235
263
|
}
|
|
236
|
-
function resolveSandboxEndpoint(rawUrl
|
|
264
|
+
function resolveSandboxEndpoint(rawUrl) {
|
|
237
265
|
const url = new URL(rawUrl);
|
|
238
|
-
const basePath = url.pathname.replace(/\/+$/, "")
|
|
266
|
+
const basePath = url.pathname.replace(/\/+$/, "");
|
|
239
267
|
return {
|
|
240
268
|
// Socket.IO treats a URL pathname as a namespace, so connect to the origin
|
|
241
|
-
// and pass the derived HTTP transport path separately.
|
|
269
|
+
// and pass the derived HTTP transport path separately. SANDBOX_URL's pathname is the
|
|
270
|
+
// sole source of truth for the socket path — it must include whatever base the control
|
|
271
|
+
// plane expects (e.g. "/v0/gapi" or "/internal/v0/gapi"), and "/workers/ws" is appended.
|
|
242
272
|
sandboxUrl: url.origin,
|
|
243
|
-
workerSocketPath:
|
|
273
|
+
workerSocketPath: `${basePath}/workers/ws`
|
|
244
274
|
};
|
|
245
275
|
}
|
|
246
276
|
function loadWorkerConfigFromEnv(env = process.env) {
|
|
247
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`);
|
|
248
279
|
const token = env.WORKER_TOKEN?.trim() || void 0;
|
|
249
280
|
const sandboxEndpoint = resolveSandboxEndpoint(
|
|
250
|
-
env.SANDBOX_URL ?? "https://api.sandbox.filingramp.com/v0"
|
|
251
|
-
env.WORKER_SOCKET_PATH
|
|
281
|
+
env.SANDBOX_URL ?? "https://api.sandbox.filingramp.com/v0/gapi"
|
|
252
282
|
);
|
|
253
283
|
return {
|
|
254
284
|
workerId,
|
|
285
|
+
workerIdFile,
|
|
255
286
|
token,
|
|
256
287
|
sandboxUrl: sandboxEndpoint.sandboxUrl,
|
|
257
288
|
workerSocketPath: sandboxEndpoint.workerSocketPath,
|
|
@@ -286,7 +317,10 @@ function loadWorkerConfigFromEnv(env = process.env) {
|
|
|
286
317
|
// 0 disables auto-suspend; the sandbox drives pause/resume explicitly by default
|
|
287
318
|
maxIdleSeconds: Number(env.MICROVM_MAX_IDLE_SECONDS ?? 900),
|
|
288
319
|
suspendedDurationSeconds: Number(env.MICROVM_SUSPENDED_DURATION_SECONDS ?? 28800),
|
|
289
|
-
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)
|
|
290
324
|
},
|
|
291
325
|
fargate: {
|
|
292
326
|
region: env.FARGATE_AWS_REGION ?? env.AWS_REGION,
|
|
@@ -2491,8 +2525,8 @@ var RemoteExecutor = class {
|
|
|
2491
2525
|
body: result.body ? Buffer.from(result.body, "base64") : Buffer.alloc(0)
|
|
2492
2526
|
};
|
|
2493
2527
|
}
|
|
2494
|
-
request(method,
|
|
2495
|
-
const url = new URL(this.base +
|
|
2528
|
+
request(method, path8, body) {
|
|
2529
|
+
const url = new URL(this.base + path8);
|
|
2496
2530
|
const lib = url.protocol === "https:" ? import_https.default : import_http5.default;
|
|
2497
2531
|
const payload = body !== void 0 ? JSON.stringify(body) : void 0;
|
|
2498
2532
|
return new Promise((resolve, reject) => {
|
|
@@ -2513,7 +2547,7 @@ var RemoteExecutor = class {
|
|
|
2513
2547
|
res.on("end", () => {
|
|
2514
2548
|
const text = Buffer.concat(chunks).toString();
|
|
2515
2549
|
if (res.statusCode && res.statusCode >= 400) {
|
|
2516
|
-
reject(new Error(`vm-agent ${method} ${
|
|
2550
|
+
reject(new Error(`vm-agent ${method} ${path8} \u2192 ${res.statusCode}: ${text}`));
|
|
2517
2551
|
return;
|
|
2518
2552
|
}
|
|
2519
2553
|
if (res.statusCode === 204 || !text) {
|
|
@@ -2540,6 +2574,23 @@ var import_https2 = __toESM(require("https"));
|
|
|
2540
2574
|
var import_client_lambda_microvms = require("@aws-sdk/client-lambda-microvms");
|
|
2541
2575
|
var AUTH_TOKEN_TTL_MINUTES = 60;
|
|
2542
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
|
+
}
|
|
2543
2594
|
var LambdaMicrovmExecutor = class {
|
|
2544
2595
|
constructor(config) {
|
|
2545
2596
|
this.config = config;
|
|
@@ -2549,10 +2600,15 @@ var LambdaMicrovmExecutor = class {
|
|
|
2549
2600
|
async create(spec) {
|
|
2550
2601
|
const imageIdentifier = this.config.stackImageArns?.[spec.stack] ?? this.config.imageArn;
|
|
2551
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
|
+
];
|
|
2552
2607
|
const run = await this.client.send(
|
|
2553
2608
|
new import_client_lambda_microvms.RunMicrovmCommand({
|
|
2554
2609
|
imageIdentifier,
|
|
2555
|
-
|
|
2610
|
+
// Required by CreateMicrovmShellAuthToken and the /shell WebSocket used by vm-console.
|
|
2611
|
+
ingressNetworkConnectors,
|
|
2556
2612
|
egressNetworkConnectors: this.config.egressConnectorArns,
|
|
2557
2613
|
executionRoleArn: this.config.executionRoleArn,
|
|
2558
2614
|
maximumDurationInSeconds: this.config.maximumDurationInSeconds ?? 28800,
|
|
@@ -2569,9 +2625,11 @@ var LambdaMicrovmExecutor = class {
|
|
|
2569
2625
|
vmId: spec.vmId,
|
|
2570
2626
|
sessionId: spec.sessionId,
|
|
2571
2627
|
stack: spec.stack,
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
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
|
|
2575
2633
|
})
|
|
2576
2634
|
);
|
|
2577
2635
|
if (!run.microvmId || !run.endpoint) {
|
|
@@ -2634,6 +2692,31 @@ var LambdaMicrovmExecutor = class {
|
|
|
2634
2692
|
list() {
|
|
2635
2693
|
return [...this.vms.values()].map((v) => v.handle);
|
|
2636
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
|
+
}
|
|
2637
2720
|
async proxyHttp(vmId, request) {
|
|
2638
2721
|
const vm = this.mustGet(vmId);
|
|
2639
2722
|
const auth = await this.getAuthHeaders(vm);
|
|
@@ -2701,6 +2784,47 @@ var LambdaMicrovmExecutor = class {
|
|
|
2701
2784
|
);
|
|
2702
2785
|
return handle;
|
|
2703
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
|
+
}
|
|
2704
2828
|
mustGet(vmId) {
|
|
2705
2829
|
const vm = this.vms.get(vmId);
|
|
2706
2830
|
if (!vm) throw new Error(`vm ${vmId} not found`);
|
|
@@ -3273,6 +3397,8 @@ function detectEngines(config) {
|
|
|
3273
3397
|
}
|
|
3274
3398
|
|
|
3275
3399
|
// src/executor/index.ts
|
|
3400
|
+
var VmResourceGoneError = class extends Error {
|
|
3401
|
+
};
|
|
3276
3402
|
function findNotAttachedVms(executor, records) {
|
|
3277
3403
|
return records.filter((record) => !executor.get(record.vmId));
|
|
3278
3404
|
}
|
|
@@ -3302,11 +3428,25 @@ var MultiEngineExecutor = class {
|
|
|
3302
3428
|
constructor(executors, defaultEngine) {
|
|
3303
3429
|
this.executors = executors;
|
|
3304
3430
|
this.defaultEngine = defaultEngine;
|
|
3431
|
+
this.creations = /* @__PURE__ */ new Map();
|
|
3305
3432
|
}
|
|
3306
3433
|
get engines() {
|
|
3307
3434
|
return [...this.executors.keys()];
|
|
3308
3435
|
}
|
|
3309
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) {
|
|
3310
3450
|
const engine = spec.engine ?? this.defaultEngine;
|
|
3311
3451
|
const executor = this.executors.get(engine);
|
|
3312
3452
|
if (!executor) {
|
|
@@ -3317,22 +3457,27 @@ var MultiEngineExecutor = class {
|
|
|
3317
3457
|
const handle = await executor.create(spec);
|
|
3318
3458
|
return { ...handle, engine };
|
|
3319
3459
|
}
|
|
3320
|
-
|
|
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) {
|
|
3321
3466
|
return this.route(vmId).pause(vmId);
|
|
3322
3467
|
}
|
|
3323
|
-
resume(vmId) {
|
|
3468
|
+
async resume(vmId) {
|
|
3324
3469
|
return this.route(vmId).resume(vmId);
|
|
3325
3470
|
}
|
|
3326
|
-
stop(vmId) {
|
|
3471
|
+
async stop(vmId) {
|
|
3327
3472
|
return this.route(vmId).stop(vmId);
|
|
3328
3473
|
}
|
|
3329
|
-
restore(vmId) {
|
|
3474
|
+
async restore(vmId) {
|
|
3330
3475
|
return this.route(vmId).restore(vmId);
|
|
3331
3476
|
}
|
|
3332
|
-
destroy(vmId) {
|
|
3477
|
+
async destroy(vmId) {
|
|
3333
3478
|
return this.route(vmId).destroy(vmId);
|
|
3334
3479
|
}
|
|
3335
|
-
proxyHttp(vmId, request) {
|
|
3480
|
+
async proxyHttp(vmId, request) {
|
|
3336
3481
|
return this.route(vmId).proxyHttp(vmId, request);
|
|
3337
3482
|
}
|
|
3338
3483
|
get(vmId) {
|
|
@@ -3351,15 +3496,14 @@ var MultiEngineExecutor = class {
|
|
|
3351
3496
|
}
|
|
3352
3497
|
return void 0;
|
|
3353
3498
|
}
|
|
3354
|
-
exec(vmId, request) {
|
|
3499
|
+
async exec(vmId, request) {
|
|
3355
3500
|
const executor = this.route(vmId);
|
|
3356
|
-
if (!executor.exec)
|
|
3501
|
+
if (!executor.exec) throw new Error("executor does not support guest exec");
|
|
3357
3502
|
return executor.exec(vmId, request);
|
|
3358
3503
|
}
|
|
3359
|
-
writeFiles(vmId, files) {
|
|
3504
|
+
async writeFiles(vmId, files) {
|
|
3360
3505
|
const executor = this.route(vmId);
|
|
3361
|
-
if (!executor.writeFiles)
|
|
3362
|
-
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");
|
|
3363
3507
|
return executor.writeFiles(vmId, files);
|
|
3364
3508
|
}
|
|
3365
3509
|
onExit(vmId, handler) {
|
|
@@ -3367,6 +3511,15 @@ var MultiEngineExecutor = class {
|
|
|
3367
3511
|
if (!executor.onExit) throw new Error("executor does not support job run mode");
|
|
3368
3512
|
executor.onExit(vmId, handler);
|
|
3369
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
|
+
}
|
|
3370
3523
|
/**
|
|
3371
3524
|
* Recover VMs sandbox says this workerId owns but that aren't currently tracked (a worker
|
|
3372
3525
|
* restart, most likely). Runs in parallel — probing many VMs serially would needlessly
|
|
@@ -3375,9 +3528,16 @@ var MultiEngineExecutor = class {
|
|
|
3375
3528
|
* never thrown — a VM that can't be reattached just stays unavailable until something
|
|
3376
3529
|
* touches it again (create/proxy/lifecycle), which already errors gracefully today.
|
|
3377
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
|
+
*/
|
|
3378
3538
|
async reattachAll(records) {
|
|
3379
3539
|
const pending = findNotAttachedVms(this, records);
|
|
3380
|
-
if (!pending.length) return;
|
|
3540
|
+
if (!pending.length) return [];
|
|
3381
3541
|
const results = await Promise.allSettled(
|
|
3382
3542
|
pending.map(async (record) => {
|
|
3383
3543
|
const executor = record.engine ? this.executors.get(record.engine) : void 0;
|
|
@@ -3386,18 +3546,25 @@ var MultiEngineExecutor = class {
|
|
|
3386
3546
|
if (!executor.reattach)
|
|
3387
3547
|
throw new Error(`engine "${record.engine}" does not support reattach`);
|
|
3388
3548
|
const handle = await executor.reattach(record);
|
|
3389
|
-
if (!handle) throw new
|
|
3549
|
+
if (!handle) throw new VmResourceGoneError();
|
|
3390
3550
|
})
|
|
3391
3551
|
);
|
|
3552
|
+
const externallyTerminated = [];
|
|
3392
3553
|
results.forEach((result, i) => {
|
|
3393
3554
|
const vmId = pending[i].vmId;
|
|
3394
|
-
if (result.status === "fulfilled")
|
|
3395
|
-
|
|
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 {
|
|
3396
3561
|
src_default.warn(
|
|
3397
3562
|
{ vmId, err: result.reason },
|
|
3398
3563
|
"vm reattach failed \u2014 staying unavailable until next touch"
|
|
3399
3564
|
);
|
|
3565
|
+
}
|
|
3400
3566
|
});
|
|
3567
|
+
return externallyTerminated;
|
|
3401
3568
|
}
|
|
3402
3569
|
route(vmId) {
|
|
3403
3570
|
for (const executor of this.executors.values()) {
|
|
@@ -3419,8 +3586,36 @@ function createExecutor(config) {
|
|
|
3419
3586
|
// src/socket-client.ts
|
|
3420
3587
|
var import_os9 = __toESM(require("os"));
|
|
3421
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
|
|
3422
3614
|
var HEARTBEAT_INTERVAL_MS = 1e4;
|
|
3615
|
+
var RECONCILE_INTERVAL_MS = 6e4;
|
|
3423
3616
|
var REATTACH_FALLBACK_MS = 5e3;
|
|
3617
|
+
var RECONNECTION_DELAY_MS = 1e3;
|
|
3618
|
+
var RECONNECTION_DELAY_MAX_MS = 1e4;
|
|
3424
3619
|
function primaryIp() {
|
|
3425
3620
|
for (const ifaces of Object.values(import_os9.default.networkInterfaces())) {
|
|
3426
3621
|
for (const iface of ifaces ?? []) {
|
|
@@ -3449,7 +3644,11 @@ function wrap(handler) {
|
|
|
3449
3644
|
return (payload, callback) => {
|
|
3450
3645
|
handler(payload).then((result) => callback({ ok: true, result })).catch((error) => {
|
|
3451
3646
|
src_default.error({ err: error }, "command failed");
|
|
3452
|
-
|
|
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
|
+
}
|
|
3453
3652
|
});
|
|
3454
3653
|
};
|
|
3455
3654
|
}
|
|
@@ -3457,20 +3656,32 @@ function registerCommandHandlers(socket, executor) {
|
|
|
3457
3656
|
socket.on(
|
|
3458
3657
|
WorkerEvent.VM_CREATE,
|
|
3459
3658
|
wrap(async (spec) => {
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3659
|
+
try {
|
|
3660
|
+
const handle = await executor.create(spec);
|
|
3661
|
+
if (spec.runMode === "job") {
|
|
3463
3662
|
executor.onExit(
|
|
3464
3663
|
spec.vmId,
|
|
3465
|
-
(
|
|
3664
|
+
(result2) => socket.emit(WorkerEvent.VM_RUN_EXITED, { vmId: spec.vmId, ...result2 })
|
|
3466
3665
|
);
|
|
3467
|
-
} catch (error) {
|
|
3468
|
-
await executor.destroy(spec.vmId).catch(() => {
|
|
3469
|
-
});
|
|
3470
|
-
throw error;
|
|
3471
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;
|
|
3472
3684
|
}
|
|
3473
|
-
return handle;
|
|
3474
3685
|
})
|
|
3475
3686
|
);
|
|
3476
3687
|
socket.on(
|
|
@@ -3522,7 +3733,7 @@ function registerCommandHandlers(socket, executor) {
|
|
|
3522
3733
|
);
|
|
3523
3734
|
}
|
|
3524
3735
|
function connectToSandbox(config, executor) {
|
|
3525
|
-
let assignedWorkerId;
|
|
3736
|
+
let assignedWorkerId = loadPersistedWorkerId(config.workerIdFile);
|
|
3526
3737
|
const socket = (0, import_socket.io)(config.sandboxUrl, {
|
|
3527
3738
|
path: config.workerSocketPath,
|
|
3528
3739
|
auth: (callback) => callback({
|
|
@@ -3531,12 +3742,17 @@ function connectToSandbox(config, executor) {
|
|
|
3531
3742
|
...config.token ? { token: config.token } : {}
|
|
3532
3743
|
}),
|
|
3533
3744
|
transports: ["websocket"],
|
|
3534
|
-
reconnection: true
|
|
3745
|
+
reconnection: true,
|
|
3746
|
+
reconnectionAttempts: Infinity,
|
|
3747
|
+
reconnectionDelay: RECONNECTION_DELAY_MS,
|
|
3748
|
+
reconnectionDelayMax: RECONNECTION_DELAY_MAX_MS
|
|
3535
3749
|
});
|
|
3536
3750
|
let heartbeatTimer;
|
|
3751
|
+
let reconcileTimer;
|
|
3537
3752
|
let reattachFallbackTimer;
|
|
3538
3753
|
socket.on(WorkerEvent.ASSIGNED_ID, ({ workerId }) => {
|
|
3539
3754
|
assignedWorkerId = workerId;
|
|
3755
|
+
savePersistedWorkerId(config.workerIdFile, workerId);
|
|
3540
3756
|
src_default.info({ workerId: config.workerId, assignedWorkerId }, "worker runtime ID assigned");
|
|
3541
3757
|
});
|
|
3542
3758
|
const sendHeartbeat = () => socket.emit(WorkerEvent.HEARTBEAT, buildHeartbeat(config, executor));
|
|
@@ -3545,6 +3761,17 @@ function connectToSandbox(config, executor) {
|
|
|
3545
3761
|
sendHeartbeat();
|
|
3546
3762
|
heartbeatTimer = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL_MS);
|
|
3547
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
|
+
}
|
|
3548
3775
|
socket.on("connect", () => {
|
|
3549
3776
|
src_default.info(
|
|
3550
3777
|
{ workerId: config.workerId, assignedWorkerId, url: config.sandboxUrl },
|
|
@@ -3557,10 +3784,26 @@ function connectToSandbox(config, executor) {
|
|
|
3557
3784
|
"no reattach handshake from sandbox \u2014 starting heartbeat anyway"
|
|
3558
3785
|
);
|
|
3559
3786
|
startHeartbeat();
|
|
3787
|
+
startReconcile();
|
|
3560
3788
|
}, REATTACH_FALLBACK_MS);
|
|
3561
3789
|
socket.once(WorkerEvent.REATTACH, (records) => {
|
|
3562
3790
|
clearTimeout(reattachFallbackTimer);
|
|
3563
|
-
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
|
+
}
|
|
3564
3807
|
for (const record of records) {
|
|
3565
3808
|
if (record.runMode !== "job" || !executor.get(record.vmId)) continue;
|
|
3566
3809
|
try {
|
|
@@ -3576,21 +3819,36 @@ function connectToSandbox(config, executor) {
|
|
|
3576
3819
|
}
|
|
3577
3820
|
}
|
|
3578
3821
|
startHeartbeat();
|
|
3822
|
+
startReconcile();
|
|
3579
3823
|
});
|
|
3580
3824
|
});
|
|
3581
3825
|
});
|
|
3582
3826
|
socket.on("disconnect", (reason) => {
|
|
3583
3827
|
src_default.warn({ workerId: config.workerId, reason }, "disconnected from sandbox");
|
|
3584
3828
|
clearInterval(heartbeatTimer);
|
|
3829
|
+
clearInterval(reconcileTimer);
|
|
3585
3830
|
clearTimeout(reattachFallbackTimer);
|
|
3586
3831
|
heartbeatTimer = void 0;
|
|
3832
|
+
reconcileTimer = void 0;
|
|
3587
3833
|
socket.off(WorkerEvent.REATTACH);
|
|
3588
3834
|
});
|
|
3589
3835
|
socket.on("connect_error", (error) => {
|
|
3590
|
-
src_default.error(
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
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");
|
|
3594
3852
|
});
|
|
3595
3853
|
registerCommandHandlers(socket, executor);
|
|
3596
3854
|
return socket;
|
|
@@ -3700,19 +3958,42 @@ var VncRelay = class {
|
|
|
3700
3958
|
|
|
3701
3959
|
// src/services/serial-relay.ts
|
|
3702
3960
|
var import_net3 = __toESM(require("net"));
|
|
3961
|
+
var import_ws = __toESM(require("ws"));
|
|
3703
3962
|
var SerialRelay = class {
|
|
3704
3963
|
constructor(vmId, source, socket) {
|
|
3705
3964
|
this.vmId = vmId;
|
|
3706
3965
|
this.source = source;
|
|
3707
3966
|
this.socket = socket;
|
|
3708
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;
|
|
3709
3974
|
this.inputHandler = null;
|
|
3975
|
+
this.attachHandler = null;
|
|
3976
|
+
this.detachHandler = null;
|
|
3710
3977
|
}
|
|
3711
3978
|
connect() {
|
|
3979
|
+
this.destroyed = false;
|
|
3712
3980
|
if (this.source.type === "tcp") {
|
|
3713
3981
|
this.connectTcp();
|
|
3714
|
-
} else {
|
|
3982
|
+
} else if (this.source.type === "pipe") {
|
|
3715
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);
|
|
3716
3997
|
}
|
|
3717
3998
|
this.inputHandler = (e) => {
|
|
3718
3999
|
if (e.vmId !== this.vmId) return;
|
|
@@ -3721,6 +4002,15 @@ var SerialRelay = class {
|
|
|
3721
4002
|
} else if (this.source.type === "pipe") {
|
|
3722
4003
|
const proc = this.source.process;
|
|
3723
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
|
+
}
|
|
3724
4014
|
}
|
|
3725
4015
|
};
|
|
3726
4016
|
this.socket.on(WorkerEvent.TERMINAL_INPUT, this.inputHandler);
|
|
@@ -3739,7 +4029,9 @@ var SerialRelay = class {
|
|
|
3739
4029
|
sock.on("end", () => src_default.info({ vmId: this.vmId }, "[Serial] TCP ended"));
|
|
3740
4030
|
sock.on("error", (err) => src_default.error({ vmId: this.vmId, err }, "[Serial] TCP error"));
|
|
3741
4031
|
});
|
|
3742
|
-
sock.once("error", () =>
|
|
4032
|
+
sock.once("error", () => {
|
|
4033
|
+
if (!this.destroyed) setTimeout(attempt, 1e3);
|
|
4034
|
+
});
|
|
3743
4035
|
};
|
|
3744
4036
|
attempt();
|
|
3745
4037
|
}
|
|
@@ -3754,13 +4046,65 @@ var SerialRelay = class {
|
|
|
3754
4046
|
(d) => this.socket.emit(WorkerEvent.TERMINAL_DATA, { vmId: this.vmId, data: d })
|
|
3755
4047
|
);
|
|
3756
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
|
+
}
|
|
3757
4097
|
destroy() {
|
|
4098
|
+
this.destroyed = true;
|
|
3758
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);
|
|
3759
4102
|
try {
|
|
3760
4103
|
this.conn?.destroy();
|
|
3761
4104
|
} catch {
|
|
3762
4105
|
}
|
|
3763
4106
|
this.conn = null;
|
|
4107
|
+
this.disconnectWebSocket();
|
|
3764
4108
|
src_default.info({ vmId: this.vmId }, "[Serial] relay destroyed");
|
|
3765
4109
|
}
|
|
3766
4110
|
};
|
|
@@ -3768,9 +4112,8 @@ var SerialRelay = class {
|
|
|
3768
4112
|
// src/console-relays.ts
|
|
3769
4113
|
function attachConsoleRelays(executor, sandboxSocket) {
|
|
3770
4114
|
const relays = /* @__PURE__ */ new Map();
|
|
3771
|
-
const
|
|
3772
|
-
|
|
3773
|
-
const handle = await originalCreate(spec);
|
|
4115
|
+
const attach = (handle) => {
|
|
4116
|
+
if (relays.has(handle.vmId)) return;
|
|
3774
4117
|
const entry = {};
|
|
3775
4118
|
if (handle.vncPort) {
|
|
3776
4119
|
const vnc = new VncRelay(handle.vmId, "127.0.0.1", handle.vncPort, sandboxSocket);
|
|
@@ -3784,8 +4127,22 @@ function attachConsoleRelays(executor, sandboxSocket) {
|
|
|
3784
4127
|
entry.serial = serial;
|
|
3785
4128
|
}
|
|
3786
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);
|
|
3787
4135
|
return handle;
|
|
3788
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
|
+
};
|
|
3789
4146
|
const originalDestroy = executor.destroy.bind(executor);
|
|
3790
4147
|
executor.destroy = async (vmId) => {
|
|
3791
4148
|
const entry = relays.get(vmId);
|