mod channel_api;
mod color;
mod control;
mod kill_barrier;
mod line_parse;
mod run;
mod standard_out_api;
mod template;
mod which;
mod writer_api;
use std::collections::HashMap;
use std::io;
use std::io::Write;
use std::path::PathBuf;
use std::process;
use std::process::ExitStatus;
use std::sync::mpsc;
use std::sync::Arc;
use std::sync::Mutex;
use std::thread;
pub use channel_api::ChannelCommand;
pub use color::Color;
pub use control::HandleControl;
pub use control::Signal;
pub use line_parse::LineEnding;
pub use standard_out_api::parse_command_string;
pub use standard_out_api::ConsoleCommand;
pub use writer_api::WriterCommand;
pub const CURRENT_WORKING_DIRECTORY: Option<String> = None;
#[derive(Debug)]
pub enum CommandError {
EmptyCommand,
CommandNotFound(String),
ParseError(String),
}
#[derive(Clone)]
struct Options {
restart: RestartOptions,
quiet: bool,
file_handle_flags: bool,
}
#[derive(Clone)]
pub struct InnerCommand {
name: String,
command: String,
args: Vec<String>,
cur_dir: Option<PathBuf>,
env: HashMap<String, String>,
}
impl From<InnerCommand> for process::Command {
fn from(cmd: InnerCommand) -> Self {
let mut command_process = process::Command::new(cmd.command);
command_process.args(cmd.args);
if cmd.cur_dir.is_some() {
command_process.current_dir(cmd.cur_dir.unwrap());
}
command_process.envs(cmd.env);
command_process.stdout(process::Stdio::piped());
command_process
}
}
pub trait Command: Clone
where
Self: Sized,
{
fn insert_command(cmd: InnerCommand) -> Self;
fn get_command(&self) -> &InnerCommand;
fn get_command_mut(&mut self) -> &mut InnerCommand;
fn from_argv<S, C, D, ArgType, Cmds>(
name: S,
command: C,
args: Cmds,
cur_dir: Option<D>,
) -> Result<Self, CommandError>
where
S: Into<String>,
C: Into<String>,
D: Into<PathBuf>,
ArgType: Into<String>,
Cmds: IntoIterator<Item = ArgType>,
{
let name = name.into();
let cmd = command.into();
let dir = cur_dir.map(|d| d.into());
check_command(&cmd, &dir)?;
if name.is_empty() || cmd.is_empty() {
return Err(CommandError::EmptyCommand);
}
let converted_args = args.into_iter().map(|s| s.into()).collect::<Vec<String>>();
Ok(Self::insert_command(InnerCommand {
name,
command: cmd,
args: converted_args,
cur_dir: dir,
env: HashMap::new(),
}))
}
fn from_string<S, C, D>(
name: S,
command_string: C,
cur_dir: Option<D>,
) -> Result<Self, CommandError>
where
S: Into<String>,
C: Into<String>,
D: Into<PathBuf>,
{
let (command, args) = parse_command_string(command_string)?;
let dir = cur_dir.map(|d| d.into());
check_command(&command, &dir)?;
Ok(Self::insert_command(InnerCommand {
name: name.into(),
command,
args,
cur_dir: dir,
env: HashMap::new(),
}))
}
fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
where
K: Into<String>,
V: Into<String>,
{
self.get_command_mut().env.insert(key.into(), val.into());
self
}
}
pub struct OutputMessage {
pub name: String,
pub message: OutputMessagePayload,
}
pub enum OutputMessagePayload {
Start,
Done(Option<i32>),
Stdout(line_parse::LineEnding, Vec<u8>),
Stderr(line_parse::LineEnding, Vec<u8>),
Error(io::Error),
}
pub type ExitResult = (String, Option<ExitStatus>);
pub struct CommandHandle {
handle: thread::JoinHandle<Vec<ExitResult>>,
channel: mpsc::Receiver<OutputMessage>,
kill_trigger: kill_barrier::KillBarrier,
pids: Vec<Arc<(String, Mutex<Option<u32>>)>>,
}
impl CommandHandle {
pub fn join(self) -> Result<Vec<ExitResult>, String> {
self.handle
.join()
.map_err(|_| "Thread panic'ed before exit".to_string())
}
pub fn get_output_channel(&self) -> &mpsc::Receiver<OutputMessage> {
&self.channel
}
pub fn kill(&self) {
let _ = self.kill_trigger.initiate_kill();
}
pub fn get_signaler(&self) -> control::HandleControl {
HandleControl::new(self.pids.clone(), self.kill_trigger.clone())
}
}
impl Iterator for CommandHandle {
type Item = OutputMessage;
fn next(&mut self) -> Option<Self::Item> {
self.channel.recv().ok()
}
}
impl Iterator for &CommandHandle {
type Item = OutputMessage;
fn next(&mut self) -> Option<OutputMessage> {
self.channel.recv().ok()
}
}
pub struct ControlledCommandHandle {
supervisor: thread::JoinHandle<()>,
handle: thread::JoinHandle<Vec<ExitResult>>,
kill_trigger: kill_barrier::KillBarrier,
pids: Vec<Arc<(String, Mutex<Option<u32>>)>>,
}
impl ControlledCommandHandle {
pub fn join(self) -> Result<Vec<ExitResult>, String> {
self.supervisor
.join()
.map_err(|_| "thread panic'ed before exit".to_string())?;
self.handle
.join()
.map_err(|_| "thread panic'ed before exit".to_string())
}
pub fn kill(&self) {
let _ = self.kill_trigger.initiate_kill();
}
pub fn get_signaler(&self) -> control::HandleControl {
HandleControl::new(self.pids.clone(), self.kill_trigger.clone())
}
}
#[derive(Clone)]
pub enum RestartOptions {
Continue,
Restart,
Kill,
}
pub struct Runner<C: Command> {
commands: Vec<C>,
restart: RestartOptions,
quiet: bool,
file_handle_flags: bool,
start_message_template: String,
done_message_template: String,
payload_message_template: String,
error_message_template: String,
}
impl<C: Command> Default for Runner<C> {
fn default() -> Self {
Runner::new()
}
}
impl<C: Command> Runner<C> {
pub fn new() -> Self {
Runner {
commands: Vec::new(),
restart: RestartOptions::Continue,
quiet: false,
file_handle_flags: false,
start_message_template: "{{begin_color}}SYSTEM: starting process {{name}}{{reset_color}}"
.to_string(),
done_message_template:
"{{begin_color}}{{name}}:{{reset_color}} process exited with status: {{status_code}}"
.to_string(),
payload_message_template: "{{begin_color}}{{name}}{{handle_flag}}:{{reset_color}}".to_string(),
error_message_template: "{{begin_color}}SYSTEM (e): Encountered error with process {{name}}: {{error_message}}{{reset_color}}".to_string(),
}
}
pub fn command<T: AsRef<C>>(&mut self, cmd: T) -> &mut Self {
self.commands.push(cmd.as_ref().clone());
self
}
pub fn restart(&mut self, restart_opt: RestartOptions) -> &mut Self {
self.restart = restart_opt;
self
}
pub fn quiet(&mut self, quiet_opt: bool) -> &mut Self {
self.quiet = quiet_opt;
self
}
pub fn should_show_file_handle(&mut self, file_handle_flag_opt: bool) -> &mut Self {
self.file_handle_flags = file_handle_flag_opt;
self
}
pub fn start_message_template<S: Into<String>>(&mut self, template: S) -> &mut Self {
self.start_message_template = template.into();
self
}
pub fn done_message_template<S: Into<String>>(&mut self, template: S) -> &mut Self {
self.done_message_template = template.into();
self
}
pub fn payload_message_template<S: Into<String>>(&mut self, template: S) -> &mut Self {
self.payload_message_template = template.into();
self
}
pub fn error_message_template<S: Into<String>>(&mut self, template: S) -> &mut Self {
self.error_message_template = template.into();
self
}
fn to_options(&self) -> Options {
Options {
restart: self.restart.clone(),
quiet: self.quiet,
file_handle_flags: self.file_handle_flags,
}
}
fn get_template_strings(&self) -> template::TemplateStrings {
template::TemplateStrings {
start_message_template: self.start_message_template.clone(),
done_message_template: self.done_message_template.clone(),
payload_message_template: self.payload_message_template.clone(),
error_message_template: self.error_message_template.clone(),
}
}
}
impl Runner<ChannelCommand> {
pub fn execute(&mut self) -> CommandHandle {
run_commands(self)
}
}
impl Runner<WriterCommand> {
pub fn execute<W: Write + Send + 'static>(&mut self, writer: W) -> ControlledCommandHandle {
writer_api::run_commands_writer(self, writer)
}
}
impl Runner<ConsoleCommand> {
pub fn execute(&mut self) -> ControlledCommandHandle {
standard_out_api::run_commands_stdout(self)
}
}
fn run_commands<C: Command>(runner: &Runner<C>) -> CommandHandle {
let actual_cmds = runner
.commands
.iter()
.map(|c| c.get_command().clone())
.collect();
run::run_commands_internal(actual_cmds, runner.to_options())
}
fn check_command(exec_name: &str, dir: &Option<PathBuf>) -> Result<(), CommandError> {
if which::exec_exists(exec_name, dir) {
Ok(())
} else {
Err(CommandError::CommandNotFound(exec_name.to_string()))
}
}
#[cfg(test)]
mod test {
use crate::Command;
#[test]
fn command_not_found() {
let cmd = super::ConsoleCommand::from_string(
"test",
"bogus_cmd_not_found",
super::CURRENT_WORKING_DIRECTORY,
);
match cmd {
Err(super::CommandError::CommandNotFound(name)) => {
assert_eq!(&name, "bogus_cmd_not_found",)
}
_ => panic!("bogus command didn't return CommandNotFound"),
}
}
#[test]
fn command_empty() {
let cmd = super::ConsoleCommand::from_string("test", "", super::CURRENT_WORKING_DIRECTORY);
match cmd {
Err(super::CommandError::EmptyCommand) => {}
_ => panic!("empty command didn't error out"),
}
}
}