use crate::{Config, Proc, info, logging};
use anyhow::{Context, Result};
use std::{
env,
ffi::{OsStr, OsString},
io::{BufRead, BufReader, Read},
os::unix::prelude::CommandExt,
path::PathBuf,
process::{Child, Command, Stdio},
sync::{Mutex, mpsc},
thread::spawn,
};
impl Proc {
pub fn watch_abs(&self, config: &Config) -> Result<Vec<PathBuf>> {
let current_dir = &env::current_dir().context("current directory")?;
let working_dir = self
.working_dir
.as_ref()
.map(|d| current_dir.join(d))
.unwrap_or_else(|| current_dir.clone());
let working_dir = working_dir.canonicalize().context(format!(
"failed to canonicalize working directory: {}",
working_dir.display()
))?;
let mut all_paths = config
.watch
.iter()
.map(|path| {
if path.is_absolute() {
path.clone()
} else {
current_dir.join(path)
}
})
.collect::<Vec<_>>();
#[allow(unknown_lints, nesting_too_deep)]
for (paths, rel) in [(&self.watch, false), (&self.watch_rel, true)] {
for path in paths {
let full_path = if rel {
&working_dir.join(path)
} else if path.is_absolute() {
path
} else {
¤t_dir.join(path)
};
let canonical_path = full_path.canonicalize().context(format!(
"failed to canonicalize path: {}",
full_path.display()
))?;
all_paths.push(canonical_path);
}
}
Ok(all_paths)
}
fn spawn_reader<R: Read + Send + 'static>(
&self,
reader: R,
readiness_tx: mpsc::Sender<()>,
stream_name: &'static str,
_config: &Config,
proc_output_tx: Option<mpsc::Sender<()>>,
) {
let readiness_pattern = self.readiness_pattern.clone();
let proc_name = self.name.clone();
let proc_color = self.color;
spawn(move || {
let buf_reader = BufReader::new(reader);
let mut readiness_signaled = false;
for line in buf_reader.lines() {
match line {
Err(err) => {
logging::log_proc_error(&proc_name, &format!("{err}"), proc_color);
break;
}
Ok(l) => {
logging::log_proc(&proc_name, &l, proc_color);
if let Some(ref tx) = proc_output_tx {
tx.send(()).ok();
}
if !readiness_signaled
&& let Some(ref pattern) = readiness_pattern
&& l.to_ascii_lowercase()
.contains(&pattern.to_ascii_lowercase())
{
logging::log_proc(
&proc_name,
&format!(
"[{stream_name}] Readiness pattern '{pattern}' detected in: {l}"
),
proc_color,
);
readiness_tx.send(()).ok();
readiness_signaled = true;
}
}
}
}
});
}
pub(crate) fn start_service(
&self,
process_group_id: &Mutex<i32>,
config: &Config,
proc_output_tx: Option<mpsc::Sender<()>>,
) -> Result<(Child, mpsc::Receiver<()>), String> {
let working_dir = self
.working_dir
.clone()
.unwrap_or_else(|| env::current_dir().expect("Failed to get current directory"));
logging::log_proc(
&self.name,
&format!(
"Running: {} {} (in {})",
self.command.display(),
args_display(&self.args),
working_dir.display()
),
self.color,
);
if self.cleanup_before_start {
self.cleanup();
}
let mut command = Command::new(&self.command);
if self.env_clear {
command.env_clear();
}
command
.current_dir(working_dir)
.args(&self.args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
for (key, value) in &self.env {
command.env(key, value);
}
let mut gid = process_group_id
.lock()
.expect("failed to lock process group ID");
if *gid > 0 {
let check_result = std::process::Command::new("kill")
.args(["-0", &format!("-{}", *gid)])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
match check_result {
Ok(status) if status.success() => {
command.process_group(*gid);
}
_ => {
*gid = 0;
}
}
}
let mut child = command
.spawn()
.map_err(|e| format!("Failed to start service {}: {e}", self.name))?;
if *gid == 0 {
*gid = child.id() as i32;
info!("Created process group: {}", *gid);
}
let (readiness_tx, readiness_rx) = mpsc::channel();
if let Some(stdout) = child.stdout.take() {
self.spawn_reader(
stdout,
readiness_tx.clone(),
"stdout",
config,
proc_output_tx.clone(),
);
}
if let Some(stderr) = child.stderr.take() {
self.spawn_reader(
stderr,
readiness_tx.clone(),
"stderr",
config,
proc_output_tx.clone(),
);
}
Ok((child, readiness_rx))
}
pub(crate) fn cleanup(&self) {
if let Some(args) = &self.cleanup_cmd
&& !args.is_empty()
{
logging::log_proc(
&self.name,
&format!("Running cleanup: {}", args_display(&args)),
self.color,
);
Command::new(&args[0])
.args(&args[1..])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.ok();
}
}
}
fn args_display(args: &[OsString]) -> String {
args.join(OsStr::new(" ")).to_string_lossy().to_string()
}