3fab985c14
Combined native HWND and stdio TTY evidence so compiled Windows Terminal launches no longer set CREATE_NO_WINDOW for the Python eval kernel. Fixes #7343
135 lines
5.2 KiB
TypeScript
135 lines
5.2 KiB
TypeScript
/**
|
|
* Subprocess spawn-option helpers for the Python kernel.
|
|
*
|
|
* Pure helpers (`shouldHideKernelWindow`, `consoleAttached`) live here so they
|
|
* can be unit-tested without dragging in the kernel's runtime dependencies.
|
|
* The effectful `hostHasInheritableConsole` combines a Win32 FFI probe with
|
|
* TTY evidence and is the function `kernel.ts` actually calls.
|
|
*/
|
|
import { dlopen, FFIType } from "bun:ffi";
|
|
|
|
/**
|
|
* Decide whether the long-lived Python kernel subprocess should be spawned
|
|
* with `windowsHide: true`.
|
|
*
|
|
* On Windows, Bun maps `windowsHide: true` to the `CREATE_NO_WINDOW` flag,
|
|
* which detaches the child from any inherited console. The Python kernel
|
|
* runs user code that imports NumPy/pandas; those native extensions
|
|
* (`numpy/_core/_multiarray_umath.pyd` + bundled OpenBLAS/SLEEF thread-pool
|
|
* init) can deadlock inside `LoadLibraryExW` when no console is attached,
|
|
* and a console-less child cannot receive SIGINT via
|
|
* `GenerateConsoleCtrlEvent` (the recovery path the host relies on). See
|
|
* issue #1960.
|
|
*
|
|
* So on Windows we hide only when the host itself has no console to share.
|
|
* In any launch where a console is attached — even one with every stdio
|
|
* stream redirected — the kernel inherits the parent's console, matching
|
|
* `python.exe` invoked from `cmd.exe`, which keeps native imports and
|
|
* SIGINT recovery working.
|
|
*
|
|
* Short-lived helper subprocesses elsewhere in the codebase (LSP probes,
|
|
* git, plugin installs) keep `windowsHide: true` because they don't load
|
|
* complex native modules and the brief console flash would be user-visible
|
|
* noise.
|
|
*/
|
|
export function shouldHideKernelWindow(opts: {
|
|
platform: NodeJS.Platform;
|
|
hostHasInheritableConsole: boolean;
|
|
}): boolean {
|
|
if (opts.platform !== "win32") return false;
|
|
return !opts.hostHasInheritableConsole;
|
|
}
|
|
|
|
/**
|
|
* Keep eval kernels outside the host's POSIX terminal session.
|
|
*
|
|
* User code can start an interactive shell which calls `tcsetpgrp(3)`. If the
|
|
* kernel shares OMP's session, that shell can replace OMP as the controlling
|
|
* terminal's foreground process group and the host is then stopped by SIGTTIN
|
|
* on its next stdin read. Bun implements `detached: true` with `setsid(2)` on
|
|
* POSIX, making the kernel a session leader with no controlling terminal.
|
|
*/
|
|
export function shouldDetachKernel(platform: NodeJS.Platform): boolean {
|
|
return platform !== "win32";
|
|
}
|
|
|
|
/**
|
|
* Combine native Win32 and stdio TTY evidence of an inheritable console.
|
|
*
|
|
* `GetConsoleWindow()` detects classic consoles even when every stdio stream
|
|
* is redirected. TTY detection covers ConPTY-backed terminals, where compiled
|
|
* hosts can receive a null HWND despite being attached to Windows Terminal.
|
|
* Either signal must preserve console inheritance: `CREATE_NO_WINDOW` can
|
|
* deadlock NumPy native-extension loading in the Python child.
|
|
*/
|
|
export function consoleAttached(opts: {
|
|
nativeConsole?: boolean | null;
|
|
stdinIsTTY: boolean;
|
|
stdoutIsTTY: boolean;
|
|
stderrIsTTY: boolean;
|
|
}): boolean {
|
|
if (opts.nativeConsole === true) return true;
|
|
return opts.stdinIsTTY || opts.stdoutIsTTY || opts.stderrIsTTY;
|
|
}
|
|
|
|
/**
|
|
* Probe `kernel32.dll!GetConsoleWindow()` to detect whether the current
|
|
* Windows process owns a console window.
|
|
*
|
|
* Returns `true` for a non-NULL HWND, `false` when NULL, and `null` when the
|
|
* probe itself fails (off-Windows, FFI disabled, or unexpected kernel32
|
|
* layout). A false result is not conclusive for ConPTY-backed terminals, so
|
|
* callers must also inspect the stdio TTY signals.
|
|
*
|
|
* Cached on first call because in practice the console attachment of a
|
|
* long-lived OMP host never changes for the lifetime of the process, and
|
|
* we don't want to re-dlopen kernel32 on every kernel spawn.
|
|
*/
|
|
type ConsoleProbeResult = boolean | null;
|
|
let cachedWindowsConsoleProbe: { value: ConsoleProbeResult } | undefined;
|
|
|
|
function probeWindowsConsoleWindow(): ConsoleProbeResult {
|
|
if (cachedWindowsConsoleProbe) return cachedWindowsConsoleProbe.value;
|
|
let value: ConsoleProbeResult = null;
|
|
try {
|
|
const lib = dlopen("kernel32.dll", {
|
|
GetConsoleWindow: { args: [], returns: FFIType.ptr },
|
|
});
|
|
try {
|
|
const hwnd = lib.symbols.GetConsoleWindow();
|
|
// FFIType.ptr returns `Pointer | null`; a 0 pointer should also be
|
|
// treated as NULL defensively in case Bun ever returns 0n / 0.
|
|
value = hwnd !== null && hwnd !== 0;
|
|
} finally {
|
|
lib.close();
|
|
}
|
|
} catch {
|
|
value = null;
|
|
}
|
|
cachedWindowsConsoleProbe = { value };
|
|
return value;
|
|
}
|
|
|
|
/** Reset the cached Win32 probe result. Test-only; not part of the public surface. */
|
|
export function __resetWindowsConsoleProbeCache(): void {
|
|
cachedWindowsConsoleProbe = undefined;
|
|
}
|
|
|
|
/**
|
|
* Whether the host process owns a console its children can inherit.
|
|
*
|
|
* On Windows, `GetConsoleWindow()` detects classic consoles and all-stdio-
|
|
* redirected launches while stdio TTYs detect ConPTY-backed terminals. Either
|
|
* signal is sufficient. Other platforms use the same TTY evidence, although
|
|
* `windowsHide` is a no-op there.
|
|
*/
|
|
export function hostHasInheritableConsole(): boolean {
|
|
const nativeConsole = process.platform === "win32" ? probeWindowsConsoleWindow() : null;
|
|
return consoleAttached({
|
|
nativeConsole,
|
|
stdinIsTTY: !!process.stdin.isTTY,
|
|
stdoutIsTTY: !!process.stdout.isTTY,
|
|
stderrIsTTY: !!process.stderr.isTTY,
|
|
});
|
|
}
|