withruntime 0.8.4 → 0.9.0
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/COMPATIBILITY.md +145 -0
- package/E2B.md +23 -13
- package/README.md +8 -5
- package/README.src.md +8 -5
- package/compatibility-lock.json +281 -0
- package/dist/ai-harness/index.js +2 -2
- package/dist/api-defaults.d.ts +17 -0
- package/dist/api-defaults.js +22 -0
- package/dist/blaxel/preview.d.ts +1 -1
- package/dist/blaxel/preview.js +1 -1
- package/dist/blaxel/process.d.ts +1 -1
- package/dist/blaxel/process.js +11 -2
- package/dist/cli-words.d.ts +27 -0
- package/dist/cli-words.js +135 -0
- package/dist/cli.js +162 -31
- package/dist/client.d.ts +4 -1
- package/dist/client.js +8 -2
- package/dist/cloudflare/index.d.ts +294 -0
- package/dist/cloudflare/index.js +554 -0
- package/dist/cloudflare/worker.d.ts +415 -0
- package/dist/cloudflare/worker.js +297 -0
- package/dist/codesandbox/guest-service.d.ts +31 -0
- package/dist/codesandbox/guest-service.js +453 -0
- package/dist/codesandbox/index.d.ts +386 -0
- package/dist/codesandbox/index.js +1313 -0
- package/dist/compat/core.d.ts +39 -0
- package/dist/compat/core.js +199 -0
- package/dist/compat/files.d.ts +26 -0
- package/dist/compat/files.js +115 -0
- package/dist/compat/shell.d.ts +4 -0
- package/dist/compat/shell.js +165 -0
- package/dist/daytona/context.js +2 -1
- package/dist/daytona/process.d.ts +1 -1
- package/dist/daytona/process.js +27 -4
- package/dist/e2b/code-interpreter.d.ts +4 -5
- package/dist/e2b/code-interpreter.js +54 -56
- package/dist/e2b/commands.d.ts +5 -4
- package/dist/e2b/commands.js +37 -29
- package/dist/e2b/errors.js +1 -1
- package/dist/e2b/filesystem.d.ts +8 -6
- package/dist/e2b/filesystem.js +127 -28
- package/dist/e2b/sandbox.d.ts +4 -3
- package/dist/e2b/sandbox.js +10 -10
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +15 -2
- package/dist/freestyle/index.d.ts +301 -0
- package/dist/freestyle/index.js +310 -0
- package/dist/index.d.ts +1 -0
- package/dist/mcp.js +2 -2
- package/dist/modal/index.d.ts +205 -0
- package/dist/modal/index.js +485 -0
- package/dist/network-cli.js +1 -1
- package/dist/openai-agents/index.js +9 -26
- package/dist/products/audit.d.ts +3 -0
- package/dist/products/index.d.ts +2 -0
- package/dist/products/index.js +2 -0
- package/dist/products/interpreter.d.ts +11 -4
- package/dist/products/interpreter.js +33 -9
- package/dist/products/jobs.js +3 -2
- package/dist/products/limits.d.ts +10 -0
- package/dist/products/previews.d.ts +10 -2
- package/dist/products/previews.js +3 -2
- package/dist/products/tailscale.d.ts +57 -0
- package/dist/products/tailscale.js +17 -0
- package/dist/products/watch.d.ts +25 -4
- package/dist/products/watch.js +75 -14
- package/dist/runloop/index.d.ts +340 -0
- package/dist/runloop/index.js +438 -0
- package/dist/runloop/pagination.d.ts +46 -0
- package/dist/runloop/pagination.js +82 -0
- package/dist/runloop/snapshots.d.ts +52 -0
- package/dist/runloop/snapshots.js +244 -0
- package/dist/sandbox.d.ts +40 -9
- package/dist/sandbox.js +273 -76
- package/dist/snapshots.d.ts +11 -1
- package/dist/snapshots.js +11 -1
- package/dist/sprites/index.d.ts +251 -0
- package/dist/sprites/index.js +552 -0
- package/dist/tar.d.ts +8 -1
- package/dist/tar.js +185 -35
- package/dist/tools/index.js +2 -1
- package/dist/transport.d.ts +20 -2
- package/dist/transport.js +48 -11
- package/dist/types.d.ts +52 -3
- package/dist/vercel/command.js +12 -1
- package/dist/vercel/errors.d.ts +2 -2
- package/dist/vercel/errors.js +12 -1
- package/dist/vercel/filesystem.d.ts +21 -18
- package/dist/vercel/filesystem.js +95 -48
- package/dist/vercel/sandbox.js +5 -5
- package/package.json +45 -3
|
@@ -0,0 +1,1313 @@
|
|
|
1
|
+
import { all, client, CompatibilityError, create, destroy, execOptions, only, quote, ready, } from "../compat/core.js";
|
|
2
|
+
import { GuestFiles } from "../compat/files.js";
|
|
3
|
+
import { ManagedTasks } from "./guest-service.js";
|
|
4
|
+
export { CompatibilityError };
|
|
5
|
+
export class VMTier {
|
|
6
|
+
name;
|
|
7
|
+
cpuCores;
|
|
8
|
+
memoryGiB;
|
|
9
|
+
diskGB;
|
|
10
|
+
constructor(name, cpuCores, memoryGiB, diskGB = 20) {
|
|
11
|
+
this.name = name;
|
|
12
|
+
this.cpuCores = cpuCores;
|
|
13
|
+
this.memoryGiB = memoryGiB;
|
|
14
|
+
this.diskGB = diskGB;
|
|
15
|
+
}
|
|
16
|
+
static Pico = new VMTier("pico", 1, 2);
|
|
17
|
+
static Nano = new VMTier("nano", 2, 4);
|
|
18
|
+
static Micro = new VMTier("micro", 4, 8);
|
|
19
|
+
static Small = new VMTier("small", 8, 16);
|
|
20
|
+
static Medium = new VMTier("medium", 16, 32);
|
|
21
|
+
static Large = new VMTier("large", 32, 64);
|
|
22
|
+
static XLarge = new VMTier("xlarge", 64, 128);
|
|
23
|
+
static All = [
|
|
24
|
+
VMTier.Pico,
|
|
25
|
+
VMTier.Nano,
|
|
26
|
+
VMTier.Micro,
|
|
27
|
+
VMTier.Small,
|
|
28
|
+
VMTier.Medium,
|
|
29
|
+
VMTier.Large,
|
|
30
|
+
VMTier.XLarge,
|
|
31
|
+
];
|
|
32
|
+
static fromName(name) {
|
|
33
|
+
const tier = this.All.find((t) => t.name === name);
|
|
34
|
+
if (!tier)
|
|
35
|
+
throw new TypeError(`Unknown VM tier: ${name}`);
|
|
36
|
+
return tier;
|
|
37
|
+
}
|
|
38
|
+
static fromSpecs(specs) {
|
|
39
|
+
return this.All.find((t) => t.cpuCores >= specs.cpu && t.memoryGiB >= specs.memGiB && t.diskGB >= (specs.diskGB ?? 0));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export class CodeSandbox {
|
|
43
|
+
sandboxes;
|
|
44
|
+
constructor(apiToken, options = {}) {
|
|
45
|
+
this.sandboxes = new Sandboxes(client({ ...options, apiKey: options.apiKey ?? apiToken }));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const metadata = (s) => ({
|
|
49
|
+
id: s.id,
|
|
50
|
+
createdAt: new Date(s.info.createdAt),
|
|
51
|
+
updatedAt: new Date(s.info.createdAt),
|
|
52
|
+
title: s.info.name ?? undefined,
|
|
53
|
+
description: s.info.labels["cs.description"],
|
|
54
|
+
privacy: s.info.labels["cs.privacy"] === "public"
|
|
55
|
+
? "unlisted"
|
|
56
|
+
: s.info.labels["cs.privacy"] === "public-hosts"
|
|
57
|
+
? "private"
|
|
58
|
+
: (s.info.labels["cs.privacy"] ?? "private"),
|
|
59
|
+
tags: JSON.parse(s.info.labels["cs.tags"] ?? "[]"),
|
|
60
|
+
});
|
|
61
|
+
export class Sandboxes {
|
|
62
|
+
runtime;
|
|
63
|
+
constructor(runtime) {
|
|
64
|
+
this.runtime = runtime;
|
|
65
|
+
}
|
|
66
|
+
async create(options = {}) {
|
|
67
|
+
only("CodeSandbox create", options, [
|
|
68
|
+
"id",
|
|
69
|
+
"title",
|
|
70
|
+
"description",
|
|
71
|
+
"tags",
|
|
72
|
+
"privacy",
|
|
73
|
+
"vmTier",
|
|
74
|
+
"hibernationTimeoutSeconds",
|
|
75
|
+
]);
|
|
76
|
+
const labels = {
|
|
77
|
+
"compat.provider": "codesandbox",
|
|
78
|
+
"cs.tags": JSON.stringify([...new Set([...(options.tags ?? []), "sdk"])]),
|
|
79
|
+
"cs.privacy": options.privacy ?? "public-hosts",
|
|
80
|
+
"cs.description": options.description ?? "",
|
|
81
|
+
};
|
|
82
|
+
if (options.id) {
|
|
83
|
+
if (options.vmTier)
|
|
84
|
+
throw new CompatibilityError("CodeSandbox", "changing a fork's VM tier");
|
|
85
|
+
const source = await this.runtime.sandboxes.get(options.id);
|
|
86
|
+
const fork = await source.fork({ name: options.title, labels });
|
|
87
|
+
if (options.hibernationTimeoutSeconds !== undefined)
|
|
88
|
+
await fork.update({ idlePauseSeconds: options.hibernationTimeoutSeconds });
|
|
89
|
+
return new Sandbox(fork, "FORK");
|
|
90
|
+
}
|
|
91
|
+
const tier = options.vmTier ?? VMTier.Pico;
|
|
92
|
+
const sandbox = await create(this.runtime, "codesandbox", {
|
|
93
|
+
name: options.title,
|
|
94
|
+
labels,
|
|
95
|
+
vcpu: tier.cpuCores,
|
|
96
|
+
memoryMiB: tier.memoryGiB * 1024,
|
|
97
|
+
diskMiB: Math.ceil((tier.diskGB * 1e9) / 1048576),
|
|
98
|
+
idlePauseSeconds: options.hibernationTimeoutSeconds,
|
|
99
|
+
persistent: true,
|
|
100
|
+
}, {}, async (sandbox) => {
|
|
101
|
+
await sandbox.exec(["sudo", "mkdir", "-p", "/project/sandbox"], { check: true });
|
|
102
|
+
await sandbox.exec("sudo chown $(id -u):$(id -g) /project/sandbox", { check: true });
|
|
103
|
+
});
|
|
104
|
+
return new Sandbox(sandbox, "CLEAN");
|
|
105
|
+
}
|
|
106
|
+
async resume(id) {
|
|
107
|
+
const s = await this.runtime.sandboxes.get(id);
|
|
108
|
+
const boot = s.state === "paused" ? "RESUME" : s.state === "stopped" ? "CLEAN" : "RUNNING";
|
|
109
|
+
return new Sandbox(await ready(s), boot);
|
|
110
|
+
}
|
|
111
|
+
async hibernate(id) {
|
|
112
|
+
await (await this.runtime.sandboxes.get(id)).pause();
|
|
113
|
+
}
|
|
114
|
+
async shutdown(id) {
|
|
115
|
+
await (await this.runtime.sandboxes.get(id)).stop();
|
|
116
|
+
}
|
|
117
|
+
async delete(id) {
|
|
118
|
+
await destroy(await this.runtime.sandboxes.get(id));
|
|
119
|
+
}
|
|
120
|
+
async restart(id, options = {}) {
|
|
121
|
+
only("CodeSandbox restart", options, ["hibernationTimeoutSeconds"]);
|
|
122
|
+
const s = await this.runtime.sandboxes.get(id);
|
|
123
|
+
await s.stop();
|
|
124
|
+
await s.restart();
|
|
125
|
+
if (options.hibernationTimeoutSeconds !== undefined)
|
|
126
|
+
await s.update({ idlePauseSeconds: options.hibernationTimeoutSeconds });
|
|
127
|
+
return new Sandbox(s, "CLEAN");
|
|
128
|
+
}
|
|
129
|
+
fork(id, options = {}) {
|
|
130
|
+
return this.create({ ...options, id });
|
|
131
|
+
}
|
|
132
|
+
async get(id) {
|
|
133
|
+
return metadata(await this.runtime.sandboxes.get(id));
|
|
134
|
+
}
|
|
135
|
+
async list(options = {}) {
|
|
136
|
+
only("CodeSandbox list", options, [
|
|
137
|
+
"tags",
|
|
138
|
+
"status",
|
|
139
|
+
"limit",
|
|
140
|
+
"pagination",
|
|
141
|
+
"direction",
|
|
142
|
+
"orderBy",
|
|
143
|
+
]);
|
|
144
|
+
if (options.orderBy === "updated_at")
|
|
145
|
+
throw new CompatibilityError("CodeSandbox", "sorting by updated_at");
|
|
146
|
+
let sandboxes = (await all(this.runtime, "codesandbox"))
|
|
147
|
+
.filter((s) => (!options.status || s.state === "running") &&
|
|
148
|
+
(options.tags ?? []).every((tag) => metadata(s).tags.includes(tag)))
|
|
149
|
+
.map(metadata);
|
|
150
|
+
sandboxes.sort((a, b) => (a.createdAt.getTime() - b.createdAt.getTime()) * (options.direction === "desc" ? -1 : 1));
|
|
151
|
+
const totalCount = sandboxes.length, page = options.pagination?.page ?? 1, pageSize = options.pagination?.pageSize ?? options.limit ?? 50;
|
|
152
|
+
sandboxes = sandboxes.slice((page - 1) * pageSize, page * pageSize);
|
|
153
|
+
const hasMore = page * pageSize < totalCount;
|
|
154
|
+
return {
|
|
155
|
+
sandboxes,
|
|
156
|
+
totalCount,
|
|
157
|
+
hasMore,
|
|
158
|
+
pagination: { currentPage: page, nextPage: hasMore ? page + 1 : null, pageSize },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export class Sandbox {
|
|
163
|
+
native;
|
|
164
|
+
bootupType;
|
|
165
|
+
constructor(native, bootupType) {
|
|
166
|
+
this.native = native;
|
|
167
|
+
this.bootupType = bootupType;
|
|
168
|
+
}
|
|
169
|
+
get id() {
|
|
170
|
+
return this.native.id;
|
|
171
|
+
}
|
|
172
|
+
get cluster() {
|
|
173
|
+
return this.native.info.region;
|
|
174
|
+
}
|
|
175
|
+
async updateHibernationTimeout(timeoutSeconds) {
|
|
176
|
+
await this.native.update({ idlePauseSeconds: timeoutSeconds });
|
|
177
|
+
}
|
|
178
|
+
async connect(options = {}) {
|
|
179
|
+
only("CodeSandbox session", options, ["id", "env", "permission"]);
|
|
180
|
+
if (options.permission === "read")
|
|
181
|
+
throw new CompatibilityError("CodeSandbox", "read-only editor sessions");
|
|
182
|
+
const client = new SandboxClient(this.native, options.env ?? {});
|
|
183
|
+
await client.initialize();
|
|
184
|
+
return client;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
export class CommandError extends Error {
|
|
188
|
+
exitCode;
|
|
189
|
+
output;
|
|
190
|
+
constructor(message, exitCode, output) {
|
|
191
|
+
super(message);
|
|
192
|
+
this.exitCode = exitCode;
|
|
193
|
+
this.output = output;
|
|
194
|
+
this.name = "CommandError";
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function emitter() {
|
|
198
|
+
const listeners = new Set();
|
|
199
|
+
return {
|
|
200
|
+
event: (listener) => {
|
|
201
|
+
listeners.add(listener);
|
|
202
|
+
return { dispose: () => listeners.delete(listener) };
|
|
203
|
+
},
|
|
204
|
+
fire: (value) => {
|
|
205
|
+
for (const listener of listeners)
|
|
206
|
+
listener(value);
|
|
207
|
+
},
|
|
208
|
+
clear: () => listeners.clear(),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
export class Command {
|
|
212
|
+
process;
|
|
213
|
+
command;
|
|
214
|
+
name;
|
|
215
|
+
startAgain;
|
|
216
|
+
onOutput;
|
|
217
|
+
onStatusChange;
|
|
218
|
+
outputEvent = emitter();
|
|
219
|
+
statusEvent = emitter();
|
|
220
|
+
output = "";
|
|
221
|
+
result;
|
|
222
|
+
resolveResult;
|
|
223
|
+
rejectResult;
|
|
224
|
+
generation = 0;
|
|
225
|
+
subscription = new AbortController();
|
|
226
|
+
transition = Promise.resolve();
|
|
227
|
+
currentStatus = "RUNNING";
|
|
228
|
+
get status() {
|
|
229
|
+
return this.currentStatus;
|
|
230
|
+
}
|
|
231
|
+
set status(value) {
|
|
232
|
+
if (value === this.currentStatus)
|
|
233
|
+
return;
|
|
234
|
+
this.currentStatus = value;
|
|
235
|
+
this.statusEvent.fire(value);
|
|
236
|
+
}
|
|
237
|
+
constructor(process, command, name, startAgain) {
|
|
238
|
+
this.process = process;
|
|
239
|
+
this.command = command;
|
|
240
|
+
this.name = name;
|
|
241
|
+
this.startAgain = startAgain;
|
|
242
|
+
this.onOutput = this.outputEvent.event;
|
|
243
|
+
this.onStatusChange = this.statusEvent.event;
|
|
244
|
+
this.result = new Promise((resolve, reject) => {
|
|
245
|
+
this.resolveResult = resolve;
|
|
246
|
+
this.rejectResult = reject;
|
|
247
|
+
});
|
|
248
|
+
this.result.catch(() => undefined);
|
|
249
|
+
void this.follow(process, this.generation, this.subscription.signal);
|
|
250
|
+
}
|
|
251
|
+
setStatus(status) {
|
|
252
|
+
this.status = status;
|
|
253
|
+
}
|
|
254
|
+
async follow(process, generation, signal) {
|
|
255
|
+
try {
|
|
256
|
+
let ended = false;
|
|
257
|
+
for await (const event of process.output({ signal })) {
|
|
258
|
+
if (generation !== this.generation)
|
|
259
|
+
return;
|
|
260
|
+
if (event.type === "stdout" || event.type === "stderr") {
|
|
261
|
+
this.output += event.data;
|
|
262
|
+
this.outputEvent.fire(event.data);
|
|
263
|
+
}
|
|
264
|
+
else if (event.type === "truncated") {
|
|
265
|
+
throw new Error("Command output was truncated by the host.");
|
|
266
|
+
}
|
|
267
|
+
else if (event.type === "exit") {
|
|
268
|
+
ended = true;
|
|
269
|
+
this.setStatus(event.exitCode === 0 ? "FINISHED" : "ERROR");
|
|
270
|
+
if (event.exitCode !== 0)
|
|
271
|
+
throw new CommandError(`Command failed with exit code ${event.exitCode ?? "unknown"}`, event.exitCode ?? 1, this.output);
|
|
272
|
+
this.resolveResult(this.output);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (!ended && generation === this.generation)
|
|
276
|
+
throw new Error("Command output ended before its exit status arrived.");
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
if (generation !== this.generation)
|
|
280
|
+
return;
|
|
281
|
+
this.setStatus("ERROR");
|
|
282
|
+
this.rejectResult(error instanceof Error ? error : new Error(String(error)));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/** Detach this client's subscription without terminating the guest command. */
|
|
286
|
+
detach() {
|
|
287
|
+
this.generation++;
|
|
288
|
+
this.subscription.abort();
|
|
289
|
+
this.setStatus("ERROR");
|
|
290
|
+
this.rejectResult(new Error("Sandbox client is disconnected."));
|
|
291
|
+
}
|
|
292
|
+
async open(dimensions) {
|
|
293
|
+
if (dimensions)
|
|
294
|
+
await this.process.resize(dimensions.cols, dimensions.rows);
|
|
295
|
+
return this.output;
|
|
296
|
+
}
|
|
297
|
+
/** Internal task bridge: read without yielding between log handoff and events. */
|
|
298
|
+
get bufferedOutput() {
|
|
299
|
+
return this.output;
|
|
300
|
+
}
|
|
301
|
+
waitUntilComplete() {
|
|
302
|
+
return this.result;
|
|
303
|
+
}
|
|
304
|
+
serial(work) {
|
|
305
|
+
const pending = this.transition.then(work);
|
|
306
|
+
this.transition = pending.catch(() => undefined);
|
|
307
|
+
return pending;
|
|
308
|
+
}
|
|
309
|
+
restart() {
|
|
310
|
+
return this.serial(async () => {
|
|
311
|
+
if (this.status !== "RUNNING")
|
|
312
|
+
throw new Error("Command is not running");
|
|
313
|
+
if (!this.startAgain)
|
|
314
|
+
throw new CompatibilityError("CodeSandbox", "restarting a command from a different client");
|
|
315
|
+
// Fence the old subscription before killing: its delayed exit must not
|
|
316
|
+
// complete a waiter or overwrite the replacement command's status.
|
|
317
|
+
this.generation++;
|
|
318
|
+
this.subscription.abort();
|
|
319
|
+
try {
|
|
320
|
+
await this.process.kill("SIGKILL");
|
|
321
|
+
this.process = await this.startAgain();
|
|
322
|
+
this.subscription = new AbortController();
|
|
323
|
+
void this.follow(this.process, this.generation, this.subscription.signal);
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
this.setStatus("ERROR");
|
|
327
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
328
|
+
this.rejectResult(failure);
|
|
329
|
+
throw failure;
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
kill() {
|
|
334
|
+
return this.serial(async () => {
|
|
335
|
+
this.generation++;
|
|
336
|
+
this.subscription.abort();
|
|
337
|
+
try {
|
|
338
|
+
await this.process.kill("SIGKILL");
|
|
339
|
+
this.setStatus("KILLED");
|
|
340
|
+
this.rejectResult(new CommandError("Command failed with exit code unknown", 1, this.output));
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
this.setStatus("ERROR");
|
|
344
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
345
|
+
this.rejectResult(failure);
|
|
346
|
+
throw failure;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
export class FileSystem {
|
|
352
|
+
native;
|
|
353
|
+
files;
|
|
354
|
+
constructor(native) {
|
|
355
|
+
this.native = native;
|
|
356
|
+
this.files = new GuestFiles(async () => native, "/project/sandbox");
|
|
357
|
+
}
|
|
358
|
+
async writeFile(path, content, options = {}) {
|
|
359
|
+
only("CodeSandbox file write", options, ["create", "overwrite"]);
|
|
360
|
+
const exists = await this.files.exists(path);
|
|
361
|
+
if (!exists && options.create === false)
|
|
362
|
+
throw new Error(`File does not exist: ${path}`);
|
|
363
|
+
if (exists && options.overwrite === false)
|
|
364
|
+
throw new Error(`File already exists: ${path}`);
|
|
365
|
+
await this.files.writeFile(path, content);
|
|
366
|
+
}
|
|
367
|
+
writeTextFile(path, content, options) {
|
|
368
|
+
return this.writeFile(path, new TextEncoder().encode(content), options);
|
|
369
|
+
}
|
|
370
|
+
async batchWrite(files) {
|
|
371
|
+
for (const f of files)
|
|
372
|
+
await this.files.writeFile(f.path, f.content);
|
|
373
|
+
}
|
|
374
|
+
readFile(path) {
|
|
375
|
+
return this.files.readFile(path);
|
|
376
|
+
}
|
|
377
|
+
readTextFile(path) {
|
|
378
|
+
return this.files.readTextFile(path);
|
|
379
|
+
}
|
|
380
|
+
mkdir(path, recursive = false) {
|
|
381
|
+
return this.files.mkdir(path, recursive);
|
|
382
|
+
}
|
|
383
|
+
remove(path, recursive = false) {
|
|
384
|
+
return this.files.remove(path, recursive);
|
|
385
|
+
}
|
|
386
|
+
rename(from, to, overwrite = false) {
|
|
387
|
+
return this.files.rename(from, to, overwrite);
|
|
388
|
+
}
|
|
389
|
+
copy(from, to, recursive = false, overwrite = false) {
|
|
390
|
+
return this.files.copy(from, to, recursive, overwrite);
|
|
391
|
+
}
|
|
392
|
+
async readdir(path) {
|
|
393
|
+
return (await this.files.readdir(path)).map((e) => ({
|
|
394
|
+
name: e.name,
|
|
395
|
+
type: e.type === "directory" ? "directory" : "file",
|
|
396
|
+
isSymlink: e.type === "symlink",
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
async stat(path) {
|
|
400
|
+
const e = await this.files.stat(path);
|
|
401
|
+
const r = await this.native.exec(["stat", "-c", "%X %Y %Z", "--", this.files.path(path)], {
|
|
402
|
+
check: true,
|
|
403
|
+
});
|
|
404
|
+
const [atime, mtime, ctime] = r.stdout.trim().split(" ").map(Number);
|
|
405
|
+
return {
|
|
406
|
+
type: e.type === "directory" ? "directory" : "file",
|
|
407
|
+
isSymlink: e.type === "symlink",
|
|
408
|
+
size: e.size,
|
|
409
|
+
atime: atime * 1000,
|
|
410
|
+
mtime: mtime * 1000,
|
|
411
|
+
ctime: ctime * 1000,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const commandPath = (id) => `/workspace/.runtime-compat/codesandbox/commands/${encodeURIComponent(id)}.json`;
|
|
416
|
+
export class SandboxClient {
|
|
417
|
+
native;
|
|
418
|
+
env;
|
|
419
|
+
fs;
|
|
420
|
+
commands;
|
|
421
|
+
ports;
|
|
422
|
+
tasks;
|
|
423
|
+
setup;
|
|
424
|
+
workspacePath = "/project/sandbox";
|
|
425
|
+
disconnected = false;
|
|
426
|
+
attached = new Set();
|
|
427
|
+
previewURLs = new Map();
|
|
428
|
+
stopKeepAlive;
|
|
429
|
+
constructor(native, env) {
|
|
430
|
+
this.native = native;
|
|
431
|
+
this.env = env;
|
|
432
|
+
this.fs = new FileSystem(native);
|
|
433
|
+
this.tasks = new Tasks(this, native);
|
|
434
|
+
this.setup = new Setup(this, native);
|
|
435
|
+
const runBackground = async (command, options = {}) => {
|
|
436
|
+
only("CodeSandbox command", options, ["env", "cwd", "name", "dimensions", "asGlobalSession"]);
|
|
437
|
+
if (options.asGlobalSession)
|
|
438
|
+
throw new CompatibilityError("CodeSandbox", "global editor sessions");
|
|
439
|
+
if (this.disconnected)
|
|
440
|
+
throw new Error("Sandbox client is disconnected.");
|
|
441
|
+
const { stdin: _stdin, ...spawnOptions } = await execOptions(native, {
|
|
442
|
+
env: { ...this.env, ...options.env },
|
|
443
|
+
cwd: options.cwd ?? this.workspacePath,
|
|
444
|
+
});
|
|
445
|
+
// The published SDK treats arrays as sequential shell commands, not argv.
|
|
446
|
+
const saved = {
|
|
447
|
+
version: 1,
|
|
448
|
+
command: Array.isArray(command) ? command.join(" && ") : command,
|
|
449
|
+
name: options.name,
|
|
450
|
+
options: {
|
|
451
|
+
...spawnOptions,
|
|
452
|
+
env: { ...spawnOptions.env },
|
|
453
|
+
pty: { ...(options.dimensions ?? { cols: 128, rows: 24 }) },
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
return this.attach(await this.start(saved), saved);
|
|
457
|
+
};
|
|
458
|
+
this.commands = {
|
|
459
|
+
runBackground,
|
|
460
|
+
run: async (command, options) => (await runBackground(command, options)).waitUntilComplete(),
|
|
461
|
+
getAll: async () => {
|
|
462
|
+
this.assertConnected();
|
|
463
|
+
const commands = [];
|
|
464
|
+
for (const process of await native.processes.list()) {
|
|
465
|
+
if (process.state !== "running" || !(await native.files.exists(commandPath(process.id))))
|
|
466
|
+
continue;
|
|
467
|
+
const saved = JSON.parse(await native.files.readText(commandPath(process.id)));
|
|
468
|
+
if (saved.version !== 1 || typeof saved.command !== "string" || !saved.options)
|
|
469
|
+
throw new Error(`Invalid saved CodeSandbox command: ${process.id}`);
|
|
470
|
+
if (saved.taskId === undefined)
|
|
471
|
+
commands.push(this.attach(await native.processes.get(process.id), saved));
|
|
472
|
+
}
|
|
473
|
+
return commands;
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
this.ports = {
|
|
477
|
+
get: async (port) => {
|
|
478
|
+
if (native.info.labels["cs.privacy"] === "private")
|
|
479
|
+
throw new CompatibilityError("CodeSandbox", "private port host authentication; use Runtime signed preview URLs");
|
|
480
|
+
const p = await native.previews.create(port, { visibility: "public" });
|
|
481
|
+
this.previewURLs.set(port, p.urlWithToken ?? p.url);
|
|
482
|
+
return { port, host: new URL(p.urlWithToken ?? p.url).host };
|
|
483
|
+
},
|
|
484
|
+
getAll: async () => (await native.previews.list()).map((p) => ({ port: p.port, host: new URL(p.url).host })),
|
|
485
|
+
waitForPort: async (port, options = {}) => {
|
|
486
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
487
|
+
throw new TypeError("Invalid port");
|
|
488
|
+
await native.exec([
|
|
489
|
+
"python3",
|
|
490
|
+
"-c",
|
|
491
|
+
'import socket,sys,time\nport=int(sys.argv[1]); end=time.monotonic()+float(sys.argv[2])/1000\nwhile True:\n try:\n s=socket.create_connection(("127.0.0.1",port),timeout=0.25); s.close(); break\n except OSError:\n if time.monotonic() >= end: raise TimeoutError("Port did not open")\n time.sleep(0.1)',
|
|
492
|
+
String(port),
|
|
493
|
+
String(options.timeoutMs ?? 30000),
|
|
494
|
+
], { check: true, timeoutMs: (options.timeoutMs ?? 30000) + 1000 });
|
|
495
|
+
return this.ports.get(port);
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
async initialize() {
|
|
500
|
+
await this.tasks.getAll();
|
|
501
|
+
await this.setup.initialize(true);
|
|
502
|
+
}
|
|
503
|
+
previewURL(port) {
|
|
504
|
+
const url = this.previewURLs.get(port);
|
|
505
|
+
if (!url)
|
|
506
|
+
throw new Error("Port has not been exposed");
|
|
507
|
+
return url;
|
|
508
|
+
}
|
|
509
|
+
assertConnected() {
|
|
510
|
+
if (this.disconnected)
|
|
511
|
+
throw new Error("Sandbox client is disconnected.");
|
|
512
|
+
}
|
|
513
|
+
async start(saved) {
|
|
514
|
+
this.assertConnected();
|
|
515
|
+
const process = await this.native.spawn(saved.command, saved.options);
|
|
516
|
+
try {
|
|
517
|
+
// Arguments and environment may contain credentials. Keep them in the
|
|
518
|
+
// guest's private file, never resource labels or public process metadata.
|
|
519
|
+
await this.native.files.write(commandPath(process.id), JSON.stringify(saved), {
|
|
520
|
+
mode: 0o600,
|
|
521
|
+
});
|
|
522
|
+
return process;
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
try {
|
|
526
|
+
await process.kill("SIGKILL");
|
|
527
|
+
}
|
|
528
|
+
catch (cleanup) {
|
|
529
|
+
throw new AggregateError([error, cleanup], "Command metadata failed and cleanup could not be confirmed");
|
|
530
|
+
}
|
|
531
|
+
throw error;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
attach(process, saved) {
|
|
535
|
+
const command = new Command(process, saved.command, saved.name, () => this.start(saved));
|
|
536
|
+
this.attached.add(command);
|
|
537
|
+
return command;
|
|
538
|
+
}
|
|
539
|
+
get id() {
|
|
540
|
+
return this.native.id;
|
|
541
|
+
}
|
|
542
|
+
get state() {
|
|
543
|
+
return this.disconnected ? "DISCONNECTED" : "CONNECTED";
|
|
544
|
+
}
|
|
545
|
+
async disconnect() {
|
|
546
|
+
this.disconnected = true;
|
|
547
|
+
this.tasks.dispose();
|
|
548
|
+
this.setup.dispose();
|
|
549
|
+
for (const command of this.attached)
|
|
550
|
+
command.detach();
|
|
551
|
+
this.attached.clear();
|
|
552
|
+
this.stopKeepAlive?.();
|
|
553
|
+
}
|
|
554
|
+
async reconnect() {
|
|
555
|
+
await ready(await this.native.refresh());
|
|
556
|
+
this.disconnected = false;
|
|
557
|
+
await this.tasks.reconnect();
|
|
558
|
+
await this.setup.reconnect();
|
|
559
|
+
}
|
|
560
|
+
keepActiveWhileConnected(enabled) {
|
|
561
|
+
this.stopKeepAlive?.();
|
|
562
|
+
if (enabled)
|
|
563
|
+
this.stopKeepAlive = this.native.keepAlive();
|
|
564
|
+
}
|
|
565
|
+
dispose() {
|
|
566
|
+
this.disconnected = true;
|
|
567
|
+
this.tasks.dispose();
|
|
568
|
+
this.setup.dispose();
|
|
569
|
+
for (const command of this.attached)
|
|
570
|
+
command.detach();
|
|
571
|
+
this.attached.clear();
|
|
572
|
+
this.stopKeepAlive?.();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/** Configured tasks share durable native processes between client connections. */
|
|
576
|
+
export class Tasks {
|
|
577
|
+
client;
|
|
578
|
+
native;
|
|
579
|
+
cached;
|
|
580
|
+
managed;
|
|
581
|
+
loaded = [];
|
|
582
|
+
cancelled = new AbortController();
|
|
583
|
+
constructor(client, native) {
|
|
584
|
+
this.client = client;
|
|
585
|
+
this.native = native;
|
|
586
|
+
this.managed = new ManagedTasks(native);
|
|
587
|
+
}
|
|
588
|
+
getAll() {
|
|
589
|
+
return (this.cached ??= this.load().catch((error) => {
|
|
590
|
+
this.cached = undefined;
|
|
591
|
+
throw error;
|
|
592
|
+
}));
|
|
593
|
+
}
|
|
594
|
+
async get(id) {
|
|
595
|
+
return (await this.getAll()).find((task) => task.id === id);
|
|
596
|
+
}
|
|
597
|
+
async reconnect() {
|
|
598
|
+
if (this.cached)
|
|
599
|
+
for (const task of await this.cached)
|
|
600
|
+
task.dispose();
|
|
601
|
+
this.cached = undefined;
|
|
602
|
+
this.cancelled = new AbortController();
|
|
603
|
+
await this.getAll();
|
|
604
|
+
}
|
|
605
|
+
dispose() {
|
|
606
|
+
this.cancelled.abort();
|
|
607
|
+
for (const task of this.loaded)
|
|
608
|
+
task.dispose();
|
|
609
|
+
}
|
|
610
|
+
async load() {
|
|
611
|
+
const path = `${this.client.workspacePath}/.codesandbox/tasks.json`;
|
|
612
|
+
let config;
|
|
613
|
+
if (await this.native.files.exists(path)) {
|
|
614
|
+
config = JSON.parse(await this.native.files.readText(path));
|
|
615
|
+
only("CodeSandbox tasks config", config, ["$schema", "tasks", "setupTasks"]);
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
const packagePath = `${this.client.workspacePath}/package.json`;
|
|
619
|
+
if (!(await this.native.files.exists(packagePath)))
|
|
620
|
+
return [];
|
|
621
|
+
const pkg = JSON.parse(await this.native.files.readText(packagePath));
|
|
622
|
+
config = {
|
|
623
|
+
tasks: Object.fromEntries(Object.keys(pkg.scripts ?? {}).map((name) => [
|
|
624
|
+
name,
|
|
625
|
+
{ name, command: `npm run ${quote(name)}` },
|
|
626
|
+
])),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
const tasks = [];
|
|
630
|
+
for (const [id, definition] of Object.entries(config.tasks ?? {})) {
|
|
631
|
+
only("CodeSandbox task", definition, ["name", "command", "runAtStart", "preview"]);
|
|
632
|
+
if (typeof definition.name !== "string" || typeof definition.command !== "string")
|
|
633
|
+
throw new TypeError(`Task ${id} requires a name and command`);
|
|
634
|
+
if (definition.runAtStart !== undefined && typeof definition.runAtStart !== "boolean")
|
|
635
|
+
throw new TypeError(`Task ${id} runAtStart must be a boolean`);
|
|
636
|
+
if (definition.preview) {
|
|
637
|
+
only("CodeSandbox task preview", definition.preview, ["port", "prLink", "pr-link"]);
|
|
638
|
+
if (definition.preview.port !== undefined &&
|
|
639
|
+
(!Number.isInteger(definition.preview.port) ||
|
|
640
|
+
definition.preview.port < 1 ||
|
|
641
|
+
definition.preview.port > 65535))
|
|
642
|
+
throw new TypeError(`Task ${id} has an invalid preview port`);
|
|
643
|
+
}
|
|
644
|
+
tasks.push(new Task(this.client, id, structuredClone(definition), this.managed));
|
|
645
|
+
}
|
|
646
|
+
if (tasks.length) {
|
|
647
|
+
const steps = (config.setupTasks ?? []).map((step) => typeof step === "string" ? { name: step, command: step } : step);
|
|
648
|
+
for (const step of steps) {
|
|
649
|
+
only("CodeSandbox setup step", step, ["name", "command"]);
|
|
650
|
+
if (typeof step.name !== "string" || typeof step.command !== "string")
|
|
651
|
+
throw new TypeError("Setup steps require a name and command");
|
|
652
|
+
}
|
|
653
|
+
const { env } = await execOptions(this.native, {});
|
|
654
|
+
await this.managed.install(config.tasks ?? {}, env ?? {}, { steps, source: setupRunner });
|
|
655
|
+
for (const task of tasks)
|
|
656
|
+
await task.reconnect();
|
|
657
|
+
}
|
|
658
|
+
this.loaded = tasks;
|
|
659
|
+
if (tasks.length)
|
|
660
|
+
this.monitor();
|
|
661
|
+
return tasks;
|
|
662
|
+
}
|
|
663
|
+
monitor() {
|
|
664
|
+
const signal = this.cancelled.signal;
|
|
665
|
+
void (async () => {
|
|
666
|
+
while (!signal.aborted) {
|
|
667
|
+
try {
|
|
668
|
+
for (const run of await this.managed.list(signal)) {
|
|
669
|
+
if (signal.aborted)
|
|
670
|
+
return;
|
|
671
|
+
this.loaded.find((task) => task.id === run.taskId)?.observe(run.process);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
catch (error) {
|
|
675
|
+
if (signal.aborted)
|
|
676
|
+
return;
|
|
677
|
+
for (const task of this.loaded)
|
|
678
|
+
task.unavailable(error);
|
|
679
|
+
}
|
|
680
|
+
await new Promise((resolve) => {
|
|
681
|
+
const stop = () => {
|
|
682
|
+
clearTimeout(timer);
|
|
683
|
+
signal.removeEventListener("abort", stop);
|
|
684
|
+
resolve();
|
|
685
|
+
};
|
|
686
|
+
const timer = setTimeout(stop, 500);
|
|
687
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
688
|
+
if (signal.aborted)
|
|
689
|
+
stop();
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
})();
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
export class Task {
|
|
696
|
+
client;
|
|
697
|
+
id;
|
|
698
|
+
definition;
|
|
699
|
+
managed;
|
|
700
|
+
process;
|
|
701
|
+
processId;
|
|
702
|
+
fault;
|
|
703
|
+
disposed = false;
|
|
704
|
+
opened = false;
|
|
705
|
+
streamedLength = 0;
|
|
706
|
+
openedLength = 0;
|
|
707
|
+
outputEvent = emitter();
|
|
708
|
+
statusEvent = emitter();
|
|
709
|
+
onOutput = this.outputEvent.event;
|
|
710
|
+
onStatusChange = this.statusEvent.event;
|
|
711
|
+
subscriptions = [];
|
|
712
|
+
transition = Promise.resolve();
|
|
713
|
+
exposed = [];
|
|
714
|
+
constructor(client, id, definition, managed) {
|
|
715
|
+
this.client = client;
|
|
716
|
+
this.id = id;
|
|
717
|
+
this.definition = definition;
|
|
718
|
+
this.managed = managed;
|
|
719
|
+
}
|
|
720
|
+
async reconnect() {
|
|
721
|
+
const process = await this.managed.get(this.id);
|
|
722
|
+
if (process)
|
|
723
|
+
this.observe(process);
|
|
724
|
+
}
|
|
725
|
+
observe(process) {
|
|
726
|
+
if (this.disposed)
|
|
727
|
+
return;
|
|
728
|
+
this.fault = undefined;
|
|
729
|
+
if (process.id === this.processId)
|
|
730
|
+
return;
|
|
731
|
+
this.process?.detach();
|
|
732
|
+
this.processId = process.id;
|
|
733
|
+
this.exposed = [];
|
|
734
|
+
this.streamedLength = 0;
|
|
735
|
+
this.openedLength = 0;
|
|
736
|
+
const command = new Command(process, this.command, this.name, () => this.managed.start(this.id));
|
|
737
|
+
if (process.info.state !== "running")
|
|
738
|
+
command.status = process.info.exitCode === 0 ? "FINISHED" : "ERROR";
|
|
739
|
+
this.attach(command);
|
|
740
|
+
this.statusEvent.fire(this.status);
|
|
741
|
+
}
|
|
742
|
+
unavailable(error) {
|
|
743
|
+
if (this.disposed)
|
|
744
|
+
return;
|
|
745
|
+
this.fault = error;
|
|
746
|
+
this.statusEvent.fire("ERROR");
|
|
747
|
+
}
|
|
748
|
+
get name() {
|
|
749
|
+
return this.definition.name;
|
|
750
|
+
}
|
|
751
|
+
get command() {
|
|
752
|
+
return this.definition.command;
|
|
753
|
+
}
|
|
754
|
+
get runAtStart() {
|
|
755
|
+
return Boolean(this.definition.runAtStart);
|
|
756
|
+
}
|
|
757
|
+
get status() {
|
|
758
|
+
if (this.fault)
|
|
759
|
+
return "ERROR";
|
|
760
|
+
return this.process?.status ?? "IDLE";
|
|
761
|
+
}
|
|
762
|
+
get ports() {
|
|
763
|
+
return this.exposed;
|
|
764
|
+
}
|
|
765
|
+
attach(command) {
|
|
766
|
+
for (const subscription of this.subscriptions)
|
|
767
|
+
subscription.dispose();
|
|
768
|
+
this.process = command;
|
|
769
|
+
this.subscriptions = [
|
|
770
|
+
command.onStatusChange((status) => this.statusEvent.fire(status)),
|
|
771
|
+
command.onOutput((output) => {
|
|
772
|
+
const start = this.streamedLength;
|
|
773
|
+
this.streamedLength += output.length;
|
|
774
|
+
if (this.opened && this.streamedLength > this.openedLength)
|
|
775
|
+
this.outputEvent.fire(output.slice(Math.max(0, this.openedLength - start)));
|
|
776
|
+
}),
|
|
777
|
+
];
|
|
778
|
+
}
|
|
779
|
+
async open(dimensions) {
|
|
780
|
+
if (!this.process)
|
|
781
|
+
throw new Error("Task is not running");
|
|
782
|
+
await this.process.open(dimensions);
|
|
783
|
+
const output = await this.managed.output(this.processId);
|
|
784
|
+
const followed = this.process.bufferedOutput;
|
|
785
|
+
this.openedLength = Math.max(output.length, followed.length);
|
|
786
|
+
this.opened = true;
|
|
787
|
+
if (followed.length > output.length)
|
|
788
|
+
this.outputEvent.fire(followed.slice(output.length));
|
|
789
|
+
return output;
|
|
790
|
+
}
|
|
791
|
+
serial(work) {
|
|
792
|
+
const result = this.transition.then(work);
|
|
793
|
+
this.transition = result.catch(() => undefined);
|
|
794
|
+
return result;
|
|
795
|
+
}
|
|
796
|
+
run() {
|
|
797
|
+
return this.serial(async () => {
|
|
798
|
+
if (this.client.state !== "CONNECTED")
|
|
799
|
+
throw new Error("Sandbox client is disconnected.");
|
|
800
|
+
this.observe(await this.managed.start(this.id));
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
restart() {
|
|
804
|
+
return this.run();
|
|
805
|
+
}
|
|
806
|
+
stop() {
|
|
807
|
+
return this.serial(async () => {
|
|
808
|
+
if (this.client.state !== "CONNECTED")
|
|
809
|
+
throw new Error("Sandbox client is disconnected.");
|
|
810
|
+
const current = await this.managed.stop(this.id);
|
|
811
|
+
if (current) {
|
|
812
|
+
this.observe(current);
|
|
813
|
+
await this.process.kill();
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
async waitForPort(timeout = 30000) {
|
|
818
|
+
if (this.exposed.length)
|
|
819
|
+
return this.exposed[0];
|
|
820
|
+
const port = this.definition.preview?.port;
|
|
821
|
+
if (!port)
|
|
822
|
+
throw new CompatibilityError("CodeSandbox", "automatic task-to-port discovery; configure task.preview.port");
|
|
823
|
+
try {
|
|
824
|
+
const result = await this.client.ports.waitForPort(port, { timeoutMs: timeout });
|
|
825
|
+
const opened = { port: result.port, url: this.client.previewURL(result.port) };
|
|
826
|
+
this.exposed = [opened];
|
|
827
|
+
return opened;
|
|
828
|
+
}
|
|
829
|
+
catch (error) {
|
|
830
|
+
if (error.stderr?.includes("TimeoutError: Port did not open"))
|
|
831
|
+
throw new Error("Timeout waiting for port", { cause: error });
|
|
832
|
+
throw error;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
dispose() {
|
|
836
|
+
this.disposed = true;
|
|
837
|
+
for (const subscription of this.subscriptions)
|
|
838
|
+
subscription.dispose();
|
|
839
|
+
this.subscriptions = [];
|
|
840
|
+
this.process?.detach();
|
|
841
|
+
this.outputEvent.clear();
|
|
842
|
+
this.statusEvent.clear();
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
const setupRoot = "/workspace/.runtime-compat/codesandbox/setup";
|
|
846
|
+
// The guest owns the sequence and lock; progress survives SDK disconnects.
|
|
847
|
+
const setupRunner = String.raw `import errno,fcntl,json,os,pty,select,signal,struct,subprocess,sys,termios,threading
|
|
848
|
+
with open(sys.argv[1]) as f: config=json.load(f)
|
|
849
|
+
root=os.path.dirname(sys.argv[1])
|
|
850
|
+
os.umask(0o077)
|
|
851
|
+
lock=open(os.path.join(root,'lock'),'a')
|
|
852
|
+
try: fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB)
|
|
853
|
+
except BlockingIOError: sys.exit(0)
|
|
854
|
+
state={'state':'IN_PROGRESS','currentStepIndex':0,'runId':config['runId'],'config':config['config'],'steps':[dict(x,status='IDLE',log=os.path.join(root,config['runId']+'-'+str(i)+'.log')) for i,x in enumerate(config['steps'])]}
|
|
855
|
+
def save():
|
|
856
|
+
temp=os.path.join(root,'progress-'+config['runId']+'.tmp')
|
|
857
|
+
with open(temp,'w') as f: json.dump(state,f)
|
|
858
|
+
os.replace(temp,os.path.join(root,'progress.json'))
|
|
859
|
+
child=None
|
|
860
|
+
def stop(signum,frame):
|
|
861
|
+
if child is not None and child.poll() is None:
|
|
862
|
+
try: os.killpg(child.pid,signal.SIGTERM)
|
|
863
|
+
except ProcessLookupError: pass
|
|
864
|
+
try: child.wait(timeout=2)
|
|
865
|
+
except subprocess.TimeoutExpired:
|
|
866
|
+
try: os.killpg(child.pid,signal.SIGKILL)
|
|
867
|
+
except ProcessLookupError: pass
|
|
868
|
+
child.wait()
|
|
869
|
+
state['state']='STOPPED'
|
|
870
|
+
save()
|
|
871
|
+
sys.exit(128+signum)
|
|
872
|
+
signal.signal(signal.SIGTERM,stop)
|
|
873
|
+
signal.signal(signal.SIGINT,stop)
|
|
874
|
+
save()
|
|
875
|
+
try:
|
|
876
|
+
for i,step in enumerate(state['steps']):
|
|
877
|
+
state['currentStepIndex']=i
|
|
878
|
+
master,slave=pty.openpty()
|
|
879
|
+
fcntl.ioctl(slave,termios.TIOCSWINSZ,struct.pack('HHHH',24,128,0,0))
|
|
880
|
+
step['tty']=os.ttyname(slave)
|
|
881
|
+
save()
|
|
882
|
+
with open(step['log'],'wb',buffering=0) as output:
|
|
883
|
+
finished=threading.Event()
|
|
884
|
+
errors=[]
|
|
885
|
+
def collect():
|
|
886
|
+
try:
|
|
887
|
+
while True:
|
|
888
|
+
ready,_,_=select.select([master],[],[],0.05)
|
|
889
|
+
if not ready:
|
|
890
|
+
if finished.is_set(): break
|
|
891
|
+
continue
|
|
892
|
+
try: data=os.read(master,65536)
|
|
893
|
+
except OSError as e:
|
|
894
|
+
if e.errno==errno.EIO: break
|
|
895
|
+
raise
|
|
896
|
+
if not data: break
|
|
897
|
+
output.write(data)
|
|
898
|
+
except BaseException as e:
|
|
899
|
+
errors.append(str(e))
|
|
900
|
+
try: os.killpg(child.pid,signal.SIGKILL)
|
|
901
|
+
except ProcessLookupError: pass
|
|
902
|
+
finally: os.close(master)
|
|
903
|
+
try: child=subprocess.Popen(['bash','-c',step['command']],cwd=config['cwd'],stdin=slave,stdout=slave,stderr=slave,start_new_session=True)
|
|
904
|
+
finally: os.close(slave)
|
|
905
|
+
collector=threading.Thread(target=collect,daemon=True)
|
|
906
|
+
collector.start()
|
|
907
|
+
code=child.wait()
|
|
908
|
+
finished.set()
|
|
909
|
+
collector.join()
|
|
910
|
+
if errors: code=125
|
|
911
|
+
step['status']='SUCCEEDED' if code==0 else 'FAILED'
|
|
912
|
+
if code!=0:
|
|
913
|
+
state['state']='STOPPED'
|
|
914
|
+
save()
|
|
915
|
+
sys.exit(code)
|
|
916
|
+
save()
|
|
917
|
+
state['state']='FINISHED'
|
|
918
|
+
save()
|
|
919
|
+
except BaseException:
|
|
920
|
+
if state['state']=='IN_PROGRESS':
|
|
921
|
+
state['state']='STOPPED'
|
|
922
|
+
save()
|
|
923
|
+
raise
|
|
924
|
+
`;
|
|
925
|
+
// The cached progress can be stale: the step may have ended and its terminal
|
|
926
|
+
// closed, or on Linux its /dev/pts number been reused. Resize only while the
|
|
927
|
+
// guest's own progress still names this run's running step and terminal; a
|
|
928
|
+
// terminal that has gone needs no resize. O_NOCTTY keeps it from becoming the
|
|
929
|
+
// helper's controlling terminal.
|
|
930
|
+
const resizeStep = String.raw `import errno,fcntl,json,os,struct,sys,termios
|
|
931
|
+
progress,run,index,tty,rows,cols=sys.argv[1:7]
|
|
932
|
+
try:
|
|
933
|
+
with open(progress) as f: state=json.load(f)
|
|
934
|
+
except FileNotFoundError: sys.exit(0)
|
|
935
|
+
steps=state.get('steps') or []
|
|
936
|
+
i=int(index)
|
|
937
|
+
if state.get('runId')!=run or i>=len(steps) or steps[i].get('status')!='IDLE' or steps[i].get('tty')!=tty: sys.exit(0)
|
|
938
|
+
try: fd=os.open(tty,os.O_RDWR|os.O_NOCTTY)
|
|
939
|
+
except OSError as e:
|
|
940
|
+
if e.errno in (errno.ENOENT,errno.ENXIO,errno.EIO): sys.exit(0)
|
|
941
|
+
raise
|
|
942
|
+
try: fcntl.ioctl(fd,termios.TIOCSWINSZ,struct.pack('HHHH',int(rows),int(cols),0,0))
|
|
943
|
+
finally: os.close(fd)
|
|
944
|
+
`;
|
|
945
|
+
export class Setup {
|
|
946
|
+
client;
|
|
947
|
+
native;
|
|
948
|
+
progress = { state: "IDLE", currentStepIndex: 0, config: "", steps: [] };
|
|
949
|
+
steps = [];
|
|
950
|
+
loaded;
|
|
951
|
+
active;
|
|
952
|
+
starting;
|
|
953
|
+
failure;
|
|
954
|
+
ignoredRunId;
|
|
955
|
+
managedBoot = false;
|
|
956
|
+
cancelled = new AbortController();
|
|
957
|
+
changed = emitter();
|
|
958
|
+
onSetupProgressChange = this.changed.event;
|
|
959
|
+
constructor(client, native) {
|
|
960
|
+
this.client = client;
|
|
961
|
+
this.native = native;
|
|
962
|
+
}
|
|
963
|
+
get status() {
|
|
964
|
+
return this.progress.state;
|
|
965
|
+
}
|
|
966
|
+
get currentStepIndex() {
|
|
967
|
+
return this.progress.currentStepIndex;
|
|
968
|
+
}
|
|
969
|
+
getSteps() {
|
|
970
|
+
return this.steps;
|
|
971
|
+
}
|
|
972
|
+
async initialize(automatic = false) {
|
|
973
|
+
await (this.loaded ??= this.load());
|
|
974
|
+
if (automatic && this.status === "IDLE")
|
|
975
|
+
await this.run();
|
|
976
|
+
if (this.status === "IN_PROGRESS")
|
|
977
|
+
this.monitor();
|
|
978
|
+
}
|
|
979
|
+
async reconnect() {
|
|
980
|
+
await this.active;
|
|
981
|
+
this.cancelled = new AbortController();
|
|
982
|
+
this.failure = undefined;
|
|
983
|
+
this.ignoredRunId = undefined;
|
|
984
|
+
this.loaded = undefined;
|
|
985
|
+
await this.initialize(true);
|
|
986
|
+
}
|
|
987
|
+
async load() {
|
|
988
|
+
const path = `${this.client.workspacePath}/.codesandbox/tasks.json`;
|
|
989
|
+
const config = (await this.native.files.exists(path))
|
|
990
|
+
? JSON.parse(await this.native.files.readText(path))
|
|
991
|
+
: {};
|
|
992
|
+
const definitions = (config.setupTasks ?? []).map((step) => typeof step === "string" ? { name: step, command: step } : step);
|
|
993
|
+
for (const definition of definitions) {
|
|
994
|
+
only("CodeSandbox setup step", definition, ["name", "command"]);
|
|
995
|
+
if (typeof definition.name !== "string" || typeof definition.command !== "string")
|
|
996
|
+
throw new TypeError("Setup steps require a name and command");
|
|
997
|
+
}
|
|
998
|
+
const identity = JSON.stringify(definitions);
|
|
999
|
+
this.progress = {
|
|
1000
|
+
state: definitions.length ? "IDLE" : "FINISHED",
|
|
1001
|
+
currentStepIndex: 0,
|
|
1002
|
+
config: identity,
|
|
1003
|
+
steps: definitions.map((step) => ({ ...step, status: "IDLE" })),
|
|
1004
|
+
};
|
|
1005
|
+
if (await this.native.files.exists(`${setupRoot}/progress.json`)) {
|
|
1006
|
+
const stored = JSON.parse(await this.native.files.readText(`${setupRoot}/progress.json`));
|
|
1007
|
+
if (stored.config === identity)
|
|
1008
|
+
this.progress = stored;
|
|
1009
|
+
}
|
|
1010
|
+
this.steps = definitions.map((definition, index) => new Step(this, index, definition));
|
|
1011
|
+
const boot = await new ManagedTasks(this.native).boot();
|
|
1012
|
+
this.managedBoot = Boolean(boot && boot.config === identity && boot.state !== "ready");
|
|
1013
|
+
if (this.managedBoot) {
|
|
1014
|
+
if (boot.state === "failed") {
|
|
1015
|
+
this.progress.state = "STOPPED";
|
|
1016
|
+
this.failure = new Error(boot.error);
|
|
1017
|
+
}
|
|
1018
|
+
else if (this.status === "IDLE")
|
|
1019
|
+
this.progress.state = "IN_PROGRESS";
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
run() {
|
|
1023
|
+
return (this.starting ??= this.start().finally(() => {
|
|
1024
|
+
this.starting = undefined;
|
|
1025
|
+
}));
|
|
1026
|
+
}
|
|
1027
|
+
async start() {
|
|
1028
|
+
await this.initialize();
|
|
1029
|
+
if (this.client.state !== "CONNECTED")
|
|
1030
|
+
throw new Error("Sandbox client is disconnected.");
|
|
1031
|
+
if (this.status === "IN_PROGRESS" || !this.steps.length)
|
|
1032
|
+
return;
|
|
1033
|
+
this.managedBoot = false;
|
|
1034
|
+
await this.active;
|
|
1035
|
+
this.ignoredRunId = this.progress.runId;
|
|
1036
|
+
const runId = crypto.randomUUID();
|
|
1037
|
+
const input = `${setupRoot}/${runId}.json`;
|
|
1038
|
+
const config = {
|
|
1039
|
+
runId,
|
|
1040
|
+
config: this.progress.config,
|
|
1041
|
+
cwd: this.client.workspacePath,
|
|
1042
|
+
steps: this.steps.map((step) => ({ name: step.name, command: step.command })),
|
|
1043
|
+
};
|
|
1044
|
+
await this.native.files.write(input, JSON.stringify(config), { mode: 0o600 });
|
|
1045
|
+
const { stdin: _stdin, ...options } = await execOptions(this.native, {
|
|
1046
|
+
cwd: this.client.workspacePath,
|
|
1047
|
+
});
|
|
1048
|
+
const process = await this.native.spawn(["python3", "-c", setupRunner, input], options);
|
|
1049
|
+
try {
|
|
1050
|
+
await this.native.files.write(`${setupRoot}/${runId}.process.json`, JSON.stringify({ id: process.id }), { mode: 0o600 });
|
|
1051
|
+
// Do not return a connection before the guest has published ownership.
|
|
1052
|
+
// A second connection can otherwise launch a duplicate supervisor while
|
|
1053
|
+
// the first Python interpreter is still starting.
|
|
1054
|
+
const until = Date.now() + 5000;
|
|
1055
|
+
for (;;) {
|
|
1056
|
+
if (await this.native.files.exists(`${setupRoot}/progress.json`)) {
|
|
1057
|
+
const accepted = JSON.parse(await this.native.files.readText(`${setupRoot}/progress.json`));
|
|
1058
|
+
if (accepted.config === config.config && accepted.runId !== this.ignoredRunId)
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
if (Date.now() >= until)
|
|
1062
|
+
throw new Error("Setup supervisor did not acknowledge startup");
|
|
1063
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
catch (error) {
|
|
1067
|
+
try {
|
|
1068
|
+
await process.kill("SIGKILL");
|
|
1069
|
+
}
|
|
1070
|
+
catch (cleanup) {
|
|
1071
|
+
throw new AggregateError([error, cleanup], "Setup tracking failed and process cleanup could not be confirmed");
|
|
1072
|
+
}
|
|
1073
|
+
throw error;
|
|
1074
|
+
}
|
|
1075
|
+
this.failure = undefined;
|
|
1076
|
+
this.progress = {
|
|
1077
|
+
...this.progress,
|
|
1078
|
+
state: "IN_PROGRESS",
|
|
1079
|
+
runId,
|
|
1080
|
+
currentStepIndex: 0,
|
|
1081
|
+
steps: config.steps.map((step) => ({ ...step, status: "IDLE" })),
|
|
1082
|
+
};
|
|
1083
|
+
this.changed.fire();
|
|
1084
|
+
// Native process failure is observed independently of progress-file polling.
|
|
1085
|
+
void process
|
|
1086
|
+
.wait({ signal: this.cancelled.signal })
|
|
1087
|
+
.then(async (result) => {
|
|
1088
|
+
if (this.progress.runId !== runId || this.cancelled.signal.aborted)
|
|
1089
|
+
return;
|
|
1090
|
+
if (result.exitCode !== 0) {
|
|
1091
|
+
if (await this.native.files.exists(`${setupRoot}/progress.json`)) {
|
|
1092
|
+
const stored = JSON.parse(await this.native.files.readText(`${setupRoot}/progress.json`));
|
|
1093
|
+
if (stored.runId === runId) {
|
|
1094
|
+
this.progress = stored;
|
|
1095
|
+
for (const step of this.steps)
|
|
1096
|
+
await step.refresh();
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
this.failure = new Error("Setup Failed");
|
|
1100
|
+
this.progress.state = "STOPPED";
|
|
1101
|
+
this.changed.fire();
|
|
1102
|
+
}
|
|
1103
|
+
})
|
|
1104
|
+
.catch((error) => {
|
|
1105
|
+
if (this.cancelled.signal.aborted)
|
|
1106
|
+
return;
|
|
1107
|
+
this.failure = error instanceof Error ? error : new Error(String(error));
|
|
1108
|
+
this.progress.state = "STOPPED";
|
|
1109
|
+
this.changed.fire();
|
|
1110
|
+
});
|
|
1111
|
+
this.monitor();
|
|
1112
|
+
}
|
|
1113
|
+
monitor() {
|
|
1114
|
+
if (this.active)
|
|
1115
|
+
return;
|
|
1116
|
+
const signal = this.cancelled.signal;
|
|
1117
|
+
this.active = (async () => {
|
|
1118
|
+
while (this.status === "IN_PROGRESS") {
|
|
1119
|
+
signal.throwIfAborted();
|
|
1120
|
+
if (this.managedBoot) {
|
|
1121
|
+
const boot = await new ManagedTasks(this.native).boot(signal);
|
|
1122
|
+
if (boot?.state === "failed")
|
|
1123
|
+
throw new Error(boot.error);
|
|
1124
|
+
}
|
|
1125
|
+
if (await this.native.files.exists(`${setupRoot}/progress.json`, { signal })) {
|
|
1126
|
+
const next = JSON.parse(await this.native.files.readText(`${setupRoot}/progress.json`, { signal }));
|
|
1127
|
+
if (next.config === this.progress.config && next.runId !== this.ignoredRunId) {
|
|
1128
|
+
this.progress = next;
|
|
1129
|
+
for (const step of this.steps)
|
|
1130
|
+
await step.refresh();
|
|
1131
|
+
if (this.status === "IN_PROGRESS" && next.runId) {
|
|
1132
|
+
const tracking = `${setupRoot}/${next.runId}.process.json`;
|
|
1133
|
+
if (await this.native.files.exists(tracking, { signal })) {
|
|
1134
|
+
const { id } = JSON.parse(await this.native.files.readText(tracking, { signal }));
|
|
1135
|
+
const process = await this.native.processes.get(id);
|
|
1136
|
+
if (process.info.state !== "running") {
|
|
1137
|
+
// Read again after observing exit: the supervisor writes its
|
|
1138
|
+
// final progress before it exits; the earlier read can race.
|
|
1139
|
+
this.progress = JSON.parse(await this.native.files.readText(`${setupRoot}/progress.json`, { signal }));
|
|
1140
|
+
if (this.progress.state === "IN_PROGRESS")
|
|
1141
|
+
this.progress.state = "STOPPED";
|
|
1142
|
+
for (const step of this.steps)
|
|
1143
|
+
await step.refresh();
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
this.changed.fire();
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
if (this.status !== "IN_PROGRESS")
|
|
1151
|
+
break;
|
|
1152
|
+
await new Promise((resolve, reject) => {
|
|
1153
|
+
const abort = () => {
|
|
1154
|
+
clearTimeout(timer);
|
|
1155
|
+
reject(signal.reason instanceof Error
|
|
1156
|
+
? signal.reason
|
|
1157
|
+
: new Error("Setup subscription cancelled"));
|
|
1158
|
+
};
|
|
1159
|
+
const timer = setTimeout(() => {
|
|
1160
|
+
signal.removeEventListener("abort", abort);
|
|
1161
|
+
resolve();
|
|
1162
|
+
}, 100);
|
|
1163
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1164
|
+
if (signal.aborted)
|
|
1165
|
+
abort();
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
})()
|
|
1169
|
+
.catch((error) => {
|
|
1170
|
+
this.failure = error instanceof Error ? error : new Error(String(error));
|
|
1171
|
+
this.progress.state = "STOPPED";
|
|
1172
|
+
this.changed.fire();
|
|
1173
|
+
})
|
|
1174
|
+
.finally(() => {
|
|
1175
|
+
this.active = undefined;
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
async waitUntilComplete() {
|
|
1179
|
+
await this.initialize();
|
|
1180
|
+
if (this.failure)
|
|
1181
|
+
throw this.failure;
|
|
1182
|
+
if (this.status === "STOPPED")
|
|
1183
|
+
throw new Error("Setup Failed");
|
|
1184
|
+
if (this.status === "FINISHED")
|
|
1185
|
+
return;
|
|
1186
|
+
await new Promise((resolve, reject) => {
|
|
1187
|
+
const listener = this.onSetupProgressChange(() => {
|
|
1188
|
+
if (this.status === "FINISHED" || this.status === "STOPPED") {
|
|
1189
|
+
listener.dispose();
|
|
1190
|
+
if (this.status === "FINISHED")
|
|
1191
|
+
resolve();
|
|
1192
|
+
else
|
|
1193
|
+
reject(this.failure ?? new Error("Setup Failed"));
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
step(index) {
|
|
1199
|
+
return this.progress.steps[index];
|
|
1200
|
+
}
|
|
1201
|
+
async output(index) {
|
|
1202
|
+
const log = this.step(index).log;
|
|
1203
|
+
if (!log || !(await this.native.files.exists(log)))
|
|
1204
|
+
return "";
|
|
1205
|
+
return this.native.files.readText(log);
|
|
1206
|
+
}
|
|
1207
|
+
async open(index, dimensions) {
|
|
1208
|
+
if (dimensions &&
|
|
1209
|
+
(!Number.isInteger(dimensions.cols) ||
|
|
1210
|
+
!Number.isInteger(dimensions.rows) ||
|
|
1211
|
+
dimensions.cols < 1 ||
|
|
1212
|
+
dimensions.rows < 1 ||
|
|
1213
|
+
dimensions.cols > 65535 ||
|
|
1214
|
+
dimensions.rows > 65535))
|
|
1215
|
+
throw new TypeError("Invalid terminal dimensions");
|
|
1216
|
+
const available = () => this.step(index).tty || this.step(index).status !== "IDLE";
|
|
1217
|
+
if (!available())
|
|
1218
|
+
await new Promise((resolve, reject) => {
|
|
1219
|
+
const changed = () => {
|
|
1220
|
+
if (available()) {
|
|
1221
|
+
listener.dispose();
|
|
1222
|
+
resolve();
|
|
1223
|
+
}
|
|
1224
|
+
else if (this.status === "STOPPED") {
|
|
1225
|
+
listener.dispose();
|
|
1226
|
+
reject(new Error("Step Failed"));
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
const listener = this.onSetupProgressChange(changed);
|
|
1230
|
+
changed();
|
|
1231
|
+
});
|
|
1232
|
+
const step = this.step(index);
|
|
1233
|
+
if (dimensions && step.tty && step.status === "IDLE")
|
|
1234
|
+
await this.native.exec([
|
|
1235
|
+
"python3",
|
|
1236
|
+
"-c",
|
|
1237
|
+
resizeStep,
|
|
1238
|
+
`${setupRoot}/progress.json`,
|
|
1239
|
+
this.progress.runId ?? "",
|
|
1240
|
+
String(index),
|
|
1241
|
+
step.tty,
|
|
1242
|
+
String(dimensions.rows),
|
|
1243
|
+
String(dimensions.cols),
|
|
1244
|
+
], { check: true });
|
|
1245
|
+
}
|
|
1246
|
+
dispose() {
|
|
1247
|
+
const error = new Error("Sandbox client is disconnected.");
|
|
1248
|
+
this.cancelled.abort(error);
|
|
1249
|
+
if (this.status === "IDLE" || this.status === "IN_PROGRESS") {
|
|
1250
|
+
this.failure = error;
|
|
1251
|
+
this.progress.state = "STOPPED";
|
|
1252
|
+
this.changed.fire();
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
export class Step {
|
|
1257
|
+
setup;
|
|
1258
|
+
index;
|
|
1259
|
+
definition;
|
|
1260
|
+
output = "";
|
|
1261
|
+
previousStatus = "IDLE";
|
|
1262
|
+
outputEvent = emitter();
|
|
1263
|
+
statusEvent = emitter();
|
|
1264
|
+
onOutput = this.outputEvent.event;
|
|
1265
|
+
onStatusChange = this.statusEvent.event;
|
|
1266
|
+
constructor(setup, index, definition) {
|
|
1267
|
+
this.setup = setup;
|
|
1268
|
+
this.index = index;
|
|
1269
|
+
this.definition = definition;
|
|
1270
|
+
}
|
|
1271
|
+
get name() {
|
|
1272
|
+
return this.definition.name;
|
|
1273
|
+
}
|
|
1274
|
+
get command() {
|
|
1275
|
+
return this.definition.command;
|
|
1276
|
+
}
|
|
1277
|
+
get status() {
|
|
1278
|
+
return this.setup.step(this.index).status;
|
|
1279
|
+
}
|
|
1280
|
+
async refresh() {
|
|
1281
|
+
const next = await this.setup.output(this.index);
|
|
1282
|
+
if (next.length > this.output.length)
|
|
1283
|
+
this.outputEvent.fire(next.slice(this.output.length));
|
|
1284
|
+
this.output = next;
|
|
1285
|
+
if (this.status !== this.previousStatus) {
|
|
1286
|
+
this.previousStatus = this.status;
|
|
1287
|
+
this.statusEvent.fire(this.status);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
async open(dimensions) {
|
|
1291
|
+
await this.setup.open(this.index, dimensions);
|
|
1292
|
+
await this.refresh();
|
|
1293
|
+
return this.output;
|
|
1294
|
+
}
|
|
1295
|
+
async waitUntilComplete() {
|
|
1296
|
+
if (this.status === "FAILED" || this.setup.status === "STOPPED")
|
|
1297
|
+
throw new Error("Step Failed");
|
|
1298
|
+
if (this.status === "SUCCEEDED" || this.status === "SKIPPED")
|
|
1299
|
+
return;
|
|
1300
|
+
await new Promise((resolve, reject) => {
|
|
1301
|
+
const listener = this.setup.onSetupProgressChange(() => {
|
|
1302
|
+
if (this.status === "SUCCEEDED" || this.status === "SKIPPED") {
|
|
1303
|
+
listener.dispose();
|
|
1304
|
+
resolve();
|
|
1305
|
+
}
|
|
1306
|
+
else if (this.status === "FAILED" || this.setup.status === "STOPPED") {
|
|
1307
|
+
listener.dispose();
|
|
1308
|
+
reject(new Error("Step Failed"));
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
}
|