| 214 | |
| 215 | /** Exported for unit tests only. Do not use directly. */ |
| 216 | export class SSHProcess implements KaosProcess { |
| 217 | readonly stdin: Writable; |
| 218 | readonly stdout: Readable; |
| 219 | readonly stderr: Readable; |
| 220 | readonly pid: number = -1; |
| 221 | |
| 222 | private _exitCode: number | null = null; |
| 223 | private readonly _exitPromise: Promise<number>; |
| 224 | private readonly _channel: ClientChannel; |
| 225 | private _disposed = false; |
| 226 | |
| 227 | constructor(channel: ClientChannel) { |
| 228 | this._channel = channel; |
| 229 | this.stdin = channel; |
| 230 | this.stdout = new BufferedReadable(channel as unknown as Readable); |
| 231 | this.stderr = new BufferedReadable(channel.stderr); |
| 232 | |
| 233 | this._exitPromise = new Promise<number>((resolve) => { |
| 234 | // Listen to 'close' on the channel, not 'exit', to ensure all |
| 235 | // buffered output is flushed before we resolve. |
| 236 | channel.on('close', (code: number | null) => { |
| 237 | // Some ssh2 backends surface the exit status only on 'close'. |
| 238 | this._exitCode ??= code ?? 1; |
| 239 | resolve(this._exitCode); |
| 240 | }); |
| 241 | channel.on('exit', (code: number | null) => { |
| 242 | this._exitCode = code ?? 1; |
| 243 | }); |
| 244 | }); |
| 245 | } |
| 246 | |
| 247 | get exitCode(): number | null { |
| 248 | return this._exitCode; |
| 249 | } |
| 250 | |
| 251 | async wait(): Promise<number> { |
| 252 | return this._exitPromise; |
| 253 | } |
| 254 | |
| 255 | kill(signal?: NodeJS.Signals): Promise<void> { |
| 256 | // SSH signals must be stripped of the "SIG" prefix (RFC 4254 §6.9): |
| 257 | // e.g. 'SIGTERM' → 'TERM', 'SIGKILL' → 'KILL', 'SIGINT' → 'INT'. |
| 258 | // Honor the caller's requested signal so that remote processes can |
| 259 | // perform graceful shutdown on SIGTERM/SIGINT. |
| 260 | const rawSignal = signal ?? 'SIGTERM'; |
| 261 | const sshSignal = rawSignal.startsWith('SIG') ? rawSignal.slice(3) : rawSignal; |
| 262 | this._channel.signal(sshSignal); |
| 263 | return Promise.resolve(); |
| 264 | } |
| 265 | |
| 266 | dispose(): void { |
| 267 | if (this._disposed) return; |
| 268 | this._disposed = true; |
| 269 | this.stdin.destroy(); |
| 270 | this.stdout.destroy(); |
| 271 | this.stderr.destroy(); |
| 272 | } |
| 273 | } |
nothing calls this directly
no outgoing calls
no test coverage detected