Replace 0.12 command calls with exec() on the container. In Sandbox SDK 0.12, exec() runs each command string in a long-lived bash session that starts in /workspace. In 1.0, each call starts one process, and nothing carries over from one call to the next.
Your 1.0 class replaces the 0.12 Sandbox class, and has the container getter, the ensureRunning() method, and the ENV constant from Replace the Sandbox class. Call ensureRunning() at the start of each method that runs a command.
This page replaces exec(), execStream(), createSession(), getSession(), deleteSession(), and the timeout and commandTimeoutMs options. For file calls, refer to Change file calls. For startProcess(), refer to Move background processes.
Pass the command as an array of arguments, with the working directory and variables that 0.12 gave it:
const result = await sandbox.exec("npm test");
if (!result.success) {
console.error(result.stderr);
}const process = await this.container.exec(["npm", "test"], {
cwd: "/workspace",
env: ENV,
});
const result = await process.output();
if (result.exitCode !== 0) {
console.error(new TextDecoder().decode(result.stderr));
}exec() starts in /, even when the image sets WORKDIR. It does not inherit the variables that start() sets, except PATH. output() waits for the process to exit, and returns its output as bytes. As in 0.12, a non-zero exit code does not throw.
exec() runs the command without a shell. For pipes, redirects, and variables, run the 0.12 string with bash:
const process = await this.container.exec(
["bash", "-c", "npm test 2>&1 | tee /tmp/test.log"],
{ cwd: "/workspace", env: ENV },
);When the executable does not exist, exec() throws an error such as Command `npm` was not found in the container. 0.12 bash returned exit code 127 instead. A command that runs through bash -c still returns 127.
To replace execStream(), or exec() with stream: true and onOutput, return stdout from the process as a response body:
const stream = await sandbox.execStream("npm test");
return new Response(stream, {
headers: { "Content-Type": "text/event-stream" },
});const process = await this.container.exec(["npm", "test"], {
cwd: "/workspace",
env: ENV,
stderr: "combined",
});
return new Response(process.stdout, {
headers: { "Content-Type": "text/event-stream" },
});stderr: "combined" sends standard error in the same stream. When the command writes to both streams at nearly the same time, lines can arrive out of order. To keep the order, redirect in a shell instead, such as ["sh", "-c", "npm test 2>&1"]. Keep Content-Type: text/event-stream. With other content types, the response can arrive all at once when the process exits.
0.12 sent JSON events that parseSSEStream() decoded. The 1.0 body holds the output of the command. Read it with fetch() and a stream reader instead of EventSource. To send each line as a server-sent event, refer to Stream command output.
A 0.12 session kept a working directory and variables across calls. Keep them in an object, and pass it to each call:
const build = await sandbox.createSession({
cwd: "/workspace/app",
env: { CI: "1" },
});
await build.exec("npm ci");
await build.exec("npm test");const build = { cwd: "/workspace/app", env: { ...ENV, CI: "1" } };
const install = await this.container.exec(["npm", "ci"], build);
await install.exitCode;
const test = await this.container.exec(["npm", "test"], build);
const result = await test.output();exec() resolves when the process starts, so wait for exitCode before the next step.
In 0.12, a cd or export in one exec() call carried into the next call, including in the default session. In 1.0, it lasts until its command exits. Run steps that depend on each other in one command, such as ["bash", "-c", "cd app && npm ci && npm test"].
ExecutionSession methods map like the top-level methods. Remove deleteSession(), which has nothing to delete. To keep the work of two sessions apart, as the isolation option did, give each one its own sandbox name.
Start the command with GNU coreutils timeout, which stops the command and every process it started:
const result = await sandbox.exec("npm test", { timeout: 60_000 });const process = await this.container.exec(
["timeout", "--kill-after=5", "60", "bash", "-c", "npm test"],
{ cwd: "/workspace", env: ENV },
);
const result = await process.output();
// Exit code 124 means the command ran longer than 60 seconds
const timedOut = result.exitCode === 124;0.12 threw CommandError with the message Command timeout after 60000ms, and the command kept running. In 1.0, a command that times out stops, so it cannot change files after the error.
To cancel a command from your code instead, pass an AbortSignal. For how to clear its timer, refer to exec().
Run a command with and without the options, from a method on your class:
const command = [
"sh",
"-c",
'echo "pwd=$(pwd) NODE_ENV=${NODE_ENV:-unset}"',
];
const bare = await this.container.exec(command);
const withOptions = await this.container.exec(command, {
cwd: "/workspace",
env: ENV,
});The output of bare shows the defaults, and the output of withOptions shows what 0.12 gave each command:
pwd=/ NODE_ENV=unset
pwd=/workspace NODE_ENV=test