@molecule/api-code-sandbox-flyio 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/exec.d.ts.map +1 -1
- package/dist/exec.js +114 -8
- package/package.json +8 -8
package/dist/exec.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../src/exec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AAGzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAKjD;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAA;AAE9C;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAA;AAW5C;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,QAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../src/exec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AAGzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAKjD;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAA;AAE9C;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAA;AAW5C;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,QAAkB,CAAA;AA6BxD;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,QAAS,CAAA;AAY5C;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAA;AAEzC,0FAA0F;AAC1F,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,eAAe,CAAC,CAAA;AAE7F;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,eAAe,GAAG,UAAU,CAiBlE;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,WAAW,GAAG,SAAS,EAC7B,UAAU,EAAE,MAAM,GACjB,MAAM,CAGR;AAmDD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,WAAW,GAAG,SAAS,EAC7B,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,UAAU,CAAC,CAqBrB"}
|
package/dist/exec.js
CHANGED
|
@@ -49,6 +49,24 @@ const DEFAULT_TIMEOUT_MS = 600_000;
|
|
|
49
49
|
* would try to materialize the whole thing in one response body.
|
|
50
50
|
*/
|
|
51
51
|
export const MAX_CAPTURED_OUTPUT_BYTES = 5 * 1024 * 1024;
|
|
52
|
+
/**
|
|
53
|
+
* Decoded bytes read per exec call when collecting a detached command's output.
|
|
54
|
+
*
|
|
55
|
+
* Fly documents NO output ceiling for the Machines API: the exec response's
|
|
56
|
+
* `flydv1.ExecResponse` schema types `stdout` and `stderr` as plain,
|
|
57
|
+
* unconstrained strings (https://docs.machines.dev/openapi.json). The live API
|
|
58
|
+
* still returns SHORT output with `exit_code: 0` once a response is large —
|
|
59
|
+
* measured 2026-10-18 in production (X1 flash-m175): one read of a
|
|
60
|
+
* 1,421,432-byte database file came back 897,144 bytes long, exactly one
|
|
61
|
+
* 512 KiB slice missing, with no error at all. So the collector asks for no
|
|
62
|
+
* more than this per response and checks every response's decoded length
|
|
63
|
+
* against what it asked for, shrinking the request when the transport cuts it.
|
|
64
|
+
*/
|
|
65
|
+
const COLLECT_CHUNK_BYTES = 128 * 1024;
|
|
66
|
+
/** Smallest chunk a shortened response is re-read with, before giving up. */
|
|
67
|
+
const MIN_COLLECT_CHUNK_BYTES = 16 * 1024;
|
|
68
|
+
/** Read attempts per chunk before the collection is reported as truncated. */
|
|
69
|
+
const COLLECT_ATTEMPTS = 4;
|
|
52
70
|
/**
|
|
53
71
|
* Base64 chunk size for spilling a large script to a file, comfortably under
|
|
54
72
|
* {@link FLY_EXEC_MAX_ARG_BYTES} once the `printf … | base64 -d` wrapper is added.
|
|
@@ -188,6 +206,96 @@ export async function execCommand(rawExec, command, opts, defaultCwd) {
|
|
|
188
206
|
}
|
|
189
207
|
return await execDetached(rawExec, script, budgetMs);
|
|
190
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Sizes of a detached run's two output spool files, in bytes. A file the
|
|
211
|
+
* command never created reads as 0.
|
|
212
|
+
* @param rawExec - Transport callback issuing one Fly exec call.
|
|
213
|
+
* @param outPath - Absolute path of the stdout spool.
|
|
214
|
+
* @param errPath - Absolute path of the stderr spool.
|
|
215
|
+
* @returns Both byte counts.
|
|
216
|
+
* @throws {Error} When the sizes cannot be read at all.
|
|
217
|
+
*/
|
|
218
|
+
async function spoolSizes(rawExec, outPath, errPath) {
|
|
219
|
+
const sizes = toExecResult(await rawExec([
|
|
220
|
+
'sh',
|
|
221
|
+
'-c',
|
|
222
|
+
`stat -c %s ${outPath} 2>/dev/null || echo 0; stat -c %s ${errPath} 2>/dev/null || echo 0`,
|
|
223
|
+
], CONTROL_EXEC_TIMEOUT_SECONDS));
|
|
224
|
+
if (sizes.exitCode !== 0) {
|
|
225
|
+
throw new Error(`Failed to inspect the detached command's output files: ${sizes.stderr || sizes.stdout}`);
|
|
226
|
+
}
|
|
227
|
+
const [out, err] = sizes.stdout
|
|
228
|
+
.trim()
|
|
229
|
+
.split('\n')
|
|
230
|
+
.map((value) => Number(value.trim()));
|
|
231
|
+
return {
|
|
232
|
+
stdout: Number.isFinite(out) ? out : 0,
|
|
233
|
+
stderr: Number.isFinite(err) ? err : 0,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Reads `size` bytes of a spool file back through exec in LENGTH-CHECKED
|
|
238
|
+
* chunks, shrinking the chunk whenever a response comes back short.
|
|
239
|
+
*
|
|
240
|
+
* One raw `head -c 5 MiB` of the spool is the shape that lost a whole slice of
|
|
241
|
+
* a database in production (see {@link COLLECT_CHUNK_BYTES}): Fly truncated
|
|
242
|
+
* the response and still reported success. Every chunk here is verified against
|
|
243
|
+
* the number of bytes asked for, retried at half the size when it is short (a
|
|
244
|
+
* truncating transport cuts at a response size, so a smaller request survives
|
|
245
|
+
* it), and a chunk that will not arrive whole fails the collection by name
|
|
246
|
+
* instead of returning silently shortened output.
|
|
247
|
+
*
|
|
248
|
+
* The bytes are base64-encoded on the Machine and decoded here as BYTES, so a
|
|
249
|
+
* chunk boundary falling inside a multi-byte UTF-8 character cannot corrupt it.
|
|
250
|
+
* @param rawExec - Transport callback issuing one Fly exec call.
|
|
251
|
+
* @param path - Absolute path of the spool file.
|
|
252
|
+
* @param size - Bytes to collect (already capped by the caller).
|
|
253
|
+
* @returns The collected text.
|
|
254
|
+
* @throws {Error} When a chunk cannot be read whole from the transport.
|
|
255
|
+
*/
|
|
256
|
+
async function collectSpool(rawExec, path, size) {
|
|
257
|
+
const parts = [];
|
|
258
|
+
let chunkBytes = COLLECT_CHUNK_BYTES;
|
|
259
|
+
for (let offset = 0; offset < size;) {
|
|
260
|
+
let received = null;
|
|
261
|
+
let want = 0;
|
|
262
|
+
let receivedBytes = 0;
|
|
263
|
+
for (let attempt = 1; attempt <= COLLECT_ATTEMPTS; attempt++) {
|
|
264
|
+
want = Math.min(chunkBytes, size - offset);
|
|
265
|
+
const read = toExecResult(await rawExec(['sh', '-c', `tail -c +${offset + 1} ${path} 2>/dev/null | head -c ${want} | base64 -w0`], CONTROL_EXEC_TIMEOUT_SECONDS));
|
|
266
|
+
if (read.exitCode !== 0) {
|
|
267
|
+
throw new Error(`Failed to read the detached command's output back: ${read.stderr || read.stdout}`);
|
|
268
|
+
}
|
|
269
|
+
const decoded = Buffer.from(read.stdout.trim(), 'base64');
|
|
270
|
+
receivedBytes = decoded.length;
|
|
271
|
+
if (decoded.length === want) {
|
|
272
|
+
received = decoded;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
// Short: the transport cut this response. Ask for less, so the next
|
|
276
|
+
// attempt fits under whatever its ceiling turns out to be.
|
|
277
|
+
const nextChunkBytes = Math.max(MIN_COLLECT_CHUNK_BYTES, Math.floor(want / 2));
|
|
278
|
+
logger.warn('Fly returned a short output read; retrying in a smaller chunk', {
|
|
279
|
+
path,
|
|
280
|
+
offset,
|
|
281
|
+
want,
|
|
282
|
+
received: decoded.length,
|
|
283
|
+
nextChunkBytes,
|
|
284
|
+
attempt,
|
|
285
|
+
});
|
|
286
|
+
chunkBytes = nextChunkBytes;
|
|
287
|
+
}
|
|
288
|
+
if (received === null) {
|
|
289
|
+
throw new Error(`The detached command's output could not be collected: the Fly exec transport ` +
|
|
290
|
+
`returned ${receivedBytes} of ${want} bytes at offset ${offset} of ${path}, over ` +
|
|
291
|
+
`${COLLECT_ATTEMPTS} reads with the chunk shrunk to ${chunkBytes} bytes. The command ` +
|
|
292
|
+
'itself finished; this is an output-collection (transport) failure — run it again.');
|
|
293
|
+
}
|
|
294
|
+
parts.push(received);
|
|
295
|
+
offset += want;
|
|
296
|
+
}
|
|
297
|
+
return Buffer.concat(parts).toString('utf8');
|
|
298
|
+
}
|
|
191
299
|
/**
|
|
192
300
|
* Runs a script detached inside the Machine and polls for its completion.
|
|
193
301
|
*
|
|
@@ -235,14 +343,12 @@ async function execDetached(rawExec, script, budgetMs) {
|
|
|
235
343
|
break;
|
|
236
344
|
}
|
|
237
345
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
const stdout = separator === -1 ? collect.stdout : collect.stdout.slice(0, separator);
|
|
245
|
-
const stderr = separator === -1 ? '' : collect.stdout.slice(separator + '\n__MOL_STDERR__\n'.length);
|
|
346
|
+
// Collect the spools in length-checked chunks BEFORE any cleanup: a single
|
|
347
|
+
// raw read of a large stdout is exactly what came back shortened (exit 0)
|
|
348
|
+
// in production, and the spool is the only copy there is.
|
|
349
|
+
const sizes = await spoolSizes(rawExec, outPath, errPath);
|
|
350
|
+
const stdout = await collectSpool(rawExec, outPath, Math.min(sizes.stdout, MAX_CAPTURED_OUTPUT_BYTES));
|
|
351
|
+
const stderr = await collectSpool(rawExec, errPath, Math.min(sizes.stderr, MAX_CAPTURED_OUTPUT_BYTES));
|
|
246
352
|
if (rc === null) {
|
|
247
353
|
// Still running at the deadline. The bookkeeping files are deliberately NOT
|
|
248
354
|
// removed — the process owns them and will keep writing.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-code-sandbox-flyio",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "Fly.io Machines (Firecracker microVM) code sandbox provider",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
],
|
|
31
31
|
"license": "Apache-2.0",
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@molecule/api-bond": "1.0.
|
|
34
|
-
"@molecule/api-code-sandbox": "1.4.
|
|
35
|
-
"@types/node": "26.
|
|
33
|
+
"@molecule/api-bond": "1.0.3",
|
|
34
|
+
"@molecule/api-code-sandbox": "1.4.1",
|
|
35
|
+
"@types/node": "26.6.5",
|
|
36
36
|
"typescript": "6.0.3",
|
|
37
|
-
"vitest": "4.1.
|
|
37
|
+
"vitest": "4.1.12"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@molecule/api-bond": "^1.0.1",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"access": "public"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@aws-sdk/client-s3": "3.
|
|
61
|
-
"@aws-sdk/s3-request-presigner": "3.
|
|
62
|
-
"@molecule/api-proxy-agent": "1.1.
|
|
60
|
+
"@aws-sdk/client-s3": "3.1156.0",
|
|
61
|
+
"@aws-sdk/s3-request-presigner": "3.1156.0",
|
|
62
|
+
"@molecule/api-proxy-agent": "1.1.2"
|
|
63
63
|
}
|
|
64
64
|
}
|