diff --git a/Cargo.toml b/Cargo.toml index 39fa427..d753c67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,4 @@ edition = "2021" [dependencies] clap = { version = "4.5.23", features = ["derive"] } +lazy-regex = "3.3.0" diff --git a/src/main.rs b/src/main.rs index bbbbfa9..098a0c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,17 @@ +mod server; + use clap::{Parser, Subcommand, ValueEnum}; +use lazy_regex::{lazy_regex, Lazy, Regex}; +use server::{Server, ServerReference}; use std::cell::LazyCell; use std::env; -use std::error::Error; -use std::fmt::{Display, Formatter}; -use std::hash::{Hash, Hasher}; -use std::ops::Deref; +use std::hash::Hash; use std::path::PathBuf; +use std::process::Stdio; use std::str::FromStr; const SERVERS_ENV_VAR: &str = "MSSH_SERVERS"; +type ShellCmd = std::process::Command; /// Uploads a file or executes a command on multiple configured servers /// @@ -42,6 +45,9 @@ enum Command { /// The directory where to upload to #[arg(short = 'p', long, default_value = "plugins")] upload_directory: PathBuf, + /// Skip the confirmation dialog + #[arg(long, default_value = "false", default_missing_value = "true", num_args = 0..1)] + no_confirm: bool, }, /// Execute a command on the servers #[command(visible_short_flag_alias = 'c')] @@ -62,162 +68,6 @@ enum OldVersionPolicy { Delete, } -#[derive(Debug, Clone)] -enum ServerReference { - Resolved(Server), - Name(String), -} - -impl ServerReference { - pub fn get_name(&self) -> &str { - match self { - ServerReference::Resolved(server) => &server.ssh_name, - ServerReference::Name(name) => name, - } - } - - #[allow(dead_code)] - pub fn resolve(self, configured_servers: &[Server]) -> Option { - match self { - ServerReference::Resolved(server) => Some(server), - ServerReference::Name(name) => Self::resolve_server_name(&name, configured_servers), - } - } - - #[allow(dead_code)] - pub fn resolve_lazy(self, provider: &mut LazyCell) -> Option - where - S: Deref, - F: FnOnce() -> S, - { - match self { - ServerReference::Resolved(server) => Some(server), - ServerReference::Name(name) => Self::resolve_server_name(&name, provider), - } - } - - pub fn try_resolve_lazy( - self, - provider: &mut LazyCell, F>, - ) -> Result, E> - where - S: Deref, - F: FnOnce() -> Result, - E: Clone, - { - match self { - ServerReference::Resolved(server) => Ok(Some(server)), - ServerReference::Name(name) => provider - .as_ref() - .map_err(|e| e.clone()) - .map(|servers| Self::resolve_server_name(&name, servers)), - } - } - - fn resolve_server_name(name: &str, servers: &[Server]) -> Option { - servers - .iter() - .find(|server| server.ssh_name == name) - .cloned() - } -} - -impl FromStr for ServerReference { - type Err = ServerReferenceParseError; - - fn from_str(s: &str) -> Result { - Server::from_str(s) - .map(Self::Resolved) - .or_else(|_| Ok(Self::Name(s.to_string()))) - } -} - -impl PartialEq for ServerReference { - fn eq(&self, other: &Self) -> bool { - self.get_name() == other.get_name() - } -} - -impl Eq for ServerReference {} - -impl Hash for ServerReference { - fn hash(&self, state: &mut H) { - self.get_name().hash(state); - } -} - -impl Display for ServerReference { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - ServerReference::Resolved(server) => write!(f, "{}", server), - ServerReference::Name(name) => write!(f, "{}", name), - } - } -} - -#[derive(Debug)] -enum ServerReferenceParseError {} - -impl Display for ServerReferenceParseError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) //replace that with an actual implementation if there ever are any variants - } -} - -impl Error for ServerReferenceParseError {} - -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -struct Server { - pub ssh_name: String, - pub server_directory_path: PathBuf, -} - -impl FromStr for Server { - type Err = ServerParseError; - - fn from_str(s: &str) -> Result { - s.split_once(':') - .ok_or(ServerParseError::MissingServerDirectory) - .map(|(name, directory)| Self { - ssh_name: name.to_string(), - server_directory_path: PathBuf::from(directory), - }) - } -} - -impl Display for Server { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}:{:?}", self.ssh_name, self.server_directory_path) - } -} - -#[derive(Debug)] -enum ServerParseError { - MissingServerDirectory, -} - -impl Display for ServerParseError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - ServerParseError::MissingServerDirectory => { - write!( - f, - "String is not specifying a server directory. Please use an empty string after a \ - double colon to point to the home directory, e.g: 'lobby:'" - ) - } - } - } -} - -impl Error for ServerParseError {} - -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -struct PluginInfo { - pub name: String, - pub version: Option, -} - fn main() -> Result<(), String> { let args = Args::parse(); dbg!(&args); @@ -242,6 +92,56 @@ fn main() -> Result<(), String> { dbg!(&servers); + match args.command { + Command::Upload { + file, + old_version_policy, + upload_directory, + no_confirm, + } => { + todo!() + } + Command::Command { command } => { + start_ssh_agent()?; + for server in servers { + println!("Running command on '{}'...", server.ssh_name); + ShellCmd::new("ssh") + .arg(server.ssh_name) + .arg(format!("cd {:?}; {command}", server.server_directory_path)) + .spawn() + .map_err(|_| "failed to start ssh command".to_string())? + .wait() + .map_err(|e| format!("failed to wait for ssh command completion: {e}"))?; + } + println!("Done!") + } + } + + Ok(()) +} + +fn start_ssh_agent() -> Result<(), String> { + //start the ssh agent + let agent_output = ShellCmd::new("ssh-agent") + .arg("-s") + .stdout(Stdio::piped()) + .output() + .map_err(|e| format!("failed to start ssh agent: {e}"))?; + let agent_output = String::from_utf8_lossy(&agent_output.stdout); + + //set the env vars from the agent + static ENV_VAR_REGEX: Lazy = lazy_regex!("(.+?)=(.+?);"); + for capture in ENV_VAR_REGEX.captures_iter(&agent_output) { + let (_, [env_var, value]) = capture.extract(); + env::set_var(env_var, value); + } + + //add the ssh key + ShellCmd::new("ssh-add") + .spawn() + .map_err(|e| format!("failed to add ssh key: {}", e))? + .wait() + .expect("failed to wait on ssh-add"); Ok(()) } @@ -263,9 +163,9 @@ fn parse_server_configuration(config_str: &str) -> Result, String> { #[cfg(test)] mod test { - use crate::{parse_server_configuration, Server, ServerReference}; + use crate::parse_server_configuration; + use crate::server::Server; use std::path::PathBuf; - use std::str::FromStr; #[test] fn test_parse_server_configuration() { @@ -285,19 +185,4 @@ mod test { servers ); } - - #[test] - fn test_server_reference_from_str() { - assert_eq!( - ServerReference::Name("foo".to_string()), - ServerReference::from_str("foo").unwrap() - ); - assert_eq!( - ServerReference::Resolved(Server { - ssh_name: "crea".to_string(), - server_directory_path: PathBuf::from("server/creative2") - }), - ServerReference::from_str("crea:server/creative2").unwrap() - ); - } } diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..bdf6d99 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,180 @@ +use std::cell::LazyCell; +use std::error::Error; +use std::fmt::{Display, Formatter}; +use std::hash::{Hash, Hasher}; +use std::ops::Deref; +use std::path::PathBuf; +use std::str::FromStr; + +#[derive(Debug, Clone)] +pub enum ServerReference { + Resolved(Server), + Name(String), +} + +impl ServerReference { + pub fn get_name(&self) -> &str { + match self { + ServerReference::Resolved(server) => &server.ssh_name, + ServerReference::Name(name) => name, + } + } + + #[allow(dead_code)] + pub fn resolve(self, configured_servers: &[Server]) -> Option { + match self { + ServerReference::Resolved(server) => Some(server), + ServerReference::Name(name) => Self::resolve_server_name(&name, configured_servers), + } + } + + #[allow(dead_code)] + pub fn resolve_lazy(self, provider: &mut LazyCell) -> Option + where + S: Deref, + F: FnOnce() -> S, + { + match self { + ServerReference::Resolved(server) => Some(server), + ServerReference::Name(name) => Self::resolve_server_name(&name, provider), + } + } + + pub fn try_resolve_lazy( + self, + provider: &mut LazyCell, F>, + ) -> Result, E> + where + S: Deref, + F: FnOnce() -> Result, + E: Clone, + { + match self { + ServerReference::Resolved(server) => Ok(Some(server)), + ServerReference::Name(name) => provider + .as_ref() + .map_err(|e| e.clone()) + .map(|servers| Self::resolve_server_name(&name, servers)), + } + } + + fn resolve_server_name(name: &str, servers: &[Server]) -> Option { + servers + .iter() + .find(|server| server.ssh_name == name) + .cloned() + } +} + +impl FromStr for ServerReference { + type Err = ServerReferenceParseError; + + fn from_str(s: &str) -> Result { + Server::from_str(s) + .map(Self::Resolved) + .or_else(|_| Ok(Self::Name(s.to_string()))) + } +} + +impl PartialEq for ServerReference { + fn eq(&self, other: &Self) -> bool { + self.get_name() == other.get_name() + } +} + +impl Eq for ServerReference {} + +impl Hash for ServerReference { + fn hash(&self, state: &mut H) { + self.get_name().hash(state); + } +} + +impl Display for ServerReference { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ServerReference::Resolved(server) => write!(f, "{}", server), + ServerReference::Name(name) => write!(f, "{}", name), + } + } +} + +#[derive(Debug)] +pub enum ServerReferenceParseError {} + +impl Display for ServerReferenceParseError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) //replace that with an actual implementation if there ever are any variants + } +} + +impl Error for ServerReferenceParseError {} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Server { + pub ssh_name: String, + pub server_directory_path: PathBuf, +} + +impl FromStr for Server { + type Err = ServerParseError; + + fn from_str(s: &str) -> Result { + s.split_once(':') + .ok_or(ServerParseError::MissingServerDirectory) + .map(|(name, directory)| Self { + ssh_name: name.to_string(), + server_directory_path: PathBuf::from(directory), + }) + } +} + +impl Display for Server { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{:?}", self.ssh_name, self.server_directory_path) + } +} + +#[derive(Debug)] +pub enum ServerParseError { + MissingServerDirectory, +} + +impl Display for ServerParseError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ServerParseError::MissingServerDirectory => { + write!( + f, + "String is not specifying a server directory. Please use an empty string after a \ + double colon to point to the home directory, e.g: 'lobby:'" + ) + } + } + } +} + +impl Error for ServerParseError {} + +#[cfg(test)] +mod test_server_reference { + use crate::server::{Server, ServerReference}; + use std::path::PathBuf; + use std::str::FromStr; + + #[test] + fn test_from_str() { + assert_eq!( + ServerReference::Name("foo".to_string()), + ServerReference::from_str("foo").unwrap() + ); + assert_eq!( + ServerReference::Resolved(Server { + ssh_name: "crea".to_string(), + server_directory_path: PathBuf::from("server/creative2") + }), + ServerReference::from_str("crea:server/creative2").unwrap() + ); + } +} +