From 182e6fa930df0c7f091250f86a21c69e0e9791b1 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:40:26 -0600 Subject: [PATCH] wip --- docs/cli/commands.json | 23 ++++++++++++++---- docs/cli/index.md | 4 ++-- docs/cli/start.md | 8 ++++++- docs/cli/stop.md | 4 ++-- pitchfork.toml | 4 +++- pitchfork.usage.kdl | 3 ++- src/cli/disable.rs | 16 +------------ src/cli/enable.rs | 16 +------------ src/cli/run.rs | 54 +++++++----------------------------------- src/cli/start.rs | 18 ++++++++++---- src/cli/stop.rs | 21 ++++++++++------ src/daemon.rs | 9 +++++++ src/ipc/client.rs | 51 ++++++++++++++++++++++++++++++++++++++- src/ipc/mod.rs | 6 +++-- src/pitchfork_toml.rs | 1 + src/supervisor.rs | 35 ++++++++++++++++++--------- 16 files changed, 161 insertions(+), 112 deletions(-) diff --git a/docs/cli/commands.json b/docs/cli/commands.json index b516896..9125f42 100644 --- a/docs/cli/commands.json +++ b/docs/cli/commands.json @@ -415,7 +415,7 @@ "full_cmd": [ "start" ], - "usage": "start [ID]...", + "usage": "start [-f --force] [ID]...", "subcommands": {}, "args": [ { @@ -444,6 +444,20 @@ "required": true, "hide": false } + }, + { + "name": "force", + "usage": "-f --force", + "help": "Stop the daemon if it is already running", + "help_first_line": "Stop the daemon if it is already running", + "short": [ + "f" + ], + "long": [ + "force" + ], + "hide": false, + "global": false } ], "mounts": [], @@ -485,15 +499,16 @@ "full_cmd": [ "stop" ], - "usage": "stop ", + "usage": "stop [ID]...", "subcommands": {}, "args": [ { "name": "ID", - "usage": "", + "usage": "[ID]...", "help": "The name of the daemon to stop", "help_first_line": "The name of the daemon to stop", - "required": true, + "required": false, + "var": true, "hide": false } ], diff --git a/docs/cli/index.md b/docs/cli/index.md index f819377..1de9428 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -17,9 +17,9 @@ - [`pitchfork list [--hide-header]`](/cli/list.md) - [`pitchfork logs [-n ] [-t --tail] [ID]...`](/cli/logs.md) - [`pitchfork run [-f --force] [RUN]...`](/cli/run.md) -- [`pitchfork start [ID]...`](/cli/start.md) +- [`pitchfork start [-f --force] [ID]...`](/cli/start.md) - [`pitchfork status `](/cli/status.md) -- [`pitchfork stop `](/cli/stop.md) +- [`pitchfork stop [ID]...`](/cli/stop.md) - [`pitchfork supervisor `](/cli/supervisor.md) - [`pitchfork supervisor run [-f --force]`](/cli/supervisor/run.md) - [`pitchfork supervisor start [-f --force]`](/cli/supervisor/start.md) diff --git a/docs/cli/start.md b/docs/cli/start.md index 5609dc2..1270040 100644 --- a/docs/cli/start.md +++ b/docs/cli/start.md @@ -1,6 +1,6 @@ # `pitchfork start` -- **Usage**: `pitchfork start [ID]...` +- **Usage**: `pitchfork start [-f --force] [ID]...` - **Aliases**: `s` Starts a daemon from a pitchfork.toml file @@ -10,3 +10,9 @@ Starts a daemon from a pitchfork.toml file ### `[ID]...` ID of the daemon(s) in pitchfork.toml to start + +## Flags + +### `-f --force` + +Stop the daemon if it is already running diff --git a/docs/cli/stop.md b/docs/cli/stop.md index 1e529c7..5923446 100644 --- a/docs/cli/stop.md +++ b/docs/cli/stop.md @@ -1,12 +1,12 @@ # `pitchfork stop` -- **Usage**: `pitchfork stop ` +- **Usage**: `pitchfork stop [ID]...` - **Aliases**: `kill` Sends a stop signal to a daemon ## Arguments -### `` +### `[ID]...` The name of the daemon to stop diff --git a/pitchfork.toml b/pitchfork.toml index 62be79b..4a6e460 100644 --- a/pitchfork.toml +++ b/pitchfork.toml @@ -1 +1,3 @@ -[daemons] +[daemons.docs] +run = "mise run docs" +auto = ["start"] diff --git a/pitchfork.usage.kdl b/pitchfork.usage.kdl index 02dafa3..fed22ac 100644 --- a/pitchfork.usage.kdl +++ b/pitchfork.usage.kdl @@ -67,6 +67,7 @@ cmd "start" help="Starts a daemon from a pitchfork.toml file" { flag "--shell-pid" hide=true { arg "" } + flag "-f --force" help="Stop the daemon if it is already running" arg "[ID]..." help="ID of the daemon(s) in pitchfork.toml to start" var=true } cmd "status" help="Display the status of a daemon" { @@ -75,7 +76,7 @@ cmd "status" help="Display the status of a daemon" { } cmd "stop" help="Sends a stop signal to a daemon" { alias "kill" - arg "" help="The name of the daemon to stop" + arg "[ID]..." help="The name of the daemon to stop" var=true } cmd "supervisor" subcommand_required=true help="Start, stop, and check the status of the pitchfork supervisor daemon" { cmd "run" help="Runs the internal pitchfork daemon in the foreground" { diff --git a/src/cli/disable.rs b/src/cli/disable.rs index 3c60e50..de86e4a 100644 --- a/src/cli/disable.rs +++ b/src/cli/disable.rs @@ -1,5 +1,4 @@ use crate::ipc::client::IpcClient; -use crate::ipc::{IpcRequest, IpcResponse}; use crate::Result; /// Prevent a daemon from restarting @@ -13,20 +12,7 @@ pub struct Disable { impl Disable { pub async fn run(&self) -> Result<()> { let ipc = IpcClient::connect().await?; - let rsp = ipc - .request(IpcRequest::Disable { - id: self.id.clone(), - }) - .await?; - match rsp { - IpcResponse::Yes => { - info!("disabled daemon {}", self.id); - } - IpcResponse::No => { - info!("daemon {} already disabled", self.id); - } - rsp => unreachable!("unexpected response: {rsp:?}"), - } + ipc.disable(self.id.clone()).await?; Ok(()) } } diff --git a/src/cli/enable.rs b/src/cli/enable.rs index 8ac8e75..0ee8422 100644 --- a/src/cli/enable.rs +++ b/src/cli/enable.rs @@ -1,5 +1,4 @@ use crate::ipc::client::IpcClient; -use crate::ipc::{IpcRequest, IpcResponse}; use crate::Result; /// Allow a daemon to start @@ -13,20 +12,7 @@ pub struct Enable { impl Enable { pub async fn run(&self) -> Result<()> { let ipc = IpcClient::connect().await?; - let rsp = ipc - .request(IpcRequest::Enable { - id: self.id.clone(), - }) - .await?; - match rsp { - IpcResponse::Yes => { - info!("enabled daemon {}", self.id); - } - IpcResponse::No => { - info!("daemon {} already enabled", self.id); - } - rsp => unreachable!("unexpected response: {rsp:?}"), - } + ipc.enable(self.id.clone()).await?; Ok(()) } } diff --git a/src/cli/run.rs b/src/cli/run.rs index b7a0364..6d590ed 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -1,5 +1,5 @@ +use crate::daemon::RunOptions; use crate::ipc::client::IpcClient; -use crate::ipc::{IpcRequest, IpcResponse}; use crate::Result; use miette::bail; @@ -24,51 +24,13 @@ impl Run { let ipc = IpcClient::connect().await?; - if self.force { - let rsp = ipc - .request(IpcRequest::Stop { - id: self.id.clone(), - }) - .await?; - match rsp { - IpcResponse::Ok => { - info!("stopped daemon {}", self.id); - } - IpcResponse::DaemonAlreadyStopped => { - info!("daemon {} already stopped", self.id); - } - rsp => unreachable!("unexpected response: {rsp:?}"), - } - } - - let rsp = ipc - .request(IpcRequest::Run { - id: self.id.clone(), - cmd: self.run.clone(), - }) - .await?; - match rsp { - IpcResponse::DaemonAlreadyRunning => { - if self.force { - bail!("failed to stop daemon {}", self.id); - } else { - info!("daemon {} already running", self.id); - } - } - IpcResponse::DaemonStart { daemon } => { - info!( - "started daemon {} with pid {}", - daemon.id, - daemon.pid.unwrap() - ); - } - IpcResponse::DaemonFailed { error } => { - bail!("Failed to start daemon {}: {}", self.id, error); - } - msg => { - debug!("ignoring message: {:?}", msg); - } - } + ipc.run(RunOptions { + id: self.id.clone(), + cmd: self.run.clone(), + shell_pid: None, + force: self.force, + }) + .await?; Ok(()) } } diff --git a/src/cli/start.rs b/src/cli/start.rs index dc563aa..f9cb482 100644 --- a/src/cli/start.rs +++ b/src/cli/start.rs @@ -1,8 +1,8 @@ +use crate::daemon::RunOptions; use crate::ipc::client::IpcClient; -use crate::ipc::IpcRequest; use crate::pitchfork_toml::PitchforkToml; use crate::Result; -use miette::IntoDiagnostic; +use miette::{ensure, IntoDiagnostic}; /// Starts a daemon from a pitchfork.toml file #[derive(Debug, clap::Args)] @@ -12,18 +12,28 @@ pub struct Start { id: Vec, #[clap(long, hide = true)] shell_pid: Option, + /// Stop the daemon if it is already running + #[clap(short, long)] + force: bool, } impl Start { pub async fn run(&self) -> Result<()> { + ensure!( + !self.id.is_empty(), + "At least one daemon ID must be provided" + ); let pt = PitchforkToml::all_merged(); let ipc = IpcClient::connect().await?; for id in &self.id { let daemon = pt.daemons.get(id); if let Some(daemon) = daemon { - ipc.request(IpcRequest::Run { + let cmd = shell_words::split(&daemon.run).into_diagnostic()?; + ipc.run(RunOptions { id: id.clone(), - cmd: shell_words::split(&daemon.run.clone()).into_diagnostic()?, + cmd, + shell_pid: self.shell_pid, + force: self.force, }) .await?; } else { diff --git a/src/cli/stop.rs b/src/cli/stop.rs index 62a173f..8d277cd 100644 --- a/src/cli/stop.rs +++ b/src/cli/stop.rs @@ -1,26 +1,33 @@ use crate::cli::supervisor::kill_or_stop; use crate::state_file::StateFile; use crate::Result; +use miette::ensure; /// Sends a stop signal to a daemon #[derive(Debug, clap::Args)] #[clap(visible_alias = "kill", verbatim_doc_comment)] pub struct Stop { /// The name of the daemon to stop - id: String, + id: Vec, } impl Stop { pub async fn run(&self) -> Result<()> { + ensure!( + !self.id.is_empty(), + "You must provide at least one daemon to stop" + ); let pid_file = StateFile::get(); - if let Some(d) = pid_file.daemons.get(&self.id) { - if let Some(pid) = d.pid { - info!("stopping {} with pid {}", self.id, pid); - kill_or_stop(pid, true).await?; - return Ok(()); + for id in &self.id { + if let Some(d) = pid_file.daemons.get(id) { + if let Some(pid) = d.pid { + info!("stopping {} with pid {}", id, pid); + kill_or_stop(pid, true).await?; + continue; + } } + warn!("{} is not running", id); } - warn!("{} is not running", self.id); Ok(()) } } diff --git a/src/daemon.rs b/src/daemon.rs index 7b1711f..6d4e4c5 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -6,9 +6,18 @@ pub struct Daemon { pub id: String, pub title: Option, pub pid: Option, + pub shell_pid: Option, pub status: DaemonStatus, } +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct RunOptions { + pub id: String, + pub cmd: Vec, + pub force: bool, + pub shell_pid: Option, +} + impl Display for Daemon { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.id) diff --git a/src/ipc/client.rs b/src/ipc/client.rs index d5d1670..e8f437a 100644 --- a/src/ipc/client.rs +++ b/src/ipc/client.rs @@ -1,3 +1,4 @@ +use crate::daemon::RunOptions; use crate::ipc::{deserialize, fs_name, serialize, IpcRequest, IpcResponse}; use crate::{supervisor, Result}; use exponential_backoff::Backoff; @@ -91,7 +92,7 @@ impl IpcClient { } } - pub async fn request(&self, msg: IpcRequest) -> Result { + async fn request(&self, msg: IpcRequest) -> Result { self.send(msg).await?; loop { if let Some(msg) = self.read().await { @@ -99,4 +100,52 @@ impl IpcClient { } } } + + pub async fn enable(&self, id: String) -> Result { + let rsp = self.request(IpcRequest::Enable { id: id.clone() }).await?; + match rsp { + IpcResponse::Yes => { + info!("enabled daemon {}", id); + Ok(true) + } + IpcResponse::No => { + info!("daemon {} already enabled", id); + Ok(false) + } + rsp => unreachable!("unexpected response: {rsp:?}"), + } + } + + pub async fn disable(&self, id: String) -> Result { + let rsp = self.request(IpcRequest::Disable { id: id.clone() }).await?; + match rsp { + IpcResponse::Yes => { + info!("disabled daemon {}", id); + Ok(true) + } + IpcResponse::No => { + info!("daemon {} already disabled", id); + Ok(false) + } + rsp => unreachable!("unexpected response: {rsp:?}"), + } + } + + pub async fn run(&self, opts: RunOptions) -> Result<()> { + info!("starting daemon {}", opts.id); + let rsp = self.request(IpcRequest::Run(opts.clone())).await?; + match rsp { + IpcResponse::DaemonStart { daemon } => { + info!("started daemon {}", daemon); + } + IpcResponse::DaemonAlreadyRunning => { + warn!("daemon {} already running", opts.id); + } + IpcResponse::DaemonFailed { error } => { + bail!("failed to start daemon {}: {error}", opts.id); + } + rsp => unreachable!("unexpected response: {rsp:?}"), + } + Ok(()) + } } diff --git a/src/ipc/mod.rs b/src/ipc/mod.rs index 0dc2577..474eb4a 100644 --- a/src/ipc/mod.rs +++ b/src/ipc/mod.rs @@ -1,4 +1,4 @@ -use crate::daemon::Daemon; +use crate::daemon::{Daemon, RunOptions}; use crate::env; use crate::Result; use interprocess::local_socket::{GenericFilePath, Name, ToFsName}; @@ -25,7 +25,8 @@ pub enum IpcMessage { pub enum IpcRequest { Connect, Stop { id: String }, - Run { id: String, cmd: Vec }, + GetActiveDaemons, + Run(RunOptions), Enable { id: String }, Disable { id: String }, } @@ -36,6 +37,7 @@ pub enum IpcResponse { Yes, No, Error(String), + ActiveDaemons(Vec), DaemonAlreadyStopped, DaemonAlreadyRunning, DaemonStart { daemon: Daemon }, diff --git a/src/pitchfork_toml.rs b/src/pitchfork_toml.rs index 3e501d6..23633a9 100644 --- a/src/pitchfork_toml.rs +++ b/src/pitchfork_toml.rs @@ -67,6 +67,7 @@ impl PitchforkToml { #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct PitchforkTomlDaemon { pub run: String, + #[serde(skip_serializing_if = "Vec::is_empty")] pub auto: Vec, } diff --git a/src/supervisor.rs b/src/supervisor.rs index b144a54..fbe8e79 100644 --- a/src/supervisor.rs +++ b/src/supervisor.rs @@ -1,4 +1,4 @@ -use crate::daemon::Daemon; +use crate::daemon::{Daemon, RunOptions}; use crate::daemon_status::DaemonStatus; use crate::ipc::server::IpcServer; use crate::ipc::{IpcRequest, IpcResponse}; @@ -67,7 +67,7 @@ impl Supervisor { let pid = std::process::id(); info!("Starting supervisor with pid {pid}"); - self.upsert_daemon("pitchfork", Some(pid), DaemonStatus::Running) + self.upsert_daemon("pitchfork", Some(pid), DaemonStatus::Running, None) .await?; self.interval_watch()?; @@ -85,12 +85,18 @@ impl Supervisor { Ok(()) } - async fn run(&self, id: &str, cmd: Vec) -> Result { + async fn run(&self, opts: RunOptions) -> Result { + let id = &opts.id; + let cmd = opts.cmd; let daemon = self.get_daemon(id).await; if let Some(daemon) = daemon { if let Some(pid) = daemon.pid { - warn!("daemon {id} already running with pid {}", pid); - return Ok(IpcResponse::DaemonAlreadyRunning); + if opts.force { + self.stop(id).await?; + } else { + warn!("daemon {id} already running with pid {pid}"); + return Ok(IpcResponse::DaemonAlreadyRunning); + } } } let cmd = once("exec".to_string()) @@ -110,7 +116,7 @@ impl Supervisor { let pid = child.id().unwrap(); info!("started daemon {id} with pid {pid}"); let daemon = self - .upsert_daemon(id, Some(pid), DaemonStatus::Running) + .upsert_daemon(id, Some(pid), DaemonStatus::Running, opts.shell_pid) .await?; let name = id.to_string(); tokio::spawn(async move { @@ -152,18 +158,18 @@ impl Supervisor { info!("daemon {name} exited with status {status}"); if status.success() { SUPERVISOR - .upsert_daemon(&name, None, DaemonStatus::Stopped) + .upsert_daemon(&name, None, DaemonStatus::Stopped, None) .await .unwrap(); } else { SUPERVISOR - .upsert_daemon(&name, None, DaemonStatus::Errored(status.code())) + .upsert_daemon(&name, None, DaemonStatus::Errored(status.code()), None) .await .unwrap(); } } else { SUPERVISOR - .upsert_daemon(&name, None, DaemonStatus::Errored(None)) + .upsert_daemon(&name, None, DaemonStatus::Errored(None), None) .await .unwrap(); } @@ -178,7 +184,8 @@ impl Supervisor { if let Some(pid) = daemon.pid { PROCS.refresh_processes(); if PROCS.is_running(pid) { - self.upsert_daemon(id, None, DaemonStatus::Stopped).await?; + self.upsert_daemon(id, None, DaemonStatus::Stopped, None) + .await?; PROCS.kill_async(pid).await?; } return Ok(IpcResponse::Ok); @@ -307,7 +314,7 @@ impl Supervisor { IpcResponse::Ok } IpcRequest::Stop { id } => self.stop(&id).await?, - IpcRequest::Run { id, cmd } => self.run(&id, cmd).await?, + IpcRequest::Run(opts) => self.run(opts).await?, IpcRequest::Enable { id } => { if self.enable(id).await? { IpcResponse::Yes @@ -322,6 +329,10 @@ impl Supervisor { IpcResponse::No } } + IpcRequest::GetActiveDaemons => { + let daemons = self.active_daemons().await; + IpcResponse::ActiveDaemons(daemons) + } }; Ok(rsp) } @@ -364,6 +375,7 @@ impl Supervisor { id: &str, pid: Option, status: DaemonStatus, + shell_pid: Option, ) -> Result { info!("upserting daemon: {id} pid: {pid:?} status: {status}"); let mut state_file = self.state_file.lock().await; @@ -372,6 +384,7 @@ impl Supervisor { title: pid.and_then(|pid| PROCS.title(pid)), pid, status, + shell_pid, }; state_file.daemons.insert(id.to_string(), daemon.clone()); if let Err(err) = state_file.write() {