|
| 1 | +import { dlopen, ptr } from "bun:ffi" |
| 2 | + |
| 3 | +const STD_INPUT_HANDLE = -10 |
| 4 | +const ENABLE_PROCESSED_INPUT = 0x0001 |
| 5 | + |
| 6 | +const kernel = () => |
| 7 | + dlopen("kernel32.dll", { |
| 8 | + GetStdHandle: { args: ["i32"], returns: "ptr" }, |
| 9 | + GetConsoleMode: { args: ["ptr", "ptr"], returns: "i32" }, |
| 10 | + SetConsoleMode: { args: ["ptr", "u32"], returns: "i32" }, |
| 11 | + SetConsoleCtrlHandler: { args: ["ptr", "i32"], returns: "i32" }, |
| 12 | + }) |
| 13 | + |
| 14 | +let k32: ReturnType<typeof kernel> | undefined |
| 15 | + |
| 16 | +function load() { |
| 17 | + if (process.platform !== "win32") return false |
| 18 | + try { |
| 19 | + k32 ??= kernel() |
| 20 | + return true |
| 21 | + } catch { |
| 22 | + return false |
| 23 | + } |
| 24 | +} |
| 25 | + |
| 26 | +/** |
| 27 | + * Clear ENABLE_PROCESSED_INPUT on the console stdin handle. |
| 28 | + */ |
| 29 | +export function win32DisableProcessedInput() { |
| 30 | + if (process.platform !== "win32") return |
| 31 | + if (!process.stdin.isTTY) return |
| 32 | + if (!load()) return |
| 33 | + |
| 34 | + const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE) |
| 35 | + const buf = new Uint32Array(1) |
| 36 | + if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return |
| 37 | + |
| 38 | + const mode = buf[0]! |
| 39 | + if ((mode & ENABLE_PROCESSED_INPUT) === 0) return |
| 40 | + k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT) |
| 41 | +} |
| 42 | + |
| 43 | +/** |
| 44 | + * Tell Windows to ignore CTRL_C_EVENT for this process. |
| 45 | + * |
| 46 | + * SetConsoleCtrlHandler(NULL, TRUE) makes the process ignore Ctrl+C |
| 47 | + * signals at the OS level. Belt-and-suspenders alongside disabling |
| 48 | + * ENABLE_PROCESSED_INPUT. |
| 49 | + */ |
| 50 | +export function win32IgnoreCtrlC() { |
| 51 | + if (process.platform !== "win32") return |
| 52 | + if (!process.stdin.isTTY) return |
| 53 | + if (!load()) return |
| 54 | + |
| 55 | + k32!.symbols.SetConsoleCtrlHandler(null, 1) |
| 56 | +} |
| 57 | + |
| 58 | +let unhook: (() => void) | undefined |
| 59 | + |
| 60 | +/** |
| 61 | + * Keep ENABLE_PROCESSED_INPUT disabled. |
| 62 | + * |
| 63 | + * On Windows, Ctrl+C becomes a CTRL_C_EVENT (instead of stdin input) when |
| 64 | + * ENABLE_PROCESSED_INPUT is set. Various runtimes can re-apply console modes |
| 65 | + * (sometimes on a later tick), and the flag is console-global, not per-process. |
| 66 | + * |
| 67 | + * We combine: |
| 68 | + * - A `setRawMode(...)` hook to re-clear after known raw-mode toggles. |
| 69 | + * - A low-frequency poll as a backstop for native/external mode changes. |
| 70 | + */ |
| 71 | +export function win32InstallCtrlCGuard() { |
| 72 | + if (process.platform !== "win32") return |
| 73 | + if (!process.stdin.isTTY) return |
| 74 | + if (!load()) return |
| 75 | + if (unhook) return unhook |
| 76 | + |
| 77 | + const stdin = process.stdin as any |
| 78 | + const original = stdin.setRawMode |
| 79 | + |
| 80 | + const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE) |
| 81 | + const buf = new Uint32Array(1) |
| 82 | + |
| 83 | + const enforce = () => { |
| 84 | + if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return |
| 85 | + const mode = buf[0]! |
| 86 | + if ((mode & ENABLE_PROCESSED_INPUT) === 0) return |
| 87 | + k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT) |
| 88 | + } |
| 89 | + |
| 90 | + // Some runtimes can re-apply console modes on the next tick; enforce twice. |
| 91 | + const later = () => { |
| 92 | + enforce() |
| 93 | + setImmediate(enforce) |
| 94 | + } |
| 95 | + |
| 96 | + if (typeof original === "function") { |
| 97 | + stdin.setRawMode = (mode: boolean) => { |
| 98 | + const result = original.call(stdin, mode) |
| 99 | + later() |
| 100 | + return result |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + // Ensure it's cleared immediately too (covers any earlier mode changes). |
| 105 | + later() |
| 106 | + |
| 107 | + const interval = setInterval(enforce, 100) |
| 108 | + |
| 109 | + unhook = () => { |
| 110 | + clearInterval(interval) |
| 111 | + if (typeof original === "function") { |
| 112 | + stdin.setRawMode = original |
| 113 | + } |
| 114 | + unhook = undefined |
| 115 | + } |
| 116 | + |
| 117 | + return unhook |
| 118 | +} |
0 commit comments