* Execute command with streaming I/O. * Shared implementation that delegates process spawning to subclass.
(command: string, options: ExecOptions)
| 98 | * Shared implementation that delegates process spawning to subclass. |
| 99 | */ |
| 100 | async exec(command: string, options: ExecOptions): Promise<ExecStream> { |
| 101 | const startTime = performance.now(); |
| 102 | |
| 103 | // Short-circuit if already aborted |
| 104 | if (options.abortSignal?.aborted) { |
| 105 | throw new RuntimeError("Operation aborted before execution", "exec"); |
| 106 | } |
| 107 | |
| 108 | // Build command parts |
| 109 | const parts: string[] = []; |
| 110 | |
| 111 | // Add cd command |
| 112 | parts.push(this.cdCommand(options.cwd)); |
| 113 | |
| 114 | // Add environment variable exports (user env first, then non-interactive overrides) |
| 115 | const envVars = { ...options.env, ...NON_INTERACTIVE_ENV_VARS }; |
| 116 | for (const [key, value] of Object.entries(envVars)) { |
| 117 | parts.push(buildShellExport(key, value, (envValue) => shescape.quote(envValue))); |
| 118 | } |
| 119 | |
| 120 | // Add the actual command |
| 121 | parts.push(command); |
| 122 | |
| 123 | // Join all parts with && to ensure each step succeeds before continuing |
| 124 | let fullCommand = parts.join(" && "); |
| 125 | |
| 126 | // Wrap in bash for consistent shell behavior |
| 127 | fullCommand = `bash -c ${shescape.quote(fullCommand)}`; |
| 128 | |
| 129 | // Optionally wrap with timeout |
| 130 | if (options.timeout !== undefined) { |
| 131 | const remoteTimeout = Math.ceil(options.timeout) + 1; |
| 132 | fullCommand = `timeout -s KILL ${remoteTimeout} ${fullCommand}`; |
| 133 | } |
| 134 | |
| 135 | // Spawn the remote process (SSH or Docker) |
| 136 | const timeoutMs = options.timeout !== undefined ? options.timeout * 1000 : undefined; |
| 137 | const deadlineMs = timeoutMs !== undefined ? Date.now() + timeoutMs : undefined; |
| 138 | const spawnResult = await this.spawnRemoteProcess(fullCommand, { |
| 139 | ...options, |
| 140 | deadlineMs, |
| 141 | }); |
| 142 | const { process: childProcess } = spawnResult; |
| 143 | |
| 144 | // Short-lived commands can close stdin before writes/close complete. |
| 145 | if (childProcess.stdin) { |
| 146 | attachStreamErrorHandler(childProcess.stdin, `${this.commandPrefix} stdin`, { |
| 147 | logger: log, |
| 148 | }); |
| 149 | } |
| 150 | |
| 151 | // Wrap in DisposableProcess for cleanup |
| 152 | const disposable = new DisposableProcess(childProcess); |
| 153 | |
| 154 | // Track if we killed the process due to timeout or abort |
| 155 | let timedOut = false; |
| 156 | let aborted = false; |
| 157 |
no test coverage detected