Refactor: Move server logic into server module

Add start_ssh_agent
Implement Command sending
This commit is contained in:
Leonard Steppy 2024-12-12 11:59:00 +01:00
parent 05d9ea3a54
commit df1292c9c3
3 changed files with 243 additions and 177 deletions

View File

@ -5,3 +5,4 @@ edition = "2021"
[dependencies]
clap = { version = "4.5.23", features = ["derive"] }
lazy-regex = "3.3.0"

View File

@ -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<Server> {
match self {
ServerReference::Resolved(server) => Some(server),
ServerReference::Name(name) => Self::resolve_server_name(&name, configured_servers),
}
}
#[allow(dead_code)]
pub fn resolve_lazy<S, F>(self, provider: &mut LazyCell<S, F>) -> Option<Server>
where
S: Deref<Target = [Server]>,
F: FnOnce() -> S,
{
match self {
ServerReference::Resolved(server) => Some(server),
ServerReference::Name(name) => Self::resolve_server_name(&name, provider),
}
}
pub fn try_resolve_lazy<S, F, E>(
self,
provider: &mut LazyCell<Result<S, E>, F>,
) -> Result<Option<Server>, E>
where
S: Deref<Target = [Server]>,
F: FnOnce() -> Result<S, E>,
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<Server> {
servers
.iter()
.find(|server| server.ssh_name == name)
.cloned()
}
}
impl FromStr for ServerReference {
type Err = ServerReferenceParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<H: Hasher>(&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<Self, Self::Err> {
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<String>,
}
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<Regex> = 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<Vec<Server>, 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()
);
}
}

180
src/server.rs Normal file
View File

@ -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<Server> {
match self {
ServerReference::Resolved(server) => Some(server),
ServerReference::Name(name) => Self::resolve_server_name(&name, configured_servers),
}
}
#[allow(dead_code)]
pub fn resolve_lazy<S, F>(self, provider: &mut LazyCell<S, F>) -> Option<Server>
where
S: Deref<Target = [Server]>,
F: FnOnce() -> S,
{
match self {
ServerReference::Resolved(server) => Some(server),
ServerReference::Name(name) => Self::resolve_server_name(&name, provider),
}
}
pub fn try_resolve_lazy<S, F, E>(
self,
provider: &mut LazyCell<Result<S, E>, F>,
) -> Result<Option<Server>, E>
where
S: Deref<Target = [Server]>,
F: FnOnce() -> Result<S, E>,
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<Server> {
servers
.iter()
.find(|server| server.ssh_name == name)
.cloned()
}
}
impl FromStr for ServerReference {
type Err = ServerReferenceParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<H: Hasher>(&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<Self, Self::Err> {
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()
);
}
}