diff --git a/src/runtime/cli/open.rs b/src/runtime/cli/open.rs index a14c65fa5c8b..79c0bdb76e72 100644 --- a/src/runtime/cli/open.rs +++ b/src/runtime/cli/open.rs @@ -452,6 +452,8 @@ fn auto_close(spawned: *mut SpawnedEditorContext) { stderr: sync::SyncStdio::Inherit, stdout: sync::SyncStdio::Inherit, stdin: sync::SyncStdio::Inherit, + #[cfg(unix)] + forward_signals: false, #[cfg(windows)] windows: crate::api::bun::process::WindowsOptions { loop_: bun_jsc::EventLoopHandle::init_mini(bun_event_loop::MiniEventLoop::init_global( diff --git a/src/spawn/process.rs b/src/spawn/process.rs index 0703ddf82c72..c11379074ded 100644 --- a/src/spawn/process.rs +++ b/src/spawn/process.rs @@ -2290,6 +2290,9 @@ mod spawn_process_body { pub use_execve_on_macos: bool, pub argv0: Option<*const c_char>, + #[cfg(unix)] + pub forward_signals: bool, + #[cfg(windows)] pub windows: WindowsOptions, #[cfg(not(windows))] @@ -2357,6 +2360,8 @@ mod spawn_process_body { envp: None, use_execve_on_macos: false, argv0: None, + #[cfg(unix)] + forward_signals: true, #[cfg(windows)] windows: Default::default(), #[cfg(not(windows))] @@ -3135,8 +3140,12 @@ mod spawn_process_body { } } - Bun__currentSyncPID.store(0, core::sync::atomic::Ordering::Relaxed); - let _signals = SignalForwarding::register(); + let _signals = if options.forward_signals { + Bun__currentSyncPID.store(0, core::sync::atomic::Ordering::Relaxed); + Some(SignalForwarding::register()) + } else { + None + }; // SAFETY: caller-built argv/envp are null-terminated C-string // arrays with argv[0] non-null; valid for this call. @@ -3146,17 +3155,19 @@ mod spawn_process_body { Err(err) => return Ok(Err(err)), Ok(proces) => proces, }; - // Negative → kill() in the C++ signal forwarder targets the pgroup, so - // a SIGTERM/SIGINT delivered to `bun run` reaches every descendant - // that hasn't `setsid()`-escaped. - Bun__currentSyncPID.store( - if no_orphans { - -i64::from(process.pid) - } else { - i64::from(process.pid) - }, - core::sync::atomic::Ordering::Relaxed, - ); + if options.forward_signals { + // Negative → kill() in the C++ signal forwarder targets the pgroup, so + // a SIGTERM/SIGINT delivered to `bun run` reaches every descendant + // that hasn't `setsid()`-escaped. + Bun__currentSyncPID.store( + if no_orphans { + -i64::from(process.pid) + } else { + i64::from(process.pid) + }, + core::sync::atomic::Ordering::Relaxed, + ); + } let mut jc = JobControl { prev: 0, @@ -3216,7 +3227,9 @@ mod spawn_process_body { // `siblings` by reference while still mutated below. Restructure into a single // RAII state struct (or run cleanup inline at each return). - Bun__sendPendingSignalIfNecessary(); + if options.forward_signals { + Bun__sendPendingSignalIfNecessary(); + } let mut out: [Vec; 2] = [Vec::new(), Vec::new()]; let mut out_fds: [Fd; 2] = [ diff --git a/test/js/bun/util/open-in-editor-gc.test.ts b/test/js/bun/util/open-in-editor-gc.test.ts index d63a41ee700b..980cea719ff2 100644 --- a/test/js/bun/util/open-in-editor-gc.test.ts +++ b/test/js/bun/util/open-in-editor-gc.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test"; import { bunEnv, bunExe, isLinux, tempDir } from "harness"; -import { existsSync, symlinkSync } from "node:fs"; +import { chmodSync, existsSync, symlinkSync } from "node:fs"; import { join } from "node:path"; // On Linux, JSC uses SIGPWR to suspend/resume threads for GC and the libpas @@ -53,3 +53,75 @@ test.skipIf(!isLinux)("Bun.openInEditor does not break GC signal handling", asyn await Promise.all(runs); }); + +test.skipIf(!isLinux)("Bun.openInEditor does not steal process signal handlers", async () => { + const sleep = ["/usr/bin/sleep", "/bin/sleep"].find(p => existsSync(p)); + expect(sleep).toBeDefined(); + + using dir = tempDir("open-in-editor-signal", {}); + const editor = join(String(dir), "fake-editor"); + const ready = join(String(dir), "ready"); + const shellQuote = (path: string) => `'${path.replaceAll("'", "'\\''")}'`; + const sleepPath = sleep!; + await Bun.write( + editor, + `#!/bin/sh +${shellQuote(sleepPath)} 0.1 +printf ready > ${shellQuote(ready)} +exec ${shellQuote(sleepPath)} 0.3 +`, + ); + chmodSync(editor, 0o755); + + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const editor = process.argv[2]; + const ready = process.argv[3]; + let got = false; + + process.on("SIGUSR2", () => { + got = true; + }); + + Bun.openInEditor("0.3", { editor }); + + for (let i = 0; i < 100 && !(await Bun.file(ready).exists()); i++) { + await Bun.sleep(10); + } + + if (!(await Bun.file(ready).exists())) { + console.error("editor did not start"); + process.exit(1); + } + + process.kill(process.pid, "SIGUSR2"); + + for (let i = 0; i < 100 && !got; i++) { + await Bun.sleep(10); + } + + if (!got) { + console.error("SIGUSR2 handler was not called"); + process.exit(1); + } + + console.log("ok"); + `, + editor, + ready, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("ok"); + expect(proc.signalCode).toBeNull(); + expect(exitCode).toBe(0); +});