From af29785692d31dd7e095bd14474d4481b32ae57b Mon Sep 17 00:00:00 2001 From: Federico Ponzi Date: Sat, 2 Nov 2024 13:37:05 +0000 Subject: [PATCH] horustctl: initial client side implementation and support for status command --- Cargo.lock | 5 ++ horustctl/Cargo.toml | 7 +++ horustctl/README.md | 14 +++++ horustctl/src/main.rs | 93 +++++++++++++++++---------------- horustctl/tests/cli.rs | 116 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 191 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aa9d86f..e8c5202 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -531,10 +531,15 @@ name = "horustctl" version = "0.1.0" dependencies = [ "anyhow", + "assert_cmd", "clap", "crossbeam", "env_logger", + "horust-commands-lib", "log", + "predicates", + "rand 0.8.5", + "tempdir", ] [[package]] diff --git a/horustctl/Cargo.toml b/horustctl/Cargo.toml index adb4f26..8183c8d 100644 --- a/horustctl/Cargo.toml +++ b/horustctl/Cargo.toml @@ -10,3 +10,10 @@ log = "~0.4" env_logger = "~0.11" crossbeam = "~0.8" clap = { version = "~4.5", features = ["derive"] } +horust-commands-lib = { path = "../commands" } + +[dev-dependencies] +assert_cmd = "~2.0" +predicates = "~3.1" +rand = "~0.8" +tempdir = "~0.3" diff --git a/horustctl/README.md b/horustctl/README.md index e69de29..0ce0968 100644 --- a/horustctl/README.md +++ b/horustctl/README.md @@ -0,0 +1,14 @@ +## Horustctl + +A command line interface to interact with an horust process. It works using Unix Domain Sockets. +Each horust process will create a new uds socket inside /var/run/horust/horust-.sock folder (can be configured). + +You can use horustctl to interact with your horust process. The communication is handled by the `commands` crate. They +exchange protobuf messages. + +Supported commands: + +* status [servicename]: get the status of your service `servicename`. If not specified, it will return the status for + all services. +* change : can be used to change the status of `servicename`. + Supported `newstatus` options are start, stop. diff --git a/horustctl/src/main.rs b/horustctl/src/main.rs index c604a04..8f6cf92 100644 --- a/horustctl/src/main.rs +++ b/horustctl/src/main.rs @@ -1,23 +1,26 @@ -use anyhow::{bail, Result}; +use anyhow::{anyhow, bail, Result}; use clap::{Args, Parser, Subcommand}; use env_logger::Env; +use horust_commands_lib::{get_path, ClientHandler}; use log::debug; -use std::env; use std::fs::read_dir; -use std::io::Write; -use std::os::unix::net::UnixStream; +use std::os::unix::fs::FileTypeExt; use std::path::PathBuf; /// Simple program to greet a person #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] -struct HourstctlArgs { - /// Optional if only one horust is running in the system. +struct HorustctlArgs { + /// The pid of the horust process you want to query. Optional if only one horust is running in the system. #[arg(short, long)] - pid: Option, + pid: Option, #[arg(short, long, default_value = "/var/run/horust/")] - sockets_folder_path: PathBuf, + uds_folder_path: PathBuf, + + // Specify the full path of the socket. It takes precedence other over arguments. + #[arg(long)] + socket_path: Option, #[command(subcommand)] commands: Commands, @@ -35,20 +38,29 @@ struct StatusArgs { fn main() -> Result<()> { env_logger::Builder::from_env(Env::default().default_filter_or("debug")).init(); - let args = HourstctlArgs::parse(); + let args = HorustctlArgs::parse(); debug!("args: {args:?}"); - let uds_path = get_uds_path(args.pid, args.sockets_folder_path)?; + let uds_path = args.socket_path.unwrap_or_else(|| { + get_uds_path(args.pid, args.uds_folder_path).expect("Failed to get uds_path.") + }); + let mut uds_handler = ClientHandler::new_client(&uds_path)?; match &args.commands { Commands::Status(status_args) => { debug!("Status command received: {status_args:?}"); - debug!("uds path : {uds_path:?}") + debug!("uds path : {uds_path:?}"); + let (service_name, service_status) = + uds_handler.send_status_request(status_args.service_name.clone().unwrap())?; + println!( + "Current status for '{service_name}' is: '{}'.", + service_status.as_str_name() + ); } } Ok(()) } -fn get_uds_path(pid: Option, sockets_folder_path: PathBuf) -> Result { +fn get_uds_path(pid: Option, sockets_folder_path: PathBuf) -> Result { if !sockets_folder_path.exists() { bail!("the specified sockets folder path '{sockets_folder_path:?}' does not exists."); } @@ -56,38 +68,31 @@ fn get_uds_path(pid: Option, sockets_folder_path: PathBuf) -> Result 0 { - bail!("There is more than one socket in {sockets_folder_path:?}.Please use --pid to specify the pid of the horust process you want to talk to."); - } - ret - } else { - pid.map(|p| format!("{p}.uds")).unwrap() - }; - debug!("Socket filename: {socket_file_name}"); - Ok(sockets_folder_path.join(socket_file_name)) -} - -fn handle_status(socket_path: PathBuf) -> Result<()> { - // `args` returns the arguments passed to the program - let args: Vec = env::args().map(|x| x.to_string()).collect(); + let socket_path = match pid { + None => { + let mut readdir_iter = read_dir(&sockets_folder_path)? + .filter_map(|d| d.ok()) // unwrap results + .filter_map(|d| -> Option { + let is_socket = d.file_type().ok()?.is_socket(); + let name = d.file_name(); + let name = name.to_string_lossy(); + if is_socket && name.starts_with("horust-") && name.ends_with(".sock") { + Some(name.to_string()) + } else { + None + } + }); - // Connect to socket - let mut stream = match UnixStream::connect(&socket_path) { - Err(_) => panic!("server is not running"), - Ok(stream) => stream, + let ret = readdir_iter + .next() + .ok_or_else(|| anyhow!("No socket found in {sockets_folder_path:?}"))?; + if readdir_iter.count() > 0 { + bail!("There is more than one socket in {sockets_folder_path:?}.Please use --pid to specify the pid of the horust process you want to talk to."); + } + sockets_folder_path.join(ret) + } + Some(pid) => get_path(&sockets_folder_path, pid), }; - - // Send message - if let Err(_) = stream.write(b"hello") { - panic!("couldn't send message") - } - Ok(()) + debug!("Socket filename: {socket_path:?}"); + Ok(socket_path) } diff --git a/horustctl/tests/cli.rs b/horustctl/tests/cli.rs index 8b13789..33a6736 100644 --- a/horustctl/tests/cli.rs +++ b/horustctl/tests/cli.rs @@ -1 +1,117 @@ +use assert_cmd::Command; +use predicates::boolean::PredicateBooleanExt; +use predicates::str::contains; +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; +use std::path::Path; +use std::thread; +use std::time::Duration; +use tempdir::TempDir; +/// Creates script and service file, and stores them in dir. +/// It will append a `command` at the top of the service, with a reference to script. +/// Returns the service name. +/// if service_name is None a random name will be used. +pub fn store_service_script( + dir: &Path, + script: &str, + service_content: Option<&str>, + filename: Option<&str>, +) -> String { + let rnd_name = thread_rng() + .sample_iter(&Alphanumeric) + .take(5) + .map(|x| x as char) + .collect::(); + let service_name = format!("{}.toml", filename.unwrap_or(rnd_name.as_str())); + let script_name = format!("{}.sh", rnd_name); + let script_path = dir.join(script_name); + std::fs::write(&script_path, script).unwrap(); + let service = format!( + r#"command = "/usr/bin/env bash {}" +{}"#, + script_path.display(), + service_content.unwrap_or("") + ); + std::fs::write(dir.join(&service_name), service).unwrap(); + service_name +} + +#[test] +fn test_cli_help() { + let mut cmd = Command::cargo_bin("horustctl").unwrap(); + cmd.args(vec!["--help"]).assert().success(); +} + +static ENVIRONMENT_SCRIPT: &str = r#"#!/usr/bin/env bash +printenv"#; + +#[test] +fn test_cli_status() { + let temp_dir = TempDir::new("horustctl").unwrap(); + let mut horust_cmd = Command::cargo_bin("horust").unwrap(); + + horust_cmd.current_dir(&temp_dir).args(vec![ + "--services-path", + temp_dir.path().display().to_string().as_str(), + "--uds-folder-path", + temp_dir.path().display().to_string().as_str(), + ]); + + store_service_script( + temp_dir.path(), + ENVIRONMENT_SCRIPT, + None, + Some("terminated"), + ); + horust_cmd.assert().success().stdout(contains("bar").not()); + // Exit after 5 seconds. + store_service_script( + temp_dir.path(), + r#"#!/usr/bin/env bash + trap 'quit=1' USR1 + touch file +i=0; +while [ "$i" -lt 5 ]; do + sleep 1 +done"#, + None, + Some("running"), + ); + + thread::spawn(move || { + horust_cmd.assert().success().stdout(contains("bar")); + }); + let mut total_wait = 0; + const MAX_WAIT_TIME: u32 = 1000; + // created by running script + while !temp_dir.path().join("file").exists() && total_wait < MAX_WAIT_TIME { + total_wait += 50; + thread::sleep(Duration::from_millis(50)); + } + let mut horustctl_cmd = Command::cargo_bin("horustctl").unwrap(); + horustctl_cmd + .current_dir(&temp_dir) + .args(vec![ + "--uds-folder-path", + temp_dir.path().display().to_string().as_str(), + "status", + "terminated.toml", + ]) + .assert() + .success() + .stdout(contains("terminated")); + + let mut horustctl_cmd = Command::cargo_bin("horustctl").unwrap(); + horustctl_cmd + .current_dir(&temp_dir) + .args(vec![ + "--uds-folder-path", + temp_dir.path().display().to_string().as_str(), + "status", + "running.toml", + ]) + .assert() + .success() + .stdout(contains("running")); +}