use std::sync::{Arc, LazyLock, OnceLock}; pub use jobserver_crate::{Acquired, Client, HelperThread}; use jobserver_crate::{FromEnv, FromEnvErrorKind}; use parking_lot::{Condvar, Mutex}; // We can only call `from_env_ext` once per process // We stick this in a global because there could be multiple rustc instances // in this process, and the jobserver is per-process. static GLOBAL_CLIENT: LazyLock> = LazyLock::new(|| { // Note that this is unsafe because it may misinterpret file descriptors // on Unix as jobserver file descriptors. We hopefully execute this near // the beginning of the process though to ensure we don't get false // positives, or in other words we try to execute this before we open // any file descriptors ourselves. let FromEnv { client, var } = unsafe { Client::from_env_ext(true) }; let error = match client { Ok(client) => return Ok(client), Err(e) => e, }; if matches!( error.kind(), FromEnvErrorKind::NoEnvVar | FromEnvErrorKind::NoJobserver | FromEnvErrorKind::NegativeFd | FromEnvErrorKind::Unsupported ) { return Ok(default_client()); } // Environment specifies jobserver, but it looks incorrect. // Safety: `error.kind()` should be `NoEnvVar` if `var == None`. let (name, value) = var.unwrap(); Err(format!( "failed to connect to jobserver from environment variable `{name}={:?}`: {error}", value )) }); // Create a new jobserver if there's no inherited one. fn default_client() -> Client { // Pick a "reasonable maximum" capping out at 32 // so we don't take everything down by hogging the process run queue. // The fixed number is used to have deterministic compilation across machines. let client = Client::new(32).expect("failed to create jobserver"); // Acquire a token for the main thread which we can release later client.acquire_raw().ok(); client } static GLOBAL_CLIENT_CHECKED: OnceLock = OnceLock::new(); pub fn initialize_checked(report_warning: impl FnOnce(&'static str)) { let client_checked = match &*GLOBAL_CLIENT { Ok(client) => client.clone(), Err(e) => { report_warning(e); default_client() } }; GLOBAL_CLIENT_CHECKED.set(client_checked).ok(); } const ACCESS_ERROR: &str = "jobserver check should have been called earlier"; pub fn client() -> Client { GLOBAL_CLIENT_CHECKED.get().expect(ACCESS_ERROR).clone() } struct ProxyData { /// The number of tokens assigned to threads. /// If this is 0, a single token is still assigned to this process, but is unused. used: u16, /// The number of threads requesting a token pending: u16, } /// This is a jobserver proxy used to ensure that we hold on to at least one token. pub struct Proxy { client: Client, data: Mutex, /// Threads which are waiting on a token will wait on this. wake_pending: Condvar, helper: OnceLock, } impl Proxy { pub fn new() -> Arc { let proxy = Arc::new(Proxy { client: client(), data: Mutex::new(ProxyData { used: 1, pending: 0 }), wake_pending: Condvar::new(), helper: OnceLock::new(), }); let proxy_ = Arc::clone(&proxy); let helper = proxy .client .clone() .into_helper_thread(move |token| { if let Ok(token) = token { let mut data = proxy_.data.lock(); if data.pending > 0 { // Give the token to a waiting thread token.drop_without_releasing(); assert!(data.used > 0); data.used += 1; data.pending -= 1; proxy_.wake_pending.notify_one(); } else { // The token is no longer needed, drop it. drop(data); drop(token); } } }) .expect("failed to create helper thread"); proxy.helper.set(helper).unwrap(); proxy } pub fn acquire_thread(&self) { let mut data = self.data.lock(); if data.used == 0 { // There was a free token around. This can // happen when all threads release their token. assert_eq!(data.pending, 0); data.used += 1; } else { // Request a token from the helper thread. We can't directly use `acquire_raw` // as we also need to be able to wait for the final token in the process which // does not get a corresponding `release_raw` call. self.helper.get().unwrap().request_token(); data.pending += 1; self.wake_pending.wait(&mut data); } } pub fn release_thread(&self) { let mut data = self.data.lock(); if data.pending > 0 { // Give the token to a waiting thread data.pending -= 1; self.wake_pending.notify_one(); } else { data.used -= 1; // Release the token unless it's the last one in the process if data.used > 0 { drop(data); self.client.release_raw().ok(); } } } }