use crate::core::lock::FileLock;
use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
static NEXT_SLOT: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug)]
pub enum JobserverPool {
Inherited {
auth: String,
#[cfg(unix)]
read_fd: Option<std::os::raw::c_int>,
#[cfg(unix)]
write_fd: Option<std::os::raw::c_int>,
#[cfg(unix)]
owned: bool,
},
Managed {
_lock: FileLock,
fifo_path: PathBuf,
slots_dir: PathBuf,
capacity: usize,
#[cfg(unix)]
open_fd: std::os::raw::c_int,
auth: String,
},
None,
}
pub struct JobToken<'a> {
pool: &'a JobserverPool,
byte: u8,
slot_lock: Option<FileLock>,
}
impl<'a> Drop for JobToken<'a> {
fn drop(&mut self) {
if self.slot_lock.take().is_none() {
self.pool.release_token(self.byte);
}
}
}
impl JobserverPool {
pub fn acquire_or_create(cache_root: &Path, token_count: usize) -> Result<Self> {
if let Ok(flags) = std::env::var("CARGO_MAKEFLAGS").or_else(|_| std::env::var("MAKEFLAGS"))
{
for token in flags.split_whitespace() {
if let Some(auth) = token.strip_prefix("--jobserver-auth=") {
let pool = Self::from_auth(auth);
#[cfg(unix)]
if let Self::Inherited {
read_fd: Some(_), ..
} = pool
{
return Ok(pool);
}
#[cfg(not(unix))]
return Ok(pool);
}
}
}
Self::create_managed(cache_root, token_count)
}
pub fn from_auth(auth: &str) -> Self {
#[cfg(unix)]
{
let (read_fd, write_fd, owned) = Self::try_open_inherited(auth);
if read_fd.is_none() {
eprintln!("[mitts] warning: inherited jobserver auth '{auth}' could not be opened; running unthrottled");
}
Self::Inherited {
auth: auth.to_string(),
read_fd,
write_fd,
owned,
}
}
#[cfg(not(unix))]
Self::Inherited {
auth: auth.to_string(),
}
}
pub fn create_managed(cache_root: &Path, token_count: usize) -> Result<Self> {
#[cfg(unix)]
{
let js_dir = cache_root.join("jobserver");
let slots_dir = js_dir.join("slots");
fs::create_dir_all(&slots_dir)
.with_context(|| format!("Failed to create jobserver directory {:?}", slots_dir))?;
let init_lock_path = js_dir.join("init.lock");
let pool_lock_path = js_dir.join("pool.lock");
let fifo_path = js_dir.join("pool.fifo");
let cap_path = js_dir.join("pool.cap");
let c_path = std::ffi::CString::new(fifo_path.to_str().unwrap_or(""))
.context("Invalid FIFO path for CString")?;
let init_lock = FileLock::acquire_exclusive(&init_lock_path)?;
let (pool_lock, fd, capacity) = match FileLock::try_acquire_exclusive(&pool_lock_path)?
{
Some(ex_pool_lock) => {
if fifo_path.exists() {
let _ = fs::remove_file(&fifo_path);
}
let res = unsafe { libc::mkfifo(c_path.as_ptr(), 0o600) };
if res != 0
&& std::io::Error::last_os_error().raw_os_error() != Some(libc::EEXIST)
{
return Err(std::io::Error::last_os_error())
.with_context(|| format!("Failed to create FIFO {:?}", fifo_path));
}
let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_RDWR | libc::O_CLOEXEC) };
if fd < 0 {
return Err(std::io::Error::last_os_error())
.with_context(|| format!("Failed to open FIFO {:?}", fifo_path));
}
let tokens = token_count.max(1);
let _ = fs::write(&cap_path, tokens.to_string());
let buf = vec![b'|'; tokens];
let written =
unsafe { libc::write(fd, buf.as_ptr() as *const libc::c_void, buf.len()) };
if written < 0 {
unsafe {
libc::close(fd);
}
return Err(std::io::Error::last_os_error()).with_context(|| {
format!("Failed to write tokens to FIFO {:?}", fifo_path)
});
}
drop(ex_pool_lock);
let sh_pool_lock = FileLock::acquire_shared(&pool_lock_path)?;
(sh_pool_lock, fd, tokens)
}
None => {
let sh_pool_lock = FileLock::acquire_shared(&pool_lock_path)?;
let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_RDWR | libc::O_CLOEXEC) };
if fd < 0 {
return Ok(Self::None);
}
let cap = fs::read_to_string(&cap_path)
.ok()
.and_then(|s| s.trim().parse::<usize>().ok())
.unwrap_or_else(|| token_count.max(1))
.max(1);
(sh_pool_lock, fd, cap)
}
};
drop(init_lock);
let auth = format!("fifo:{}", fifo_path.display());
Ok(Self::Managed {
_lock: pool_lock,
fifo_path,
slots_dir,
capacity,
open_fd: fd,
auth,
})
}
#[cfg(not(unix))]
{
let _ = (cache_root, token_count);
Ok(Self::None)
}
}
#[cfg(unix)]
fn acquire_managed_slot(slots_dir: &Path, capacity: usize) -> Result<FileLock> {
let cap = capacity.max(1);
let mut sleep_ms = 1u64;
loop {
let start = NEXT_SLOT.fetch_add(1, Ordering::Relaxed);
for offset in 0..cap {
let idx = (start.wrapping_add(offset)) % cap;
let slot_path = slots_dir.join(format!("slot_{}.lock", idx));
if let Some(lock) = FileLock::try_acquire_exclusive(&slot_path)? {
return Ok(lock);
}
}
if cap == 1 {
let slot_path = slots_dir.join("slot_0.lock");
return FileLock::acquire_exclusive(&slot_path);
}
std::thread::sleep(Duration::from_millis(sleep_ms));
sleep_ms = (sleep_ms * 2).min(50);
}
}
#[cfg(unix)]
fn try_open_inherited(
auth: &str,
) -> (
Option<std::os::raw::c_int>,
Option<std::os::raw::c_int>,
bool,
) {
if let Some(fifo_str) = auth.strip_prefix("fifo:") {
if let Ok(c_path) = std::ffi::CString::new(fifo_str) {
let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_RDWR | libc::O_CLOEXEC) };
if fd >= 0 {
return (Some(fd), Some(fd), true);
}
}
}
let parts: Vec<&str> = auth.split(',').collect();
if parts.len() == 2 {
if let (Ok(r), Ok(w)) = (
parts[0].parse::<std::os::raw::c_int>(),
parts[1].parse::<std::os::raw::c_int>(),
) {
if unsafe {
libc::fcntl(r, libc::F_GETFD) >= 0 && libc::fcntl(w, libc::F_GETFD) >= 0
} {
return (Some(r), Some(w), false);
}
}
} else if parts.len() == 1 {
if let Ok(r) = parts[0].parse::<std::os::raw::c_int>() {
if unsafe { libc::fcntl(r, libc::F_GETFD) >= 0 } {
return (Some(r), Some(r), false);
}
}
}
(None, None, false)
}
pub fn acquire_token(&self) -> Result<Option<JobToken<'_>>> {
#[cfg(unix)]
{
match self {
Self::Managed {
slots_dir,
capacity,
..
} => {
let lock = Self::acquire_managed_slot(slots_dir, *capacity)?;
return Ok(Some(JobToken {
pool: self,
byte: b'|',
slot_lock: Some(lock),
}));
}
Self::Inherited {
read_fd: Some(fd), ..
} => loop {
let mut b = 0u8;
let n = unsafe { libc::read(*fd, &mut b as *mut u8 as *mut libc::c_void, 1) };
if n == 1 {
return Ok(Some(JobToken {
pool: self,
byte: b,
slot_lock: None,
}));
}
if n < 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EINTR) {
continue;
}
return Err(anyhow::anyhow!(
"Failed to acquire jobserver token: {}",
err
));
}
return Err(anyhow::anyhow!(
"Unexpected EOF reading token from jobserver pipe"
));
},
_ => {}
}
}
Ok(None)
}
pub fn release_token(&self, byte: u8) {
#[cfg(unix)]
{
let write_fd = match self {
Self::Managed { open_fd, .. } => Some(*open_fd),
Self::Inherited { write_fd, .. } => *write_fd,
Self::None => None,
};
if let Some(fd) = write_fd {
let buf = [byte];
loop {
let n = unsafe { libc::write(fd, buf.as_ptr() as *const libc::c_void, 1) };
if n == 1 {
break;
}
if n < 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EINTR) {
continue;
}
eprintln!("mitts: warning: failed to release jobserver token: {}", err);
break;
}
}
}
}
}
pub fn auth(&self) -> Option<&str> {
match self {
Self::Inherited { auth, .. } => Some(auth),
Self::Managed { auth, .. } => Some(auth),
Self::None => None,
}
}
}
#[cfg(unix)]
impl Drop for JobserverPool {
fn drop(&mut self) {
match self {
Self::Managed { open_fd, .. } => unsafe {
libc::close(*open_fd);
},
Self::Inherited {
read_fd: Some(r),
write_fd: Some(w),
owned: true,
..
} => unsafe {
if *r == *w {
libc::close(*r);
} else {
libc::close(*r);
libc::close(*w);
}
},
_ => {}
}
}
}