Skip to content
Open
23 changes: 23 additions & 0 deletions docs/runtime/debugger.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,29 @@ bun --inspect=localhost:4000 server.ts
bun --inspect=localhost:4000/prefix server.ts
```

### Attaching to a running process

A process that was started without `--inspect` can still be debugged. Sending it `SIGUSR1` starts the inspector on the spot and prints the same banner as `--inspect`, even if the process is busy in a long-running loop and never returns to the event loop. This matches Node.js.

```sh icon="terminal" title="terminal"
kill -USR1 <pid>
```

`process._debugProcess(pid)` does the same from JavaScript and also works on Windows, where there is no `SIGUSR1`:

```sh icon="terminal" title="terminal"
bun -e 'process._debugProcess(12345)'
```

By default the inspector listens on port `6499`. To choose where it listens ahead of time, start the process with `--inspect-port`, which accepts the same `port`, `host:port`, and URL prefix forms as `--inspect`; `--inspect-port=0` picks a free port.

```sh icon="terminal" title="terminal"
bun --inspect-port=4000 server.ts # later: kill -USR1 <pid>
bun --inspect-port=localhost:4000 server.ts
```

If your program installs its own `process.on("SIGUSR1", ...)` listener, that listener receives the signal instead; the inspector shortcut comes back once the last listener is removed. To keep `SIGUSR1` at its default action (terminating the process) and never start an inspector, pass `--disable-sigusr1`. Processes started with `--inspect`, `--inspect-brk`, or `--inspect-wait` already have an inspector and ignore `SIGUSR1`.

---

## Debuggers
Expand Down
9 changes: 9 additions & 0 deletions docs/snippets/cli/run.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ bun run <file or script>
Activate Bun's debugger, set breakpoint on first line of code and wait
</ParamField>

<ParamField path="--inspect-port" type="string">
`[host:]port` for a debugger started later by `SIGUSR1` or `process._debugProcess()` (default `6499`, `0` for a free
port)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</ParamField>

<ParamField path="--disable-sigusr1" type="boolean">
Do not start the debugger on `SIGUSR1`; leave the signal at its default action
</ParamField>

### Dependency &amp; Module Resolution

<ParamField path="--preload" type="string">
Expand Down
2 changes: 1 addition & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "447082ab6897278727b44e1ba3c326ae6e1504c3";
export const WEBKIT_VERSION = "autobuild-preview-pr-287-d55f967e";
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
28 changes: 17 additions & 11 deletions src/jsc/Debugger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,21 +577,13 @@ impl Debugger {
/// thread, or when the debugger thread could not be started.
// HOST_EXPORT(Debugger__startNodeInspectorServer, c)
pub fn start_node_inspector_server(url: &mut BunString, wait_for_connection: bool) -> bool {
// Short-lived borrows only — `Debugger::create` re-enters JS and forms its
// own `&mut VirtualMachine` (see the aliasing note on
// `wait_for_debugger_if_necessary`).
let this: &VirtualMachine = VirtualMachine::get();
if !this.is_main_thread {
return false;
}
if this.debugger.is_some() || HAS_CREATED_DEBUGGER.load(Ordering::Relaxed) {
if !can_start_at_runtime() {
return false;
}

// The URL outlives the process: the debugger struct stores `'static` slices
// (CLI-arena lifetimes), so leak the runtime-provided URL the same way.
let url_bytes: &'static [u8] = Box::leak(url.to_utf8_bytes().into_boxed_slice());
this.as_mut().debugger = Some(Box::new(Debugger {
start_at_runtime(Debugger {
path_or_port: Some(url_bytes),
wait_for_connection: if wait_for_connection {
Wait::Forever
Expand All @@ -600,7 +592,21 @@ pub fn start_node_inspector_server(url: &mut BunString, wait_for_connection: boo
},
protocol: Protocol::NodeInspector,
..Default::default()
}));
})
}

/// False off the main thread or once any inspector exists (CLI flags, env, `inspector.open()`, runtime activation).
pub(crate) fn can_start_at_runtime() -> bool {
let this: &VirtualMachine = VirtualMachine::get();
this.is_main_thread && this.debugger.is_none() && !HAS_CREATED_DEBUGGER.load(Ordering::Relaxed)
}

/// Shared by `inspector.open()` and SIGUSR1 / `process._debugProcess`; the caller has checked [`can_start_at_runtime`].
pub(crate) fn start_at_runtime(config: Debugger) -> bool {
// `&` only: `Debugger::create` re-enters JS and forms its own `&mut VirtualMachine` (see `wait_for_debugger_if_necessary`).
let this: &VirtualMachine = VirtualMachine::get();
debug_assert!(can_start_at_runtime());
this.as_mut().debugger = Some(Box::new(config));

// Frontends need positions that map back to the original source, so stop
// minifying and caching transpiled output for code loaded from now on.
Expand Down
Loading