@lizard-build/sdk 0.1.49 → 0.1.51
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 +25 -6
- package/dist/index.d.mts +207 -86
- package/dist/index.d.ts +207 -86
- package/dist/index.js +521 -154
- package/dist/index.mjs +521 -154
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -548,7 +548,9 @@ var Process = class {
|
|
|
548
548
|
envs: opts?.envs,
|
|
549
549
|
user: opts?.user,
|
|
550
550
|
workdir: opts?.workdir,
|
|
551
|
-
timeoutMs: opts?.timeoutMs
|
|
551
|
+
timeoutMs: opts?.timeoutMs,
|
|
552
|
+
// The pid event is opt-in: older CLIs print any unknown data line as output.
|
|
553
|
+
pidEvent: opts?.onPid ? true : void 0
|
|
552
554
|
}),
|
|
553
555
|
signal: AbortSignal.timeout(opts?.timeoutMs ?? 6e4)
|
|
554
556
|
});
|
|
@@ -556,11 +558,6 @@ var Process = class {
|
|
|
556
558
|
if (!streaming) return res.json();
|
|
557
559
|
return this.consumeStream(res, opts);
|
|
558
560
|
}
|
|
559
|
-
/**
|
|
560
|
-
* Read an SSE exec stream, handing each line to the caller as it arrives while
|
|
561
|
-
* still accumulating the full result. Events are `{stream, line}` for output and
|
|
562
|
-
* `{exitCode}` at the end.
|
|
563
|
-
*/
|
|
564
561
|
/**
|
|
565
562
|
* List the processes this sandbox is currently running.
|
|
566
563
|
*
|
|
@@ -584,8 +581,8 @@ var Process = class {
|
|
|
584
581
|
/**
|
|
585
582
|
* Signal a running process. Defaults to `SIGTERM`.
|
|
586
583
|
*
|
|
587
|
-
* The signal goes to the process
|
|
588
|
-
* killing `sh -c 'sleep 100'` otherwise leaves the sleep running.
|
|
584
|
+
* The signal goes to the whole process tree, so a shell's children die with
|
|
585
|
+
* it — killing `sh -c 'sleep 100'` otherwise leaves the sleep running.
|
|
589
586
|
*
|
|
590
587
|
* The pid comes from {@link list}, or from the `pid` event at the start of a
|
|
591
588
|
* streaming `exec`.
|
|
@@ -604,6 +601,11 @@ var Process = class {
|
|
|
604
601
|
});
|
|
605
602
|
if (!res.ok) await handleApiError(res);
|
|
606
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* Read an SSE exec stream, handing each line to the caller as it arrives while
|
|
606
|
+
* still accumulating the full result. Events are `{pid}` first (when asked for
|
|
607
|
+
* with `pidEvent`), `{stream, line}` for output and `{exitCode}` at the end.
|
|
608
|
+
*/
|
|
607
609
|
async consumeStream(res, opts) {
|
|
608
610
|
const reader = res.body?.getReader();
|
|
609
611
|
if (!reader) return { stdout: "", stderr: "", exitCode: 0 };
|
|
@@ -654,33 +656,37 @@ var Fs = class {
|
|
|
654
656
|
/**
|
|
655
657
|
* Write a file into the sandbox filesystem.
|
|
656
658
|
*
|
|
657
|
-
* Creates parent directories automatically if they don't exist.
|
|
658
|
-
*
|
|
659
|
+
* Creates parent directories automatically if they don't exist. A string is
|
|
660
|
+
* written as UTF-8; bytes (`Uint8Array`, `Buffer` or `ArrayBuffer`) are written
|
|
661
|
+
* exactly as given, so binary files round-trip with {@link readBytes}.
|
|
662
|
+
*
|
|
663
|
+
* With `opts.user` the file is written as (and owned by) that user.
|
|
659
664
|
*
|
|
660
665
|
* @example
|
|
661
666
|
* ```ts
|
|
662
667
|
* await sandbox.fs.write('/app/index.js', 'console.log("hello")')
|
|
663
668
|
* ```
|
|
664
669
|
*
|
|
665
|
-
* @example Write
|
|
670
|
+
* @example Write binary data:
|
|
666
671
|
* ```ts
|
|
667
|
-
* await sandbox.fs.write('/app/
|
|
672
|
+
* await sandbox.fs.write('/app/logo.png', await readFile('logo.png'))
|
|
668
673
|
* ```
|
|
669
674
|
*/
|
|
670
675
|
async write(path, data, opts) {
|
|
671
|
-
const
|
|
676
|
+
const body = typeof data === "string" ? { path, content: data, user: opts?.user } : { path, content: bytesToBase64(data instanceof ArrayBuffer ? new Uint8Array(data) : data), encoding: "base64", user: opts?.user };
|
|
672
677
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files`, {
|
|
673
678
|
method: "POST",
|
|
674
679
|
headers: this.config.headers,
|
|
675
|
-
body: JSON.stringify(
|
|
680
|
+
body: JSON.stringify(body)
|
|
676
681
|
});
|
|
677
682
|
if (!res.ok) await handleApiError(res);
|
|
678
683
|
}
|
|
679
684
|
/**
|
|
680
685
|
* Read a file from the sandbox filesystem.
|
|
681
686
|
*
|
|
682
|
-
* @returns The file contents as a UTF-8 string.
|
|
687
|
+
* @returns The file contents as a UTF-8 string. Use {@link readBytes} for binary files.
|
|
683
688
|
*
|
|
689
|
+
* Throws `NotFoundError` if the path does not exist.
|
|
684
690
|
* @example
|
|
685
691
|
* ```ts
|
|
686
692
|
* const content = await sandbox.fs.read('/app/index.js')
|
|
@@ -748,9 +754,7 @@ var Fs = class {
|
|
|
748
754
|
* console.log(info.size, info.type)
|
|
749
755
|
* ```
|
|
750
756
|
*
|
|
751
|
-
* Throws `NotFoundError` if the path does not exist.
|
|
752
|
-
* this shipped run a guest agent without it and throw `LizardError` (501) —
|
|
753
|
-
* recreate the sandbox to use it.
|
|
757
|
+
* Throws `NotFoundError` if the path does not exist.
|
|
754
758
|
*/
|
|
755
759
|
async stat(path, opts) {
|
|
756
760
|
const url = new URL(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/stat`);
|
|
@@ -767,9 +771,6 @@ var Fs = class {
|
|
|
767
771
|
* ```ts
|
|
768
772
|
* await sandbox.fs.move('/tmp/build.log', '/app/logs/build.log')
|
|
769
773
|
* ```
|
|
770
|
-
*
|
|
771
|
-
* Sandboxes created before this shipped run a guest agent without it and throw
|
|
772
|
-
* `LizardError` (501) — recreate the sandbox to use it.
|
|
773
774
|
*/
|
|
774
775
|
async move(from, to) {
|
|
775
776
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/move`, {
|
|
@@ -798,8 +799,7 @@ var Fs = class {
|
|
|
798
799
|
* }, 1000)
|
|
799
800
|
* ```
|
|
800
801
|
*
|
|
801
|
-
*
|
|
802
|
-
* `LizardError` (501) — recreate the sandbox to use it.
|
|
802
|
+
* Not available on Firecracker sandboxes yet: the call throws `LizardError` (501).
|
|
803
803
|
*/
|
|
804
804
|
async watch(path, opts) {
|
|
805
805
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/watch`, {
|
|
@@ -845,6 +845,12 @@ var Watcher = class {
|
|
|
845
845
|
if (!res.ok) await handleApiError(res);
|
|
846
846
|
}
|
|
847
847
|
};
|
|
848
|
+
function bytesToBase64(bytes) {
|
|
849
|
+
if (typeof Buffer !== "undefined") return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
|
|
850
|
+
let s = "";
|
|
851
|
+
for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
852
|
+
return btoa(s);
|
|
853
|
+
}
|
|
848
854
|
|
|
849
855
|
// src/sandbox/desktop.ts
|
|
850
856
|
var BUTTONS = { left: "1", middle: "2", right: "3" };
|
|
@@ -1145,11 +1151,17 @@ var Sandbox = class extends SandboxClient {
|
|
|
1145
1151
|
async kill(opts) {
|
|
1146
1152
|
return SandboxClient.killSandbox(this.sandboxId, this.resolveOpts(opts));
|
|
1147
1153
|
}
|
|
1148
|
-
/**
|
|
1154
|
+
/**
|
|
1155
|
+
* Pause the sandbox: its memory, running processes and files are kept and
|
|
1156
|
+
* compute stops. Wait with `waitForStatus('paused')` before resuming.
|
|
1157
|
+
*/
|
|
1149
1158
|
async pause(opts) {
|
|
1150
1159
|
return SandboxClient.pauseSandbox(this.sandboxId, this.resolveOpts(opts));
|
|
1151
1160
|
}
|
|
1152
|
-
/**
|
|
1161
|
+
/**
|
|
1162
|
+
* Resume a paused sandbox exactly where it stopped, processes included. Wait
|
|
1163
|
+
* with `waitForStatus('running')` before running commands.
|
|
1164
|
+
*/
|
|
1153
1165
|
async resume(opts) {
|
|
1154
1166
|
return SandboxClient.resumeSandbox(this.sandboxId, this.resolveOpts(opts));
|
|
1155
1167
|
}
|
|
@@ -1182,33 +1194,82 @@ var Sandbox = class extends SandboxClient {
|
|
|
1182
1194
|
return SandboxClient.setTimeoutSandbox(this.sandboxId, timeoutMs, this.resolveOpts(opts));
|
|
1183
1195
|
}
|
|
1184
1196
|
/**
|
|
1185
|
-
*
|
|
1197
|
+
* Publish a port on a public HTTPS hostname and return the hostname without a scheme.
|
|
1186
1198
|
*
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1199
|
+
* The port is private: requests need {@link accessToken} (set by this call) as
|
|
1200
|
+
* the `X-Lizard-Access-Token` header, or `?lizard_token=<token>` in a browser.
|
|
1201
|
+
* {@link exposePort} returns the token and a ready-made browser URL together.
|
|
1189
1202
|
*
|
|
1190
1203
|
* @example
|
|
1191
1204
|
* ```ts
|
|
1192
1205
|
* // Start an HTTP server on port 3000 before exposing it.
|
|
1193
1206
|
* const hostname = await sandbox.getHost(3000)
|
|
1194
|
-
*
|
|
1207
|
+
* const res = await fetch(`https://${hostname}/`, {
|
|
1208
|
+
* headers: { 'X-Lizard-Access-Token': sandbox.accessToken! },
|
|
1209
|
+
* })
|
|
1195
1210
|
* ```
|
|
1196
1211
|
*/
|
|
1197
1212
|
async getHost(port, opts) {
|
|
1198
|
-
|
|
1199
|
-
|
|
1213
|
+
return (await this.exposePort(port, opts)).hostname;
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Publish a port and return everything needed to reach it: the hostname, a
|
|
1217
|
+
* browser `url` that carries the token, and the `accessToken` for API clients.
|
|
1218
|
+
*
|
|
1219
|
+
* @example
|
|
1220
|
+
* ```ts
|
|
1221
|
+
* const { url, hostname, accessToken } = await sandbox.exposePort(3000)
|
|
1222
|
+
* console.log(url) // open in a browser
|
|
1223
|
+
* await fetch(`https://${hostname}/api`, { headers: { 'X-Lizard-Access-Token': accessToken } })
|
|
1224
|
+
* ```
|
|
1225
|
+
*/
|
|
1226
|
+
async exposePort(port, opts) {
|
|
1227
|
+
const exposed = await SandboxClient.exposeSandboxPort(this.sandboxId, port, this.resolveOpts(opts));
|
|
1228
|
+
if (exposed.accessToken) this.accessToken = exposed.accessToken;
|
|
1229
|
+
return exposed;
|
|
1200
1230
|
}
|
|
1201
|
-
/**
|
|
1202
|
-
|
|
1203
|
-
|
|
1231
|
+
/**
|
|
1232
|
+
* Clone this running sandbox `count` times (1-10). Each fork is a copy of the
|
|
1233
|
+
* microVM at this instant -- memory, running processes and files -- and is
|
|
1234
|
+
* billed like its source. The source keeps running.
|
|
1235
|
+
*
|
|
1236
|
+
* Returns one entry per requested fork, in order: `{ sandbox, info }`, or
|
|
1237
|
+
* `{ error }` for a copy that could not be made (e.g. the account's sandbox
|
|
1238
|
+
* limit). A sandbox with a volume attached cannot be forked (`ConflictError`, 409).
|
|
1239
|
+
*
|
|
1240
|
+
* @param opts.timeoutMs Lifetime of each fork; defaults to the source's timeout.
|
|
1241
|
+
*
|
|
1242
|
+
* @example
|
|
1243
|
+
* ```ts
|
|
1244
|
+
* const forks = await sandbox.fork({ count: 2 })
|
|
1245
|
+
* for (const f of forks) if (f.sandbox) await f.sandbox.process.exec('echo hi')
|
|
1246
|
+
* ```
|
|
1247
|
+
*/
|
|
1248
|
+
async fork(opts = {}) {
|
|
1249
|
+
const result = await new PlatformClient(this.resolveOpts()).post(
|
|
1250
|
+
`/api/sandboxes/${this.sandboxId}/fork`,
|
|
1251
|
+
{ count: opts.count ?? 1, timeoutMs: opts.timeoutMs ?? 0 }
|
|
1252
|
+
);
|
|
1253
|
+
const Ctor = this.constructor;
|
|
1254
|
+
return (Array.isArray(result) ? result : []).map((entry) => {
|
|
1255
|
+
const id = entry.sandbox?.sandboxId ?? entry.sandbox?.id;
|
|
1256
|
+
if (!entry.sandbox || !id) return { error: entry.error ?? "fork failed" };
|
|
1257
|
+
return { sandbox: new Ctor({ ...this.resolveOpts(), sandboxId: id }), info: { ...entry.sandbox, sandboxId: id } };
|
|
1258
|
+
});
|
|
1204
1259
|
}
|
|
1205
|
-
/**
|
|
1260
|
+
/**
|
|
1261
|
+
* Save this sandbox -- memory, running processes and files -- as a private
|
|
1262
|
+
* snapshot. A Firecracker snapshot is `ready` as soon as this returns (about 2 s);
|
|
1263
|
+
* start copies of it with {@link Sandbox.restore} (about 0.4 s each). The source
|
|
1264
|
+
* keeps running. `poolSize` applies to container sandboxes only.
|
|
1265
|
+
*/
|
|
1206
1266
|
snapshot(name = `Snapshot ${this.sandboxId}`, opts = {}) {
|
|
1207
1267
|
return new PlatformClient(this.resolveOpts()).post(`/api/sandboxes/${this.sandboxId}/snapshot`, { name, poolSize: opts.poolSize ?? 5 });
|
|
1208
1268
|
}
|
|
1209
1269
|
unexpose(port) {
|
|
1210
1270
|
return new PlatformClient(this.resolveOpts()).delete(`/api/sandboxes/${this.sandboxId}/expose/${port}`);
|
|
1211
1271
|
}
|
|
1272
|
+
/** Stream the sandbox's logs. Not available on Firecracker sandboxes yet (`LizardError`, 501). */
|
|
1212
1273
|
logs(opts = {}) {
|
|
1213
1274
|
return new PlatformClient(this.resolveOpts()).events(query(`/api/sandboxes/${this.sandboxId}/logs`, { tail: opts.tail }), { signal: opts.signal });
|
|
1214
1275
|
}
|
|
@@ -1218,7 +1279,7 @@ var Sandbox = class extends SandboxClient {
|
|
|
1218
1279
|
static getSnapshot(snapshotId, opts) {
|
|
1219
1280
|
return new PlatformClient(opts ?? {}).get(`/api/sandbox-snapshots/${encodeURIComponent(snapshotId)}`);
|
|
1220
1281
|
}
|
|
1221
|
-
/** Start a sandbox from
|
|
1282
|
+
/** Start a new sandbox from a snapshot, in the snapshot's project and region. */
|
|
1222
1283
|
static async restore(snapshotId, opts) {
|
|
1223
1284
|
const client = new PlatformClient(opts ?? {});
|
|
1224
1285
|
const snapshot = await this.getSnapshot(snapshotId, opts);
|
|
@@ -1233,7 +1294,7 @@ var Sandbox = class extends SandboxClient {
|
|
|
1233
1294
|
static setSnapshotWarmPool(snapshotId, poolSize, opts) {
|
|
1234
1295
|
return new PlatformClient(opts ?? {}).patch(`/api/sandbox-snapshots/${encodeURIComponent(snapshotId)}`, { poolSize });
|
|
1235
1296
|
}
|
|
1236
|
-
/** Release idle warm copies; retain saved state and existing claimed sandboxes. */
|
|
1297
|
+
/** Release idle warm copies (container snapshots); retain saved state and existing claimed sandboxes. */
|
|
1237
1298
|
static pauseSnapshot(snapshotId, opts) {
|
|
1238
1299
|
return new PlatformClient(opts ?? {}).post(`/api/sandbox-snapshots/${encodeURIComponent(snapshotId)}/pause`, {});
|
|
1239
1300
|
}
|
|
@@ -1772,7 +1833,7 @@ var Volume = class _Volume {
|
|
|
1772
1833
|
* "the scratch disk for this task" no longer has to store an id between runs.
|
|
1773
1834
|
*
|
|
1774
1835
|
* An existing volume is returned as-is — `sizeGb` applies only to a fresh create,
|
|
1775
|
-
* so an existing volume keeps its size.
|
|
1836
|
+
* so an existing volume keeps its size.
|
|
1776
1837
|
*/
|
|
1777
1838
|
static async getOrCreate(projectId, name, opts) {
|
|
1778
1839
|
const config = new ConnectionConfig(opts);
|
|
@@ -1815,11 +1876,15 @@ var Volume = class _Volume {
|
|
|
1815
1876
|
/**
|
|
1816
1877
|
* Resize a volume, by name or by id, to `sizeGb`.
|
|
1817
1878
|
*
|
|
1818
|
-
*
|
|
1819
|
-
*
|
|
1820
|
-
*
|
|
1821
|
-
*
|
|
1822
|
-
*
|
|
1879
|
+
* **Firecracker volumes cannot be resized yet**: volumes created for the default
|
|
1880
|
+
* Firecracker sandboxes throw `LizardError` (400) with `code: 'volume_not_resizable'`.
|
|
1881
|
+
* Create the volume at the size you need, or copy the data to a new, larger one.
|
|
1882
|
+
*
|
|
1883
|
+
* On a container volume the change is in place and online: the size is a quota,
|
|
1884
|
+
* so no data is copied and a mounted sandbox sees the new size immediately. Both
|
|
1885
|
+
* growing and shrinking are allowed, but a shrink must leave at least 10% of the
|
|
1886
|
+
* new size free — otherwise it throws `ConflictError` with
|
|
1887
|
+
* `code: 'volume_too_full_to_shrink'`.
|
|
1823
1888
|
*
|
|
1824
1889
|
* Other failures carry a `code` too: `invalid_volume_size`, `volume_not_resizable`,
|
|
1825
1890
|
* `volume_resize_in_progress`, `volume_not_provisioned`,
|
|
@@ -1845,7 +1910,7 @@ var Volume = class _Volume {
|
|
|
1845
1910
|
if (!res.ok) await handleApiError(res);
|
|
1846
1911
|
return res.json();
|
|
1847
1912
|
}
|
|
1848
|
-
/** Resize this volume
|
|
1913
|
+
/** Resize this volume — see the static {@link Volume.resize} (not supported for Firecracker volumes yet). */
|
|
1849
1914
|
async resize(projectId, sizeGb) {
|
|
1850
1915
|
return _Volume.resize(projectId, this.volumeId, sizeGb, {
|
|
1851
1916
|
apiKey: this.config.apiKey,
|
|
@@ -1968,7 +2033,7 @@ var Execution = class {
|
|
|
1968
2033
|
this.stderr = "";
|
|
1969
2034
|
/** Rich output items (images, JSON, HTML, plain values). */
|
|
1970
2035
|
this.results = [];
|
|
1971
|
-
/** Monotonically increasing counter for this context. */
|
|
2036
|
+
/** Monotonically increasing counter for this context (the Python kernel's `In[n]`). */
|
|
1972
2037
|
this.executionCount = 0;
|
|
1973
2038
|
}
|
|
1974
2039
|
get success() {
|
|
@@ -1979,25 +2044,317 @@ var Execution = class {
|
|
|
1979
2044
|
}
|
|
1980
2045
|
};
|
|
1981
2046
|
|
|
2047
|
+
// src/code-interpreter/runner.ts
|
|
2048
|
+
var RUNNER_SOURCE = String.raw`# Lizard code runner: executes code for CodeSandbox.run_code / runCode inside the sandbox.
|
|
2049
|
+
# Python runs in a persistent Jupyter kernel per context when the template has one
|
|
2050
|
+
# (the interpreter template does); everything else runs as a fresh process.
|
|
2051
|
+
# Events are JSON lines on stdout; an event over 24000 chars is sent as "part" lines.
|
|
2052
|
+
import base64, json, os, re, shutil, signal, subprocess, sys, threading, time
|
|
2053
|
+
ROOT = "/tmp/.lizard-code"
|
|
2054
|
+
LANGS = {"python": "python3", "javascript": "node", "bash": "bash"}
|
|
2055
|
+
ALIASES = {"py": "python", "python3": "python", "js": "javascript", "node": "javascript", "sh": "bash"}
|
|
2056
|
+
ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
|
|
2057
|
+
LOCK = threading.Lock()
|
|
2058
|
+
|
|
2059
|
+
|
|
2060
|
+
def emit(ev):
|
|
2061
|
+
s = json.dumps(ev)
|
|
2062
|
+
with LOCK:
|
|
2063
|
+
if len(s) <= 24000:
|
|
2064
|
+
print(s, flush=True)
|
|
2065
|
+
return
|
|
2066
|
+
n = (len(s) + 23999) // 24000
|
|
2067
|
+
for i in range(n):
|
|
2068
|
+
print(json.dumps({"type": "part", "data": s[i * 24000:(i + 1) * 24000], "end": i == n - 1}), flush=True)
|
|
2069
|
+
|
|
2070
|
+
|
|
2071
|
+
def fail(msg):
|
|
2072
|
+
emit({"type": "fatal", "message": msg})
|
|
2073
|
+
sys.exit(3)
|
|
2074
|
+
|
|
2075
|
+
|
|
2076
|
+
def meta_path(ctx):
|
|
2077
|
+
return os.path.join(ROOT, ctx, "meta.json")
|
|
2078
|
+
|
|
2079
|
+
|
|
2080
|
+
def load(ctx):
|
|
2081
|
+
try:
|
|
2082
|
+
with open(meta_path(ctx)) as f:
|
|
2083
|
+
return json.load(f)
|
|
2084
|
+
except OSError:
|
|
2085
|
+
return None
|
|
2086
|
+
|
|
2087
|
+
|
|
2088
|
+
def save(m):
|
|
2089
|
+
os.makedirs(os.path.join(ROOT, m["id"]), exist_ok=True)
|
|
2090
|
+
with open(meta_path(m["id"]), "w") as f:
|
|
2091
|
+
json.dump(m, f)
|
|
2092
|
+
|
|
2093
|
+
|
|
2094
|
+
def alive(pid):
|
|
2095
|
+
try:
|
|
2096
|
+
os.kill(pid, 0)
|
|
2097
|
+
except OSError:
|
|
2098
|
+
return False
|
|
2099
|
+
try:
|
|
2100
|
+
with open("/proc/%d/stat" % pid) as f:
|
|
2101
|
+
return f.read().split(") ")[-1][:1] != "Z"
|
|
2102
|
+
except OSError:
|
|
2103
|
+
return False
|
|
2104
|
+
|
|
2105
|
+
|
|
2106
|
+
def kernel_pid(m):
|
|
2107
|
+
try:
|
|
2108
|
+
with open(os.path.join(ROOT, m["id"], "kernel.pid")) as f:
|
|
2109
|
+
return int(f.read().strip())
|
|
2110
|
+
except (OSError, ValueError):
|
|
2111
|
+
return 0
|
|
2112
|
+
|
|
2113
|
+
|
|
2114
|
+
def stop_kernel(m):
|
|
2115
|
+
pid = kernel_pid(m)
|
|
2116
|
+
if pid and alive(pid):
|
|
2117
|
+
try:
|
|
2118
|
+
os.killpg(pid, signal.SIGKILL)
|
|
2119
|
+
except OSError:
|
|
2120
|
+
pass
|
|
2121
|
+
for name in ("kernel.pid", "kernel.json"):
|
|
2122
|
+
try:
|
|
2123
|
+
os.remove(os.path.join(ROOT, m["id"], name))
|
|
2124
|
+
except OSError:
|
|
2125
|
+
pass
|
|
2126
|
+
|
|
2127
|
+
|
|
2128
|
+
def start_kernel(m):
|
|
2129
|
+
d = os.path.join(ROOT, m["id"])
|
|
2130
|
+
conn = os.path.join(d, "kernel.json")
|
|
2131
|
+
stop_kernel(m)
|
|
2132
|
+
os.makedirs(m["cwd"], exist_ok=True)
|
|
2133
|
+
log = open(os.path.join(d, "kernel.log"), "ab")
|
|
2134
|
+
p = subprocess.Popen([sys.executable, "-m", "ipykernel_launcher", "-f", conn], cwd=m["cwd"],
|
|
2135
|
+
stdin=subprocess.DEVNULL, stdout=log, stderr=log, start_new_session=True,
|
|
2136
|
+
env=dict(os.environ, PYTHONUNBUFFERED="1"))
|
|
2137
|
+
with open(os.path.join(d, "kernel.pid"), "w") as f:
|
|
2138
|
+
f.write(str(p.pid))
|
|
2139
|
+
end = time.time() + 30
|
|
2140
|
+
while time.time() < end:
|
|
2141
|
+
if os.path.exists(conn) and os.path.getsize(conn) > 0:
|
|
2142
|
+
return
|
|
2143
|
+
if p.poll() is not None:
|
|
2144
|
+
break
|
|
2145
|
+
time.sleep(0.05)
|
|
2146
|
+
with open(os.path.join(d, "kernel.log"), "rb") as f:
|
|
2147
|
+
tail = f.read()[-2000:].decode("utf-8", "replace")
|
|
2148
|
+
fail("python kernel failed to start: " + (tail or "no output"))
|
|
2149
|
+
|
|
2150
|
+
|
|
2151
|
+
def has_kernel():
|
|
2152
|
+
try:
|
|
2153
|
+
import jupyter_client, ipykernel # noqa: F401
|
|
2154
|
+
return True
|
|
2155
|
+
except ImportError:
|
|
2156
|
+
return False
|
|
2157
|
+
|
|
2158
|
+
|
|
2159
|
+
def client(m):
|
|
2160
|
+
from jupyter_client import BlockingKernelClient
|
|
2161
|
+
if not (kernel_pid(m) and alive(kernel_pid(m))):
|
|
2162
|
+
start_kernel(m)
|
|
2163
|
+
kc = BlockingKernelClient()
|
|
2164
|
+
kc.load_connection_file(os.path.join(ROOT, m["id"], "kernel.json"))
|
|
2165
|
+
kc.start_channels()
|
|
2166
|
+
kc.wait_for_ready(timeout=30)
|
|
2167
|
+
return kc
|
|
2168
|
+
|
|
2169
|
+
|
|
2170
|
+
def shell_reply(kc, msg_id, timeout):
|
|
2171
|
+
end = time.time() + timeout
|
|
2172
|
+
while time.time() < end:
|
|
2173
|
+
try:
|
|
2174
|
+
r = kc.get_shell_msg(timeout=max(0.01, end - time.time()))
|
|
2175
|
+
except Exception:
|
|
2176
|
+
return None
|
|
2177
|
+
if r["parent_header"].get("msg_id") == msg_id:
|
|
2178
|
+
return r
|
|
2179
|
+
return None
|
|
2180
|
+
|
|
2181
|
+
|
|
2182
|
+
def silent(kc, code):
|
|
2183
|
+
shell_reply(kc, kc.execute(code, silent=True, store_history=False), 10)
|
|
2184
|
+
|
|
2185
|
+
|
|
2186
|
+
BEST = ("image/png", "image/jpeg", "image/svg+xml", "text/html", "text/markdown", "application/json", "text/latex", "text/plain")
|
|
2187
|
+
|
|
2188
|
+
|
|
2189
|
+
def run_python(m, code, envs, timeout):
|
|
2190
|
+
kc = client(m)
|
|
2191
|
+
if envs:
|
|
2192
|
+
silent(kc, "import os as _lz_os\n_lz_saved = {k: _lz_os.environ.get(k) for k in %r}\n_lz_os.environ.update(%r)" % (list(envs), envs))
|
|
2193
|
+
msg_id = kc.execute(code, store_history=True, allow_stdin=False)
|
|
2194
|
+
end = time.time() + timeout
|
|
2195
|
+
timed_out = False
|
|
2196
|
+
while True:
|
|
2197
|
+
left = end - time.time()
|
|
2198
|
+
if left <= 0:
|
|
2199
|
+
timed_out = True
|
|
2200
|
+
break
|
|
2201
|
+
try:
|
|
2202
|
+
msg = kc.get_iopub_msg(timeout=left)
|
|
2203
|
+
except Exception:
|
|
2204
|
+
continue
|
|
2205
|
+
if msg["parent_header"].get("msg_id") != msg_id:
|
|
2206
|
+
continue
|
|
2207
|
+
t, c = msg["msg_type"], msg["content"]
|
|
2208
|
+
if t == "stream":
|
|
2209
|
+
emit({"type": "stderr" if c.get("name") == "stderr" else "stdout", "data": c.get("text", "")})
|
|
2210
|
+
elif t in ("execute_result", "display_data"):
|
|
2211
|
+
data = c.get("data", {})
|
|
2212
|
+
mime = next((k for k in BEST if k in data), next(iter(data), None))
|
|
2213
|
+
if mime:
|
|
2214
|
+
v = data[mime]
|
|
2215
|
+
emit({"type": "result", "mime": mime, "data": v if isinstance(v, str) else json.dumps(v)})
|
|
2216
|
+
elif t == "error":
|
|
2217
|
+
emit({"type": "error", "name": c.get("ename", "Error"), "message": c.get("evalue", ""),
|
|
2218
|
+
"traceback": ANSI.sub("", "\n".join(c.get("traceback", [])))})
|
|
2219
|
+
elif t == "status" and c.get("execution_state") == "idle":
|
|
2220
|
+
break
|
|
2221
|
+
count = 0
|
|
2222
|
+
if timed_out:
|
|
2223
|
+
pid = kernel_pid(m)
|
|
2224
|
+
if pid:
|
|
2225
|
+
try:
|
|
2226
|
+
os.kill(pid, signal.SIGINT)
|
|
2227
|
+
except OSError:
|
|
2228
|
+
pass
|
|
2229
|
+
emit({"type": "error", "name": "TimeoutError", "message": "execution timed out after %gs; the kernel was interrupted" % timeout, "traceback": ""})
|
|
2230
|
+
else:
|
|
2231
|
+
r = shell_reply(kc, msg_id, 10)
|
|
2232
|
+
count = (r or {}).get("content", {}).get("execution_count") or 0
|
|
2233
|
+
if envs and not timed_out:
|
|
2234
|
+
silent(kc, "for _lz_k, _lz_v in _lz_saved.items():\n _lz_os.environ.pop(_lz_k, None) if _lz_v is None else _lz_os.environ.__setitem__(_lz_k, _lz_v)")
|
|
2235
|
+
kc.stop_channels()
|
|
2236
|
+
emit({"type": "done", "execution_count": count})
|
|
2237
|
+
|
|
2238
|
+
|
|
2239
|
+
def run_process(m, code, envs, timeout):
|
|
2240
|
+
interp = LANGS[m["language"]]
|
|
2241
|
+
if shutil.which(interp) is None:
|
|
2242
|
+
emit({"type": "error", "name": "LanguageUnavailable", "message": "%s is not installed in this sandbox" % interp, "traceback": ""})
|
|
2243
|
+
emit({"type": "done", "execution_count": 0})
|
|
2244
|
+
return
|
|
2245
|
+
d = os.path.join(ROOT, m["id"])
|
|
2246
|
+
src = os.path.join(d, "cell" + {"node": ".js", "bash": ".sh"}.get(interp, ".py"))
|
|
2247
|
+
with open(src, "w") as f:
|
|
2248
|
+
f.write(code)
|
|
2249
|
+
os.makedirs(m["cwd"], exist_ok=True)
|
|
2250
|
+
p = subprocess.Popen([interp, src], cwd=m["cwd"], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
|
2251
|
+
stderr=subprocess.PIPE, env=dict(os.environ, **(envs or {})), start_new_session=True)
|
|
2252
|
+
err_tail = []
|
|
2253
|
+
|
|
2254
|
+
def pump(stream, kind):
|
|
2255
|
+
for line in iter(stream.readline, b""):
|
|
2256
|
+
text = line.decode("utf-8", "replace")
|
|
2257
|
+
if kind == "stderr":
|
|
2258
|
+
err_tail.append(text)
|
|
2259
|
+
del err_tail[:-50]
|
|
2260
|
+
emit({"type": kind, "data": text})
|
|
2261
|
+
|
|
2262
|
+
ts = [threading.Thread(target=pump, args=(p.stdout, "stdout")), threading.Thread(target=pump, args=(p.stderr, "stderr"))]
|
|
2263
|
+
for t in ts:
|
|
2264
|
+
t.start()
|
|
2265
|
+
try:
|
|
2266
|
+
rc = p.wait(timeout=timeout)
|
|
2267
|
+
except subprocess.TimeoutExpired:
|
|
2268
|
+
os.killpg(p.pid, signal.SIGKILL)
|
|
2269
|
+
rc = None
|
|
2270
|
+
for t in ts:
|
|
2271
|
+
t.join(5)
|
|
2272
|
+
m["count"] = m.get("count", 0) + 1
|
|
2273
|
+
save(m)
|
|
2274
|
+
if rc is None:
|
|
2275
|
+
emit({"type": "error", "name": "TimeoutError", "message": "execution timed out after %gs" % timeout, "traceback": ""})
|
|
2276
|
+
elif rc != 0:
|
|
2277
|
+
emit({"type": "error", "name": "ExitError", "message": "exited with code %d" % rc, "traceback": "".join(err_tail)})
|
|
2278
|
+
emit({"type": "done", "execution_count": m["count"]})
|
|
2279
|
+
|
|
2280
|
+
|
|
2281
|
+
def ctx_for(a):
|
|
2282
|
+
lang = (a.get("language") or "python").lower()
|
|
2283
|
+
lang = ALIASES.get(lang, lang)
|
|
2284
|
+
if lang not in LANGS:
|
|
2285
|
+
fail("unsupported language %r; use python, javascript or bash" % lang)
|
|
2286
|
+
return lang
|
|
2287
|
+
|
|
2288
|
+
|
|
2289
|
+
def main():
|
|
2290
|
+
a = json.loads(base64.b64decode(sys.argv[1]))
|
|
2291
|
+
act = a["action"]
|
|
2292
|
+
os.makedirs(ROOT, exist_ok=True)
|
|
2293
|
+
if act == "create" or (act == "run" and not a.get("context")):
|
|
2294
|
+
lang = ctx_for(a)
|
|
2295
|
+
cid = a.get("context") or ("default-" + lang)
|
|
2296
|
+
m = load(cid) if act == "run" else None
|
|
2297
|
+
if m is None:
|
|
2298
|
+
m = {"id": cid if act == "run" else a["id"], "language": lang, "cwd": a.get("cwd") or "/home/user"}
|
|
2299
|
+
save(m)
|
|
2300
|
+
if lang == "python" and act == "create" and has_kernel():
|
|
2301
|
+
client(m).stop_channels()
|
|
2302
|
+
if act == "create":
|
|
2303
|
+
emit({"type": "context", "id": m["id"], "language": m["language"], "cwd": m["cwd"]})
|
|
2304
|
+
return
|
|
2305
|
+
elif act == "list":
|
|
2306
|
+
out = []
|
|
2307
|
+
for name in sorted(os.listdir(ROOT)):
|
|
2308
|
+
m = load(name)
|
|
2309
|
+
if m and not name.startswith("default-"):
|
|
2310
|
+
out.append({"id": m["id"], "language": m["language"], "cwd": m["cwd"]})
|
|
2311
|
+
emit({"type": "contexts", "data": out})
|
|
2312
|
+
return
|
|
2313
|
+
else:
|
|
2314
|
+
m = load(a["context"])
|
|
2315
|
+
if m is None:
|
|
2316
|
+
fail("context %r not found" % a["context"])
|
|
2317
|
+
if act == "delete":
|
|
2318
|
+
stop_kernel(m)
|
|
2319
|
+
shutil.rmtree(os.path.join(ROOT, m["id"]), ignore_errors=True)
|
|
2320
|
+
emit({"type": "ok"})
|
|
2321
|
+
return
|
|
2322
|
+
if act == "restart":
|
|
2323
|
+
if m["language"] == "python" and has_kernel():
|
|
2324
|
+
start_kernel(m)
|
|
2325
|
+
m["count"] = 0
|
|
2326
|
+
save(m)
|
|
2327
|
+
emit({"type": "ok"})
|
|
2328
|
+
return
|
|
2329
|
+
code = a.get("code")
|
|
2330
|
+
if code is None:
|
|
2331
|
+
with open(a["codeFile"]) as f:
|
|
2332
|
+
code = f.read()
|
|
2333
|
+
os.remove(a["codeFile"])
|
|
2334
|
+
if m["language"] == "python" and has_kernel():
|
|
2335
|
+
run_python(m, code, a.get("envs") or {}, a.get("timeout") or 60)
|
|
2336
|
+
else:
|
|
2337
|
+
run_process(m, code, a.get("envs") or {}, a.get("timeout") or 60)
|
|
2338
|
+
|
|
2339
|
+
|
|
2340
|
+
main()
|
|
2341
|
+
`;
|
|
2342
|
+
|
|
1982
2343
|
// src/code-interpreter/index.ts
|
|
1983
|
-
var
|
|
2344
|
+
var DEFAULT_RUN_TIMEOUT_MS = 6e4;
|
|
2345
|
+
var RUNNER_OVERHEAD_MS = 45e3;
|
|
2346
|
+
var MAX_INLINE_ARGS = 1e5;
|
|
1984
2347
|
var CodeSandbox = class extends Sandbox {
|
|
1985
|
-
get serverUrl() {
|
|
1986
|
-
return this.getHost(CODE_INTERPRETER_PORT).then((h) => `https://${h}`);
|
|
1987
|
-
}
|
|
1988
2348
|
/**
|
|
1989
|
-
* Execute code
|
|
2349
|
+
* Execute code and wait for it to finish.
|
|
1990
2350
|
*
|
|
1991
|
-
*
|
|
1992
|
-
*
|
|
2351
|
+
* Python keeps its state per context (the default context, per language,
|
|
2352
|
+
* when none is given). Code errors appear in `execution.error`; request
|
|
2353
|
+
* failures throw.
|
|
1993
2354
|
*
|
|
1994
2355
|
* @param code Source code to run.
|
|
1995
2356
|
* @param opts Language, context, env vars, timeout, and streaming callbacks.
|
|
1996
2357
|
*
|
|
1997
|
-
* @returns Execution result with stdout, stderr, results, and any error.
|
|
1998
|
-
*
|
|
1999
|
-
* Code errors appear in `execution.error`; request failures throw.
|
|
2000
|
-
*
|
|
2001
2358
|
* @example
|
|
2002
2359
|
* ```ts
|
|
2003
2360
|
* const result = await sandbox.runCode(`
|
|
@@ -2011,63 +2368,42 @@ var CodeSandbox = class extends Sandbox {
|
|
|
2011
2368
|
if (opts?.context && opts?.language) {
|
|
2012
2369
|
throw new Error("Provide context or language, not both");
|
|
2013
2370
|
}
|
|
2014
|
-
const
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
} else if (item.type === "stderr") {
|
|
2042
|
-
execution.stderr += item.data;
|
|
2043
|
-
opts?.onStderr?.(item.data);
|
|
2044
|
-
} else if (item.type === "result") {
|
|
2045
|
-
const out = item;
|
|
2046
|
-
execution.results.push(out);
|
|
2047
|
-
opts?.onResult?.(out);
|
|
2048
|
-
} else if (item.type === "error") {
|
|
2049
|
-
const err = new ExecutionError(
|
|
2050
|
-
item.name,
|
|
2051
|
-
item.message,
|
|
2052
|
-
item.traceback
|
|
2053
|
-
);
|
|
2054
|
-
execution.error = err;
|
|
2055
|
-
opts?.onError?.(err);
|
|
2056
|
-
} else if (item.type === "done") {
|
|
2057
|
-
execution.executionCount = item.execution_count ?? 0;
|
|
2058
|
-
break;
|
|
2059
|
-
}
|
|
2371
|
+
const timeoutMs = opts?.timeoutMs ?? DEFAULT_RUN_TIMEOUT_MS;
|
|
2372
|
+
const args = {
|
|
2373
|
+
action: "run",
|
|
2374
|
+
code,
|
|
2375
|
+
envs: opts?.envs ?? {},
|
|
2376
|
+
timeout: timeoutMs / 1e3
|
|
2377
|
+
};
|
|
2378
|
+
if (opts?.context) args.context = opts.context.id;
|
|
2379
|
+
else args.language = opts?.language ?? "python";
|
|
2380
|
+
const execution = new Execution();
|
|
2381
|
+
await this.runner(args, timeoutMs, (item) => {
|
|
2382
|
+
if (item.type === "stdout") {
|
|
2383
|
+
execution.stdout += item.data;
|
|
2384
|
+
opts?.onStdout?.(item.data);
|
|
2385
|
+
} else if (item.type === "stderr") {
|
|
2386
|
+
execution.stderr += item.data;
|
|
2387
|
+
opts?.onStderr?.(item.data);
|
|
2388
|
+
} else if (item.type === "result") {
|
|
2389
|
+
const out = { type: "result", mime: item.mime, data: item.data };
|
|
2390
|
+
execution.results.push(out);
|
|
2391
|
+
opts?.onResult?.(out);
|
|
2392
|
+
} else if (item.type === "error") {
|
|
2393
|
+
const err = new ExecutionError(item.name, item.message, item.traceback);
|
|
2394
|
+
execution.error = err;
|
|
2395
|
+
opts?.onError?.(err);
|
|
2396
|
+
} else if (item.type === "done") {
|
|
2397
|
+
execution.executionCount = item.execution_count ?? 0;
|
|
2060
2398
|
}
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
clearTimeout(timer);
|
|
2064
|
-
}
|
|
2399
|
+
});
|
|
2400
|
+
return execution;
|
|
2065
2401
|
}
|
|
2066
2402
|
/**
|
|
2067
2403
|
* Create a new isolated execution context.
|
|
2068
2404
|
*
|
|
2069
|
-
*
|
|
2070
|
-
*
|
|
2405
|
+
* A Python context is its own kernel: its own variables, and `cwd` as its
|
|
2406
|
+
* working directory. Other languages get the working directory only.
|
|
2071
2407
|
*
|
|
2072
2408
|
* @example
|
|
2073
2409
|
* ```ts
|
|
@@ -2077,40 +2413,80 @@ var CodeSandbox = class extends Sandbox {
|
|
|
2077
2413
|
* ```
|
|
2078
2414
|
*/
|
|
2079
2415
|
async createContext(opts) {
|
|
2080
|
-
const
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2416
|
+
const id = `ctx-${randomId()}`;
|
|
2417
|
+
let ctx;
|
|
2418
|
+
await this.runner(
|
|
2419
|
+
{ action: "create", id, language: opts?.language ?? "python", cwd: opts?.cwd ?? "/home/user" },
|
|
2420
|
+
DEFAULT_RUN_TIMEOUT_MS,
|
|
2421
|
+
(ev) => {
|
|
2422
|
+
if (ev.type === "context") ctx = { id: ev.id, language: ev.language, cwd: ev.cwd };
|
|
2423
|
+
}
|
|
2424
|
+
);
|
|
2425
|
+
if (!ctx) throw new LizardError("Failed to create context");
|
|
2426
|
+
return ctx;
|
|
2090
2427
|
}
|
|
2091
|
-
/**
|
|
2092
|
-
* List all active execution contexts in this sandbox.
|
|
2093
|
-
*/
|
|
2428
|
+
/** List the contexts created with {@link createContext} in this sandbox. */
|
|
2094
2429
|
async listContexts() {
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2430
|
+
let out = [];
|
|
2431
|
+
await this.runner({ action: "list" }, DEFAULT_RUN_TIMEOUT_MS, (ev) => {
|
|
2432
|
+
if (ev.type === "contexts") out = ev.data;
|
|
2433
|
+
});
|
|
2434
|
+
return out;
|
|
2098
2435
|
}
|
|
2099
|
-
/**
|
|
2100
|
-
* Delete an execution context and free its resources.
|
|
2101
|
-
*/
|
|
2436
|
+
/** Delete an execution context, stopping its kernel. */
|
|
2102
2437
|
async deleteContext(context) {
|
|
2103
2438
|
const id = typeof context === "string" ? context : context.id;
|
|
2104
|
-
|
|
2105
|
-
|
|
2439
|
+
await this.runner({ action: "delete", context: id }, DEFAULT_RUN_TIMEOUT_MS, () => {
|
|
2440
|
+
});
|
|
2106
2441
|
}
|
|
2107
|
-
/**
|
|
2108
|
-
* Restart a context, clearing all variables and state.
|
|
2109
|
-
*/
|
|
2442
|
+
/** Restart a context, clearing all variables and state. */
|
|
2110
2443
|
async restartContext(context) {
|
|
2111
2444
|
const id = typeof context === "string" ? context : context.id;
|
|
2112
|
-
|
|
2113
|
-
|
|
2445
|
+
await this.runner({ action: "restart", context: id }, DEFAULT_RUN_TIMEOUT_MS, () => {
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
/** Run the in-sandbox runner with `args` and hand each of its events to `onEvent`. */
|
|
2449
|
+
async runner(args, timeoutMs, onEvent) {
|
|
2450
|
+
let encoded = toBase64(JSON.stringify(args));
|
|
2451
|
+
if (encoded.length > MAX_INLINE_ARGS && typeof args.code === "string") {
|
|
2452
|
+
const codeFile = `/tmp/.lizard-code/in-${randomId()}`;
|
|
2453
|
+
await this.fs.write(codeFile, args.code);
|
|
2454
|
+
const { code: _code, ...rest } = args;
|
|
2455
|
+
encoded = toBase64(JSON.stringify({ ...rest, codeFile }));
|
|
2456
|
+
}
|
|
2457
|
+
let part = "";
|
|
2458
|
+
let fatal;
|
|
2459
|
+
let sawEvent = false;
|
|
2460
|
+
const stderr = [];
|
|
2461
|
+
const result = await this.process.exec(["python3", "-c", RUNNER_SOURCE, encoded], {
|
|
2462
|
+
timeoutMs: timeoutMs + RUNNER_OVERHEAD_MS,
|
|
2463
|
+
onStdout: (line) => {
|
|
2464
|
+
let ev;
|
|
2465
|
+
try {
|
|
2466
|
+
ev = JSON.parse(line);
|
|
2467
|
+
if (ev.type === "part") {
|
|
2468
|
+
part += ev.data;
|
|
2469
|
+
if (!ev.end) return;
|
|
2470
|
+
ev = JSON.parse(part);
|
|
2471
|
+
part = "";
|
|
2472
|
+
}
|
|
2473
|
+
} catch {
|
|
2474
|
+
part = "";
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
sawEvent = true;
|
|
2478
|
+
if (ev.type === "fatal") fatal = ev.message;
|
|
2479
|
+
else onEvent(ev);
|
|
2480
|
+
},
|
|
2481
|
+
onStderr: (line) => {
|
|
2482
|
+
stderr.push(line);
|
|
2483
|
+
}
|
|
2484
|
+
});
|
|
2485
|
+
if (fatal) throw new LizardError(fatal);
|
|
2486
|
+
if (result.exitCode !== 0 || !sawEvent) {
|
|
2487
|
+
const detail = stderr.slice(-20).join("\n").trim() || `exit code ${result.exitCode}`;
|
|
2488
|
+
throw new LizardError(`Code runner failed (python3 is required in the sandbox): ${detail}`);
|
|
2489
|
+
}
|
|
2114
2490
|
}
|
|
2115
2491
|
static async create(templateOrOpts, opts) {
|
|
2116
2492
|
return super.create(templateOrOpts, opts);
|
|
@@ -2119,26 +2495,17 @@ var CodeSandbox = class extends Sandbox {
|
|
|
2119
2495
|
return super.connect(sandboxId, opts);
|
|
2120
2496
|
}
|
|
2121
2497
|
};
|
|
2122
|
-
CodeSandbox.defaultTemplate = "
|
|
2123
|
-
|
|
2124
|
-
const
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
buf = lines.pop() ?? "";
|
|
2134
|
-
for (const line of lines) {
|
|
2135
|
-
if (line.trim()) yield line;
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
if (buf.trim()) yield buf;
|
|
2139
|
-
} finally {
|
|
2140
|
-
reader.releaseLock();
|
|
2141
|
-
}
|
|
2498
|
+
CodeSandbox.defaultTemplate = "interpreter";
|
|
2499
|
+
function randomId() {
|
|
2500
|
+
const c = globalThis.crypto;
|
|
2501
|
+
return c?.randomUUID ? c.randomUUID().replace(/-/g, "").slice(0, 16) : Math.random().toString(16).slice(2, 18);
|
|
2502
|
+
}
|
|
2503
|
+
function toBase64(text) {
|
|
2504
|
+
if (typeof Buffer !== "undefined") return Buffer.from(text, "utf8").toString("base64");
|
|
2505
|
+
const bytes = new TextEncoder().encode(text);
|
|
2506
|
+
let s = "";
|
|
2507
|
+
for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
2508
|
+
return btoa(s);
|
|
2142
2509
|
}
|
|
2143
2510
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2144
2511
|
0 && (module.exports = {
|