| 70 | } |
| 71 | |
| 72 | export class Runner { |
| 73 | private readonly db: Database; |
| 74 | private readonly opts: { |
| 75 | cwd: string | undefined; |
| 76 | env: Record<string, string> | undefined; |
| 77 | logMaxBytes: number; |
| 78 | retentionMs: number; |
| 79 | sweepIntervalMs: number; |
| 80 | defaultTimeoutMs: number; |
| 81 | heartbeatIntervalMs: number; |
| 82 | now: () => number; |
| 83 | }; |
| 84 | private readonly records = new Map<string, ExecRecord>(); |
| 85 | private sweepTimer: NodeJS.Timeout | undefined; |
| 86 | private disposed = false; |
| 87 | |
| 88 | constructor(init: RunnerInit) { |
| 89 | this.db = init.db; |
| 90 | this.opts = { |
| 91 | cwd: init.cwd, |
| 92 | env: init.env, |
| 93 | logMaxBytes: init.logMaxBytes ?? DEFAULTS.logMaxBytes, |
| 94 | retentionMs: init.retentionMs ?? DEFAULTS.retentionMs, |
| 95 | sweepIntervalMs: init.sweepIntervalMs ?? DEFAULTS.sweepIntervalMs, |
| 96 | defaultTimeoutMs: init.defaultTimeoutMs ?? DEFAULTS.defaultTimeoutMs, |
| 97 | heartbeatIntervalMs: init.heartbeatIntervalMs ?? 0, |
| 98 | now: init.now ?? Date.now, |
| 99 | }; |
| 100 | initializeExecSchema(this.db); |
| 101 | if (init.resetSchema !== false) clearExecState(this.db); |
| 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 |
nothing calls this directly
no outgoing calls
no test coverage detected