Skip to content
Open
22 changes: 22 additions & 0 deletions docs/runtime/debugger.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,28 @@ 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 the port ahead of time, start the process with `--inspect-port`; `--inspect-port=0` picks a free port.

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

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
8 changes: 8 additions & 0 deletions docs/snippets/cli/run.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,14 @@ 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">
Port for the debugger started later by `SIGUSR1` or `process._debugProcess()` (default `6499`, `0` for a free port)
</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
37 changes: 26 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 {
if !can_start_at_runtime() {
return false;
}
if this.debugger.is_some() || HAS_CREATED_DEBUGGER.load(Ordering::Relaxed) {
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,30 @@ pub fn start_node_inspector_server(url: &mut BunString, wait_for_connection: boo
},
protocol: Protocol::NodeInspector,
..Default::default()
}));
})
}

/// False when an inspector is already configured (CLI `--inspect`,
/// `BUN_INSPECT`, `inspector.open()`, or an earlier runtime activation) or
/// when called off the main thread.
Comment thread
robobun marked this conversation as resolved.
Outdated
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 tail of every "start the inspector after startup" path
/// (`inspector.open()`, SIGUSR1 / `process._debugProcess`): install
/// `config` on the main VM, switch the transpiler to debuggable output, spawn
/// the debugger thread, and install Bun's inspector controller. Returns false
/// (leaving `vm.debugger` unset) if the thread could not be started. Caller
/// has checked [`can_start_at_runtime`].
Comment thread
robobun marked this conversation as resolved.
Outdated
pub(crate) fn start_at_runtime(config: Debugger) -> 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`).
Comment thread
robobun marked this conversation as resolved.
Outdated
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
Loading