From 92c66815fa398d3212e52f340016e24bb2dab4a9 Mon Sep 17 00:00:00 2001 From: Heitor Toledo Lassarote de Paula Date: Wed, 8 Jan 2025 19:02:26 -0300 Subject: [PATCH 1/5] [#260] Show failed Nix command on exit Problem: Many errors result from attempts to build commands which might error with a non-zero exit code. The failed commands are not displayed to the user, however. Solution: For every `([A-Za-z])Exit(Option)` error, change it to `\1Exit(Option, String)`, where the `String` contains the failed command. Format each `tokyo` command using `{:?}` and save it to the corresponding error. Note that `Command` cannot be copied or cloned, and it's not possible to access a command's `std` field to get the underlying command, so we resort to putting the `Debug`-formatted command. --- src/bin/activate.rs | 72 ++++++++++++++++++++++++++------------------- src/cli.rs | 28 +++++++++--------- src/deploy.rs | 26 ++++++++-------- src/push.rs | 45 ++++++++++++++++------------ 4 files changed, 95 insertions(+), 76 deletions(-) diff --git a/src/bin/activate.rs b/src/bin/activate.rs index 9cff9fab..1029c5bc 100644 --- a/src/bin/activate.rs +++ b/src/bin/activate.rs @@ -125,53 +125,57 @@ struct RevokeOpts { pub enum DeactivateError { #[error("Failed to execute the rollback command: {0}")] Rollback(std::io::Error), - #[error("The rollback resulted in a bad exit code: {0:?}")] - RollbackExit(Option), + #[error("The rollback resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] + RollbackExit(Option, String), #[error("Failed to run command for listing generations: {0}")] ListGen(std::io::Error), - #[error("Command for listing generations resulted in a bad exit code: {0:?}")] - ListGenExit(Option), + #[error("Command for listing generations resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] + ListGenExit(Option, String), #[error("Error converting generation list output to utf8: {0}")] DecodeListGenUtf8(std::string::FromUtf8Error), #[error("Failed to run command for deleting generation: {0}")] DeleteGen(std::io::Error), - #[error("Command for deleting generations resulted in a bad exit code: {0:?}")] - DeleteGenExit(Option), + #[error("Command for deleting generations resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] + DeleteGenExit(Option, String), #[error("Failed to run command for re-activating the last generation: {0}")] Reactivate(std::io::Error), - #[error("Command for re-activating the last generation resulted in a bad exit code: {0:?}")] - ReactivateExit(Option), + #[error("Command for re-activating the last generation resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] + ReactivateExit(Option, String), } pub async fn deactivate(profile_path: &str) -> Result<(), DeactivateError> { warn!("De-activating due to error"); - let nix_env_rollback_exit_status = Command::new("nix-env") + let mut nix_env_rollback_command = Command::new("nix-env"); + nix_env_rollback_command .arg("-p") .arg(&profile_path) - .arg("--rollback") + .arg("--rollback"); + let nix_env_rollback_exit_status = nix_env_rollback_command .status() .await .map_err(DeactivateError::Rollback)?; match nix_env_rollback_exit_status.code() { Some(0) => (), - a => return Err(DeactivateError::RollbackExit(a)), + a => return Err(DeactivateError::RollbackExit(a, format!("{:?}", nix_env_rollback_command))), }; debug!("Listing generations"); - let nix_env_list_generations_out = Command::new("nix-env") + let mut nix_env_list_generations_command = Command::new("nix-env"); + nix_env_list_generations_command .arg("-p") .arg(&profile_path) - .arg("--list-generations") + .arg("--list-generations"); + let nix_env_list_generations_out = nix_env_list_generations_command .output() .await .map_err(DeactivateError::ListGen)?; match nix_env_list_generations_out.status.code() { Some(0) => (), - a => return Err(DeactivateError::ListGenExit(a)), + a => return Err(DeactivateError::ListGenExit(a, format!("{:?}", nix_env_list_generations_command))), }; let generations_list = String::from_utf8(nix_env_list_generations_out.stdout) @@ -190,32 +194,36 @@ pub async fn deactivate(profile_path: &str) -> Result<(), DeactivateError> { debug!("Removing generation entry {}", last_generation_line); warn!("Removing generation by ID {}", last_generation_id); - let nix_env_delete_generation_exit_status = Command::new("nix-env") + let mut nix_env_delete_generation_command = Command::new("nix-env"); + nix_env_delete_generation_command .arg("-p") .arg(&profile_path) .arg("--delete-generations") - .arg(last_generation_id) + .arg(last_generation_id); + let nix_env_delete_generation_exit_status = nix_env_delete_generation_command .status() .await .map_err(DeactivateError::DeleteGen)?; match nix_env_delete_generation_exit_status.code() { Some(0) => (), - a => return Err(DeactivateError::DeleteGenExit(a)), + a => return Err(DeactivateError::DeleteGenExit(a, format!("{:?}", nix_env_list_generations_command))), }; info!("Attempting to re-activate the last generation"); - let re_activate_exit_status = Command::new(format!("{}/deploy-rs-activate", profile_path)) + let mut re_activate_command = Command::new(format!("{}/deploy-rs-activate", profile_path)); + re_activate_command .env("PROFILE", &profile_path) - .current_dir(&profile_path) + .current_dir(&profile_path); + let re_activate_exit_status = re_activate_command .status() .await .map_err(DeactivateError::Reactivate)?; match re_activate_exit_status.code() { Some(0) => (), - a => return Err(DeactivateError::ReactivateExit(a)), + a => return Err(DeactivateError::ReactivateExit(a,format!("{:?}", re_activate_command))), }; Ok(()) @@ -366,13 +374,13 @@ pub async fn wait(temp_path: PathBuf, closure: String, activation_timeout: Optio pub enum ActivateError { #[error("Failed to execute the command for setting profile: {0}")] SetProfile(std::io::Error), - #[error("The command for setting profile resulted in a bad exit code: {0:?}")] - SetProfileExit(Option), + #[error("The command for setting profile resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] + SetProfileExit(Option, String), #[error("Failed to execute the activation script: {0}")] RunActivate(std::io::Error), - #[error("The activation script resulted in a bad exit code: {0:?}")] - RunActivateExit(Option), + #[error("The activation script resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] + RunActivateExit(Option, String), #[error("There was an error de-activating after an error was encountered: {0}")] Deactivate(#[from] DeactivateError), @@ -393,11 +401,13 @@ pub async fn activate( ) -> Result<(), ActivateError> { if !dry_activate { info!("Activating profile"); - let nix_env_set_exit_status = Command::new("nix-env") + let mut nix_env_set_command = Command::new("nix-env"); + nix_env_set_command .arg("-p") .arg(&profile_path) .arg("--set") - .arg(&closure) + .arg(&closure); + let nix_env_set_exit_status = nix_env_set_command .status() .await .map_err(ActivateError::SetProfile)?; @@ -407,7 +417,7 @@ pub async fn activate( if auto_rollback && !dry_activate { deactivate(&profile_path).await?; } - return Err(ActivateError::SetProfileExit(a)); + return Err(ActivateError::SetProfileExit(a,format!("{:?}", nix_env_set_command))); } }; } @@ -420,11 +430,13 @@ pub async fn activate( &profile_path }; - let activate_status = match Command::new(format!("{}/deploy-rs-activate", activation_location)) + let mut activate_command = Command::new(format!("{}/deploy-rs-activate", activation_location)); + activate_command .env("PROFILE", activation_location) .env("DRY_ACTIVATE", if dry_activate { "1" } else { "0" }) .env("BOOT", if boot { "1" } else { "0" }) - .current_dir(activation_location) + .current_dir(activation_location); + let activate_status = match activate_command .status() .await .map_err(ActivateError::RunActivate) @@ -445,7 +457,7 @@ pub async fn activate( if auto_rollback { deactivate(&profile_path).await?; } - return Err(ActivateError::RunActivateExit(a)); + return Err(ActivateError::RunActivateExit(a, format!("{:?}", activate_command))); } }; diff --git a/src/cli.rs b/src/cli.rs index f3bce4df..ad620ff9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -128,8 +128,8 @@ async fn test_flake_support() -> Result { pub enum CheckDeploymentError { #[error("Failed to execute Nix checking command: {0}")] NixCheck(#[from] std::io::Error), - #[error("Nix checking command resulted in a bad exit code: {0:?}")] - NixCheckExit(Option), + #[error("Nix checking command resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] + NixCheckExit(Option, String), } async fn check_deployment( @@ -158,7 +158,7 @@ async fn check_deployment( match check_status.code() { Some(0) => (), - a => return Err(CheckDeploymentError::NixCheckExit(a)), + a => return Err(CheckDeploymentError::NixCheckExit(a, format!("{:?}", check_command))), }; Ok(()) @@ -170,8 +170,8 @@ pub enum GetDeploymentDataError { NixEval(std::io::Error), #[error("Failed to read output from evaluation: {0}")] NixEvalOut(std::io::Error), - #[error("Evaluation resulted in a bad exit code: {0:?}")] - NixEvalExit(Option), + #[error("Evaluation resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] + NixEvalExit(Option, String), #[error("Error converting evaluation output to utf8: {0}")] DecodeUtf8(#[from] std::string::FromUtf8Error), #[error("Error decoding the JSON from evaluation: {0}")] @@ -190,14 +190,14 @@ async fn get_deployment_data( info!("Evaluating flake in {}", flake.repo); - let mut c = if supports_flakes { + let mut eval_command = if supports_flakes { Command::new("nix") } else { Command::new("nix-instantiate") }; if supports_flakes { - c.arg("eval") + eval_command.arg("eval") .arg("--json") .arg(format!("{}#deploy", flake.repo)) // We use --apply instead of --expr so that we don't have to deal with builtins.getFlake @@ -205,7 +205,7 @@ async fn get_deployment_data( match (&flake.node, &flake.profile) { (Some(node), Some(profile)) => { // Ignore all nodes and all profiles but the one we're evaluating - c.arg(format!( + eval_command.arg(format!( r#" deploy: (deploy // {{ @@ -223,7 +223,7 @@ async fn get_deployment_data( } (Some(node), None) => { // Ignore all nodes but the one we're evaluating - c.arg(format!( + eval_command.arg(format!( r#" deploy: (deploy // {{ @@ -237,12 +237,12 @@ async fn get_deployment_data( } (None, None) => { // We need to evaluate all profiles of all nodes anyway, so just do it strictly - c.arg("deploy: deploy") + eval_command.arg("deploy: deploy") } (None, Some(_)) => return Err(GetDeploymentDataError::ProfileNoNode), } } else { - c + eval_command .arg("--strict") .arg("--read-write-mode") .arg("--json") @@ -251,9 +251,9 @@ async fn get_deployment_data( .arg(format!("let r = import {}/.; in if builtins.isFunction r then (r {{}}).deploy else r.deploy", flake.repo)) }; - c.args(extra_build_args); + eval_command.args(extra_build_args); - let build_child = c + let build_child = eval_command .stdout(Stdio::piped()) .spawn() .map_err(GetDeploymentDataError::NixEval)?; @@ -265,7 +265,7 @@ async fn get_deployment_data( match build_output.status.code() { Some(0) => (), - a => return Err(GetDeploymentDataError::NixEvalExit(a)), + a => return Err(GetDeploymentDataError::NixEvalExit(a, format!("{:?}", eval_command))), }; let data_json = String::from_utf8(build_output.stdout)?; diff --git a/src/deploy.rs b/src/deploy.rs index 9f79d646..53027cb3 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -264,9 +264,9 @@ pub enum ConfirmProfileError { #[error("Failed to run confirmation command over SSH (the server should roll back): {0}")] SSHConfirm(std::io::Error), #[error( - "Confirming activation over SSH resulted in a bad exit code (the server should roll back): {0:?}" + "Confirming activation over SSH resulted in a bad exit code (the server should roll back): {0:?}. The failed command is provided below:\n{1}" )] - SSHConfirmExit(Option), + SSHConfirmExit(Option, String), } pub async fn confirm_profile( @@ -315,7 +315,7 @@ pub async fn confirm_profile( match ssh_confirm_exit_status.code() { Some(0) => (), - a => return Err(ConfirmProfileError::SSHConfirmExit(a)), + a => return Err(ConfirmProfileError::SSHConfirmExit(a, format!("{:?}", ssh_confirm_command))), }; info!("Deployment confirmed."); @@ -330,13 +330,13 @@ pub enum DeployProfileError { #[error("Failed to run activation command over SSH: {0}")] SSHActivate(std::io::Error), - #[error("Activating over SSH resulted in a bad exit code: {0:?}")] - SSHActivateExit(Option), + #[error("Activating over SSH resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] + SSHActivateExit(Option, String), #[error("Failed to run wait command over SSH: {0}")] SSHWait(std::io::Error), - #[error("Waiting over SSH resulted in a bad exit code: {0:?}")] - SSHWaitExit(Option), + #[error("Waiting over SSH resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] + SSHWaitExit(Option, String), #[error("Failed to pipe to child stdin: {0}")] SSHActivatePipe(std::io::Error), @@ -425,7 +425,7 @@ pub async fn deploy_profile( match ssh_activate_exit_status.code() { Some(0) => (), - a => return Err(DeployProfileError::SSHActivateExit(a)), + a => return Err(DeployProfileError::SSHActivateExit(a, format!("{:?}", ssh_activate_command))), }; if dry_activate { @@ -480,7 +480,7 @@ pub async fn deploy_profile( Err(x) => Some(DeployProfileError::SSHActivate(x)), Ok(ref x) => match x.status.code() { Some(0) => None, - a => Some(DeployProfileError::SSHActivateExit(a)), + a => Some(DeployProfileError::SSHActivateExit(a, format!("{:?}", ssh_activate_command))), }, }; @@ -508,7 +508,7 @@ pub async fn deploy_profile( debug!("Wait command ended"); match x.map_err(DeployProfileError::SSHWait)?.code() { Some(0) => (), - a => return Err(DeployProfileError::SSHWaitExit(a)), + a => return Err(DeployProfileError::SSHWaitExit(a, format!("{:?}", ssh_wait_command))), }; }, x = recv_activate => { @@ -538,8 +538,8 @@ pub enum RevokeProfileError { #[error("Error revoking deployment: {0}")] SSHRevoke(std::io::Error), - #[error("Revoking over SSH resulted in a bad exit code: {0:?}")] - SSHRevokeExit(Option), + #[error("Revoking over SSH resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] + SSHRevokeExit(Option, String), #[error("Deployment data invalid: {0}")] InvalidDeployDataDefs(#[from] DeployDataDefsError), @@ -592,7 +592,7 @@ pub async fn revoke( Err(x) => Err(RevokeProfileError::SSHRevoke(x)), Ok(ref x) => match x.status.code() { Some(0) => Ok(()), - a => Err(RevokeProfileError::SSHRevokeExit(a)), + a => Err(RevokeProfileError::SSHRevokeExit(a,format!("{:?}", ssh_activate_command))), }, } } diff --git a/src/push.rs b/src/push.rs index 864c3369..bd1bcc8b 100644 --- a/src/push.rs +++ b/src/push.rs @@ -13,8 +13,8 @@ use tokio::process::Command; pub enum PushProfileError { #[error("Failed to run Nix show-derivation command: {0}")] ShowDerivation(std::io::Error), - #[error("Nix show-derivation command resulted in a bad exit code: {0:?}")] - ShowDerivationExit(Option), + #[error("Nix show-derivation command resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] + ShowDerivationExit(Option, String), #[error("Nix show-derivation command output contained an invalid UTF-8 sequence: {0}")] ShowDerivationUtf8(std::str::Utf8Error), #[error("Failed to parse the output of nix show-derivation: {0}")] @@ -23,8 +23,8 @@ pub enum PushProfileError { ShowDerivationEmpty, #[error("Failed to run Nix build command: {0}")] Build(std::io::Error), - #[error("Nix build command resulted in a bad exit code: {0:?}")] - BuildExit(Option), + #[error("Nix build command resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] + BuildExit(Option, String), #[error( "Activation script deploy-rs-activate does not exist in profile.\n\ Did you forget to use deploy-rs#lib.<...>.activate.<...> on your profile path?" @@ -35,12 +35,12 @@ pub enum PushProfileError { ActivateRsDoesntExist, #[error("Failed to run Nix sign command: {0}")] Sign(std::io::Error), - #[error("Nix sign command resulted in a bad exit code: {0:?}")] - SignExit(Option), + #[error("Nix sign command resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] + SignExit(Option, String), #[error("Failed to run Nix copy command: {0}")] Copy(std::io::Error), - #[error("Nix copy command resulted in a bad exit code: {0:?}")] - CopyExit(Option), + #[error("Nix copy command resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] + CopyExit(Option, String), #[error("The remote building option is not supported when using legacy nix")] RemoteBuildWithLegacyNix, @@ -101,7 +101,7 @@ pub async fn build_profile_locally(data: &PushProfileData<'_>, derivation_name: match build_exit_status.code() { Some(0) => (), - a => return Err(PushProfileError::BuildExit(a)), + a => return Err(PushProfileError::BuildExit(a,format!("{:?}", build_command))), }; if !Path::new( @@ -134,19 +134,21 @@ pub async fn build_profile_locally(data: &PushProfileData<'_>, derivation_name: data.deploy_data.profile_name, data.deploy_data.node_name ); - let sign_exit_status = Command::new("nix") + let mut sign_command = Command::new("nix"); + sign_command .arg("sign-paths") .arg("-r") .arg("-k") .arg(local_key) - .arg(&data.deploy_data.profile.profile_settings.path) + .arg(&data.deploy_data.profile.profile_settings.path); + let sign_exit_status = sign_command .status() .await .map_err(PushProfileError::Sign)?; match sign_exit_status.code() { Some(0) => (), - a => return Err(PushProfileError::SignExit(a)), + a => return Err(PushProfileError::SignExit(a, format!("{:?}", sign_command))), }; } Ok(()) @@ -169,11 +171,15 @@ pub async fn build_profile_remotely(data: &PushProfileData<'_>, derivation_name: // copy the derivation to remote host so it can be built there - let copy_command_status = Command::new("nix").arg("copy") + let mut copy_command = Command::new("nix"); + copy_command + .arg("copy") .arg("-s") // fetch dependencies from substitures, not localhost .arg("--to").arg(&store_address) .arg("--derivation").arg(derivation_name) .env("NIX_SSHOPTS", ssh_opts_str.clone()) + .stdout(Stdio::null()); + let copy_command_status = copy_command .stdout(Stdio::null()) .status() .await @@ -181,7 +187,7 @@ pub async fn build_profile_remotely(data: &PushProfileData<'_>, derivation_name: match copy_command_status.code() { Some(0) => (), - a => return Err(PushProfileError::CopyExit(a)), + a => return Err(PushProfileError::CopyExit(a, format!("{:?}", copy_command))), }; let mut build_command = Command::new("nix"); @@ -203,7 +209,7 @@ pub async fn build_profile_remotely(data: &PushProfileData<'_>, derivation_name: match build_exit_status.code() { Some(0) => (), - a => return Err(PushProfileError::BuildExit(a)), + a => return Err(PushProfileError::BuildExit(a,format!("{:?}", build_command))), }; @@ -230,7 +236,7 @@ pub async fn build_profile(data: PushProfileData<'_>) -> Result<(), PushProfileE match show_derivation_output.status.code() { Some(0) => (), - a => return Err(PushProfileError::ShowDerivationExit(a)), + a => return Err(PushProfileError::ShowDerivationExit(a, format!("{:?}", show_derivation_command))), }; let derivation_info: HashMap<&str, serde_json::value::Value> = serde_json::from_str( @@ -322,18 +328,19 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr None => &data.deploy_data.node.node_settings.hostname, }; - let copy_exit_status = copy_command + copy_command .arg("--to") .arg(format!("ssh://{}@{}", data.deploy_defs.ssh_user, hostname)) .arg(&data.deploy_data.profile.profile_settings.path) - .env("NIX_SSHOPTS", ssh_opts_str) + .env("NIX_SSHOPTS", ssh_opts_str); + let copy_exit_status = copy_command .status() .await .map_err(PushProfileError::Copy)?; match copy_exit_status.code() { Some(0) => (), - a => return Err(PushProfileError::CopyExit(a)), + a => return Err(PushProfileError::CopyExit(a, format!("{:?}", copy_command))), }; } From f01f5fc8ebc06f2903a1465d8660b337ea2da6f1 Mon Sep 17 00:00:00 2001 From: Heitor Toledo Lassarote de Paula Date: Fri, 10 Jan 2025 13:13:23 -0300 Subject: [PATCH 2/5] [#260] Abstract some of the boilerplate Problem: It's common to spawn a command and occasionally fail during running the command or because it exited, leading to code repetition. Solution: Create a `command` module to abstract some of the boilerplate by wrapping `tokio::process::Command`. Create a `run` function that abstracts most of this boilerplate. In some parts, the way commands are executed are a bit different, so instead we leave them as it is. --- src/bin/activate.rs | 154 ++++++++++++++++++++++---------------- src/cli.rs | 63 ++++++++-------- src/command.rs | 127 +++++++++++++++++++++++++++++++ src/deploy.rs | 171 +++++++++++++++++++++++++++++------------- src/lib.rs | 1 + src/push.rs | 178 ++++++++++++++++++++++++-------------------- 6 files changed, 464 insertions(+), 230 deletions(-) create mode 100644 src/command.rs diff --git a/src/bin/activate.rs b/src/bin/activate.rs index 1029c5bc..f17a4196 100644 --- a/src/bin/activate.rs +++ b/src/bin/activate.rs @@ -9,7 +9,6 @@ use signal_hook::{consts::signal::SIGHUP, iterator::Signals}; use clap::Clap; use tokio::fs; -use tokio::process::Command; use tokio::sync::mpsc; use tokio::time::timeout; @@ -24,6 +23,8 @@ use thiserror::Error; use log::{debug, error, info, warn}; +use deploy::command; + /// Remote activation utility for deploy-rs #[derive(Clap, Debug)] #[clap(version = "1.0", author = "Serokell ")] @@ -121,63 +122,80 @@ struct RevokeOpts { profile_name: Option, } +#[derive(Error, Debug)] +pub enum RollbackError {} + +impl command::HasCommandError for RollbackError { + fn title() -> String { + "Nix rollback".to_string() + } +} + +#[derive(Error, Debug)] +pub enum ListGenError {} + +impl command::HasCommandError for ListGenError { + fn title() -> String { + "Nix list generations".to_string() + } +} + +#[derive(Error, Debug)] +pub enum DeleteGenError {} + +impl command::HasCommandError for DeleteGenError { + fn title() -> String { + "Nix delete generations".to_string() + } +} + +#[derive(Error, Debug)] +pub enum ReactivateError {} + +impl command::HasCommandError for ReactivateError { + fn title() -> String { + "Nix reactive last generation".to_string() + } +} + #[derive(Error, Debug)] pub enum DeactivateError { - #[error("Failed to execute the rollback command: {0}")] - Rollback(std::io::Error), - #[error("The rollback resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] - RollbackExit(Option, String), - #[error("Failed to run command for listing generations: {0}")] - ListGen(std::io::Error), - #[error("Command for listing generations resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] - ListGenExit(Option, String), + #[error("{0}")] + Rollback(#[from] command::CommandError), + #[error("{0}")] + ListGen(#[from] command::CommandError), #[error("Error converting generation list output to utf8: {0}")] DecodeListGenUtf8(std::string::FromUtf8Error), - #[error("Failed to run command for deleting generation: {0}")] - DeleteGen(std::io::Error), - #[error("Command for deleting generations resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] - DeleteGenExit(Option, String), - #[error("Failed to run command for re-activating the last generation: {0}")] - Reactivate(std::io::Error), - #[error("Command for re-activating the last generation resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] - ReactivateExit(Option, String), + #[error("{0}")] + DeleteGen(#[from] command::CommandError), + #[error("{0}")] + Reactivate(#[from] command::CommandError), } pub async fn deactivate(profile_path: &str) -> Result<(), DeactivateError> { warn!("De-activating due to error"); - let mut nix_env_rollback_command = Command::new("nix-env"); + let mut nix_env_rollback_command = command::Command::new("nix-env"); nix_env_rollback_command .arg("-p") .arg(&profile_path) - .arg("--rollback"); - let nix_env_rollback_exit_status = nix_env_rollback_command - .status() + .arg("--rollback") + .run() .await .map_err(DeactivateError::Rollback)?; - match nix_env_rollback_exit_status.code() { - Some(0) => (), - a => return Err(DeactivateError::RollbackExit(a, format!("{:?}", nix_env_rollback_command))), - }; - debug!("Listing generations"); - let mut nix_env_list_generations_command = Command::new("nix-env"); + let mut nix_env_list_generations_command = command::Command::new("nix-env"); nix_env_list_generations_command .arg("-p") .arg(&profile_path) .arg("--list-generations"); let nix_env_list_generations_out = nix_env_list_generations_command - .output() + .run() .await .map_err(DeactivateError::ListGen)?; - match nix_env_list_generations_out.status.code() { - Some(0) => (), - a => return Err(DeactivateError::ListGenExit(a, format!("{:?}", nix_env_list_generations_command))), - }; - let generations_list = String::from_utf8(nix_env_list_generations_out.stdout) .map_err(DeactivateError::DecodeListGenUtf8)?; @@ -194,38 +212,26 @@ pub async fn deactivate(profile_path: &str) -> Result<(), DeactivateError> { debug!("Removing generation entry {}", last_generation_line); warn!("Removing generation by ID {}", last_generation_id); - let mut nix_env_delete_generation_command = Command::new("nix-env"); + let mut nix_env_delete_generation_command = command::Command::new("nix-env"); nix_env_delete_generation_command .arg("-p") .arg(&profile_path) .arg("--delete-generations") - .arg(last_generation_id); - let nix_env_delete_generation_exit_status = nix_env_delete_generation_command - .status() + .arg(last_generation_id) + .run() .await .map_err(DeactivateError::DeleteGen)?; - match nix_env_delete_generation_exit_status.code() { - Some(0) => (), - a => return Err(DeactivateError::DeleteGenExit(a, format!("{:?}", nix_env_list_generations_command))), - }; - info!("Attempting to re-activate the last generation"); - let mut re_activate_command = Command::new(format!("{}/deploy-rs-activate", profile_path)); + let mut re_activate_command = command::Command::new(format!("{}/deploy-rs-activate", profile_path)); re_activate_command .env("PROFILE", &profile_path) - .current_dir(&profile_path); - let re_activate_exit_status = re_activate_command - .status() + .current_dir(&profile_path) + .run() .await .map_err(DeactivateError::Reactivate)?; - match re_activate_exit_status.code() { - Some(0) => (), - a => return Err(DeactivateError::ReactivateExit(a,format!("{:?}", re_activate_command))), - }; - Ok(()) } @@ -370,17 +376,31 @@ pub async fn wait(temp_path: PathBuf, closure: String, activation_timeout: Optio Ok(()) } +#[derive(Error, Debug)] +pub enum SetProfileError {} + +impl command::HasCommandError for SetProfileError { + fn title() -> String { + "Nix profile set".to_string() + } +} + +#[derive(Error, Debug)] +pub enum RunActivateError {} + +impl command::HasCommandError for RunActivateError { + fn title() -> String { + "Nix activation script".to_string() + } +} + #[derive(Error, Debug)] pub enum ActivateError { - #[error("Failed to execute the command for setting profile: {0}")] - SetProfile(std::io::Error), - #[error("The command for setting profile resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] - SetProfileExit(Option, String), + #[error("{0}")] + SetProfile(#[from] command::CommandError), - #[error("Failed to execute the activation script: {0}")] - RunActivate(std::io::Error), - #[error("The activation script resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] - RunActivateExit(Option, String), + #[error("{0}")] + RunActivate(#[from] command::CommandError), #[error("There was an error de-activating after an error was encountered: {0}")] Deactivate(#[from] DeactivateError), @@ -401,7 +421,7 @@ pub async fn activate( ) -> Result<(), ActivateError> { if !dry_activate { info!("Activating profile"); - let mut nix_env_set_command = Command::new("nix-env"); + let mut nix_env_set_command = command::Command::new("nix-env"); nix_env_set_command .arg("-p") .arg(&profile_path) @@ -410,14 +430,16 @@ pub async fn activate( let nix_env_set_exit_status = nix_env_set_command .status() .await - .map_err(ActivateError::SetProfile)?; + .map_err(|err| { + ActivateError::SetProfile(command::CommandError::RunError(err)) + })?; match nix_env_set_exit_status.code() { Some(0) => (), a => { if auto_rollback && !dry_activate { deactivate(&profile_path).await?; } - return Err(ActivateError::SetProfileExit(a,format!("{:?}", nix_env_set_command))); + return Err(ActivateError::SetProfile(command::CommandError::Exit(a, format!("{:?}", nix_env_set_command)))); } }; } @@ -430,7 +452,7 @@ pub async fn activate( &profile_path }; - let mut activate_command = Command::new(format!("{}/deploy-rs-activate", activation_location)); + let mut activate_command = command::Command::new(format!("{}/deploy-rs-activate", activation_location)); activate_command .env("PROFILE", activation_location) .env("DRY_ACTIVATE", if dry_activate { "1" } else { "0" }) @@ -439,7 +461,9 @@ pub async fn activate( let activate_status = match activate_command .status() .await - .map_err(ActivateError::RunActivate) + .map_err(|err| { + ActivateError::RunActivate(command::CommandError::RunError(err)) + }) { Ok(x) => x, Err(e) => { @@ -457,7 +481,7 @@ pub async fn activate( if auto_rollback { deactivate(&profile_path).await?; } - return Err(ActivateError::RunActivateExit(a, format!("{:?}", activate_command))); + return Err(ActivateError::RunActivate(command::CommandError::Exit(a, format!("{:?}", activate_command)))); } }; diff --git a/src/cli.rs b/src/cli.rs index ad620ff9..c0df5c90 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -9,6 +9,7 @@ use std::io::{stdin, stdout, Write}; use clap::{ArgMatches, Clap, FromArgMatches}; use crate as deploy; +use crate::command; use self::deploy::{DeployFlake, ParseFlakeError}; use futures_util::stream::{StreamExt, TryStreamExt}; @@ -124,12 +125,19 @@ async fn test_flake_support() -> Result { .success()) } +#[derive(Error, Debug)] +pub enum NixCheckError {} + +impl command::HasCommandError for NixCheckError { + fn title() -> String { + "Nix checking".to_string() + } +} + #[derive(Error, Debug)] pub enum CheckDeploymentError { - #[error("Failed to execute Nix checking command: {0}")] - NixCheck(#[from] std::io::Error), - #[error("Nix checking command resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] - NixCheckExit(Option, String), + #[error("{0}")] + NixCheck(#[from] command::CommandError), } async fn check_deployment( @@ -140,8 +148,8 @@ async fn check_deployment( info!("Running checks for flake in {}", repo); let mut check_command = match supports_flakes { - true => Command::new("nix"), - false => Command::new("nix-build"), + true => command::Command::new("nix"), + false => command::Command::new("nix-build"), }; if supports_flakes { @@ -154,24 +162,24 @@ async fn check_deployment( check_command.args(extra_build_args); - let check_status = check_command.status().await?; - - match check_status.code() { - Some(0) => (), - a => return Err(CheckDeploymentError::NixCheckExit(a, format!("{:?}", check_command))), - }; + check_command.run().await.map_err(CheckDeploymentError::NixCheck)?; Ok(()) } +#[derive(Error, Debug)] +pub enum NixEvalError {} + +impl command::HasCommandError for NixEvalError { + fn title() -> String { + "Nix eval".to_string() + } +} + #[derive(Error, Debug)] pub enum GetDeploymentDataError { - #[error("Failed to execute nix eval command: {0}")] - NixEval(std::io::Error), - #[error("Failed to read output from evaluation: {0}")] - NixEvalOut(std::io::Error), - #[error("Evaluation resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] - NixEvalExit(Option, String), + #[error("{0}")] + NixEval(#[from] command::CommandError), #[error("Error converting evaluation output to utf8: {0}")] DecodeUtf8(#[from] std::string::FromUtf8Error), #[error("Error decoding the JSON from evaluation: {0}")] @@ -191,9 +199,9 @@ async fn get_deployment_data( info!("Evaluating flake in {}", flake.repo); let mut eval_command = if supports_flakes { - Command::new("nix") + command::Command::new("nix") } else { - Command::new("nix-instantiate") + command::Command::new("nix-instantiate") }; if supports_flakes { @@ -253,20 +261,11 @@ async fn get_deployment_data( eval_command.args(extra_build_args); - let build_child = eval_command + let build_output = eval_command .stdout(Stdio::piped()) - .spawn() - .map_err(GetDeploymentDataError::NixEval)?; - - let build_output = build_child - .wait_with_output() + .run() .await - .map_err(GetDeploymentDataError::NixEvalOut)?; - - match build_output.status.code() { - Some(0) => (), - a => return Err(GetDeploymentDataError::NixEvalExit(a, format!("{:?}", eval_command))), - }; + .map_err(GetDeploymentDataError::NixEval)?; let data_json = String::from_utf8(build_output.stdout)?; diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 00000000..f30345d5 --- /dev/null +++ b/src/command.rs @@ -0,0 +1,127 @@ +use std::ffi::OsStr; +use std::fmt; +use std::fmt::Debug; +use std::future::Future; +use thiserror::Error; +use tokio::process::Command as TokioCommand; + +pub trait HasCommandError { + fn title() -> String; +} + +#[derive(Error, Debug)] +pub enum CommandError { + RunError(std::io::Error), + Exit(Option, String), + OtherError(T), +} + +impl fmt::Display for CommandError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CommandError::RunError(err) => write!( + f, + "Failed to run {} command: {}", + T::title(), + err, + ), + CommandError::Exit(exit_code, cmd) => write!( + f, + "{} command resulted in a bad exit code: {:?}. The failed command is provided below:\n{}", + T::title(), + exit_code, + cmd, + ), + CommandError::OtherError(err) => write!(f, "{}", err), + } + } +} + +/// A wrapper over `tokio::process::Command` to provide the `run` method commonly used by `deploy`. +#[derive(Debug)] +pub struct Command { + pub command: TokioCommand, +} + +impl Command { + pub fn new>(program: S) -> Command { + Command { + command: TokioCommand::new(program), + } + } + + pub fn arg>(&mut self, arg: S) -> &mut Command { + self.command.arg(arg); + self + } + + pub fn args(&mut self, args: I) -> &mut Command + where + I: IntoIterator, + S: AsRef, + { + self.command.args(args); + self + } + + pub fn env(&mut self, key: K, val: V) -> &mut Command + where + K: AsRef, + V: AsRef, + { + self.command.env(key, val); + self + } + + pub fn output(&mut self) -> impl Future> { + self.command.output() + } + + pub fn current_dir>(&mut self, dir: P) -> &mut Command { + self.command.current_dir(dir); + self + } + + pub fn stdin>(&mut self, cfg: T) -> &mut Command { + self.command.stdin(cfg); + self + } + + pub fn stdout>(&mut self, cfg: T) -> &mut Command { + self.command.stdout(cfg); + self + } + + pub fn stderr>(&mut self, cfg: T) -> &mut Command { + self.command.stderr(cfg); + self + } + + pub fn spawn(&mut self) -> std::io::Result { + self.command.spawn() + } + + pub fn status(&mut self) -> impl Future> { + self.command.status() + } + + pub async fn run( + &mut self, + ) -> Result> { + let output = self + .command + .output() + .await + .map_err(CommandError::RunError)?; + match output.status.code() { + Some(0) => Ok(output), + exit_code => Err(CommandError::Exit(exit_code, format!("{:?}", self.command))), + } + } +} + +impl fmt::Display for Command { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.command.fmt(f) + } +} diff --git a/src/deploy.rs b/src/deploy.rs index 53027cb3..100b8088 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -7,9 +7,9 @@ use log::{debug, info, trace}; use std::path::Path; use thiserror::Error; -use tokio::{io::AsyncWriteExt, process::Command}; +use tokio::io::AsyncWriteExt; -use crate::{DeployDataDefsError, DeployDefs, ProfileInfo}; +use crate::{command, DeployDataDefsError, DeployDefs, ProfileInfo}; struct ActivateCommandData<'a> { sudo: &'a Option, @@ -259,14 +259,21 @@ async fn handle_sudo_stdin(ssh_activate_child: &mut tokio::process::Child, deplo } } + +#[derive(Error, Debug)] +pub enum SSHConfirmError { +} + +impl command::HasCommandError for SSHConfirmError { + fn title() -> String { + "SSH confirmation command (the server should roll back)".to_string() + } +} + #[derive(Error, Debug)] pub enum ConfirmProfileError { - #[error("Failed to run confirmation command over SSH (the server should roll back): {0}")] - SSHConfirm(std::io::Error), - #[error( - "Confirming activation over SSH resulted in a bad exit code (the server should roll back): {0:?}. The failed command is provided below:\n{1}" - )] - SSHConfirmExit(Option, String), + #[error("{0}")] + SSHConfirm(#[from] command::CommandError), } pub async fn confirm_profile( @@ -275,7 +282,7 @@ pub async fn confirm_profile( temp_path: &Path, ssh_addr: &str, ) -> Result<(), ConfirmProfileError> { - let mut ssh_confirm_command = Command::new("ssh"); + let mut ssh_confirm_command = command::Command::new("ssh"); ssh_confirm_command .arg(ssh_addr) .stdin(std::process::Stdio::piped()); @@ -299,23 +306,29 @@ pub async fn confirm_profile( let mut ssh_confirm_child = ssh_confirm_command .arg(confirm_command) .spawn() - .map_err(ConfirmProfileError::SSHConfirm)?; + .map_err(|err| { + ConfirmProfileError::SSHConfirm(command::CommandError::RunError(err)) + })?; if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { trace!("[confirm] Piping in sudo password"); handle_sudo_stdin(&mut ssh_confirm_child, deploy_defs) .await - .map_err(ConfirmProfileError::SSHConfirm)?; + .map_err(|err| { + ConfirmProfileError::SSHConfirm(command::CommandError::RunError(err)) + })?; } let ssh_confirm_exit_status = ssh_confirm_child .wait() .await - .map_err(ConfirmProfileError::SSHConfirm)?; + .map_err(|err| { + ConfirmProfileError::SSHConfirm(command::CommandError::RunError(err)) + })?; match ssh_confirm_exit_status.code() { Some(0) => (), - a => return Err(ConfirmProfileError::SSHConfirmExit(a, format!("{:?}", ssh_confirm_command))), + a => return Err(ConfirmProfileError::SSHConfirm(command::CommandError::Exit(a, format!("{:?}", ssh_confirm_command)))), }; info!("Deployment confirmed."); @@ -324,22 +337,35 @@ pub async fn confirm_profile( } #[derive(Error, Debug)] -pub enum DeployProfileError { +pub enum SSHActivateError { #[error("Failed to spawn activation command over SSH: {0}")] - SSHSpawnActivate(std::io::Error), + SpawnActivateError(std::io::Error), + #[error("Failed to pipe to child stdin: {0}")] + ActivatePipeError(std::io::Error), +} - #[error("Failed to run activation command over SSH: {0}")] - SSHActivate(std::io::Error), - #[error("Activating over SSH resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] - SSHActivateExit(Option, String), +impl command::HasCommandError for SSHActivateError { + fn title() -> String { + "SSH activation command".to_string() + } +} - #[error("Failed to run wait command over SSH: {0}")] - SSHWait(std::io::Error), - #[error("Waiting over SSH resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] - SSHWaitExit(Option, String), +#[derive(Error, Debug)] +pub enum SSHWaitError {} - #[error("Failed to pipe to child stdin: {0}")] - SSHActivatePipe(std::io::Error), +impl command::HasCommandError for SSHWaitError { + fn title() -> String { + "SSH wait command".to_string() + } +} + +#[derive(Error, Debug)] +pub enum DeployProfileError { + #[error("{0}")] + SSHActivate(#[from] command::CommandError), + + #[error("{0}")] + SSHWait(#[from] command::CommandError), #[error("Error confirming deployment: {0}")] Confirm(#[from] ConfirmProfileError), @@ -396,7 +422,7 @@ pub async fn deploy_profile( let ssh_addr = format!("{}@{}", deploy_defs.ssh_user, hostname); - let mut ssh_activate_command = Command::new("ssh"); + let mut ssh_activate_command = command::Command::new("ssh"); ssh_activate_command .arg(&ssh_addr) .stdin(std::process::Stdio::piped()); @@ -409,23 +435,35 @@ pub async fn deploy_profile( let mut ssh_activate_child = ssh_activate_command .arg(self_activate_command) .spawn() - .map_err(DeployProfileError::SSHSpawnActivate)?; + .map_err(|err| { + DeployProfileError::SSHActivate(command::CommandError::OtherError( + SSHActivateError::SpawnActivateError(err)) + ) + })?; if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { trace!("[activate] Piping in sudo password"); handle_sudo_stdin(&mut ssh_activate_child, deploy_defs) .await - .map_err(DeployProfileError::SSHActivatePipe)?; + .map_err(|err| { + DeployProfileError::SSHActivate(command::CommandError::OtherError( + SSHActivateError::ActivatePipeError(err) + )) + })?; } let ssh_activate_exit_status = ssh_activate_child .wait() .await - .map_err(DeployProfileError::SSHActivate)?; + .map_err(|err| { + DeployProfileError::SSHActivate(command::CommandError::RunError(err)) + })?; match ssh_activate_exit_status.code() { Some(0) => (), - a => return Err(DeployProfileError::SSHActivateExit(a, format!("{:?}", ssh_activate_command))), + exit_code => return Err(DeployProfileError::SSHActivate( + command::CommandError::Exit(exit_code, format!("{:?}", ssh_activate_command)) + )), }; if dry_activate { @@ -450,18 +488,26 @@ pub async fn deploy_profile( let mut ssh_activate_child = ssh_activate_command .arg(self_activate_command) .spawn() - .map_err(DeployProfileError::SSHSpawnActivate)?; + .map_err(|err| { + DeployProfileError::SSHActivate(command::CommandError::OtherError( + SSHActivateError::SpawnActivateError(err) + )) + })?; if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { trace!("[activate] Piping in sudo password"); handle_sudo_stdin(&mut ssh_activate_child, deploy_defs) .await - .map_err(DeployProfileError::SSHActivatePipe)?; + .map_err(|err| { + DeployProfileError::SSHActivate(command::CommandError::OtherError( + SSHActivateError::ActivatePipeError(err) + )) + })?; } info!("Creating activation waiter"); - let mut ssh_wait_command = Command::new("ssh"); + let mut ssh_wait_command = command::Command::new("ssh"); ssh_wait_command .arg(&ssh_addr) .stdin(std::process::Stdio::piped()); @@ -477,10 +523,12 @@ pub async fn deploy_profile( let o = ssh_activate_child.wait_with_output().await; let maybe_err = match o { - Err(x) => Some(DeployProfileError::SSHActivate(x)), + Err(x) => Some(DeployProfileError::SSHActivate(command::CommandError::RunError(x))), Ok(ref x) => match x.status.code() { Some(0) => None, - a => Some(DeployProfileError::SSHActivateExit(a, format!("{:?}", ssh_activate_command))), + a => Some(DeployProfileError::SSHActivate( + command::CommandError::Exit(a, format!("{:?}", ssh_activate_command)) + )), }, }; @@ -494,21 +542,29 @@ pub async fn deploy_profile( let mut ssh_wait_child = ssh_wait_command .arg(self_wait_command) .spawn() - .map_err(DeployProfileError::SSHWait)?; + .map_err(|err| { + DeployProfileError::SSHWait(command::CommandError::RunError(err)) + })?; if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { trace!("[wait] Piping in sudo password"); handle_sudo_stdin(&mut ssh_wait_child, deploy_defs) .await - .map_err(DeployProfileError::SSHActivatePipe)?; + .map_err(|err| { + DeployProfileError::SSHActivate(command::CommandError::OtherError( + SSHActivateError::ActivatePipeError(err) + )) + })?; } tokio::select! { x = ssh_wait_child.wait() => { debug!("Wait command ended"); - match x.map_err(DeployProfileError::SSHWait)?.code() { + match x.map_err(|err| DeployProfileError::SSHWait(command::CommandError::RunError(err)))?.code() { Some(0) => (), - a => return Err(DeployProfileError::SSHWaitExit(a, format!("{:?}", ssh_wait_command))), + a => return Err(DeployProfileError::SSHWait( + command::CommandError::Exit(a, format!("{:?}", ssh_wait_command)) + )), }; }, x = recv_activate => { @@ -525,21 +581,28 @@ pub async fn deploy_profile( thread .await - .map_err(|x| DeployProfileError::SSHActivate(x.into()))?; + .map_err(|x| DeployProfileError::SSHActivate(command::CommandError::RunError(x.into())))?; } Ok(()) } #[derive(Error, Debug)] -pub enum RevokeProfileError { +pub enum SSHRevokeError { #[error("Failed to spawn revocation command over SSH: {0}")] - SSHSpawnRevoke(std::io::Error), + SpawnRevokeError(std::io::Error) +} - #[error("Error revoking deployment: {0}")] - SSHRevoke(std::io::Error), - #[error("Revoking over SSH resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] - SSHRevokeExit(Option, String), +impl command::HasCommandError for SSHRevokeError { + fn title() -> String { + "SSH revoke command".to_string() + } +} + +#[derive(Error, Debug)] +pub enum RevokeProfileError { + #[error("{0}")] + SSHRevoke(#[from] command::CommandError), #[error("Deployment data invalid: {0}")] InvalidDeployDataDefs(#[from] DeployDataDefsError), @@ -565,7 +628,7 @@ pub async fn revoke( let ssh_addr = format!("{}@{}", deploy_defs.ssh_user, hostname); - let mut ssh_activate_command = Command::new("ssh"); + let mut ssh_activate_command = command::Command::new("ssh"); ssh_activate_command .arg(&ssh_addr) .stdin(std::process::Stdio::piped()); @@ -577,22 +640,28 @@ pub async fn revoke( let mut ssh_revoke_child = ssh_activate_command .arg(self_revoke_command) .spawn() - .map_err(RevokeProfileError::SSHSpawnRevoke)?; + .map_err(|err| { + RevokeProfileError::SSHRevoke(command::CommandError::OtherError( + SSHRevokeError::SpawnRevokeError(err) + )) + })?; if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { trace!("[revoke] Piping in sudo password"); handle_sudo_stdin(&mut ssh_revoke_child, deploy_defs) .await - .map_err(RevokeProfileError::SSHRevoke)?; + .map_err(|err| { + RevokeProfileError::SSHRevoke(command::CommandError::RunError(err)) + })?; } let result = ssh_revoke_child.wait_with_output().await; match result { - Err(x) => Err(RevokeProfileError::SSHRevoke(x)), + Err(x) => Err(RevokeProfileError::SSHRevoke(command::CommandError::RunError(x))), Ok(ref x) => match x.status.code() { Some(0) => Ok(()), - a => Err(RevokeProfileError::SSHRevokeExit(a,format!("{:?}", ssh_activate_command))), + a => Err(RevokeProfileError::SSHRevoke(command::CommandError::Exit(a, format!("{:?}", ssh_activate_command)))), }, } } diff --git a/src/lib.rs b/src/lib.rs index 61fac6a5..6e953361 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -148,6 +148,7 @@ pub fn init_logger( } pub mod cli; +pub mod command; pub mod data; pub mod deploy; pub mod push; diff --git a/src/push.rs b/src/push.rs index bd1bcc8b..b142054f 100644 --- a/src/push.rs +++ b/src/push.rs @@ -7,24 +7,67 @@ use std::collections::HashMap; use std::path::Path; use std::process::Stdio; use thiserror::Error; -use tokio::process::Command; + +use crate::command; #[derive(Error, Debug)] -pub enum PushProfileError { - #[error("Failed to run Nix show-derivation command: {0}")] - ShowDerivation(std::io::Error), - #[error("Nix show-derivation command resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] - ShowDerivationExit(Option, String), +pub enum ShowDerivationError { #[error("Nix show-derivation command output contained an invalid UTF-8 sequence: {0}")] - ShowDerivationUtf8(std::str::Utf8Error), + Utf8(std::str::Utf8Error), #[error("Failed to parse the output of nix show-derivation: {0}")] - ShowDerivationParse(serde_json::Error), + Parse(serde_json::Error), #[error("Nix show-derivation output is empty")] - ShowDerivationEmpty, - #[error("Failed to run Nix build command: {0}")] - Build(std::io::Error), - #[error("Nix build command resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] - BuildExit(Option, String), + Empty, +} + +impl command::HasCommandError for ShowDerivationError { + fn title() -> String { + "Nix show derivation".to_string() + } +} + +#[derive(Error, Debug)] +pub enum BuildError {} + +impl command::HasCommandError for BuildError { + fn title() -> String { + "Nix build".to_string() + } +} + +#[derive(Error, Debug)] +pub enum SignError {} + +impl command::HasCommandError for SignError { + fn title() -> String { + "Nix sign".to_string() + } +} + +#[derive(Error, Debug)] +pub enum CopyError {} + +impl command::HasCommandError for CopyError { + fn title() -> String { + "Nix copy".to_string() + } +} + +#[derive(Error, Debug)] +pub enum PathInfoError {} + +impl command::HasCommandError for PathInfoError { + fn title() -> String { + "Nix path-info".to_string() + } +} + +#[derive(Error, Debug)] +pub enum PushProfileError { + #[error("{0}")] + ShowDerivation(#[from] command::CommandError), + #[error("{0}")] + Build(#[from] command::CommandError), #[error( "Activation script deploy-rs-activate does not exist in profile.\n\ Did you forget to use deploy-rs#lib.<...>.activate.<...> on your profile path?" @@ -33,19 +76,14 @@ pub enum PushProfileError { #[error("Activation script activate-rs does not exist in profile.\n\ Is there a mismatch in deploy-rs used in the flake you're deploying and deploy-rs command you're running?")] ActivateRsDoesntExist, - #[error("Failed to run Nix sign command: {0}")] - Sign(std::io::Error), - #[error("Nix sign command resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] - SignExit(Option, String), - #[error("Failed to run Nix copy command: {0}")] - Copy(std::io::Error), - #[error("Nix copy command resulted in a bad exit code: {0:?}. The failed command is provided below:\n{1}")] - CopyExit(Option, String), + #[error("{0}")] + Sign(#[from] command::CommandError), + #[error("{0}")] + Copy(#[from] command::CommandError), #[error("The remote building option is not supported when using legacy nix")] RemoteBuildWithLegacyNix, - - #[error("Failed to run Nix path-info command: {0}")] - PathInfo(std::io::Error), + #[error("{0}")] + PathInfo(#[from] command::CommandError), } pub struct PushProfileData<'a> { @@ -66,9 +104,9 @@ pub async fn build_profile_locally(data: &PushProfileData<'_>, derivation_name: ); let mut build_command = if data.supports_flakes { - Command::new("nix") + command::Command::new("nix") } else { - Command::new("nix-build") + command::Command::new("nix-build") }; if data.supports_flakes { @@ -92,18 +130,13 @@ pub async fn build_profile_locally(data: &PushProfileData<'_>, derivation_name: build_command.args(data.extra_build_args); - let build_exit_status = build_command + build_command // Logging should be in stderr, this just stops the store path from printing for no reason .stdout(Stdio::null()) - .status() + .run() .await .map_err(PushProfileError::Build)?; - match build_exit_status.code() { - Some(0) => (), - a => return Err(PushProfileError::BuildExit(a,format!("{:?}", build_command))), - }; - if !Path::new( format!( "{}/deploy-rs-activate", @@ -134,22 +167,16 @@ pub async fn build_profile_locally(data: &PushProfileData<'_>, derivation_name: data.deploy_data.profile_name, data.deploy_data.node_name ); - let mut sign_command = Command::new("nix"); + let mut sign_command = command::Command::new("nix"); sign_command .arg("sign-paths") .arg("-r") .arg("-k") .arg(local_key) - .arg(&data.deploy_data.profile.profile_settings.path); - let sign_exit_status = sign_command - .status() + .arg(&data.deploy_data.profile.profile_settings.path) + .run() .await .map_err(PushProfileError::Sign)?; - - match sign_exit_status.code() { - Some(0) => (), - a => return Err(PushProfileError::SignExit(a, format!("{:?}", sign_command))), - }; } Ok(()) } @@ -171,26 +198,19 @@ pub async fn build_profile_remotely(data: &PushProfileData<'_>, derivation_name: // copy the derivation to remote host so it can be built there - let mut copy_command = Command::new("nix"); + let mut copy_command = command::Command::new("nix"); copy_command .arg("copy") .arg("-s") // fetch dependencies from substitures, not localhost .arg("--to").arg(&store_address) .arg("--derivation").arg(derivation_name) .env("NIX_SSHOPTS", ssh_opts_str.clone()) - .stdout(Stdio::null()); - let copy_command_status = copy_command .stdout(Stdio::null()) - .status() + .run() .await .map_err(PushProfileError::Copy)?; - match copy_command_status.code() { - Some(0) => (), - a => return Err(PushProfileError::CopyExit(a, format!("{:?}", copy_command))), - }; - - let mut build_command = Command::new("nix"); + let mut build_command = command::Command::new("nix"); build_command .arg("build").arg(derivation_name) .arg("--eval-store").arg("auto") @@ -200,19 +220,13 @@ pub async fn build_profile_remotely(data: &PushProfileData<'_>, derivation_name: debug!("build command: {:?}", build_command); - let build_exit_status = build_command + build_command // Logging should be in stderr, this just stops the store path from printing for no reason .stdout(Stdio::null()) - .status() + .run() .await .map_err(PushProfileError::Build)?; - match build_exit_status.code() { - Some(0) => (), - a => return Err(PushProfileError::BuildExit(a,format!("{:?}", build_command))), - }; - - Ok(()) } @@ -223,32 +237,38 @@ pub async fn build_profile(data: PushProfileData<'_>) -> Result<(), PushProfileE ); // `nix-store --query --deriver` doesn't work on invalid paths, so we parse output of show-derivation :( - let mut show_derivation_command = Command::new("nix"); + let mut show_derivation_command = command::Command::new("nix"); show_derivation_command .arg("show-derivation") .arg(&data.deploy_data.profile.profile_settings.path); let show_derivation_output = show_derivation_command - .output() + .run() .await .map_err(PushProfileError::ShowDerivation)?; - match show_derivation_output.status.code() { - Some(0) => (), - a => return Err(PushProfileError::ShowDerivationExit(a, format!("{:?}", show_derivation_command))), - }; - let derivation_info: HashMap<&str, serde_json::value::Value> = serde_json::from_str( - std::str::from_utf8(&show_derivation_output.stdout) - .map_err(PushProfileError::ShowDerivationUtf8)?, + std::str::from_utf8(&show_derivation_output.stdout).map_err(|err| { + PushProfileError::ShowDerivation(command::CommandError::OtherError( + ShowDerivationError::Utf8(err) + )) + })? ) - .map_err(PushProfileError::ShowDerivationParse)?; + .map_err(|err| { + PushProfileError::ShowDerivation(command::CommandError::OtherError( + ShowDerivationError::Parse(err) + )) + })?; let &deriver = derivation_info .keys() .next() - .ok_or(PushProfileError::ShowDerivationEmpty)?; + .ok_or( + PushProfileError::ShowDerivation(command::CommandError::OtherError( + ShowDerivationError::Empty + )) + )?; let new_deriver = &if data.supports_flakes { // Since nix 2.15.0 'nix build .drv' will build only the .drv file itself, not the @@ -258,11 +278,11 @@ pub async fn build_profile(data: PushProfileData<'_>) -> Result<(), PushProfileE deriver.to_owned() }; - let path_info_output = Command::new("nix") + let path_info_output = command::Command::new("nix") .arg("--experimental-features").arg("nix-command") .arg("path-info") .arg(&deriver) - .output().await + .run().await .map_err(PushProfileError::PathInfo)?; let deriver = if std::str::from_utf8(&path_info_output.stdout).map(|s| s.trim()) == Ok(deriver) { @@ -312,7 +332,7 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr data.deploy_data.profile_name, data.deploy_data.node_name ); - let mut copy_command = Command::new("nix"); + let mut copy_command = command::Command::new("nix"); copy_command.arg("copy"); if data.deploy_data.merged_settings.fast_connection != Some(true) { @@ -332,16 +352,10 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr .arg("--to") .arg(format!("ssh://{}@{}", data.deploy_defs.ssh_user, hostname)) .arg(&data.deploy_data.profile.profile_settings.path) - .env("NIX_SSHOPTS", ssh_opts_str); - let copy_exit_status = copy_command - .status() + .env("NIX_SSHOPTS", ssh_opts_str) + .run() .await .map_err(PushProfileError::Copy)?; - - match copy_exit_status.code() { - Some(0) => (), - a => return Err(PushProfileError::CopyExit(a, format!("{:?}", copy_command))), - }; } Ok(()) From 36bae3946a4e7b053fe31cf911a1553f212678ef Mon Sep 17 00:00:00 2001 From: Heitor Toledo Lassarote de Paula Date: Fri, 10 Jan 2025 17:49:04 -0300 Subject: [PATCH 3/5] [#260] Improve the output for failed Nix commands Problem: The output when a command failed for deployment would show the error message in the console output and _then_ the error message, which was confusing. Solution: Show the error message together with the error in an order that makes sense. This is done by capturing the output in the error message and printinting it in an exit error. We also use `Stdio::null` rather than `Stdio::piped` to avoid printing to the console. --- src/bin/activate.rs | 24 ++++++++++++++---------- src/cli.rs | 2 +- src/command.rs | 34 +++++++++++++++++++--------------- src/deploy.rs | 35 ++++++++++++++++++++--------------- 4 files changed, 54 insertions(+), 41 deletions(-) diff --git a/src/bin/activate.rs b/src/bin/activate.rs index f17a4196..1ba0e7af 100644 --- a/src/bin/activate.rs +++ b/src/bin/activate.rs @@ -427,19 +427,21 @@ pub async fn activate( .arg(&profile_path) .arg("--set") .arg(&closure); - let nix_env_set_exit_status = nix_env_set_command - .status() + let nix_env_set_exit_output = nix_env_set_command + .output() .await .map_err(|err| { ActivateError::SetProfile(command::CommandError::RunError(err)) })?; - match nix_env_set_exit_status.code() { + match nix_env_set_exit_output.status.code() { Some(0) => (), - a => { + _exit_code => { if auto_rollback && !dry_activate { deactivate(&profile_path).await?; } - return Err(ActivateError::SetProfile(command::CommandError::Exit(a, format!("{:?}", nix_env_set_command)))); + return Err(ActivateError::SetProfile( + command::CommandError::Exit(nix_env_set_exit_output, format!("{:?}", nix_env_set_command)) + )); } }; } @@ -458,8 +460,8 @@ pub async fn activate( .env("DRY_ACTIVATE", if dry_activate { "1" } else { "0" }) .env("BOOT", if boot { "1" } else { "0" }) .current_dir(activation_location); - let activate_status = match activate_command - .status() + let activate_output = match activate_command + .output() .await .map_err(|err| { ActivateError::RunActivate(command::CommandError::RunError(err)) @@ -475,13 +477,15 @@ pub async fn activate( }; if !dry_activate { - match activate_status.code() { + match activate_output.status.code() { Some(0) => (), - a => { + _exit_code => { if auto_rollback { deactivate(&profile_path).await?; } - return Err(ActivateError::RunActivate(command::CommandError::Exit(a, format!("{:?}", activate_command)))); + return Err(ActivateError::RunActivate( + command::CommandError::Exit(activate_output, format!("{:?}", activate_command)) + )); } }; diff --git a/src/cli.rs b/src/cli.rs index c0df5c90..031ed1b1 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -262,7 +262,7 @@ async fn get_deployment_data( eval_command.args(extra_build_args); let build_output = eval_command - .stdout(Stdio::piped()) + .stdout(Stdio::null()) .run() .await .map_err(GetDeploymentDataError::NixEval)?; diff --git a/src/command.rs b/src/command.rs index f30345d5..b4ea9055 100644 --- a/src/command.rs +++ b/src/command.rs @@ -12,26 +12,30 @@ pub trait HasCommandError { #[derive(Error, Debug)] pub enum CommandError { RunError(std::io::Error), - Exit(Option, String), + Exit(std::process::Output, String), OtherError(T), } impl fmt::Display for CommandError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - CommandError::RunError(err) => write!( - f, - "Failed to run {} command: {}", - T::title(), - err, - ), - CommandError::Exit(exit_code, cmd) => write!( - f, - "{} command resulted in a bad exit code: {:?}. The failed command is provided below:\n{}", - T::title(), - exit_code, - cmd, - ), + CommandError::RunError(err) => { + write!(f, "Failed to run {} command: {}", T::title(), err,) + } + CommandError::Exit(output, cmd) => { + let stderr = match String::from_utf8(output.stderr.clone()) { + Ok(stderr) => stderr, + Err(_err) => format!("{:?}", output.stderr), + }; + write!( + f, + "{} command resulted in a bad exit code: {:?}. The failed command is provided below:\n{}\nThe stderr output is provided below:\n{}", + T::title(), + output.status.code(), + cmd, + stderr, + ) + } CommandError::OtherError(err) => write!(f, "{}", err), } } @@ -115,7 +119,7 @@ impl Command { .map_err(CommandError::RunError)?; match output.status.code() { Some(0) => Ok(output), - exit_code => Err(CommandError::Exit(exit_code, format!("{:?}", self.command))), + _exit_code => Err(CommandError::Exit(output, format!("{:?}", self.command))), } } } diff --git a/src/deploy.rs b/src/deploy.rs index 100b8088..e401f8c9 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -319,16 +319,18 @@ pub async fn confirm_profile( })?; } - let ssh_confirm_exit_status = ssh_confirm_child - .wait() + let ssh_confirm_exit_output = ssh_confirm_child + .wait_with_output() .await .map_err(|err| { ConfirmProfileError::SSHConfirm(command::CommandError::RunError(err)) })?; - match ssh_confirm_exit_status.code() { + match ssh_confirm_exit_output.status.code() { Some(0) => (), - a => return Err(ConfirmProfileError::SSHConfirm(command::CommandError::Exit(a, format!("{:?}", ssh_confirm_command)))), + _exit_code => return Err(ConfirmProfileError::SSHConfirm( + command::CommandError::Exit(ssh_confirm_exit_output, format!("{:?}", ssh_confirm_command)) + )), }; info!("Deployment confirmed."); @@ -453,16 +455,16 @@ pub async fn deploy_profile( } let ssh_activate_exit_status = ssh_activate_child - .wait() + .wait_with_output() .await .map_err(|err| { DeployProfileError::SSHActivate(command::CommandError::RunError(err)) })?; - match ssh_activate_exit_status.code() { + match ssh_activate_exit_status.status.code() { Some(0) => (), - exit_code => return Err(DeployProfileError::SSHActivate( - command::CommandError::Exit(exit_code, format!("{:?}", ssh_activate_command)) + _exit_code => return Err(DeployProfileError::SSHActivate( + command::CommandError::Exit(ssh_activate_exit_status, format!("{:?}", ssh_activate_command)) )), }; @@ -526,8 +528,8 @@ pub async fn deploy_profile( Err(x) => Some(DeployProfileError::SSHActivate(command::CommandError::RunError(x))), Ok(ref x) => match x.status.code() { Some(0) => None, - a => Some(DeployProfileError::SSHActivate( - command::CommandError::Exit(a, format!("{:?}", ssh_activate_command)) + _exit_code => Some(DeployProfileError::SSHActivate( + command::CommandError::Exit(x.clone(), format!("{:?}", ssh_activate_command)) )), }, }; @@ -558,12 +560,13 @@ pub async fn deploy_profile( } tokio::select! { - x = ssh_wait_child.wait() => { + x = ssh_wait_child.wait_with_output() => { debug!("Wait command ended"); - match x.map_err(|err| DeployProfileError::SSHWait(command::CommandError::RunError(err)))?.code() { + let output = x.map_err(|err| DeployProfileError::SSHWait(command::CommandError::RunError(err)))?; + match output.status.code() { Some(0) => (), - a => return Err(DeployProfileError::SSHWait( - command::CommandError::Exit(a, format!("{:?}", ssh_wait_command)) + _exit_code => return Err(DeployProfileError::SSHWait( + command::CommandError::Exit(output, format!("{:?}", ssh_wait_command)) )), }; }, @@ -661,7 +664,9 @@ pub async fn revoke( Err(x) => Err(RevokeProfileError::SSHRevoke(command::CommandError::RunError(x))), Ok(ref x) => match x.status.code() { Some(0) => Ok(()), - a => Err(RevokeProfileError::SSHRevoke(command::CommandError::Exit(a, format!("{:?}", ssh_activate_command)))), + _exit_code => Err(RevokeProfileError::SSHRevoke( + command::CommandError::Exit(x.clone(), format!("{:?}", ssh_activate_command)) + )), }, } } From c87f4f882e7cc54b1dc723f7afffb3674cc3b0e3 Mon Sep 17 00:00:00 2001 From: Heitor Toledo Lassarote de Paula Date: Fri, 10 Jan 2025 19:45:55 -0300 Subject: [PATCH 4/5] fixup! [#260] Abstract some of the boilerplate --- src/bin/activate.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/activate.rs b/src/bin/activate.rs index 1ba0e7af..944caca3 100644 --- a/src/bin/activate.rs +++ b/src/bin/activate.rs @@ -154,7 +154,7 @@ pub enum ReactivateError {} impl command::HasCommandError for ReactivateError { fn title() -> String { - "Nix reactive last generation".to_string() + "Nix reactivate last generation".to_string() } } From b8ffefb1f4f9f5cce56d4d821b2672edb1cc9d27 Mon Sep 17 00:00:00 2001 From: Heitor Toledo Lassarote de Paula Date: Wed, 15 Jan 2025 11:25:20 -0300 Subject: [PATCH 5/5] fixup! [#260] Abstract some of the boilerplate --- src/bin/activate.rs | 24 ++++++++++------- src/cli.rs | 17 ++++++------ src/command.rs | 63 ++------------------------------------------- src/deploy.rs | 10 +++---- src/push.rs | 50 ++++++++++++++++++++--------------- 5 files changed, 59 insertions(+), 105 deletions(-) diff --git a/src/bin/activate.rs b/src/bin/activate.rs index 944caca3..ac0a4da1 100644 --- a/src/bin/activate.rs +++ b/src/bin/activate.rs @@ -9,6 +9,7 @@ use signal_hook::{consts::signal::SIGHUP, iterator::Signals}; use clap::Clap; use tokio::fs; +use tokio::process::Command; use tokio::sync::mpsc; use tokio::time::timeout; @@ -175,23 +176,24 @@ pub enum DeactivateError { pub async fn deactivate(profile_path: &str) -> Result<(), DeactivateError> { warn!("De-activating due to error"); - let mut nix_env_rollback_command = command::Command::new("nix-env"); + let mut nix_env_rollback_command = Command::new("nix-env"); nix_env_rollback_command .arg("-p") .arg(&profile_path) - .arg("--rollback") + .arg("--rollback"); + command::Command::new(nix_env_rollback_command) .run() .await .map_err(DeactivateError::Rollback)?; debug!("Listing generations"); - let mut nix_env_list_generations_command = command::Command::new("nix-env"); + let mut nix_env_list_generations_command = Command::new("nix-env"); nix_env_list_generations_command .arg("-p") .arg(&profile_path) .arg("--list-generations"); - let nix_env_list_generations_out = nix_env_list_generations_command + let nix_env_list_generations_out = command::Command::new(nix_env_list_generations_command) .run() .await .map_err(DeactivateError::ListGen)?; @@ -212,22 +214,24 @@ pub async fn deactivate(profile_path: &str) -> Result<(), DeactivateError> { debug!("Removing generation entry {}", last_generation_line); warn!("Removing generation by ID {}", last_generation_id); - let mut nix_env_delete_generation_command = command::Command::new("nix-env"); + let mut nix_env_delete_generation_command = Command::new("nix-env"); nix_env_delete_generation_command .arg("-p") .arg(&profile_path) .arg("--delete-generations") - .arg(last_generation_id) + .arg(last_generation_id); + command::Command::new(nix_env_delete_generation_command) .run() .await .map_err(DeactivateError::DeleteGen)?; info!("Attempting to re-activate the last generation"); - let mut re_activate_command = command::Command::new(format!("{}/deploy-rs-activate", profile_path)); + let mut re_activate_command = Command::new(format!("{}/deploy-rs-activate", profile_path)); re_activate_command .env("PROFILE", &profile_path) - .current_dir(&profile_path) + .current_dir(&profile_path); + command::Command::new(re_activate_command) .run() .await .map_err(DeactivateError::Reactivate)?; @@ -421,7 +425,7 @@ pub async fn activate( ) -> Result<(), ActivateError> { if !dry_activate { info!("Activating profile"); - let mut nix_env_set_command = command::Command::new("nix-env"); + let mut nix_env_set_command = Command::new("nix-env"); nix_env_set_command .arg("-p") .arg(&profile_path) @@ -454,7 +458,7 @@ pub async fn activate( &profile_path }; - let mut activate_command = command::Command::new(format!("{}/deploy-rs-activate", activation_location)); + let mut activate_command = Command::new(format!("{}/deploy-rs-activate", activation_location)); activate_command .env("PROFILE", activation_location) .env("DRY_ACTIVATE", if dry_activate { "1" } else { "0" }) diff --git a/src/cli.rs b/src/cli.rs index 031ed1b1..379e7caa 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -148,8 +148,8 @@ async fn check_deployment( info!("Running checks for flake in {}", repo); let mut check_command = match supports_flakes { - true => command::Command::new("nix"), - false => command::Command::new("nix-build"), + true => Command::new("nix"), + false => Command::new("nix-build"), }; if supports_flakes { @@ -162,7 +162,7 @@ async fn check_deployment( check_command.args(extra_build_args); - check_command.run().await.map_err(CheckDeploymentError::NixCheck)?; + command::Command::new(check_command).run().await.map_err(CheckDeploymentError::NixCheck)?; Ok(()) } @@ -199,9 +199,9 @@ async fn get_deployment_data( info!("Evaluating flake in {}", flake.repo); let mut eval_command = if supports_flakes { - command::Command::new("nix") + Command::new("nix") } else { - command::Command::new("nix-instantiate") + Command::new("nix-instantiate") }; if supports_flakes { @@ -259,10 +259,11 @@ async fn get_deployment_data( .arg(format!("let r = import {}/.; in if builtins.isFunction r then (r {{}}).deploy else r.deploy", flake.repo)) }; - eval_command.args(extra_build_args); + eval_command + .args(extra_build_args) + .stdout(Stdio::null()); - let build_output = eval_command - .stdout(Stdio::null()) + let build_output = command::Command::new(eval_command) .run() .await .map_err(GetDeploymentDataError::NixEval)?; diff --git a/src/command.rs b/src/command.rs index b4ea9055..89ab1760 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,7 +1,5 @@ -use std::ffi::OsStr; use std::fmt; use std::fmt::Debug; -use std::future::Future; use thiserror::Error; use tokio::process::Command as TokioCommand; @@ -48,65 +46,8 @@ pub struct Command { } impl Command { - pub fn new>(program: S) -> Command { - Command { - command: TokioCommand::new(program), - } - } - - pub fn arg>(&mut self, arg: S) -> &mut Command { - self.command.arg(arg); - self - } - - pub fn args(&mut self, args: I) -> &mut Command - where - I: IntoIterator, - S: AsRef, - { - self.command.args(args); - self - } - - pub fn env(&mut self, key: K, val: V) -> &mut Command - where - K: AsRef, - V: AsRef, - { - self.command.env(key, val); - self - } - - pub fn output(&mut self) -> impl Future> { - self.command.output() - } - - pub fn current_dir>(&mut self, dir: P) -> &mut Command { - self.command.current_dir(dir); - self - } - - pub fn stdin>(&mut self, cfg: T) -> &mut Command { - self.command.stdin(cfg); - self - } - - pub fn stdout>(&mut self, cfg: T) -> &mut Command { - self.command.stdout(cfg); - self - } - - pub fn stderr>(&mut self, cfg: T) -> &mut Command { - self.command.stderr(cfg); - self - } - - pub fn spawn(&mut self) -> std::io::Result { - self.command.spawn() - } - - pub fn status(&mut self) -> impl Future> { - self.command.status() + pub fn new(command: TokioCommand) -> Command { + Command { command } } pub async fn run( diff --git a/src/deploy.rs b/src/deploy.rs index e401f8c9..389148bf 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -7,7 +7,7 @@ use log::{debug, info, trace}; use std::path::Path; use thiserror::Error; -use tokio::io::AsyncWriteExt; +use tokio::{io::AsyncWriteExt, process::Command}; use crate::{command, DeployDataDefsError, DeployDefs, ProfileInfo}; @@ -282,7 +282,7 @@ pub async fn confirm_profile( temp_path: &Path, ssh_addr: &str, ) -> Result<(), ConfirmProfileError> { - let mut ssh_confirm_command = command::Command::new("ssh"); + let mut ssh_confirm_command = Command::new("ssh"); ssh_confirm_command .arg(ssh_addr) .stdin(std::process::Stdio::piped()); @@ -424,7 +424,7 @@ pub async fn deploy_profile( let ssh_addr = format!("{}@{}", deploy_defs.ssh_user, hostname); - let mut ssh_activate_command = command::Command::new("ssh"); + let mut ssh_activate_command = Command::new("ssh"); ssh_activate_command .arg(&ssh_addr) .stdin(std::process::Stdio::piped()); @@ -509,7 +509,7 @@ pub async fn deploy_profile( info!("Creating activation waiter"); - let mut ssh_wait_command = command::Command::new("ssh"); + let mut ssh_wait_command = Command::new("ssh"); ssh_wait_command .arg(&ssh_addr) .stdin(std::process::Stdio::piped()); @@ -631,7 +631,7 @@ pub async fn revoke( let ssh_addr = format!("{}@{}", deploy_defs.ssh_user, hostname); - let mut ssh_activate_command = command::Command::new("ssh"); + let mut ssh_activate_command = Command::new("ssh"); ssh_activate_command .arg(&ssh_addr) .stdin(std::process::Stdio::piped()); diff --git a/src/push.rs b/src/push.rs index b142054f..254b8dfc 100644 --- a/src/push.rs +++ b/src/push.rs @@ -7,6 +7,7 @@ use std::collections::HashMap; use std::path::Path; use std::process::Stdio; use thiserror::Error; +use tokio::process::Command; use crate::command; @@ -104,9 +105,9 @@ pub async fn build_profile_locally(data: &PushProfileData<'_>, derivation_name: ); let mut build_command = if data.supports_flakes { - command::Command::new("nix") + Command::new("nix") } else { - command::Command::new("nix-build") + Command::new("nix-build") }; if data.supports_flakes { @@ -128,11 +129,12 @@ pub async fn build_profile_locally(data: &PushProfileData<'_>, derivation_name: (false, true) => build_command.arg("--no-link"), }; - build_command.args(data.extra_build_args); - build_command + .args(data.extra_build_args) // Logging should be in stderr, this just stops the store path from printing for no reason - .stdout(Stdio::null()) + .stdout(Stdio::null()); + + command::Command::new(build_command) .run() .await .map_err(PushProfileError::Build)?; @@ -167,13 +169,14 @@ pub async fn build_profile_locally(data: &PushProfileData<'_>, derivation_name: data.deploy_data.profile_name, data.deploy_data.node_name ); - let mut sign_command = command::Command::new("nix"); + let mut sign_command = Command::new("nix"); sign_command .arg("sign-paths") .arg("-r") .arg("-k") .arg(local_key) - .arg(&data.deploy_data.profile.profile_settings.path) + .arg(&data.deploy_data.profile.profile_settings.path); + command::Command::new(sign_command) .run() .await .map_err(PushProfileError::Sign)?; @@ -198,31 +201,32 @@ pub async fn build_profile_remotely(data: &PushProfileData<'_>, derivation_name: // copy the derivation to remote host so it can be built there - let mut copy_command = command::Command::new("nix"); + let mut copy_command = Command::new("nix"); copy_command .arg("copy") .arg("-s") // fetch dependencies from substitures, not localhost .arg("--to").arg(&store_address) .arg("--derivation").arg(derivation_name) .env("NIX_SSHOPTS", ssh_opts_str.clone()) - .stdout(Stdio::null()) + .stdout(Stdio::null()); + command::Command::new(copy_command) .run() .await .map_err(PushProfileError::Copy)?; - let mut build_command = command::Command::new("nix"); + let mut build_command = Command::new("nix"); build_command .arg("build").arg(derivation_name) .arg("--eval-store").arg("auto") .arg("--store").arg(&store_address) .args(data.extra_build_args) - .env("NIX_SSHOPTS", ssh_opts_str.clone()); + .env("NIX_SSHOPTS", ssh_opts_str.clone()) + // Logging should be in stderr, this just stops the store path from printing for no reason + .stdout(Stdio::null()); debug!("build command: {:?}", build_command); - build_command - // Logging should be in stderr, this just stops the store path from printing for no reason - .stdout(Stdio::null()) + command::Command::new(build_command) .run() .await .map_err(PushProfileError::Build)?; @@ -237,13 +241,13 @@ pub async fn build_profile(data: PushProfileData<'_>) -> Result<(), PushProfileE ); // `nix-store --query --deriver` doesn't work on invalid paths, so we parse output of show-derivation :( - let mut show_derivation_command = command::Command::new("nix"); + let mut show_derivation_command = Command::new("nix"); show_derivation_command .arg("show-derivation") .arg(&data.deploy_data.profile.profile_settings.path); - let show_derivation_output = show_derivation_command + let show_derivation_output = command::Command::new(show_derivation_command) .run() .await .map_err(PushProfileError::ShowDerivation)?; @@ -278,11 +282,14 @@ pub async fn build_profile(data: PushProfileData<'_>) -> Result<(), PushProfileE deriver.to_owned() }; - let path_info_output = command::Command::new("nix") + let mut path_info_command = Command::new("nix"); + path_info_command .arg("--experimental-features").arg("nix-command") .arg("path-info") - .arg(&deriver) - .run().await + .arg(&deriver); + let path_info_output = command::Command::new(path_info_command) + .run() + .await .map_err(PushProfileError::PathInfo)?; let deriver = if std::str::from_utf8(&path_info_output.stdout).map(|s| s.trim()) == Ok(deriver) { @@ -332,7 +339,7 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr data.deploy_data.profile_name, data.deploy_data.node_name ); - let mut copy_command = command::Command::new("nix"); + let mut copy_command = Command::new("nix"); copy_command.arg("copy"); if data.deploy_data.merged_settings.fast_connection != Some(true) { @@ -352,7 +359,8 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr .arg("--to") .arg(format!("ssh://{}@{}", data.deploy_defs.ssh_user, hostname)) .arg(&data.deploy_data.profile.profile_settings.path) - .env("NIX_SSHOPTS", ssh_opts_str) + .env("NIX_SSHOPTS", ssh_opts_str); + command::Command::new(copy_command) .run() .await .map_err(PushProfileError::Copy)?;