diff --git a/packages/server/src/workspace/modal.ts b/packages/server/src/workspace/modal.ts index 86391d3b180..1f01bbd32df 100644 --- a/packages/server/src/workspace/modal.ts +++ b/packages/server/src/workspace/modal.ts @@ -13,15 +13,30 @@ trap 'rm -f -- "$pidfile"' EXIT "$@" ` +// Modal's VM runtime accepts process-group signals without delivering them +// (kill(-pgid) returns 0 and nothing dies; direct-pid signals work), so the +// group is enumerated from /proc and each member is signalled directly. The +// second pass catches children forked between scan and signal. const KILL = ` +pidfile=$1 +sig=$2 i=0 -while [ ! -s "$1" ] && [ "$i" -lt 250 ]; do sleep 0.02; i=$((i + 1)); done -if [ -s "$1" ]; then - pid=$(cat "$1") - /bin/kill "-$2" "-$pid" 2>/dev/null || true -else - exit 47 -fi +while [ ! -s "$pidfile" ] && [ "$i" -lt 250 ]; do sleep 0.02; i=$((i + 1)); done +[ -s "$pidfile" ] || exit 47 +target=$(cat "$pidfile") +pass=0 +while [ "$pass" -lt 2 ]; do + for stat in /proc/[0-9]*/stat; do + [ -e "$stat" ] || continue + pid=\${stat#/proc/} + pid=\${pid%/stat} + set -- $(sed "s/.*) //" "$stat" 2>/dev/null) + if [ "\${3:-}" = "$target" ]; then + /bin/kill "-$sig" "$pid" 2>/dev/null || true + fi + done + pass=$((pass + 1)) +done ` export interface ModalImageSpec { @@ -54,7 +69,15 @@ export const createModalSandbox = async (options: ModalSandboxOptions) => { const app = await client.apps.fromName(options.app, { createIfMissing: true }) const imageSpec = options.image ?? ubuntuImage const image = client.images.fromRegistry(imageSpec.registry).dockerfileCommands([...imageSpec.dockerfileCommands]) - const sandbox = await client.sandboxes.create(app, image, options.sandbox) + // Always Modal's Full-VM runtime (beta, enabled per account): a real kernel + // with real device nodes, so workspaces can run Docker and other + // kernel-dependent workloads. Costs versus gVisor, measured Aug 2026: + // per-exec floor ~285-535ms versus ~90-165ms, and filesystem snapshots only + // (no memory snapshots — acceptable; fs-snapshot is the persistence design). + const sandbox = await client.sandboxes.create(app, image, { + ...options.sandbox, + experimentalOptions: { ...options.sandbox?.experimentalOptions, vm_runtime: true }, + }) return { driver: makeModalDriver(sandbox), sandbox, @@ -64,12 +87,14 @@ export const createModalSandbox = async (options: ModalSandboxOptions) => { /** * Adapts Modal exec to the Environment driver. Files intentionally has no native - * overrides: Modal exec and filesystem tools share the same roughly 175ms floor, - * so the derived exec defaults are the simplest implementation with no measured loss. + * overrides: exec latency dominates payload work (VM runtime floor measured + * ~285-535ms per exec, Aug 2026), so the derived exec defaults are the simplest + * implementation with no measured loss. * * Modal cannot signal a ContainerProcess. Each command therefore starts a new * process group and records its leader in a unique pid file; kill runs a second - * sandbox command that signals that group. Pid files are removed best-effort. + * sandbox command that enumerates that group from /proc and signals each member + * directly (see KILL). Pid files are removed best-effort. */ export const makeModalDriver = (sandbox: Sandbox): Driver => { const spawn = Effect.fnUntraced(function* (command: Command) {