Skip to content
Open
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
17 changes: 17 additions & 0 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8585,6 +8585,16 @@ declare module "bun" {
* — the crash report lands here. @default "ignore"
*/
stderr?: "inherit" | "ignore";
/**
* Run the subprocess in its own session (via `setsid()`), so it has
* no controlling terminal. Some endpoint-protection / antivirus
* hooks write their rejection banner directly to `/dev/tty`,
* bypassing stdio redirection — detaching keeps that output off
* the parent's terminal. Only affects the first `Bun.WebView` in
* the process (subsequent views share the same subprocess).
* @default false
*/
detached?: boolean;
}
| {
type: "webkit";
Expand All @@ -8597,6 +8607,13 @@ declare module "bun" {
* Route the host process's stderr to Bun's. @default "ignore"
*/
stderr?: "inherit" | "ignore";
/**
* Run the host subprocess in its own session (via `setsid()`), so
* it has no controlling terminal. Only affects the first
* `Bun.WebView` in the process (subsequent views share the same
* subprocess). @default false
*/
detached?: boolean;
};

/**
Expand Down
11 changes: 6 additions & 5 deletions src/runtime/webview/ChromeBackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ using namespace JSC;
// terminated) appends after core flags. All pointers nullable.
extern "C" int32_t Bun__Chrome__ensure(Zig::GlobalObject*, const char* userDataDir,
const char* path, const char* const* extraArgv, uint32_t extraArgvLen,
bool stdoutInherit, bool stderrInherit);
bool stdoutInherit, bool stderrInherit, bool detached);
extern "C" void* Blob__fromBytesWithType(JSC::JSGlobalObject*, const uint8_t* ptr, size_t len, const char* mime);
extern "C" JSC::EncodedJSValue SYSV_ABI Blob__create(Zig::GlobalObject*, void* impl);
extern "C" void Bun__eventLoop__incrementRefConcurrently(void* bunVM, int delta);
Expand Down Expand Up @@ -281,7 +281,7 @@ static constexpr us_socket_vtable_t s_cdpVTable = {

bool Transport::ensureSpawned(Zig::GlobalObject* zig, const WTF::String& userDataDir,
const WTF::String& path, const WTF::Vector<WTF::String>& extraArgv,
bool stdoutInherit, bool stderrInherit)
bool stdoutInherit, bool stderrInherit, bool detached)
{
if (m_mode != TransportMode::None && !m_dead) return true;
if (m_dead) {
Expand Down Expand Up @@ -310,7 +310,7 @@ bool Transport::ensureSpawned(Zig::GlobalObject* zig, const WTF::String& userDat
pathC.length() ? pathC.data() : nullptr,
argvPtrs.isEmpty() ? nullptr : argvPtrs.span().data(),
static_cast<uint32_t>(argvPtrs.size()),
stdoutInherit, stderrInherit);
stdoutInherit, stderrInherit, detached);
if (fd < 0) {
m_dead = true;
return false;
Expand Down Expand Up @@ -425,7 +425,7 @@ static void wsOnClose(void* ctx, unsigned short code)
// let ensureSpawned's m_dead-reset clear it.
auto pending = std::exchange(t.m_wsPending, {});
if (t.ensureSpawned(t.m_global, t.m_fallbackUserDataDir, {}, {},
t.m_fallbackStdoutInherit, t.m_fallbackStderrInherit)) {
t.m_fallbackStdoutInherit, t.m_fallbackStderrInherit, t.m_fallbackDetached)) {
// Replay over the pipe. Same cancellation check as wsOnOpen
// — skip ids close() already removed. Append the NUL
// terminator the pipe protocol needs.
Expand All @@ -451,7 +451,7 @@ static void wsOnClose(void* ctx, unsigned short code)
}

bool Transport::ensureConnected(Zig::GlobalObject* zig, const WTF::String& wsUrl, bool autoDetected,
const WTF::String& userDataDir, bool stdoutInherit, bool stderrInherit)
const WTF::String& userDataDir, bool stdoutInherit, bool stderrInherit, bool detached)
{
// Already connected — singleton semantics, first call wins.
if (m_mode != TransportMode::None && !m_dead) return true;
Expand All @@ -470,6 +470,7 @@ bool Transport::ensureConnected(Zig::GlobalObject* zig, const WTF::String& wsUrl
m_fallbackUserDataDir = userDataDir;
m_fallbackStdoutInherit = stdoutInherit;
m_fallbackStderrInherit = stderrInherit;
m_fallbackDetached = detached;
}

auto* ctx = zig->scriptExecutionContext();
Expand Down
10 changes: 6 additions & 4 deletions src/runtime/webview/ChromeBackend.h
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ class Transport {
// one Chrome, so mismatched args across views get the first-call's.
bool ensureSpawned(Zig::GlobalObject*, const WTF::String& userDataDir = {},
const WTF::String& path = {}, const WTF::Vector<WTF::String>& extraArgv = {},
bool stdoutInherit = false, bool stderrInherit = false);
bool stdoutInherit = false, bool stderrInherit = false, bool detached = false);

// Connect to an already-running Chrome's DevTools endpoint. wsUrl is
// a full ws:// URL (from DevToolsActivePort or user-supplied). Same
Expand All @@ -399,7 +399,8 @@ class Transport {
// confusing WebSocket error. autoDetected=false means explicit
// backend.url; connect failure surfaces directly.
bool ensureConnected(Zig::GlobalObject*, const WTF::String& wsUrl, bool autoDetected,
const WTF::String& userDataDir = {}, bool stdoutInherit = false, bool stderrInherit = false);
const WTF::String& userDataDir = {}, bool stdoutInherit = false, bool stderrInherit = false,
bool detached = false);

// Next CDP id — caller uses it with Command(id, ...) then calls send().
uint32_t nextId() { return m_nextId++; }
Expand Down Expand Up @@ -445,11 +446,12 @@ class Transport {
bool m_wasAutoDetected = false;
// Stashed for the wsOnClose fallback spawn. Auto-detect only runs
// when path/argv are empty (createChrome branches to spawn-mode
// otherwise), so userDataDir + stdio are the only carry-over. Set in
// ensureConnected when autoDetected=true.
// otherwise), so userDataDir + stdio + detached are the only
// carry-over. Set in ensureConnected when autoDetected=true.
WTF::String m_fallbackUserDataDir;
bool m_fallbackStdoutInherit = false;
bool m_fallbackStderrInherit = false;
bool m_fallbackDetached = false;
Comment thread
robobun marked this conversation as resolved.
bool m_dead = false;

uint32_t m_nextId = 1;
Expand Down
9 changes: 9 additions & 0 deletions src/runtime/webview/ChromeProcess.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ pub(crate) unsafe extern "C" fn Bun__Chrome__ensure(
extra_argv_len: u32,
stdout_inherit: bool,
stderr_inherit: bool,
detached: bool,
) -> i32 {
#[cfg(windows)]
{
Expand All @@ -117,6 +118,7 @@ pub(crate) unsafe extern "C" fn Bun__Chrome__ensure(
extra_argv_len,
stdout_inherit,
stderr_inherit,
detached,
);
return -1;
}
Expand Down Expand Up @@ -155,6 +157,7 @@ pub(crate) unsafe extern "C" fn Bun__Chrome__ensure(
extra,
stdout_inherit,
stderr_inherit,
detached,
) {
Ok(fd) => fd,
Err(err) => {
Expand Down Expand Up @@ -401,6 +404,7 @@ fn spawn(
extra_argv: &[*const c_char],
stdout_inherit: bool,
stderr_inherit: bool,
detached: bool,
) -> Result<Fd, bun_core::Error> {
{
let chrome = find_chrome(explicit_path).ok_or_else(|| bun_core::err!("ChromeNotFound"))?;
Expand Down Expand Up @@ -524,6 +528,11 @@ fn spawn(
// the same socket at both positions.
extra_fds: vec![Stdio::Pipe(fds[1]), Stdio::Pipe(fds[1])].into_boxed_slice(),
argv0: Some(chrome.as_ptr()),
// setsid() in the child — new session, no controlling TTY.
// Endpoint-protection hooks that intercept exec and write a
// rejection banner to /dev/tty (bypassing stdio redirection)
// can't reach the parent's terminal when Chrome has none.
detached,
..SpawnOptions::default()
};

Expand Down
17 changes: 14 additions & 3 deletions src/runtime/webview/HostProcess.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,11 @@ pub(crate) extern "C" fn Bun__WebViewHost__ensure(
global: &JSGlobalObject,
stdout_inherit: bool,
stderr_inherit: bool,
detached: bool,
) -> i32 {
#[cfg(not(target_os = "macos"))]
{
let _ = (global, stdout_inherit, stderr_inherit);
let _ = (global, stdout_inherit, stderr_inherit, detached);
return -1;
}
#[cfg(target_os = "macos")]
Expand All @@ -98,6 +99,7 @@ pub(crate) extern "C" fn Bun__WebViewHost__ensure(
std::ptr::from_ref(global.bun_vm()).cast_mut(),
stdout_inherit,
stderr_inherit,
detached,
) {
Ok(fd) => fd,
Err(err) => {
Expand Down Expand Up @@ -129,10 +131,15 @@ bun_spawn::link_impl_ProcessExit! {
}

#[cfg(target_os = "macos")]
fn spawn(vm: *mut VirtualMachine, stdout_inherit: bool, stderr_inherit: bool) -> Result<Fd, Error> {
fn spawn(
vm: *mut VirtualMachine,
stdout_inherit: bool,
stderr_inherit: bool,
detached: bool,
) -> Result<Fd, Error> {
#[cfg(not(target_os = "macos"))]
{
let _ = (vm, stdout_inherit, stderr_inherit);
let _ = (vm, stdout_inherit, stderr_inherit, detached);
return Err(bun_core::err!("Unsupported"));
}
#[cfg(target_os = "macos")]
Expand Down Expand Up @@ -185,6 +192,10 @@ fn spawn(vm: *mut VirtualMachine, stdout_inherit: bool, stderr_inherit: bool) ->
},
extra_fds: vec![Stdio::Pipe(fds[1])].into_boxed_slice(),
argv0: Some(exe.as_ptr()),
// setsid() in the child — new session, no controlling TTY. Same
// rationale as ChromeProcess.rs: keeps endpoint-protection
// /dev/tty writes off the parent's terminal.
detached,
..SpawnOptions::default()
};

Expand Down
12 changes: 6 additions & 6 deletions src/runtime/webview/JSWebView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -294,11 +294,11 @@ void JSWebView::doClose()
#if OS(DARWIN)
JSWebView* JSWebView::createAndSend(JSGlobalObject* g, Structure* structure,
uint32_t width, uint32_t height, const WTF::String& persistDir,
bool stdoutInherit, bool stderrInherit)
bool stdoutInherit, bool stderrInherit, bool detached)
{
auto* zig = defaultGlobalObject(g);
auto& c = WK::client();
if (!c.ensureSpawned(zig, stdoutInherit, stderrInherit)) return nullptr;
if (!c.ensureSpawned(zig, stdoutInherit, stderrInherit, detached)) return nullptr;

auto impl = WebViewEventTarget::create(*zig->scriptExecutionContext());
JSWebView* view = create(structure, zig, WTF::move(impl));
Expand Down Expand Up @@ -326,7 +326,7 @@ extern "C" size_t Bun__Chrome__autoDetect(char* out, size_t cap);
JSWebView* JSWebView::createChrome(JSGlobalObject* g, Structure* structure,
uint32_t width, uint32_t height, const WTF::String& userDataDir,
const WTF::String& path, const WTF::Vector<WTF::String>& extraArgv,
bool stdoutInherit, bool stderrInherit, const WTF::String& wsUrl, bool skipAutoDetect)
bool stdoutInherit, bool stderrInherit, bool detached, const WTF::String& wsUrl, bool skipAutoDetect)
{
auto* zig = defaultGlobalObject(g);
auto& t = CDP::transport();
Expand All @@ -344,7 +344,7 @@ JSWebView* JSWebView::createChrome(JSGlobalObject* g, Structure* structure,
if (!wsUrl.isEmpty()) {
ok = t.ensureConnected(zig, wsUrl, /* autoDetected */ false);
} else if (skipAutoDetect || !path.isEmpty() || !extraArgv.isEmpty()) {
ok = t.ensureSpawned(zig, userDataDir, path, extraArgv, stdoutInherit, stderrInherit);
ok = t.ensureSpawned(zig, userDataDir, path, extraArgv, stdoutInherit, stderrInherit, detached);
} else {
// Auto-detect. DevToolsActivePort URL caps at
// ws://127.0.0.1:65535/devtools/browser/<36-char-uuid> ≈ 70B.
Expand All @@ -353,9 +353,9 @@ JSWebView* JSWebView::createChrome(JSGlobalObject* g, Structure* structure,
if (len > 0) {
ok = t.ensureConnected(zig,
WTF::String::fromUTF8(std::span<const char>(buf, len)),
/* autoDetected */ true, userDataDir, stdoutInherit, stderrInherit);
/* autoDetected */ true, userDataDir, stdoutInherit, stderrInherit, detached);
} else {
ok = t.ensureSpawned(zig, userDataDir, path, extraArgv, stdoutInherit, stderrInherit);
ok = t.ensureSpawned(zig, userDataDir, path, extraArgv, stdoutInherit, stderrInherit, detached);
}
}
if (!ok) return nullptr;
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/webview/JSWebView.h
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ class JSWebView final : public WebCore::JSEventTarget {
// host spawn failed (caller throws).
static JSWebView* createAndSend(JSC::JSGlobalObject*, JSC::Structure*,
uint32_t width, uint32_t height, const WTF::String& persistDir,
bool stdoutInherit, bool stderrInherit);
bool stdoutInherit, bool stderrInherit, bool detached);
#endif

// Chrome constructor. Lazy-spawns Chrome; stores width/height for the
Expand All @@ -195,8 +195,8 @@ class JSWebView final : public WebCore::JSEventTarget {
static JSWebView* createChrome(JSC::JSGlobalObject*, JSC::Structure*,
uint32_t width, uint32_t height, const WTF::String& userDataDir,
const WTF::String& path, const WTF::Vector<WTF::String>& extraArgv,
bool stdoutInherit, bool stderrInherit, const WTF::String& wsUrl = {},
bool skipAutoDetect = false);
bool stdoutInherit, bool stderrInherit, bool detached,
const WTF::String& wsUrl = {}, bool skipAutoDetect = false);

void finishCreation(JSC::VM&);
static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype);
Expand Down
21 changes: 18 additions & 3 deletions src/runtime/webview/JSWebViewConstructor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ JSC_DEFINE_HOST_FUNCTION(constructWebView, (JSGlobalObject * globalObject, CallF
WTF::Vector<WTF::String> chromeArgv;
bool stdoutInherit = false;
bool stderrInherit = false;
bool detached = false;
bool consoleIsGlobal = false;
JSObject* consoleCallback = nullptr;

Expand Down Expand Up @@ -266,6 +267,20 @@ JSC_DEFINE_HOST_FUNCTION(constructWebView, (JSGlobalObject * globalObject, CallF
};
if (!parseStdio("stdout"_s, stdoutInherit)) return {};
if (!parseStdio("stderr"_s, stderrInherit)) return {};

// detached: setsid() in the child — new session, no controlling
// TTY. Endpoint-protection hooks that intercept exec and write a
// rejection banner to /dev/tty (bypassing stdio redirection)
// can't reach the parent's terminal. Applies only to the first
// view (the one that actually spawns the subprocess).
JSValue detachedOpt = beObj->get(globalObject, Identifier::fromString(vm, "detached"_s));
RETURN_IF_EXCEPTION(scope, {});
if (detachedOpt.isBoolean()) {
detached = detachedOpt.asBoolean();
} else if (!detachedOpt.isUndefined()) {
return Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE,
"backend.detached must be a boolean"_s);
}
}

// Initial URL — the navigate() is fired off immediately after
Expand Down Expand Up @@ -343,8 +358,8 @@ JSC_DEFINE_HOST_FUNCTION(constructWebView, (JSGlobalObject * globalObject, CallF
if (backend == WebViewBackend::Chrome) {
Bun__Feature__webview_chrome += 1;
JSWebView* view = JSWebView::createChrome(globalObject, structure, width, height,
persistDir, chromePath, chromeArgv, stdoutInherit, stderrInherit, chromeWsUrl,
chromeSkipAutoDetect);
persistDir, chromePath, chromeArgv, stdoutInherit, stderrInherit, detached,
chromeWsUrl, chromeSkipAutoDetect);
if (!view) {
return Bun::throwError(globalObject, scope, ErrorCode::ERR_DLOPEN_FAILED,
chromeWsUrl.isEmpty()
Expand All @@ -363,7 +378,7 @@ JSC_DEFINE_HOST_FUNCTION(constructWebView, (JSGlobalObject * globalObject, CallF
#else
Bun__Feature__webview_webkit += 1;
JSWebView* view = JSWebView::createAndSend(globalObject, structure, width, height, persistDir,
stdoutInherit, stderrInherit);
stdoutInherit, stderrInherit, detached);
if (!view) {
return Bun::throwError(globalObject, scope, ErrorCode::ERR_DLOPEN_FAILED,
"Failed to spawn WebView host process"_s);
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/webview/WebKitBackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ using namespace JSC;
using namespace WebViewProto;

// Spawn + process-exit watch implemented in HostProcess.rs (EVFILT_PROC).
extern "C" int32_t Bun__WebViewHost__ensure(Zig::GlobalObject*, bool stdoutInherit, bool stderrInherit);
extern "C" int32_t Bun__WebViewHost__ensure(Zig::GlobalObject*, bool stdoutInherit, bool stderrInherit, bool detached);
extern "C" void* Blob__fromMmapWithType(JSC::JSGlobalObject*, uint8_t* ptr, size_t len, const char* mime);
extern "C" JSC::EncodedJSValue SYSV_ABI Blob__create(Zig::GlobalObject*, void* impl);
extern "C" JSC::EncodedJSValue JSBuffer__fromMmap(Zig::GlobalObject*, void* ptr, size_t length);
Expand Down Expand Up @@ -127,7 +127,7 @@ void HostClient::updateKeepAlive()
WebCore::clientData(global->vm())->bunVM, want ? 1 : -1);
}

bool HostClient::ensureSpawned(Zig::GlobalObject* zig, bool stdoutInherit, bool stderrInherit)
bool HostClient::ensureSpawned(Zig::GlobalObject* zig, bool stdoutInherit, bool stderrInherit, bool detached)
{
if (sock && !dead) return true;

Expand All @@ -142,7 +142,7 @@ bool HostClient::ensureSpawned(Zig::GlobalObject* zig, bool stdoutInherit, bool
txQueue.clear();
}

int fd = Bun__WebViewHost__ensure(zig, stdoutInherit, stderrInherit);
int fd = Bun__WebViewHost__ensure(zig, stdoutInherit, stderrInherit, detached);
if (fd < 0) {
dead = true;
return false;
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/webview/WebKitBackend.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ struct HostClient {
WTF::Vector<uint8_t> txQueue;
bool sockRefd = false;

bool ensureSpawned(Zig::GlobalObject*, bool stdoutInherit, bool stderrInherit);
bool ensureSpawned(Zig::GlobalObject*, bool stdoutInherit, bool stderrInherit, bool detached);
void writeFrame(WebViewProto::Op, uint32_t viewId, const uint8_t* payload, uint32_t len);
void handleReply(const WebViewProto::Frame&, WebViewProto::Reader);
void rejectAllAndMarkDead(const WTF::String& reason);
Expand Down
Loading
Loading