use crate::errors::bail;
use crate::errors::Error;
use crate::runtime;
use std::io::BufReader;
use std::process;
use std::sync::Arc;
use std::sync::Mutex;
pub(crate) struct ChildProcess {
process_handle: Arc<Mutex<std::process::Child>>,
process_disowned: bool,
stdout: std::io::Lines<BufReader<std::process::ChildStdout>>,
stdin: Option<std::process::ChildStdin>,
command: Arc<Mutex<process::Command>>,
stderr_sender: Arc<Mutex<crossbeam_channel::Sender<String>>>,
}
impl ChildProcess {
pub(crate) fn new(
mut command: std::process::Command,
stderr_sender: crossbeam_channel::Sender<String>,
) -> Result<ChildProcess, Error> {
if std::env::var(runtime::EVCXR_IS_RUNTIME_VAR).is_ok() {
bail!("Our current binary doesn't call runtime_hook()");
}
command
.env(runtime::EVCXR_IS_RUNTIME_VAR, "1")
.env("RUST_BACKTRACE", "1")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
ChildProcess::new_internal(
Arc::new(Mutex::new(command)),
None,
Arc::new(Mutex::new(stderr_sender)),
)
}
fn new_internal(
command: Arc<Mutex<std::process::Command>>,
process_handle: Option<Arc<Mutex<std::process::Child>>>,
stderr_sender: Arc<Mutex<crossbeam_channel::Sender<String>>>,
) -> Result<ChildProcess, Error> {
let process = command.lock().unwrap().spawn();
let mut process = match process {
Ok(c) => c,
Err(error) => bail!("Failed to run '{:?}': {:?}", command, error),
};
let stdin = process.stdin.take();
let mut child_stderr =
std::io::BufRead::lines(BufReader::new(process.stderr.take().unwrap()));
let stdout = std::io::BufRead::lines(BufReader::new(process.stdout.take().unwrap()));
let process_handle = match process_handle {
Some(handle) => {
core::mem::swap(&mut *handle.lock().unwrap(), &mut process);
let _ = process.wait();
handle
}
None => Arc::new(Mutex::new(process)),
};
std::thread::spawn({
let stderr_sender = Arc::clone(&stderr_sender);
move || {
let stderr_sender = stderr_sender.lock().unwrap();
while let Some(Ok(line)) = child_stderr.next() {
let _ = stderr_sender.send(line);
}
}
});
Ok(ChildProcess {
process_handle,
process_disowned: false,
stdout,
stdin,
command,
stderr_sender,
})
}
pub(crate) fn process_handle(&self) -> Arc<Mutex<std::process::Child>> {
self.process_handle.clone()
}
pub(crate) fn restart(&mut self) -> Result<ChildProcess, Error> {
let mut process = self.process_handle.lock().unwrap();
if let Ok(None) = process.try_wait() {
let _ = process.kill();
let _ = process.wait();
}
self.process_disowned = true;
drop(process);
ChildProcess::new_internal(
Arc::clone(&self.command),
Some(self.process_handle.clone()),
Arc::clone(&self.stderr_sender),
)
}
pub(crate) fn send(&mut self, command: &str) -> Result<(), Error> {
use std::io::Write;
writeln!(self.stdin.as_mut().unwrap(), "{command}")
.map_err(|_| self.get_termination_error())?;
self.stdin.as_mut().unwrap().flush()?;
Ok(())
}
pub(crate) fn recv_line(&mut self) -> Result<String, Error> {
Ok(self
.stdout
.next()
.ok_or_else(|| self.get_termination_error())??)
}
fn get_termination_error(&mut self) -> Error {
std::mem::drop(self.stderr_sender.lock().unwrap());
let mut content = String::new();
while let Some(Ok(line)) = self.stdout.next() {
content.push_str(&line);
content.push('\n');
}
Error::SubprocessTerminated(match self.process_handle.lock().unwrap().wait() {
Ok(exit_status) => {
#[cfg(target_os = "macos")]
{
use std::os::unix::process::ExitStatusExt;
if Some(9) == exit_status.signal() {
return Error::SubprocessTerminated(
"Subprocess terminated with signal 9. This is known \
to happen when evcxr is installed via a Homebrew shell \
under emulation. Try installing rustup and evcxr without \
using Homebrew and see if that helps."
.to_owned(),
);
}
}
format!("{content}Subprocess terminated with status: {exit_status}",)
}
Err(wait_error) => format!("Subprocess didn't start: {wait_error}"),
})
}
}
impl Drop for ChildProcess {
fn drop(&mut self) {
self.stdin.take();
if !self.process_disowned {
let _ = self.process_handle.lock().unwrap().wait();
}
}
}