Skip to content

Commit

Permalink
Support --jobserver-auth=fifo:PATH
Browse files Browse the repository at this point in the history
GNU `make` 4.4, released in October 2022[^1], the default jobserver
defaults to use named pipes (via `mkfifo(3)`) on supported platforms.
This feature introduces a new style of `--jobserver-auth` —
`jobserver-auth=fifo:PATH`, which `PATH` is the path of fifo[^2].

This commit makes sure that the new style `jobserver-auth=fifo:PATH`
can be forwarded to inherited processes correctly.

The support of creating a new client with named pipe will come as a
follow-up pull request.

[^1]: https://lists.gnu.org/archive/html/info-gnu/2022-10/msg00008.html
[^2]: https://www.gnu.org/software/make/manual/html_node/POSIX-Jobserver.html
  • Loading branch information
weihanglo committed Feb 25, 2023
1 parent 23c7fa6 commit 8608a22
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 17 deletions.
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
//! The jobserver implementation can be found in [detail online][docs] but
//! basically boils down to a cross-process semaphore. On Unix this is
//! implemented with the `pipe` syscall and read/write ends of a pipe and on
//! Windows this is implemented literally with IPC semaphores.
//! Windows this is implemented literally with IPC semaphores. Starting from
//! GNU `make` version 4.4, named pipe becomes the default way in communication
//! on Unix. This crate also supports that feature in the sense of inheriting
//! and forwarding the currect environment.
//!
//! The jobserver protocol in `make` also dictates when tokens are acquired to
//! run child work, and clients using this crate should take care to implement
Expand Down
90 changes: 74 additions & 16 deletions src/unix.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
use libc::c_int;

use std::fs::File;
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Write};
use std::mem;
use std::mem::MaybeUninit;
use std::os::unix::prelude::*;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::ptr;
use std::sync::{Arc, Once};
use std::thread::{self, Builder, JoinHandle};
use std::time::Duration;

#[derive(Debug)]
pub struct Client {
read: File,
write: File,
pub enum Client {
/// `--jobserver-auth=R,W`
Pipe { read: File, write: File },
/// `--jobserver-auth=fifo:PATH`
Fifo { file: File, path: PathBuf },
}

#[derive(Debug)]
Expand All @@ -30,16 +33,18 @@ impl Client {
// wrong!
const BUFFER: [u8; 128] = [b'|'; 128];

set_nonblocking(client.write.as_raw_fd(), true)?;
let mut write = client.write();

set_nonblocking(write.as_raw_fd(), true)?;

while limit > 0 {
let n = limit.min(BUFFER.len());

(&client.write).write_all(&BUFFER[..n])?;
write.write_all(&BUFFER[..n])?;
limit -= n;
}

set_nonblocking(client.write.as_raw_fd(), false)?;
set_nonblocking(write.as_raw_fd(), false)?;

Ok(client)
}
Expand Down Expand Up @@ -77,6 +82,32 @@ impl Client {
}

pub unsafe fn open(s: &str) -> Option<Client> {
Client::from_fifo(s).or_else(|| Client::from_pipe(s))
}

/// `--jobserver-auth=fifo:PATH`
fn from_fifo(s: &str) -> Option<Client> {
let mut parts = s.splitn(2, ':');
if parts.next().unwrap() != "fifo" {
return None;
}
let path = match parts.next() {
Some(p) => Path::new(p),
None => return None,
};
let file = match OpenOptions::new().read(true).write(true).open(path) {
Ok(f) => f,
Err(_) => return None,
};
drop(set_cloexec(file.as_raw_fd(), true));
Some(Client::Fifo {
file,
path: path.into(),
})
}

/// `--jobserver-auth=R,W`
unsafe fn from_pipe(s: &str) -> Option<Client> {
let mut parts = s.splitn(2, ',');
let read = parts.next().unwrap();
let write = match parts.next() {
Expand Down Expand Up @@ -110,12 +141,28 @@ impl Client {
}

unsafe fn from_fds(read: c_int, write: c_int) -> Client {
Client {
Client::Pipe {
read: File::from_raw_fd(read),
write: File::from_raw_fd(write),
}
}

/// Gets the read end of our jobserver client.
fn read(&self) -> &File {
match self {
Client::Pipe { read, .. } => read,
Client::Fifo { file, .. } => file,
}
}

/// Gets the write end of our jobserver client.
fn write(&self) -> &File {
match self {
Client::Pipe { write, .. } => write,
Client::Fifo { file, .. } => file,
}
}

pub fn acquire(&self) -> io::Result<Acquired> {
// Ignore interrupts and keep trying if that happens
loop {
Expand Down Expand Up @@ -150,11 +197,12 @@ impl Client {
// to shut us down, so we otherwise punt all errors upwards.
unsafe {
let mut fd: libc::pollfd = mem::zeroed();
fd.fd = self.read.as_raw_fd();
let mut read = self.read();
fd.fd = read.as_raw_fd();
fd.events = libc::POLLIN;
loop {
let mut buf = [0];
match (&self.read).read(&mut buf) {
match read.read(&mut buf) {
Ok(1) => return Ok(Some(Acquired { byte: buf[0] })),
Ok(_) => {
return Err(io::Error::new(
Expand Down Expand Up @@ -192,7 +240,7 @@ impl Client {
// always quickly release a token). If that turns out to not be the
// case we'll get an error anyway!
let byte = data.map(|d| d.byte).unwrap_or(b'+');
match (&self.write).write(&[byte])? {
match self.write().write(&[byte])? {
1 => Ok(()),
_ => Err(io::Error::new(
io::ErrorKind::Other,
Expand All @@ -202,12 +250,15 @@ impl Client {
}

pub fn string_arg(&self) -> String {
format!("{},{}", self.read.as_raw_fd(), self.write.as_raw_fd())
match self {
Client::Pipe { read, write } => format!("{},{}", read.as_raw_fd(), write.as_raw_fd()),
Client::Fifo { path, .. } => format!("fifo:{}", path.to_str().unwrap()),
}
}

pub fn available(&self) -> io::Result<usize> {
let mut len = MaybeUninit::<c_int>::uninit();
cvt(unsafe { libc::ioctl(self.read.as_raw_fd(), libc::FIONREAD, len.as_mut_ptr()) })?;
cvt(unsafe { libc::ioctl(self.read().as_raw_fd(), libc::FIONREAD, len.as_mut_ptr()) })?;
Ok(unsafe { len.assume_init() } as usize)
}

Expand All @@ -216,12 +267,19 @@ impl Client {
// we'll configure the read/write file descriptors to *not* be
// cloexec, so they're inherited across the exec and specified as
// integers through `string_arg` above.
let read = self.read.as_raw_fd();
let write = self.write.as_raw_fd();
let read = self.read().as_raw_fd();
let write = self.write().as_raw_fd();
let is_pipe = match self {
Client::Pipe { .. } => true,
_ => false,
};
unsafe {
cmd.pre_exec(move || {
set_cloexec(read, false)?;
set_cloexec(write, false)?;
// Only simple pipe style needs to configure both ends.
if is_pipe {
set_cloexec(write, false)?;
}
Ok(())
});
}
Expand Down

0 comments on commit 8608a22

Please sign in to comment.