From 46fb815d95773fb0105d3034d5a6491569f66db7 Mon Sep 17 00:00:00 2001 From: James Westby Date: Thu, 1 Aug 2024 21:03:38 +0100 Subject: [PATCH] Add a simple status command --- src/cmd/mod.rs | 8 ++++- src/cmd/status.rs | 56 ++++++++++++++++++++++++++++++++ src/commands.rs | 29 +++++++++++++++++ src/target.rs | 20 ++++++++++++ src/targets/command/container.rs | 20 ++++++++++-- src/targets/command/exec.rs | 13 ++++++-- tests/test_start.rs | 25 ++++++++++++++ tests/test_status.rs | 23 +++++++++++++ 8 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 src/cmd/status.rs create mode 100644 tests/test_start.rs create mode 100644 tests/test_status.rs diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 6f6ebf5..522be5f 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -8,6 +8,7 @@ mod execute; mod list; mod run; mod start; +mod status; mod stop; use crate::cleanup::CleanupManager; @@ -17,6 +18,7 @@ pub use execute::Execute; use list::ListCommand; use run::RunCommand; use start::StartCommand; +use status::StatusCommand; use stop::StopCommand; #[derive(Parser, Debug)] @@ -54,7 +56,10 @@ pub enum Commands { /// List available targets List(ListCommand), - // TODO: status, logs + + /// Get the status of a daemon + Status(StatusCommand), + // TODO: logs } impl Execute for Commands { @@ -65,6 +70,7 @@ impl Execute for Commands { Commands::Stop(cmd) => cmd.execute(context, cleanup_manager), Commands::Build(cmd) => cmd.execute(context, cleanup_manager), Commands::List(cmd) => cmd.execute(context, cleanup_manager), + Commands::Status(cmd) => cmd.execute(context, cleanup_manager), } } } diff --git a/src/cmd/status.rs b/src/cmd/status.rs new file mode 100644 index 0000000..cd02a34 --- /dev/null +++ b/src/cmd/status.rs @@ -0,0 +1,56 @@ +use std::sync::{Arc, Mutex}; + +use anyhow::{anyhow, Result}; +use clap::Parser; + +use crate::cleanup::CleanupManager; +use crate::cmd::execute::Execute; +use crate::context::{CommandLookupResult, Context}; +use crate::outputs::OutputsManager; +use crate::target::{StatusResult, Targetable}; + +#[derive(Parser, Debug)] +pub struct StatusCommand { + /// The name of the target to get status for + pub name: String, +} + +impl Execute for StatusCommand { + fn execute( + &self, + context: Context, + _cleanup_manager: Arc>, + ) -> Result<()> { + let mut outputs = OutputsManager::default(); + match context.get_target(self.name.as_str()) { + CommandLookupResult::Found(target) => { + let builder = target.as_startable(); + if let Some(builder) = builder { + match builder.status(&context, &mut outputs) { + Ok(StatusResult::Running(msg)) => Ok(println!("[{}] {}", target.target_info().name, msg.as_str())), + Ok(StatusResult::NotRunning()) => Ok(println!("[{}] Not running", target.target_info().name)), + Err(e) => Err(e), + } + } else { + Err(anyhow!( + "Target <{}> is not startable", + self.name + )) + } + }, + CommandLookupResult::NotFound => { + Err(anyhow!( + "Target <{}> not found in config file <{}>", + self.name, + context.config_path + )) + }, + CommandLookupResult::Duplicates(duplicates) => { + Err(anyhow!( + "Target <{}> is ambiguous, possible values are <{}>, please specify the command to run using one of those names", + self.name, duplicates.join(", ") + )) + }, + } + } +} diff --git a/src/commands.rs b/src/commands.rs index c23fbb6..fb0bd79 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -200,6 +200,35 @@ pub fn stop_using_pidfile(pid_path: &std::path::PathBuf, on_stop: impl Fn()) -> Ok(()) } +pub fn status_using_pidfile(pid_path: &std::path::PathBuf) -> Result> { + let mut pid_str = std::fs::read_to_string(pid_path); + match pid_str { + Err(e) => match e.kind() { + std::io::ErrorKind::NotFound => Ok(None), + _ => Err(anyhow!( + "Error reading pid file for target at <{}>: {}", + pid_path.display(), + e + )), + }, + Ok(ref mut pid_str) => { + let pid_str = pid_str.trim().to_string(); + debug!( + "Found pid <{}> for target at <{}>", + pid_str, + pid_path.display() + ); + + let pid = nix::unistd::Pid::from_raw(pid_str.parse::()?); + if is_process_alive(pid) { + Ok(Some(format!("Process running with pid <{}>", pid))) + } else { + Ok(None) + } + } + } +} + /* use daemonize::{Daemonize, Outcome}; let daemonize = Daemonize::new() diff --git a/src/target.rs b/src/target.rs index 4652c48..a7d40a1 100644 --- a/src/target.rs +++ b/src/target.rs @@ -532,6 +532,10 @@ impl Startable for Command { // TODO: last run file? Ok(()) } + + fn status(&self, context: &Context, outputs: &mut OutputsManager) -> Result { + self.inner_as_startable().status(context, outputs) + } } impl Command { @@ -567,6 +571,20 @@ pub trait Runnable { ) -> Result<()>; } +pub enum StatusResult { + Running(String), + NotRunning(), +} + +impl From> for StatusResult { + fn from(val: Option) -> Self { + match val { + None => StatusResult::NotRunning(), + Some(s) => StatusResult::Running(s), + } + } +} + pub trait Startable { fn start( &self, @@ -582,6 +600,8 @@ pub trait Startable { outputs: &mut OutputsManager, cleanup_manager: Arc>, ) -> Result<()>; + + fn status(&self, context: &Context, outputs: &mut OutputsManager) -> Result; } pub trait Buildable { diff --git a/src/targets/command/container.rs b/src/targets/command/container.rs index 12b259f..1a997de 100644 --- a/src/targets/command/container.rs +++ b/src/targets/command/container.rs @@ -6,14 +6,18 @@ use log::{debug, info}; use validator::Validate; use crate::cleanup::CleanupManager; -use crate::commands::{run_command, spawn_command_with_pidfile, stop_using_pidfile}; +use crate::commands::{ + run_command, spawn_command_with_pidfile, status_using_pidfile, stop_using_pidfile, +}; use crate::config::ContainerCommand as ConfigContainerCommand; use crate::context::Context; use crate::default::{default_optional, default_to}; use crate::outputs::OutputsManager; use crate::rand::rand_string; use crate::shell::{escape_and_prepend, escape_and_prepend_vec, escape_string}; -use crate::target::{create_metadata_dir, CommandInfo, Runnable, Startable, TargetInfo}; +use crate::target::{ + create_metadata_dir, CommandInfo, Runnable, Startable, StatusResult, TargetInfo, +}; #[derive(Debug, Clone, Validate)] pub struct ContainerCommand { @@ -189,6 +193,18 @@ impl Startable for ContainerCommand { }; stop_using_pidfile(&pid_path, log_stop) } + + fn status(&self, _context: &Context, _outputs: &mut OutputsManager) -> Result { + let config_dir = create_metadata_dir(self.target_info.name.to_string().as_str())?; + + let pid_path = config_dir.join("pid"); + debug!( + "Searching for pid file for target <{}> at <{}>", + self.target_info.name, + pid_path.display() + ); + status_using_pidfile(&pid_path).map(|s| s.into()) + } } pub struct ContainerRunInfo { diff --git a/src/targets/command/exec.rs b/src/targets/command/exec.rs index 8ccdd2d..2a711f0 100644 --- a/src/targets/command/exec.rs +++ b/src/targets/command/exec.rs @@ -5,13 +5,15 @@ use log::{debug, info}; use validator::Validate; use crate::cleanup::CleanupManager; -use crate::commands::{run_command_with_env, spawn_command_with_pidfile, stop_using_pidfile}; +use crate::commands::{ + run_command_with_env, spawn_command_with_pidfile, status_using_pidfile, stop_using_pidfile, +}; use crate::config::ExecCommand as ConfigExecCommand; use crate::context::Context; use crate::default::{default_optional, default_to}; use crate::outputs::OutputsManager; use crate::target::create_metadata_dir; -use crate::target::{CommandInfo, Runnable, Startable, TargetInfo}; +use crate::target::{CommandInfo, Runnable, Startable, StatusResult, TargetInfo}; #[derive(Debug, Clone, Validate)] pub struct ExecCommand { @@ -147,4 +149,11 @@ impl Startable for ExecCommand { }; stop_using_pidfile(&pid_path, log_stop) } + + fn status(&self, _context: &Context, _outputs: &mut OutputsManager) -> Result { + let config_dir = create_metadata_dir(self.target_info.name.to_string().as_str())?; + + let pid_path = config_dir.join("pid"); + status_using_pidfile(&pid_path).map(|s| s.into()) + } } diff --git a/tests/test_start.rs b/tests/test_start.rs new file mode 100644 index 0000000..cae0c53 --- /dev/null +++ b/tests/test_start.rs @@ -0,0 +1,25 @@ +use assert_cmd::prelude::*; + +mod common; + +#[test] +fn test_start() { + let config_src = r#" + [command.exec.do_stuff] + command = "bash -c '$i=0; while $i<10; do i+=1; date; sleep 1; done'" + daemon = true + "#; + + let test_context = common::TestContext::new(); + test_context.write_config(config_src); + + let mut cmd = test_context.get_command(); + cmd.arg("start").arg("do_stuff"); + + cmd.assert().success(); + + let mut cmd = test_context.get_command(); + cmd.arg("stop").arg("do_stuff"); + + cmd.assert().success(); +} diff --git a/tests/test_status.rs b/tests/test_status.rs new file mode 100644 index 0000000..c866011 --- /dev/null +++ b/tests/test_status.rs @@ -0,0 +1,23 @@ +use assert_cmd::prelude::*; +use predicates::prelude::*; + +mod common; + +#[test] +fn test_status_not_started() { + let config_src = r#" + [command.exec.do_stuff] + command = "bash -c '$i=0; while $i<10; do i+=1; date; sleep 1; done'" + daemon = true + "#; + + let test_context = common::TestContext::new(); + test_context.write_config(config_src); + + let mut cmd = test_context.get_command(); + cmd.arg("status").arg("do_stuff"); + + cmd.assert().success().stdout(predicate::str::contains( + "[command.exec.do_stuff] Not running", + )); +}