Allow expected errors (#116)
* Move binary out of /nix if it is there during uninstall * Add tracing * Sorta working... * Have expected() return an err * Better handle expected errors during install * Hello trailing whitespace
This commit is contained in:
parent
78ebeacb50
commit
dad8180985
|
@ -160,6 +160,8 @@ pub use stateful::{ActionState, StatefulAction};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use tokio::task::JoinError;
|
use tokio::task::JoinError;
|
||||||
|
|
||||||
|
use crate::error::HasExpectedErrors;
|
||||||
|
|
||||||
/// An action which can be reverted or completed, with an action state
|
/// An action which can be reverted or completed, with an action state
|
||||||
///
|
///
|
||||||
/// This trait interacts with [`StatefulAction`] which does the [`ActionState`] manipulation and provides some tracing facilities.
|
/// This trait interacts with [`StatefulAction`] which does the [`ActionState`] manipulation and provides some tracing facilities.
|
||||||
|
@ -308,3 +310,9 @@ pub enum ActionError {
|
||||||
std::string::FromUtf8Error,
|
std::string::FromUtf8Error,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HasExpectedErrors for ActionError {
|
||||||
|
fn expected(&self) -> Option<Box<dyn std::error::Error>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ use std::{
|
||||||
use crate::{
|
use crate::{
|
||||||
action::ActionState,
|
action::ActionState,
|
||||||
cli::{ensure_root, interaction, signal_channel, CommandExecute},
|
cli::{ensure_root, interaction, signal_channel, CommandExecute},
|
||||||
|
error::HasExpectedErrors,
|
||||||
plan::RECEIPT_LOCATION,
|
plan::RECEIPT_LOCATION,
|
||||||
planner::Planner,
|
planner::Planner,
|
||||||
BuiltinPlanner, InstallPlan,
|
BuiltinPlanner, InstallPlan,
|
||||||
|
@ -99,7 +100,17 @@ impl CommandExecute for Install {
|
||||||
let builtin_planner = BuiltinPlanner::default()
|
let builtin_planner = BuiltinPlanner::default()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| eyre::eyre!(e))?;
|
.map_err(|e| eyre::eyre!(e))?;
|
||||||
builtin_planner.plan().await.map_err(|e| eyre!(e))?
|
let res = builtin_planner.plan().await;
|
||||||
|
match res {
|
||||||
|
Ok(plan) => plan,
|
||||||
|
Err(e) => {
|
||||||
|
if let Some(expected) = e.expected() {
|
||||||
|
eprintln!("{}", expected.red());
|
||||||
|
return Ok(ExitCode::FAILURE);
|
||||||
|
}
|
||||||
|
return Err(e.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
(Some(_), Some(_)) => return Err(eyre!("`--plan` conflicts with passing a planner, a planner creates plans, so passing an existing plan doesn't make sense")),
|
(Some(_), Some(_)) => return Err(eyre!("`--plan` conflicts with passing a planner, a planner creates plans, so passing an existing plan doesn't make sense")),
|
||||||
};
|
};
|
||||||
|
@ -120,9 +131,17 @@ impl CommandExecute for Install {
|
||||||
let (tx, rx1) = signal_channel().await?;
|
let (tx, rx1) = signal_channel().await?;
|
||||||
|
|
||||||
if let Err(err) = install_plan.install(rx1).await {
|
if let Err(err) = install_plan.install(rx1).await {
|
||||||
let error = eyre!(err).wrap_err("Install failure");
|
|
||||||
if !no_confirm {
|
if !no_confirm {
|
||||||
tracing::error!("{:?}", error);
|
let mut was_expected = false;
|
||||||
|
if let Some(expected) = err.expected() {
|
||||||
|
was_expected = true;
|
||||||
|
eprintln!("{}", expected.red())
|
||||||
|
}
|
||||||
|
if !was_expected {
|
||||||
|
let error = eyre!(err).wrap_err("Install failure");
|
||||||
|
tracing::error!("{:?}", error);
|
||||||
|
};
|
||||||
|
|
||||||
if !interaction::confirm(
|
if !interaction::confirm(
|
||||||
install_plan
|
install_plan
|
||||||
.describe_uninstall(explain)
|
.describe_uninstall(explain)
|
||||||
|
@ -134,8 +153,22 @@ impl CommandExecute for Install {
|
||||||
interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await;
|
interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await;
|
||||||
}
|
}
|
||||||
let rx2 = tx.subscribe();
|
let rx2 = tx.subscribe();
|
||||||
install_plan.uninstall(rx2).await?
|
let res = install_plan.uninstall(rx2).await;
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
if let Some(expected) = e.expected() {
|
||||||
|
eprintln!("{}", expected.red());
|
||||||
|
return Ok(ExitCode::FAILURE);
|
||||||
|
}
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if let Some(expected) = err.expected() {
|
||||||
|
eprintln!("{}", expected.red());
|
||||||
|
return Ok(ExitCode::FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
let error = eyre!(err).wrap_err("Install failure");
|
||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
use std::{path::PathBuf, process::ExitCode};
|
use std::{path::PathBuf, process::ExitCode};
|
||||||
|
|
||||||
use crate::BuiltinPlanner;
|
use crate::{error::HasExpectedErrors, BuiltinPlanner};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
use eyre::WrapErr;
|
use eyre::WrapErr;
|
||||||
|
use owo_colors::OwoColorize;
|
||||||
|
|
||||||
use crate::cli::CommandExecute;
|
use crate::cli::CommandExecute;
|
||||||
|
|
||||||
|
@ -29,7 +30,18 @@ impl CommandExecute for Plan {
|
||||||
.map_err(|e| eyre::eyre!(e))?,
|
.map_err(|e| eyre::eyre!(e))?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let install_plan = planner.plan().await.map_err(|e| eyre::eyre!(e))?;
|
let res = planner.plan().await;
|
||||||
|
|
||||||
|
let install_plan = match res {
|
||||||
|
Ok(plan) => plan,
|
||||||
|
Err(e) => {
|
||||||
|
if let Some(expected) = e.expected() {
|
||||||
|
eprintln!("{}", expected.red());
|
||||||
|
return Ok(ExitCode::FAILURE);
|
||||||
|
}
|
||||||
|
return Err(e.into());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
let json = serde_json::to_string_pretty(&install_plan)?;
|
let json = serde_json::to_string_pretty(&install_plan)?;
|
||||||
tokio::fs::write(output, json)
|
tokio::fs::write(output, json)
|
||||||
|
|
|
@ -6,6 +6,7 @@ use std::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{ensure_root, signal_channel},
|
cli::{ensure_root, signal_channel},
|
||||||
|
error::HasExpectedErrors,
|
||||||
plan::RECEIPT_LOCATION,
|
plan::RECEIPT_LOCATION,
|
||||||
InstallPlan,
|
InstallPlan,
|
||||||
};
|
};
|
||||||
|
@ -93,6 +94,33 @@ impl CommandExecute for Uninstall {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// During install, `harmonic` will store a copy of itself in `/nix/harmonic`
|
||||||
|
// If the user opted to run that particular copy of Harmonic to do this uninstall,
|
||||||
|
// well, we have a problem, since the binary would delete itself.
|
||||||
|
// Instead, detect if we're in that location, if so, move the binary and `execv` it.
|
||||||
|
if let Ok(current_exe) = std::env::current_exe() {
|
||||||
|
if current_exe.as_path() == Path::new("/nix/harmonic") {
|
||||||
|
tracing::debug!(
|
||||||
|
"Detected uninstall from `/nix/harmonic`, moving executable and re-executing"
|
||||||
|
);
|
||||||
|
let temp = std::env::temp_dir();
|
||||||
|
let temp_exe = temp.join("harmonic");
|
||||||
|
tokio::fs::copy(¤t_exe, &temp_exe)
|
||||||
|
.await
|
||||||
|
.wrap_err("Copying harmonic to tempdir")?;
|
||||||
|
let args = std::env::args();
|
||||||
|
let mut arg_vec_cstring = vec![];
|
||||||
|
for arg in args {
|
||||||
|
arg_vec_cstring.push(CString::new(arg).wrap_err("Making arg into C string")?);
|
||||||
|
}
|
||||||
|
let temp_exe_cstring = CString::new(temp_exe.to_string_lossy().into_owned())
|
||||||
|
.wrap_err("Making C string of executable path")?;
|
||||||
|
|
||||||
|
nix::unistd::execv(&temp_exe_cstring, &arg_vec_cstring)
|
||||||
|
.wrap_err("Executing copied Harmonic")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let install_receipt_string = tokio::fs::read_to_string(receipt)
|
let install_receipt_string = tokio::fs::read_to_string(receipt)
|
||||||
.await
|
.await
|
||||||
.wrap_err("Reading receipt")?;
|
.wrap_err("Reading receipt")?;
|
||||||
|
@ -111,7 +139,15 @@ impl CommandExecute for Uninstall {
|
||||||
|
|
||||||
let (_tx, rx) = signal_channel().await?;
|
let (_tx, rx) = signal_channel().await?;
|
||||||
|
|
||||||
plan.uninstall(rx).await?;
|
let res = plan.uninstall(rx).await;
|
||||||
|
if let Err(e) = res {
|
||||||
|
if let Some(expected) = e.expected() {
|
||||||
|
println!("{}", expected.red());
|
||||||
|
return Ok(ExitCode::FAILURE);
|
||||||
|
}
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(@hoverbear): It would be so nice to catch errors and offer the user a way to keep going...
|
// TODO(@hoverbear): It would be so nice to catch errors and offer the user a way to keep going...
|
||||||
// However that will require being able to link error -> step and manually setting that step as `Uncompleted`.
|
// However that will require being able to link error -> step and manually setting that step as `Uncompleted`.
|
||||||
|
|
||||||
|
|
19
src/error.rs
19
src/error.rs
|
@ -54,3 +54,22 @@ pub enum HarmonicError {
|
||||||
InstallSettingsError,
|
InstallSettingsError,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) trait HasExpectedErrors {
|
||||||
|
fn expected<'a>(&'a self) -> Option<Box<dyn std::error::Error + 'a>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasExpectedErrors for HarmonicError {
|
||||||
|
fn expected<'a>(&'a self) -> Option<Box<dyn std::error::Error + 'a>> {
|
||||||
|
match self {
|
||||||
|
HarmonicError::Action(action_error) => action_error.expected(),
|
||||||
|
HarmonicError::RecordingReceipt(_, _) => None,
|
||||||
|
HarmonicError::CopyingSelf(_) => None,
|
||||||
|
HarmonicError::SerializingReceipt(_) => None,
|
||||||
|
HarmonicError::Cancelled => None,
|
||||||
|
HarmonicError::SemVer(_) => None,
|
||||||
|
HarmonicError::Planner(planner_error) => planner_error.expected(),
|
||||||
|
HarmonicError::InstallSettings(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ impl Planner for LinuxMulti {
|
||||||
// If on NixOS, running `harmonic` is pointless
|
// If on NixOS, running `harmonic` is pointless
|
||||||
// NixOS always sets up this file as part of setting up /etc itself: https://github.com/NixOS/nixpkgs/blob/bdd39e5757d858bd6ea58ed65b4a2e52c8ed11ca/nixos/modules/system/etc/setup-etc.pl#L145
|
// NixOS always sets up this file as part of setting up /etc itself: https://github.com/NixOS/nixpkgs/blob/bdd39e5757d858bd6ea58ed65b4a2e52c8ed11ca/nixos/modules/system/etc/setup-etc.pl#L145
|
||||||
if Path::new("/etc/NIXOS").exists() {
|
if Path::new("/etc/NIXOS").exists() {
|
||||||
return Err(PlannerError::Custom(Box::new(LinuxMultiError::NixOs)));
|
return Err(PlannerError::NixOs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For now, we don't try to repair the user's Nix install or anything special.
|
// For now, we don't try to repair the user's Nix install or anything special.
|
||||||
|
@ -43,7 +43,7 @@ impl Planner for LinuxMulti {
|
||||||
.status()
|
.status()
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Err(PlannerError::Custom(Box::new(LinuxMultiError::NixExists)));
|
return Err(PlannerError::NixExists);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(vec![
|
Ok(vec![
|
||||||
|
@ -84,10 +84,6 @@ impl Into<BuiltinPlanner> for LinuxMulti {
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
enum LinuxMultiError {
|
enum LinuxMultiError {
|
||||||
#[error("NixOS already has Nix installed")]
|
|
||||||
NixOs,
|
|
||||||
#[error("`nix` is already a valid command, so it is installed")]
|
|
||||||
NixExists,
|
|
||||||
#[error("Error planning action")]
|
#[error("Error planning action")]
|
||||||
Action(
|
Action(
|
||||||
#[source]
|
#[source]
|
||||||
|
|
|
@ -81,6 +81,7 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
action::{ActionError, StatefulAction},
|
action::{ActionError, StatefulAction},
|
||||||
|
error::HasExpectedErrors,
|
||||||
settings::InstallSettingsError,
|
settings::InstallSettingsError,
|
||||||
Action, HarmonicError, InstallPlan,
|
Action, HarmonicError, InstallPlan,
|
||||||
};
|
};
|
||||||
|
@ -181,4 +182,22 @@ pub enum PlannerError {
|
||||||
/// Custom planner error
|
/// Custom planner error
|
||||||
#[error("Custom planner error")]
|
#[error("Custom planner error")]
|
||||||
Custom(#[source] Box<dyn std::error::Error + Send + Sync>),
|
Custom(#[source] Box<dyn std::error::Error + Send + Sync>),
|
||||||
|
#[error("NixOS already has Nix installed")]
|
||||||
|
NixOs,
|
||||||
|
#[error("`nix` is already a valid command, so it is installed")]
|
||||||
|
NixExists,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasExpectedErrors for PlannerError {
|
||||||
|
fn expected<'a>(&'a self) -> Option<Box<dyn std::error::Error + 'a>> {
|
||||||
|
match self {
|
||||||
|
this @ PlannerError::UnsupportedArchitecture(_) => Some(Box::new(this)),
|
||||||
|
PlannerError::Action(_) => None,
|
||||||
|
PlannerError::InstallSettings(_) => None,
|
||||||
|
PlannerError::Plist(_) => None,
|
||||||
|
PlannerError::Custom(_) => None,
|
||||||
|
this @ PlannerError::NixOs => Some(Box::new(this)),
|
||||||
|
this @ PlannerError::NixExists => Some(Box::new(this)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue