-
Notifications
You must be signed in to change notification settings - Fork 21.9k
fix(win32): use ffi to get around bun raw input/ctrl+c issues #13052
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,86 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { dlopen, ptr } from "bun:ffi" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const STD_INPUT_HANDLE = -10 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ENABLE_PROCESSED_INPUT = 0x0001 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const kernel = () => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dlopen("kernel32.dll", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| GetStdHandle: { args: ["i32"], returns: "ptr" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| GetConsoleMode: { args: ["ptr", "ptr"], returns: "i32" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SetConsoleMode: { args: ["ptr", "u32"], returns: "i32" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SetConsoleCtrlHandler: { args: ["ptr", "i32"], returns: "i32" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let k32: ReturnType<typeof kernel> | undefined | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function load() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (process.platform !== "win32") return false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| k32 ??= kernel() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Clear ENABLE_PROCESSED_INPUT on the console stdin handle. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function win32DisableProcessedInput() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (process.platform !== "win32") return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!process.stdin.isTTY) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!load()) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const buf = new Uint32Array(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const mode = buf[0]! | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ((mode & ENABLE_PROCESSED_INPUT) === 0) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Tell Windows to ignore CTRL_C_EVENT for this process. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * SetConsoleCtrlHandler(NULL, TRUE) makes the process ignore Ctrl+C | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * signals at the OS level. Belt-and-suspenders alongside disabling | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * ENABLE_PROCESSED_INPUT. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function win32IgnoreCtrlC() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (process.platform !== "win32") return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!process.stdin.isTTY) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!load()) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| k32!.symbols.SetConsoleCtrlHandler(null, 1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Continuously enforce ENABLE_PROCESSED_INPUT=off on the console. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * opentui reconfigures the console mode through native calls (not | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * process.stdin.setRawMode) so we cannot intercept them. Instead we | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * poll at a low frequency and re-clear the flag when needed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Because ENABLE_PROCESSED_INPUT is a console-level flag (not per-process), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * keeping it cleared protects every process attached to this console, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * including the parent `bun run` wrapper that we can't otherwise control. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * The fast-path (GetConsoleMode + bitmask check) is sub-microsecond; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * SetConsoleMode only fires when something re-enabled the flag. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function win32EnforceCtrlCGuard() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (process.platform !== "win32") return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!process.stdin.isTTY) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!load()) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const buf = new Uint32Array(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setInterval(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const mode = buf[0]! | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ((mode & ENABLE_PROCESSED_INPUT) === 0) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, 100) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function win32EnforceCtrlCGuard() { | |
| if (process.platform !== "win32") return | |
| if (!process.stdin.isTTY) return | |
| if (!load()) return | |
| const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE) | |
| const buf = new Uint32Array(1) | |
| setInterval(() => { | |
| if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return | |
| const mode = buf[0]! | |
| if ((mode & ENABLE_PROCESSED_INPUT) === 0) return | |
| k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT) | |
| }, 100) | |
| export function win32EnforceCtrlCGuard(): ReturnType<typeof setInterval> | undefined { | |
| if (process.platform !== "win32") return undefined | |
| if (!process.stdin.isTTY) return undefined | |
| if (!load()) return undefined | |
| const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE) | |
| const buf = new Uint32Array(1) | |
| const interval = setInterval(() => { | |
| if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return | |
| const mode = buf[0]! | |
| if ((mode & ENABLE_PROCESSED_INPUT) === 0) return | |
| k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT) | |
| }, 100) | |
| // Allow the process to exit naturally even if this interval is active. | |
| ;(interval as any)?.unref?.() | |
| return interval |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -26,6 +26,15 @@ import { EOL } from "os" | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { WebCommand } from "./cli/cmd/web" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { PrCommand } from "./cli/cmd/pr" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { SessionCommand } from "./cli/cmd/session" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { win32DisableProcessedInput, win32IgnoreCtrlC } from "./cli/cmd/tui/win32" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Disable Windows CTRL_C_EVENT as early as possible. When running under | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // `bun run` (e.g. `bun dev`), the parent bun process shares this console | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // and would be killed by the OS before any JS signal handler fires. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| win32DisableProcessedInput() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Belt-and-suspenders: even if something re-enables ENABLE_PROCESSED_INPUT | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // later (opentui raw mode, libuv, etc.), ignore the generated event. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| win32IgnoreCtrlC() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Disable Windows CTRL_C_EVENT as early as possible. When running under | |
| // `bun run` (e.g. `bun dev`), the parent bun process shares this console | |
| // and would be killed by the OS before any JS signal handler fires. | |
| win32DisableProcessedInput() | |
| // Belt-and-suspenders: even if something re-enables ENABLE_PROCESSED_INPUT | |
| // later (opentui raw mode, libuv, etc.), ignore the generated event. | |
| win32IgnoreCtrlC() | |
| // Disable Windows CTRL_C_EVENT as early as possible, but only for TUI | |
| // entrypoints. When running under `bun run` (e.g. `bun dev`), the parent | |
| // bun process shares this console and would be killed by the OS before | |
| // any JS signal handler fires. Restricting this behavior to TUI commands | |
| // avoids breaking Ctrl+C for non-TUI commands like `opencode serve`. | |
| if (process.platform === "win32") { | |
| const argv = hideBin(process.argv) | |
| const cmd = argv[0] | |
| const sub = argv[1] | |
| // TUI entrypoints: | |
| // - `opencode attach` | |
| // - `opencode tui thread` | |
| // - (optional alias) `opencode tui-thread` | |
| const isTuiCommand = | |
| cmd === "attach" || | |
| (cmd === "tui" && sub === "thread") || | |
| cmd === "tui-thread" | |
| if (isTuiCommand) { | |
| win32DisableProcessedInput() | |
| // Belt-and-suspenders: even if something re-enables ENABLE_PROCESSED_INPUT | |
| // later (opentui raw mode, libuv, etc.), ignore the generated event. | |
| win32IgnoreCtrlC() | |
| } | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
win32EnforceCtrlCGuard()is started on mount but never stopped. If the TUI unmounts/remounts or iftui()is used in a context that doesn’t hard-exit the process, this will accumulate intervals. Consider capturing the interval id and clearing it in anonCleanup/exit handler.