(command: string, options: ExecOptions = {})
| 102 | } |
| 103 | |
| 104 | exec(command: string, options: ExecOptions = {}): ExecHandle { |
| 105 | if (this.disposed) throw new Error("runner disposed"); |
| 106 | const id = options.id ?? randomUUID(); |
| 107 | const existing = this.records.get(id); |
| 108 | if (existing?.live) { |
| 109 | throw new ExecError("EEXEC_BUSY", `exec id ${id} is already running`); |
| 110 | } |
| 111 | if (existing !== undefined) this.disposeRecord(existing); |
| 112 | |
| 113 | const cwd = options.cwd ?? this.opts.cwd; |
| 114 | const env = { ...process.env, ...this.opts.env, ...options.env }; |
| 115 | // Pre-flight the cwd via dofs's stat which walks vfs_nodes / |
| 116 | // vfs_dirents in SQLite directly — no node:fs.statSync, no |
| 117 | // FUSE callback. Preserves the historical ENOENT-cwd error |
| 118 | // shape callers see when the path doesn't exist, without |
| 119 | // taking a route that could deadlock against computerd's own FUSE |
| 120 | // server. |
| 121 | if (cwd !== undefined) { |
| 122 | try { |
| 123 | stat(this.db, cwd); |
| 124 | } catch (err) { |
| 125 | return this.spawnFailed(id, err); |
| 126 | } |
| 127 | } |
| 128 | // Don't pass cwd to spawn. libuv's uv_spawn does fork + |
| 129 | // chdir + execve in the child while the parent blocks on a |
| 130 | // status pipe. If cwd lives inside computerd's own FUSE mount, the |
| 131 | // child's chdir issues a FUSE LOOKUP that computerd can't service |
| 132 | // (its event loop is stuck in uv_spawn), and the whole |
| 133 | // process deadlocks. Have /bin/sh do the chdir instead: by |
| 134 | // the time the shell runs its `cd`, computerd's event loop is back |
| 135 | // and can answer the FUSE callback normally. |
| 136 | // |
| 137 | // No `exec` prefix on the user command: the user's argument |
| 138 | // may be a compound expression (`echo a && exit 3`), and |
| 139 | // `exec` only binds to the next simple command, which would |
| 140 | // cut off everything after the first `&&`. The shell stays |
| 141 | // as the spawned process either way — the original code |
| 142 | // (`spawn("/bin/sh", ["-c", command])`) was already a shell- |
| 143 | // owned exec, so this is no change in process shape. |
| 144 | const wrapped = cwd !== undefined ? `cd ${shellQuote(cwd)} && ${command}` : command; |
| 145 | const child = spawn("/bin/sh", ["-c", wrapped], { |
| 146 | env, |
| 147 | stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"], |
| 148 | }); |
| 149 | if (options.stdin !== undefined && child.stdin) { |
| 150 | // Feed the caller's bytes then close so the child sees EOF. |
| 151 | // Ignore write/EPIPE errors: a command that never reads stdin |
| 152 | // (or exits first) must not fail the run. |
| 153 | child.stdin.on("error", () => {}); |
| 154 | child.stdin.end(Buffer.from(options.stdin)); |
| 155 | } |
| 156 | const log = createLog(this.db, id, { |
| 157 | maxBytes: this.opts.logMaxBytes, |
| 158 | now: this.opts.now, |
| 159 | }); |
| 160 | const record: ExecRecord = { |
| 161 | id, |
nothing calls this directly
no test coverage detected