use std::collections::HashMap;
use std::io::PipeReader;
use std::os::fd::{AsRawFd as _, OwnedFd};
use std::os::unix::net::UnixStream;
use std::process::{Child, Command, ExitStatus, Stdio};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, PoisonError, mpsc};
use std::time::Duration;
use super::spawn_helper::{HELLO, exit_status};
use super::wire::{Inbox, Reply, Request, SpawnSpec, Unsent, send_frame};
const ANSWER_BUDGET: Duration = Duration::from_secs(5);
static LINK: Mutex<Option<Arc<Link>>> = Mutex::new(None);
struct Link {
socket: UnixStream,
sending: Mutex<()>,
calls: Mutex<HashMap<u64, mpsc::Sender<Reply>>>,
next_call: AtomicU64,
gone: AtomicBool,
helper: Mutex<Child>,
}
fn helper_command() -> std::io::Result<Command> {
#[cfg(any(test, feature = "test-support"))]
if super::harness_helper() {
return harness_helper_command();
}
let mut command = Command::new(std::env::current_exe()?);
command.arg(super::HELPER_MODE);
Ok(command)
}
#[cfg(any(test, feature = "test-support"))]
fn harness_helper_command() -> std::io::Result<Command> {
if HELPER_BROKEN.load(Ordering::Relaxed) {
return Err(std::io::Error::from(std::io::ErrorKind::NotFound));
}
let mut command = Command::new(std::env::current_exe()?);
command
.args([
super::tests::HELPER_TEST,
"--exact",
"--ignored",
"--nocapture",
])
.env(super::tests::HELPER_ENV, "spawn-helper")
.env_remove(super::tests::FAULT_ENV);
if let Some(fault) = *HELPER_FAULT.lock().unwrap_or_else(PoisonError::into_inner) {
command.env(super::tests::FAULT_ENV, fault);
}
Ok(command)
}
impl Link {
fn start() -> Option<Arc<Self>> {
let (ours, theirs) = UnixStream::pair().ok()?;
super::wire::quiet_socket(ours.as_raw_fd());
let mut command = helper_command().ok()?;
command
.stdin(OwnedFd::from(theirs))
.stdout(Stdio::null())
.stderr(Stdio::null());
let helper = command.spawn().ok()?;
drop(command);
let reading = ours.try_clone().ok();
let link = Arc::new(Self {
socket: ours,
sending: Mutex::new(()),
calls: Mutex::new(HashMap::new()),
next_call: AtomicU64::new(1),
gone: AtomicBool::new(false),
helper: Mutex::new(helper),
});
let (hello, greeted) = mpsc::channel();
let reader = reading.and_then(|reading| {
let link = Arc::clone(&link);
std::thread::Builder::new()
.name("spawn-helper-replies".into())
.spawn(move || link.read_replies(&reading, &hello))
.ok()
});
if reader.is_none() || greeted.recv_timeout(ANSWER_BUDGET).is_err() {
let mut helper = link.helper.lock().unwrap_or_else(PoisonError::into_inner);
let _ = helper.kill();
if reader.is_none() {
let _ = helper.wait();
}
drop(helper);
link.abandon();
return None;
}
Some(link)
}
fn read_replies(&self, socket: &UnixStream, hello: &mpsc::Sender<()>) {
let mut inbox = Inbox::default();
let mut greeted = false;
let mut understood = true;
'connection: while matches!(inbox.fill(socket.as_raw_fd()), Ok(true)) {
loop {
let frame = match inbox.next_frame() {
Ok(Some(frame)) => frame,
Ok(None) => break,
Err(_) => {
understood = false;
break 'connection;
}
};
if !greeted {
if frame != HELLO {
understood = false;
break 'connection;
}
greeted = true;
let _ = hello.send(());
continue;
}
let Some(reply) = Reply::decode(&frame) else {
understood = false;
break 'connection;
};
let (Reply::Spawned { call, .. }
| Reply::SpawnFailed { call, .. }
| Reply::Exited { call, .. }) = reply;
if let Some(sender) = self.lock_calls().get(&call) {
let _ = sender.send(reply);
}
}
}
self.gone.store(true, Ordering::SeqCst);
self.lock_calls().clear();
let mut helper = self.helper.lock().unwrap_or_else(PoisonError::into_inner);
if !understood {
let _ = helper.kill();
}
let _ = helper.wait();
}
fn lock_calls(&self) -> std::sync::MutexGuard<'_, HashMap<u64, mpsc::Sender<Reply>>> {
self.calls.lock().unwrap_or_else(PoisonError::into_inner)
}
fn abandon(&self) {
self.gone.store(true, Ordering::SeqCst);
let _ = self.socket.shutdown(std::net::Shutdown::Both);
}
fn send(&self, request: &Request, descriptors: &[std::os::fd::RawFd]) -> Result<(), Unsent> {
let _one_frame = self.sending.lock().unwrap_or_else(PoisonError::into_inner);
#[cfg(any(test, feature = "test-support"))]
if matches!(request, Request::Spawn { .. }) && SEND_FAILS_ONCE.swap(false, Ordering::SeqCst)
{
return Err(Unsent::Nothing);
}
send_frame(self.socket.as_raw_fd(), &request.encode(), descriptors)
}
}
fn link() -> Option<Arc<Link>> {
let mut slot = LINK.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(link) = slot.as_ref()
&& !link.gone.load(Ordering::SeqCst)
{
return Some(Arc::clone(link));
}
*slot = Link::start();
slot.clone()
}
pub fn shutdown_spawn_helper() {
let link = LINK.lock().unwrap_or_else(PoisonError::into_inner).take();
if let Some(link) = link {
let _ = link.send(&Request::Shutdown, &[]);
link.gone.store(true, Ordering::SeqCst);
}
}
pub enum HelperSpawn {
Helped {
child: HelpedChild,
stdout: PipeReader,
stderr: PipeReader,
},
Direct(Option<&'static str>),
}
pub fn spawn_shell(command: &Command) -> HelperSpawn {
if !super::guard_host() {
super::note_no_guard_host();
return HelperSpawn::Direct(None);
}
let spec = SpawnSpec {
program: command.get_program().to_os_string(),
args: command
.get_args()
.map(std::ffi::OsStr::to_os_string)
.collect(),
env: effective_environment(command),
cwd: command
.get_current_dir()
.map(|dir| dir.as_os_str().to_os_string()),
};
let mut request = Request::Spawn { call: 0, spec };
for _ in 0..2 {
let Some(link) = link() else {
continue;
};
match link.spawn(&mut request) {
Attempt::Done(spawned) => return spawned,
Attempt::NotStarted => {}
Attempt::Refused => break,
}
}
HelperSpawn::Direct(Some(super::HELPER_NOTICE))
}
fn effective_environment(command: &Command) -> Vec<(std::ffi::OsString, std::ffi::OsString)> {
let mut environment: std::collections::BTreeMap<_, _> = std::env::vars_os().collect();
for (name, value) in command.get_envs() {
match value {
Some(value) => environment.insert(name.to_os_string(), value.to_os_string()),
None => environment.remove(name),
};
}
environment.into_iter().collect()
}
enum Attempt {
Done(HelperSpawn),
NotStarted,
Refused,
}
impl Link {
fn spawn(self: &Arc<Self>, request: &mut Request) -> Attempt {
let (Ok((stdout, stdout_end)), Ok((stderr, stderr_end))) =
(std::io::pipe(), std::io::pipe())
else {
return Attempt::NotStarted;
};
let call = self.next_call.fetch_add(1, Ordering::Relaxed);
if let Request::Spawn { call: slot, .. } = request {
*slot = call;
}
let (sender, replies) = mpsc::channel();
self.lock_calls().insert(call, sender);
let mut child = HelpedChild {
call,
pid: 0,
link: Arc::clone(self),
replies,
status: None,
settled: false,
lost: false,
unclear: false,
};
let sent = if self.gone.load(Ordering::SeqCst) {
Err(Unsent::Nothing)
} else {
self.send(request, &[stdout_end.as_raw_fd(), stderr_end.as_raw_fd()])
};
let reply = match sent {
Ok(()) => child.replies.recv_timeout(ANSWER_BUDGET).ok(),
Err(Unsent::Nothing) => {
child.settled = true;
self.abandon();
return Attempt::NotStarted;
}
Err(Unsent::Part) => None,
};
drop((stdout_end, stderr_end));
match reply {
Some(Reply::Spawned { pid, .. }) => child.pid = pid,
Some(Reply::SpawnFailed { .. }) => {
child.settled = true;
return Attempt::Refused;
}
Some(Reply::Exited { .. }) | None => {
self.abandon();
child.settled = true;
child.unclear = true;
}
}
Attempt::Done(HelperSpawn::Helped {
child,
stdout,
stderr,
})
}
}
pub struct HelpedChild {
call: u64,
pid: u32,
link: Arc<Link>,
replies: mpsc::Receiver<Reply>,
status: Option<ExitStatus>,
settled: bool,
lost: bool,
unclear: bool,
}
impl HelpedChild {
pub(crate) fn id(&self) -> u32 {
self.pid
}
pub(crate) fn notice(&self) -> Option<&'static str> {
if self.unclear {
Some(super::UNCLEAR_NOTICE)
} else {
self.lost.then_some(super::END_NOTICE)
}
}
fn note_reply(&mut self, reply: &Reply) -> Option<ExitStatus> {
if let Reply::Exited { status, .. } = reply {
self.status = Some(exit_status(*status));
}
self.status
}
fn note_gone(&mut self) -> std::io::Error {
self.lost = true;
self.settled = true;
std::io::Error::other("the spawn helper is gone")
}
pub(crate) fn try_wait(&mut self) -> std::io::Result<Option<ExitStatus>> {
if self.status.is_some() {
return Ok(self.status);
}
if self.lost || self.unclear {
return Err(self.note_gone());
}
match self.replies.try_recv() {
Ok(reply) => Ok(self.note_reply(&reply)),
Err(mpsc::TryRecvError::Empty) => Ok(None),
Err(mpsc::TryRecvError::Disconnected) => Err(self.note_gone()),
}
}
pub(crate) fn kill(&mut self) {
if !self.settled {
self.settled = true;
let _ = self.link.send(&Request::Kill { call: self.call }, &[]);
}
}
pub(crate) fn wait(&mut self) {
while self.status.is_none() && !self.lost {
match self.replies.recv() {
Ok(reply) => {
self.note_reply(&reply);
}
Err(_) => {
let _ = self.note_gone();
}
}
}
}
pub(crate) fn release(mut self) -> bool {
if self.settled {
return true;
}
self.settled = true;
#[cfg(any(test, feature = "test-support"))]
if RELEASE_FAILS_ONCE.swap(false, Ordering::SeqCst) {
return false;
}
let sent = self
.link
.send(&Request::Release { call: self.call }, &[])
.is_ok();
if !sent {
tracing::warn!(
"spawn helper: a release could not be sent; what the command left running may be ended"
);
}
sent
}
}
impl Drop for HelpedChild {
fn drop(&mut self) {
self.kill();
self.link.lock_calls().remove(&self.call);
}
}
#[cfg(any(test, feature = "test-support"))]
static SEND_FAILS_ONCE: AtomicBool = AtomicBool::new(false);
#[cfg(any(test, feature = "test-support"))]
pub fn fail_next_spawn_send_for_test() {
SEND_FAILS_ONCE.store(true, Ordering::SeqCst);
}
#[cfg(any(test, feature = "test-support"))]
static RELEASE_FAILS_ONCE: AtomicBool = AtomicBool::new(false);
#[cfg(any(test, feature = "test-support"))]
pub fn fail_next_release_send_for_test() {
RELEASE_FAILS_ONCE.store(true, Ordering::SeqCst);
}
#[cfg(any(test, feature = "test-support"))]
static HELPER_FAULT: Mutex<Option<&'static str>> = Mutex::new(None);
#[cfg(any(test, feature = "test-support"))]
pub fn set_helper_fault_for_test(fault: Option<&'static str>) {
*HELPER_FAULT.lock().unwrap_or_else(PoisonError::into_inner) = fault;
}
#[cfg(any(test, feature = "test-support"))]
static HELPER_BROKEN: AtomicBool = AtomicBool::new(false);
#[cfg(any(test, feature = "test-support"))]
pub fn break_helper_for_test(broken: bool) {
HELPER_BROKEN.store(broken, Ordering::Relaxed);
}
#[cfg(any(test, feature = "test-support"))]
pub fn helper_pid_for_test() -> Option<u32> {
let slot = LINK.lock().unwrap_or_else(PoisonError::into_inner);
let link = slot
.as_ref()
.filter(|link| !link.gone.load(Ordering::SeqCst))?;
Some(
link.helper
.lock()
.unwrap_or_else(PoisonError::into_inner)
.id(),
)
}
#[cfg(any(test, feature = "test-support"))]
pub fn kill_helper_for_test() {
let link = LINK
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
.expect("a helper runs");
let _ = link
.helper
.lock()
.unwrap_or_else(PoisonError::into_inner)
.kill();
let deadline = std::time::Instant::now() + Duration::from_secs(5);
while !link.gone.load(Ordering::SeqCst) {
assert!(
std::time::Instant::now() < deadline,
"the helper's end is seen"
);
std::thread::sleep(Duration::from_millis(5));
}
}