use anyhow::{bail, Result};
use pico_args::Arguments;
use sopht::{
create_client_connection, ChangeRestartPolicyArgs, Command, ExitStatus,
Message, ProcessStatus, RestartPolicy, SendArgs, StartArgs, PID,
};
use std::fmt::Write;
use std::fs;
use std::path::PathBuf;
fn main() {
let usage = usage();
if Arguments::from_env().finish().is_empty() {
print!("{}", usage);
return;
}
let mut args = Arguments::from_env();
if args.contains("--help") || args.contains("-h") {
print!("{}", usage);
return;
}
let cmd = match args.subcommand().expect("non UTF-8 arguments supplied") {
Some(cmd) => cmd,
None => {
print!("{}", usage);
return;
}
};
if cmd == "help" {
print!("{}", usage);
return;
}
let msg = Message::Command(match make_command(&cmd, args) {
Ok(cmd) => cmd,
Err(e) => {
eprintln!("{}", e);
return;
}
});
let mut conn = match create_client_connection() {
Ok(conn) => conn,
Err(e) => {
eprintln!("failed to connect to server. error: {}", e);
return;
}
};
if let Err(e) = conn.send(&msg) {
eprintln!("failed to send message. error: {}", e);
return;
}
let resp = match conn.recv_response() {
Ok(resp) => resp,
Err(e) => {
eprintln!("failed to receive response. error: {}", e);
return;
}
};
if resp.success {
println!("{}", resp.message);
if let Some(status) = resp.status {
println!(
"{}",
format_status(&status).expect("failed to format status")
);
}
} else {
println!("command failed. error: {}", resp.message)
}
}
fn format_status(status: &Vec<ProcessStatus>) -> Result<String> {
let mut res = String::new();
writeln!(
&mut res,
"{:03} {:10} {:15} {:20} {:25}",
"PID", "program", "status", "restart policy", "restarted?"
)?;
writeln!(
&mut res,
"---------------------------------------------------------------"
)?;
for proc in status {
let alive = match proc.exit_status {
ExitStatus::NotExited => "running",
_ => {
if proc.started {
"stopped"
} else {
"never started"
}
}
};
let restarted = match proc.restarted {
Some(pid) => format!("restarted. new PID: {:03}", pid),
None => "not restarted".to_string(),
};
let restart_policy = format!("{}", proc.restart_policy);
let prog = if proc.prog.len() >= 10 {
let mut prog: String =
proc.prog.chars().skip(proc.prog.len() - 8).collect();
prog.insert_str(0, "..");
prog
} else {
proc.prog.clone()
};
writeln!(
&mut res,
"{:03} {:10} {:15} {:20} {:25}",
proc.pid, prog, alive, restart_policy, restarted
)?;
}
Ok(res)
}
fn make_command(cmd: &String, mut args: Arguments) -> Result<Command> {
match cmd.as_str() {
"hello" => Ok(Command::Hello),
"shutdown" => Ok(Command::Shutdown),
"start" => {
let restart_args: Option<String> =
args.opt_value_from_str("--restart")?;
let restart_policy = match restart_args {
None => RestartPolicy::Never,
Some(s) => match s.as_str() {
"never" => RestartPolicy::Never,
"always" => RestartPolicy::Always,
"on-failure" => {
let retries: u32 = args.value_from_str("--retries")?;
RestartPolicy::OnFailure(retries)
}
_ => bail!("unknown restart policy \"{}\"", s),
},
};
let prog_string = args.free_from_str::<PathBuf>()?;
let prog = if let Ok(c) = fs::canonicalize(&prog_string) {
c
} else {
prog_string
}
.into_os_string()
.into_string()
.expect("could not create string from given program name");
let cli_args: Vec<String> = args
.finish()
.iter()
.map(|s| s.clone().into_string().expect("invalid UTF-8"))
.collect::<Vec<String>>();
let working_dir = std::env::current_dir()?;
Ok(Command::Start(StartArgs {
prog,
args: cli_args,
restart_policy,
working_dir,
}))
}
"output" => {
let pid = args.free_from_str::<PID>()?;
Ok(Command::Output(pid))
}
"status" => {
let count: Option<usize> = args.opt_value_from_str("--tail")?;
Ok(Command::Status(count))
}
"stop" => {
let pid = args.free_from_str::<PID>()?;
Ok(Command::Stop(pid))
}
"restart" => {
let pid = args.free_from_str::<PID>()?;
Ok(Command::Restart(pid))
}
"send" => {
let pid = args.free_from_str::<PID>()?;
let input: String = args
.finish()
.iter()
.map(|s| s.clone().into_string().expect("invalid UTF-8"))
.collect::<Vec<String>>()
.join(" ");
Ok(Command::Send(SendArgs { pid, input }))
}
"change-policy" => {
let pid = args.free_from_str::<PID>()?;
let restart_policy = match args.free_from_str::<String>()?.as_str()
{
"never" => RestartPolicy::Never,
"always" => RestartPolicy::Always,
"on-failure" => {
let retries = args.free_from_str::<u32>()?;
RestartPolicy::OnFailure(retries)
}
_x => bail!("invalid restart policy \"{}\"", _x),
};
Ok(Command::ChangeRestartPolicy(ChangeRestartPolicyArgs {
pid,
restart_policy,
}))
}
_ => bail!("unknown command \"{}\"", cmd),
}
}
fn usage() -> String {
format!(
r#"sopht v{}
cute process manager
(slightly) more sophisticated than tmux
commands:
help
prints this message
hello
requests the server greet you. kinda like a ping command, but friendlier
shutdown
shuts the server down gracefully
start <...>
attempts to run the rest of the arguments <..> as a command and begins
managing the resulting process. pass "--restart <policy>" to set the restart
policy of the process. valid restart policies are "never", "always", and
"on-failure". if setting the restart policy to "on-failure", the
"--retries <retry-count>" option must also be specified.
output <pid>
completely retrives the output of the process with the specified PID
status
retrieves the status of all managed processes
stop <pid>
stops the process with the specified PID and sets its restart policy to
"never"
restart <pid>
stops the process with the specified PID and runs the same command again,
creating a new process with a different PID.
send <pid> <...>
sends the rest of the arguments <...> as input to the process with PID
<pid> a newline is automatically appended onto the output given to the
process.
change-policy <pid> <policy>
changes the policy of process with PID <pid> to the policy given by
<policy>. unlike the "start" command, the policy must be written as one
string like "on-failure 3" or "always".
"#,
env!("CARGO_PKG_VERSION")
)
}