pub mod error;
pub mod launchd;
use anyhow::Result;
use clap::Subcommand;
#[cfg(target_os = "macos")]
use serde::Serialize;
#[derive(Debug, clap::Args)]
pub struct Args {
#[command(subcommand)]
sub: ServerSub,
#[clap(long, global = true)]
bind: Option<String>,
#[clap(long, global = true)]
json: bool,
}
#[derive(Debug, Subcommand)]
enum ServerSub {
Install {
#[clap(long)]
cargo_bin: Option<std::path::PathBuf>,
#[clap(long)]
project_root: Option<std::path::PathBuf>,
},
Uninstall,
Bootstrap,
Bootout,
Start,
Stop,
Restart,
Status,
Logs {
#[clap(long, short = 'n')]
tail: Option<usize>,
},
}
pub async fn run(args: Args) -> Result<()> {
#[cfg(not(target_os = "macos"))]
{
let _ = &args;
return Err(error::ServerError::UnsupportedPlatform.into());
}
#[cfg(target_os = "macos")]
{
run_macos(args).await
}
}
#[cfg(target_os = "macos")]
async fn run_macos(args: Args) -> Result<()> {
let bind = args
.bind
.unwrap_or_else(|| launchd::DEFAULT_BIND.to_string());
match args.sub {
ServerSub::Install {
cargo_bin,
project_root,
} => {
let outcome = launchd::install(cargo_bin.as_deref(), project_root.as_deref())
.await
.map_err(anyhow::Error::from)?;
emit(&outcome, args.json, human_install(&outcome))
}
ServerSub::Uninstall => {
let outcome = launchd::uninstall().await.map_err(anyhow::Error::from)?;
emit(&outcome, args.json, human_uninstall(&outcome))
}
ServerSub::Bootstrap => {
let outcome = launchd::bootstrap().await.map_err(anyhow::Error::from)?;
emit(&outcome, args.json, human_bootstrap(&outcome))
}
ServerSub::Bootout => {
let outcome = launchd::bootout(&bind).await.map_err(anyhow::Error::from)?;
emit(&outcome, args.json, human_stop(&outcome))
}
ServerSub::Start => {
let outcome = launchd::start(&bind).await.map_err(anyhow::Error::from)?;
emit(&outcome, args.json, human_start(&outcome))
}
ServerSub::Stop => {
let outcome = launchd::shutdown(&bind)
.await
.map_err(anyhow::Error::from)?;
emit(&outcome, args.json, human_stop(&outcome))
}
ServerSub::Restart => {
let outcome = launchd::restart(&bind).await.map_err(anyhow::Error::from)?;
emit(&outcome, args.json, human_start(&outcome))
}
ServerSub::Status => {
let outcome = launchd::status(&bind).await;
emit(&outcome, args.json, human_status(&outcome))
}
ServerSub::Logs { tail } => {
let outcome = launchd::logs(tail).await.map_err(anyhow::Error::from)?;
emit(&outcome, args.json, human_logs(&outcome))
}
}
}
#[cfg(target_os = "macos")]
fn emit<T: Serialize>(outcome: &T, json: bool, human: String) -> Result<()> {
if json {
println!("{}", serde_json::to_string_pretty(outcome)?);
} else {
println!("{human}");
}
Ok(())
}
#[cfg(target_os = "macos")]
fn human_start(outcome: &launchd::StartOutcome) -> String {
match outcome {
launchd::StartOutcome::AlreadyRunning { bind } => {
format!("bind={bind} state=already_running")
}
launchd::StartOutcome::Started { bind } => format!("bind={bind} state=started"),
}
}
#[cfg(target_os = "macos")]
fn human_stop(outcome: &launchd::StopOutcome) -> String {
format!("bind={} stopped={}", outcome.bind, outcome.stopped)
}
#[cfg(target_os = "macos")]
fn human_status(outcome: &launchd::StatusOutcome) -> String {
let state = outcome.launchd_state.as_deref().unwrap_or("unknown");
let pid = outcome
.launchd_pid
.map(|p| p.to_string())
.unwrap_or_else(|| "none".to_string());
let last_exit = outcome
.launchd_last_exit_code
.map(|c| c.to_string())
.unwrap_or_else(|| "none".to_string());
format!(
"bind={} up={} state={state} pid={pid} last_exit={last_exit}",
outcome.bind, outcome.up
)
}
#[cfg(target_os = "macos")]
fn human_install(outcome: &launchd::InstallOutcome) -> String {
let bootstrap_state = match &outcome.bootstrap {
launchd::BootstrapOutcome::Bootstrapped { .. } => "bootstrapped",
launchd::BootstrapOutcome::AlreadyLoaded { .. } => "already_loaded",
};
format!(
"installed: {} (bootstrap={bootstrap_state})",
outcome.plist_path.display()
)
}
#[cfg(target_os = "macos")]
fn human_uninstall(outcome: &launchd::UninstallOutcome) -> String {
format!("uninstalled: {}", outcome.plist_path.display())
}
#[cfg(target_os = "macos")]
fn human_bootstrap(outcome: &launchd::BootstrapOutcome) -> String {
match outcome {
launchd::BootstrapOutcome::Bootstrapped { plist_path } => {
format!("bootstrapped: {}", plist_path.display())
}
launchd::BootstrapOutcome::AlreadyLoaded { plist_path } => {
format!("already_loaded: {}", plist_path.display())
}
}
}
#[cfg(target_os = "macos")]
fn human_logs(outcome: &launchd::LogsOutcome) -> String {
format!(
"stdout={} ({} lines) stderr={} ({} lines)",
outcome.stdout_path.display(),
outcome.stdout_tail.len(),
outcome.stderr_path.display(),
outcome.stderr_tail.len()
)
}