diff --git a/src/commands/system/deploy.rs b/src/commands/system/deploy.rs new file mode 100644 index 0000000..366e452 --- /dev/null +++ b/src/commands/system/deploy.rs @@ -0,0 +1,332 @@ +use crate::client::Client; +use crate::client::ClientEvent; +use crate::client::Player; +use crate::commands::Command; +use crate::commands::argument::{ArgumentSpec, ParsedArguments}; + +use async_trait::async_trait; +use std::io::{BufRead, BufReader, Read}; +use std::process::{Child, Command as SysCommand, Stdio}; +use std::sync::Arc; +use tokio::sync::Mutex; +use tokio::task; + +pub struct DeployCommand { + running: Arc>>, +} + +impl DeployCommand { + pub fn new() -> Self { + DeployCommand { + running: Arc::new(Mutex::new(None)), + } + } +} + +#[async_trait] +impl Command for DeployCommand { + fn name(&self) -> &'static str { + "deploy" + } + fn aliases(&self) -> &[&'static str] { + &["redeploy", "update"] + } + fn category(&self) -> &'static str { + "system" + } + fn description(&self) -> &'static str { + "Pulls, builds, and restarts copper via systemctl." + } + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[] + } + async fn constructed(&mut self, _: Client) {} + async fn event(&mut self, _: Client, _: ClientEvent) {} + + async fn execute(&mut self, client: Client, player: Player, _args: ParsedArguments) { + let admin_id = "3bff3f33e6dc0410fdc61d13"; + if player._id != admin_id { + client.message("You are not authorized to deploy.").await; + return; + } + + client.message("Starting deploy sequence...").await; + + client.message("Running `git pull`...").await; + let git_pull = SysCommand::new("git") + .arg("pull") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn(); + + match git_pull { + Ok(mut child) => { + let stdout = child.stdout.take().unwrap(); + let stderr = child.stderr.take().unwrap(); + let mut reader = BufReader::new(stdout); + let mut err_reader = BufReader::new(stderr); + + let mut output = String::new(); + let mut err_output = String::new(); + + reader.read_to_string(&mut output).unwrap_or(0); + err_reader.read_to_string(&mut err_output).unwrap_or(0); + + client + .message(format!("git pull output:\n{}", output)) + .await; + if !err_output.is_empty() { + client + .message(format!("git pull error:\n{}", err_output)) + .await; + } + + let status = child.wait().unwrap(); + if !status.success() { + client.message("git pull failed. Aborting.").await; + return; + } + } + Err(e) => { + client + .message(format!("Failed to run git pull: {}", e)) + .await; + return; + } + } + + client.message("Running `cargo run --release`...").await; + let mut cargo_child = match SysCommand::new("cargo") + .arg("run") + .arg("--release") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + { + Ok(child) => child, + Err(e) => { + client + .message(format!("Failed to start cargo run: {}", e)) + .await; + return; + } + }; + + let cargo_stdout = cargo_child.stdout.take().unwrap(); + let cargo_stderr = cargo_child.stderr.take().unwrap(); + + let client_clone = client.clone(); + let found_connected = Arc::new(Mutex::new(false)); + let found_connected_cargo = found_connected.clone(); + + task::spawn_blocking(async move || { + let reader = BufReader::new(cargo_stdout); + for line in reader.lines() { + match line { + Ok(l) => { + let msg = format!("cargo: {}", l); + let _ = client_clone.message(msg.clone()).await; + if l.contains("[CONNECTED]") { + let mut found = found_connected_cargo.blocking_lock(); + *found = true; + break; + } + } + Err(_) => break, + } + } + }); + + let client_clone2 = client.clone(); + task::spawn_blocking(async move || { + let reader = BufReader::new(cargo_stderr); + for line in reader.lines() { + match line { + Ok(l) => { + let msg = format!("cargo stderr: {}", l); + let _ = client_clone2.message(msg.clone()).await; + } + Err(_) => break, + } + } + }); + + loop { + { + let found = found_connected.lock().await; + if *found { + break; + } + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + + client + .message( + "Detected [CONNECTED] from cargo run --release. Proceeding to systemctl restart.", + ) + .await; + + client.message("Running `systemctl daemon-reload`...").await; + let daemon_reload = SysCommand::new("systemctl") + .arg("daemon-reload") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn(); + + match daemon_reload { + Ok(mut child) => { + let stdout = child.stdout.take().unwrap(); + let stderr = child.stderr.take().unwrap(); + let mut reader = BufReader::new(stdout); + let mut err_reader = BufReader::new(stderr); + + let mut output = String::new(); + let mut err_output = String::new(); + + reader.read_to_string(&mut output).unwrap_or(0); + err_reader.read_to_string(&mut err_output).unwrap_or(0); + + client + .message(format!("daemon-reload output:\n{}", output)) + .await; + if !err_output.is_empty() { + client + .message(format!("daemon-reload error:\n{}", err_output)) + .await; + } + + let status = child.wait().unwrap(); + if !status.success() { + client + .message("systemctl daemon-reload failed. Aborting.") + .await; + return; + } + } + Err(e) => { + client + .message(format!("Failed to run systemctl daemon-reload: {}", e)) + .await; + return; + } + } + + client + .message("Running `systemctl restart copper`...") + .await; + let restart_copper = SysCommand::new("systemctl") + .arg("restart") + .arg("copper") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn(); + + match restart_copper { + Ok(mut child) => { + let stdout = child.stdout.take().unwrap(); + let stderr = child.stderr.take().unwrap(); + let mut reader = BufReader::new(stdout); + let mut err_reader = BufReader::new(stderr); + + let mut output = String::new(); + let mut err_output = String::new(); + + reader.read_to_string(&mut output).unwrap_or(0); + err_reader.read_to_string(&mut err_output).unwrap_or(0); + + client + .message(format!("restart copper output:\n{}", output)) + .await; + if !err_output.is_empty() { + client + .message(format!("restart copper error:\n{}", err_output)) + .await; + } + + let status = child.wait().unwrap(); + if !status.success() { + client + .message("systemctl restart copper failed. Aborting.") + .await; + return; + } + } + Err(e) => { + client + .message(format!("Failed to run systemctl restart copper: {}", e)) + .await; + return; + } + } + + client + .message("Waiting for [CONNECTED] from systemctl copper...") + .await; + let copper_status = SysCommand::new("journalctl") + .arg("-u") + .arg("copper") + .arg("-n") + .arg("50") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn(); + + match copper_status { + Ok(mut child) => { + let stdout = child.stdout.take().unwrap(); + let reader = BufReader::new(stdout); + + let mut found = false; + for line in reader.lines() { + match line { + Ok(l) => { + let msg = format!("copper log: {}", l); + client.message(msg.clone()).await; + if l.contains("[CONNECTED]") { + found = true; + break; + } + } + Err(_) => break, + } + } + if found { + client + .message("Detected [CONNECTED] from systemctl copper.") + .await; + } else { + client + .message("Did not detect [CONNECTED] from systemctl copper.") + .await; + } + let _ = child.wait(); + } + Err(e) => { + client + .message(format!("Failed to read copper logs: {}", e)) + .await; + } + } + + // Step 6: Kill cargo run --release + client.message("Killing cargo run --release...").await; + let mut running = self.running.lock().await; + if let Some(child) = running.as_mut() { + match child.kill() { + Ok(_) => { + client.message("cargo run --release killed.").await; + } + Err(e) => { + client + .message(format!("Failed to kill cargo run: {}", e)) + .await; + } + } + } else { + client.message("No running cargo process found.").await; + } + + client.message("Deploy sequence complete.").await; + } +} diff --git a/src/commands/system/mod.rs b/src/commands/system/mod.rs index 661db96..7f1f6cb 100644 --- a/src/commands/system/mod.rs +++ b/src/commands/system/mod.rs @@ -1,3 +1,3 @@ use crate::submods; -submods!(follow, help, launch, test, translate, about); +submods!(follow, help, launch, test, translate, about, deploy); diff --git a/src/main.rs b/src/main.rs index 00989a6..d10169e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -93,6 +93,7 @@ async fn main() -> Result<(), Box> { LaunchCommand, FollowCommand::new(), TestCommand::new(), + DeployCommand::new(), BalanceCommand::new(arc_pool.clone()), InventoryCommand::new(arc_pool.clone()), FishCommand::new(arc_pool.clone()),