// now: Nix-based distributed command runner // Copyright (C) 2026 Eric Rodrigues Pires // // This program is free software: you can redistribute it and/or modify it under // the terms of the GNU Affero General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for // more details. // // You should have received a copy of the GNU Affero General Public License along // with this program. If not, see . use std::{io::Write, process::Output}; use rand::distr::SampleString; use smol::{ channel::{Receiver, TryRecvError}, future::zip, io::AsyncReadExt, process::Child, }; use crate::job::JobError; /// Waits for `child` to exit while draining its pipe buffers concurrently. pub(crate) async fn wait_for_output( child: &mut Child, cancellation: Option<&Receiver<()>>, ) -> Result { let mut stdout = child.stdout.take(); let mut stderr = child.stderr.take(); let mut stdout_buf = Vec::new(); let mut stderr_buf = Vec::new(); let run = async { let (status, (out, err)) = zip( child.status(), zip( async { if let Some(pipe) = stdout.as_mut() { pipe.read_to_end(&mut stdout_buf).await?; } Ok::<(), color_eyre::Report>(()) }, async { if let Some(pipe) = stderr.as_mut() { pipe.read_to_end(&mut stderr_buf).await?; } Ok::<(), color_eyre::Report>(()) }, ), ) .await; out?; err?; let status = status.map_err(|error| JobError::Other(error.into()))?; Ok(Output { status, stdout: stdout_buf, stderr: stderr_buf, }) }; let result = if let Some(cancellation) = cancellation { smol::future::or(run, async { let _ = cancellation.recv().await; Err(JobError::RunnerAborted) }) .await } else { run.await }; let _ = child.kill(); result } pub(crate) fn write_output_to_stderr(output: &Output) -> color_eyre::Result<()> { let mut stderr = std::io::stderr(); stderr.write_all(&output.stderr)?; stderr.flush()?; Ok(()) } pub(crate) fn get_random_string(len: usize) -> String { rand::distr::Alphanumeric.sample_string(&mut rand::rng(), len) } pub(crate) fn trim_string(original: &str, max_chars: usize) -> String { debug_assert!(max_chars > 0); let mut output = String::with_capacity(max_chars); let mut iter = original.chars(); for _ in 0..max_chars - 1 { if let Some(char) = iter.next() { output.push(char); } else { return output; } } if iter.next().is_some() { output.push('…'); } output } pub(crate) fn parse_bool_from_bytes>(input: T) -> bool { let input = input.as_ref().trim_ascii(); [&b"1"[..], &b"true"[..], &b"yes"[..], &b"y"[..]] .iter() .any(|truthy_slice| input.eq_ignore_ascii_case(truthy_slice)) } pub(crate) fn job_is_aborted(receiver: &Receiver<()>, abort: Option<&Receiver<()>>) -> bool { matches!(receiver.try_recv(), Ok(()) | Err(TryRecvError::Closed)) || abort.is_some_and(|a| matches!(a.try_recv(), Ok(()) | Err(TryRecvError::Closed))) }