Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/runtime/cli/open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
41 changes: 27 additions & 14 deletions src/spawn/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down Expand Up @@ -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))]
Expand Down Expand Up @@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -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<u8>; 2] = [Vec::new(), Vec::new()];
let mut out_fds: [Fd; 2] = [
Expand Down
74 changes: 73 additions & 1 deletion test/js/bun/util/open-in-editor-gc.test.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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);
});