* Force process exit, handling the case where the terminal is gone. * When the terminal/PTY is closed (e.g., SIGHUP), process.exit() can throw * EIO errors because Bun tries to flush stdout to a dead file descriptor. * In that case, fall back to SIGKILL which always works.
(exitCode: number)
| 191 | * In that case, fall back to SIGKILL which always works. |
| 192 | */ |
| 193 | function forceExit(exitCode: number): never { |
| 194 | // Clear failsafe timer since we're exiting now |
| 195 | if (failsafeTimer !== undefined) { |
| 196 | clearTimeout(failsafeTimer) |
| 197 | failsafeTimer = undefined |
| 198 | } |
| 199 | // Drain stdin LAST, right before exit. cleanupTerminalModes() sent |
| 200 | // DISABLE_MOUSE_TRACKING early, but the terminal round-trip plus any |
| 201 | // events already in flight means bytes can arrive during the seconds |
| 202 | // of async cleanup between then and now. Draining here catches them. |
| 203 | // Use the Ink class method (not the standalone drainStdin()) so we |
| 204 | // drain the instance's stdin — when process.stdin is piped, |
| 205 | // getStdinOverride() opens /dev/tty as the real input stream and the |
| 206 | // class method knows about it; the standalone function defaults to |
| 207 | // process.stdin which would early-return on isTTY=false. |
| 208 | try { |
| 209 | instances.get(process.stdout)?.drainStdin() |
| 210 | } catch { |
| 211 | // Terminal may be gone (SIGHUP). Ignore — we are about to exit. |
| 212 | } |
| 213 | try { |
| 214 | process.exit(exitCode) |
| 215 | } catch (e) { |
| 216 | // process.exit() threw. In tests, it's mocked to throw - re-throw so test sees it. |
| 217 | // In production, it's likely EIO from dead terminal - use SIGKILL. |
| 218 | if ((process.env.NODE_ENV as string) === 'test') { |
| 219 | throw e |
| 220 | } |
| 221 | // Fall back to SIGKILL which doesn't try to flush anything. |
| 222 | process.kill(process.pid, 'SIGKILL') |
| 223 | } |
| 224 | // In tests, process.exit may be mocked to return instead of exiting. |
| 225 | // In production, we should never reach here. |
| 226 | if ((process.env.NODE_ENV as string) !== 'test') { |
| 227 | throw new Error('unreachable') |
| 228 | } |
| 229 | // TypeScript trick: cast to never since we know this only happens in tests |
| 230 | // where the mock returns instead of exiting |
| 231 | return undefined as never |
| 232 | } |
| 233 | |
| 234 | /** |
| 235 | * Set up global signal handlers for graceful shutdown |
no test coverage detected