Skip to content
22 changes: 19 additions & 3 deletions src/dotenv/env_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,14 +542,30 @@ impl Loader {
}
}

/// A `Worker`'s loader: the map, with the explicit entries marked loaded so a pipe is not read twice.
/// A `Worker`'s copy: the map plus what was already loaded, so the worker thread
/// does not read `environ`, `.env` files or an `--env-file` pipe a second time.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub fn clone_for_worker(&self) -> Result<Loader, AllocError> {
Ok(Loader {
map: self.map.clone_with_allocator()?,
default_files_loaded: EnumSet::empty(),
default_files_loaded: self.default_files_loaded,
custom_files_loaded: self.custom_files_loaded.clone()?,
quiet: false,
did_load_process: false,
did_load_process: self.did_load_process,
reject_unauthorized: Cell::new(None),
aws_credentials: None,
})
}

/// An empty `Worker` loader for the caller to fill; no env source is read over it later.
pub fn for_worker_env(&self, capacity: usize) -> Result<Loader, AllocError> {
let mut map = Map::init();
map.ensure_unused_capacity(capacity)?;
Ok(Loader {
map,
default_files_loaded: EnumSet::all(),
custom_files_loaded: self.custom_files_loaded.clone()?,
quiet: false,
did_load_process: true,
reject_unauthorized: Cell::new(None),
aws_credentials: None,
})
Expand Down
13 changes: 9 additions & 4 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4241,16 +4241,21 @@ impl VirtualMachine {
// allocated it first, `__bun_macro_context_deinit` must not free it.
SOURCE_CODE_PRINTER_FROM_MACRO.set(false);

if map.get(b"BUN_SHOW_BUN_STACKFRAMES").is_some() {
self.hide_bun_stackframes = false;
}

if bun_core::env_var::feature_flag::BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER::get()
.unwrap_or(false)
{
self.transpiler_store.enabled = false;
}

// Process settings from the launch environment. A worker's map is its own env: it inherits these.
if !self.is_main_thread() {
return;
}

if map.get(b"BUN_SHOW_BUN_STACKFRAMES").is_some() {
self.hide_bun_stackframes = false;
}

if let Some(idx) = map.map.get_index(b"NODE_CHANNEL_FD") {
let (_, kv) = map.map.swap_remove_at(idx);
let fd_s = kv.value;
Expand Down
10 changes: 8 additions & 2 deletions src/jsc/bindings/JSEnvironmentVariableMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,10 @@ JSC_DEFINE_HOST_FUNCTION(jsEditWindowsEnvVar, (JSGlobalObject * global, JSC::Cal
auto scope = DECLARE_THROW_SCOPE(global->vm());
ASSERT(callFrame->argumentCount() == 2);
ASSERT(callFrame->uncheckedArgument(0).isString());
// Only the main thread writes through to the OS environment (Node: a worker env is a MapKVStore).
auto* zigGlobal = defaultGlobalObject(global);
auto* context = zigGlobal ? zigGlobal->scriptExecutionContext() : nullptr;
bool writesOSEnv = !context || context->isMainThread();
WTF::String string1 = callFrame->uncheckedArgument(0).toWTFString(global);
RETURN_IF_EXCEPTION(scope, {});
JSValue arg2 = callFrame->uncheckedArgument(1);
Expand All @@ -532,14 +536,16 @@ JSC_DEFINE_HOST_FUNCTION(jsEditWindowsEnvVar, (JSGlobalObject * global, JSC::Cal
RETURN_IF_EXCEPTION(scope, {});
BunString k = Bun::toString(string1);
BunString v = Bun::toString(string2);
Bun__Process__editWindowsEnvVar(&k, &v);
if (writesOSEnv)
Bun__Process__editWindowsEnvVar(&k, &v);
// fetch() reads the proxy variables from the native env map.
if (isProxyEnvVarName(global->vm(), string1))
Bun__setEnvValue(global, &k, &v);
} else {
BunString k = Bun::toString(string1);
BunString v = { .tag = BunStringTag::Dead };
Bun__Process__editWindowsEnvVar(&k, &v);
if (writesOSEnv)
Bun__Process__editWindowsEnvVar(&k, &v);
if (isProxyEnvVarName(global->vm(), string1))
Bun__setEnvValue(global, &k, &v);
}
Expand Down
11 changes: 11 additions & 0 deletions src/jsc/bindings/SharedEnvStore.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ class SharedEnvStore : public ThreadSafeRefCounted<SharedEnvStore> {
return out;
}

// Every (name, value) as it is now: seeds a joining worker's native env map.
Vector<std::pair<String, String>> entries()
{
Locker locker { m_lock };
Vector<std::pair<String, String>> out;
out.reserveInitialCapacity(m_map.size());
for (const auto& entry : m_map.values())
out.append({ entry.name.isolatedCopy(), entry.value.isolatedCopy() });
return out;
}

// Windows env keys are case-insensitive. This follows bun's own Windows env
// object, not node: node only folds case for a main-rooted tree (RealEnvStore),
// and is case-sensitive for one rooted at a snapshot worker (MapKVStore).
Expand Down
43 changes: 5 additions & 38 deletions src/jsc/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -594,44 +594,11 @@ extern "C" JSC::JSGlobalObject* Zig__GlobalObject__create(void* console_client,
const auto initializeWorker = [&](WebCore::WorkerMessagingProxy& worker) -> void {
auto& options = worker.options();

if (options.env.has_value()) {
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
HashMap<String, String> map = *std::exchange(options.env, std::nullopt);
auto size = map.size();

// In theory, a GC could happen before we finish putting all the properties on the object.
// So we use a MarkedArgumentBuffer to ensure that the strings are not collected and we immediately put them on the object.
MarkedArgumentBuffer strings;
strings.ensureCapacity(size);
for (const auto& value : map.values()) {
strings.append(jsString(vm, value));
}

#if OS(WINDOWS)
JSC::JSObject* env = size
? JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), size >= JSFinalObject::maxInlineCapacity ? JSFinalObject::maxInlineCapacity : size)
: JSC::constructEmptyObject(globalObject);
#else
// Same exotic object as the main thread so writes inside the
// worker coerce to string, reject symbol keys, and validate
// defineProperty like Node's EnvSetter/EnvDefiner.
auto* envStructure = Bun::JSEnvironmentVariableMap::createStructure(vm, globalObject, globalObject->objectPrototype());
JSC::JSObject* env = Bun::JSEnvironmentVariableMap::create(vm, envStructure);
#endif
size_t i = 0;
for (auto k : map) {
// Numeric env keys hit putDirectIndex → defineOwnProperty (declares a
// ThrowScope). Seeded values are JSStrings, so this throws only on OOM
// or under a termination already requested for this starting worker.
env->putDirectMayBeIndex(globalObject, JSC::Identifier::fromString(vm, WTF::move(k.key)), strings.at(i++));
if (scope.exception()) [[unlikely]]
break;
}
globalObject->m_processEnvObject.set(vm, globalObject, env);
} else if (options.sharedEnvStore) {
// worker_threads SHARE_ENV: join the env tree the spawning thread
// resolved. Consumed like options.env, and published on the context
// before the view, which resolves its store through the context.
// options.env already seeded this VM's env loader (WebWorker__create); process.env
// is built lazily from it by createEnvironmentVariablesMap, as on the main thread.
Comment thread
robobun marked this conversation as resolved.
Resolved
if (options.sharedEnvStore) {
Comment thread
robobun marked this conversation as resolved.
Resolved
// worker_threads SHARE_ENV: join the env tree the spawning thread resolved.
// Published on the context before the view, which resolves its store through it.
Comment thread
robobun marked this conversation as resolved.
Resolved
RefPtr<Bun::SharedEnvStore> store = std::exchange(options.sharedEnvStore, nullptr);
globalObject->scriptExecutionContext()->setSharedEnvStore(*store);
globalObject->m_processEnvObject.set(vm, globalObject, Bun::createSharedEnvironmentVariablesMap(globalObject).getObject());
Expand Down
7 changes: 4 additions & 3 deletions src/jsc/bindings/webcore/JSWorker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -291,14 +291,15 @@ template<> __attribute__((minsize)) JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES
envObject->methodTable()->getOwnPropertyNames(envObject, lexicalGlobalObject, keys, JSC::DontEnumPropertiesMode::Exclude);
RETURN_IF_EXCEPTION(throwScope, {});

HashMap<String, String> env;
Vector<std::pair<String, String>> env;
env.reserveInitialCapacity(keys.size());

for (const auto& key : keys) {
JSValue value = envObject->get(lexicalGlobalObject, key);
RETURN_IF_EXCEPTION(throwScope, {});
String str = value.toWTFString(lexicalGlobalObject).isolatedCopy();
String str = value.toWTFString(lexicalGlobalObject);
RETURN_IF_EXCEPTION(throwScope, {});
env.add(key.impl()->isolatedCopy(), str);
env.append({ key.string(), WTF::move(str) });
}

options.env.emplace(WTF::move(env));
Expand Down
26 changes: 24 additions & 2 deletions src/jsc/bindings/webcore/WorkerMessagingProxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ void* WebWorker__create(
StringImpl** execArgvPtr,
size_t execArgvLen,
BunString* preloadModulesPtr,
size_t preloadModulesLen);
size_t preloadModulesLen,
bool hasEnv,
const BunString* envPairsPtr,
size_t envPairsLen);
// Raise a TerminationException in the worker VM at its next safepoint and wake its loop. Any thread.
void WebWorker__requestTermination(void*);
// Toggle the keep-alive this worker holds on the parent event loop. Parent thread.
Expand Down Expand Up @@ -141,6 +144,21 @@ ExceptionOr<void> WorkerMessagingProxy::startWorkerGlobalScope(const String& scr
})
.value_or(std::span<WTF::StringImpl*> {});

// A SHARE_ENV worker's native env map starts from the store as it is now.
if (m_options.sharedEnvStore)
m_options.env = m_options.sharedEnvStore->entries();

// [key, value, ...]; WebWorker__create copies the bytes, so these only live across the call.
Vector<BunString> envPairs;
bool hasEnv = m_options.env.has_value();
if (hasEnv) {
envPairs.reserveInitialCapacity(m_options.env->size() * 2);
for (auto& [key, value] : *m_options.env) {
envPairs.append(Bun::toString(key));
envPairs.append(Bun::toString(value));
}
}

// The thread holds a ref on the proxy until releaseWorkerThread().
ref();
BunString errorMessage = BunStringEmpty;
Expand All @@ -166,8 +184,12 @@ ExceptionOr<void> WorkerMessagingProxy::startWorkerGlobalScope(const String& scr
execArgv.data(),
execArgv.size(),
preloadModules.begin(),
preloadModules.size());
preloadModules.size(),
hasEnv,
envPairs.begin(),
envPairs.size() / 2);
m_options.preloadModules.clear();
m_options.env = std::nullopt;

if (!m_workerThread) {
m_state.store(State::Closed);
Expand Down
6 changes: 4 additions & 2 deletions src/jsc/bindings/webcore/WorkerOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct WorkerOptions {
bool mini { false };
bool unref { false };
// worker_threads `env: SHARE_ENV`: the environment tree resolved on the parent
// thread, which this worker joins instead of receiving an env snapshot.
// thread. The worker's process.env joins it instead of receiving an env snapshot.
RefPtr<Bun::SharedEnvStore> sharedEnvStore;
// Most of our code doesn't care whether `eval` was passed, because worker_threads.ts
// automatically passes a Blob URL instead of a file path if `eval` is true. But, if `eval` is
Expand All @@ -34,7 +34,9 @@ struct WorkerOptions {
// Objects transferred for either data or environmentData in the transferList
Vector<TransferredMessagePort> dataMessagePorts;
Vector<String> preloadModules;
std::optional<HashMap<String, String>> env;
// The worker's whole initial environment in enumeration order, consumed on the parent
// thread by WebWorker__create. If nullopt, the worker clones the parent's env loader.
Comment thread
robobun marked this conversation as resolved.
Resolved
std::optional<Vector<std::pair<String, String>>> env;
Vector<String> argv;
// If nullopt, inherit execArgv from the parent thread
std::optional<Vector<String>> execArgv;
Expand Down
67 changes: 59 additions & 8 deletions src/jsc/web_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,30 @@ struct WorkerVmInit {
transform_options: bun_options_types::schema::api::TransformOptions,
env_loader: bun_dotenv::Loader,
proxy_env_slots: jsc::rare_data::ProxyEnvSlots,
/// The parent's process settings, which a VM reads from the launch environment at start.
hide_bun_stackframes: bool,
aggressive_garbage_collection: virtual_machine::GCLevel,
}

/// The loader for a worker given its environment as `[key, value, ...]` pairs,
/// in `process.env` enumeration order.
Comment thread
robobun marked this conversation as resolved.
Resolved
fn worker_env_loader(
parent: &bun_dotenv::Loader,
pairs: &[BunString],
) -> Result<bun_dotenv::Loader, bun_alloc::AllocError> {
let mut loader = parent.for_worker_env(pairs.len() / 2)?;
Comment thread
robobun marked this conversation as resolved.
Resolved
for [key, value] in pairs.as_chunks::<2>().0 {
let (key, value) = (key.to_utf8(), value.to_utf8());
// Not a valid environment entry. `Bun.spawn` rejects a NUL byte in an explicit `env` too.
if key.is_empty()
|| bun_core::strings::contains_char(&key, 0)
|| bun_core::strings::contains_char(&value, 0)
{
continue;
}
loader.map.put(&key, &value)?;
}
Ok(loader)
}

enum EntryOutcome {
Expand Down Expand Up @@ -278,6 +302,9 @@ impl WebWorker {
/// keep-alive on the parent event loop, register as a child of the parent VM,
/// and spawn the thread. On any failure returns null with `error_message`
/// set and nothing to clean up.
///
/// With `has_env`, the `env_pairs_len` interleaved key/value pairs at `env_ptr`
/// are the worker's whole environment; otherwise the parent's loader is cloned.
Comment thread
robobun marked this conversation as resolved.
Resolved
#[unsafe(export_name = "WebWorker__create")]
pub(crate) unsafe extern "C" fn create(
proxy: *mut c_void,
Expand All @@ -299,6 +326,9 @@ impl WebWorker {
exec_argv_len: usize,
preload_modules_ptr: *const BunString,
preload_modules_len: usize,
has_env: bool,
env_ptr: *const BunString,
env_pairs_len: usize,
) -> *mut WebWorker {
jsc::mark_binding();
log!("[{}] create", this_context_id);
Expand Down Expand Up @@ -379,27 +409,44 @@ impl WebWorker {
transform_options.allow_ffi_cc = Some(parent_allows_ffi_cc && flags.allow_ffi_cc);
}
}
// The worker's `process.env` starts as a copy of the parent's now (as in
// Node). Proxy-env values may be RefCountedEnvValue bytes owned by the
// parent's proxy_env_storage: snapshot slots + map under its lock so
// every slice copied is backed by a ref the snapshot holds.
// Every thread shares the resolver's directory cache, so one symlink setting serves the process.
transform_options.preserve_symlinks =
Some(parent_ref.transpiler.resolver.opts.preserve_symlinks);
// The worker's env loader (what its `process.env`, fetch() and Bun.spawn
// read): exactly the given environment, or a copy of the parent's now (as
// in Node), taken under the parent's proxy-env lock so every
// RefCountedEnvValue slice in the copy is backed by a ref the snapshot holds.
Comment thread
robobun marked this conversation as resolved.
Resolved
let mut proxy_env_slots = jsc::rare_data::ProxyEnvSlots::default();
let mut env_loader = {
let env_loader = if has_env {
// SAFETY: caller passed `2 * env_pairs_len` valid strings (or `(null,0)`);
// slice borrowed from C++ for the duration of this call.
let pairs: &[BunString] = unsafe { bun_core::ffi::slice(env_ptr, env_pairs_len * 2) };
match worker_env_loader(parent_ref.env_loader(), pairs) {
Ok(loader) => loader,
Err(_) => {
*error_message = BunString::static_("Out of memory");
return core::ptr::null_mut();
}
}
} else {
let parent_slots = parent_ref.proxy_env_storage.lock();
proxy_env_slots.clone_from(&parent_slots);
match parent_ref.env_loader().clone_for_worker() {
let mut loader = match parent_ref.env_loader().clone_for_worker() {
Ok(loader) => loader,
Err(_) => {
*error_message = BunString::static_("Out of memory");
return core::ptr::null_mut();
}
}
};
proxy_env_slots.sync_into(&mut loader.map);
loader
};
proxy_env_slots.sync_into(&mut env_loader.map);
let init = WorkerVmInit {
transform_options,
env_loader,
proxy_env_slots,
hide_bun_stackframes: parent_ref.hide_bun_stackframes,
aggressive_garbage_collection: parent_ref.aggressive_garbage_collection,
};

// The construction ref: handed to C++ on success, dropped on failure.
Expand Down Expand Up @@ -675,6 +722,8 @@ impl WebWorker {
transform_options,
env_loader,
proxy_env_slots,
hide_bun_stackframes,
aggressive_garbage_collection,
} = init;

// worker-thread only field; no other thread reads `arena`.
Expand Down Expand Up @@ -718,6 +767,8 @@ impl WebWorker {
.with_mut(|a| NonNull::new(std::ptr::from_mut(a.as_mut().unwrap())));

*vm_ref.proxy_env_storage.lock() = proxy_env_slots;
vm_ref.hide_bun_stackframes = hide_bun_stackframes;
vm_ref.aggressive_garbage_collection = aggressive_garbage_collection;

vm_ref.is_main_thread = false;
VirtualMachine::set_is_main_thread_vm(false);
Expand Down
Loading
Loading