Add more failure context / Improve error structure (#296)

* wip: add more context to errors

* Add a bunch fo context

* Repair source handling

* Add remaining contexts

* Add some context, but some of it is not right...

* Tidy up contexts properly

* Get command errors working how I want

* Remove some debug statements

* Repair mac build

* Move typetag to Action

* newtypes!

* Fix doctest
This commit is contained in:
Ana Hobden 2023-03-03 14:20:17 -08:00 committed by GitHub
parent 49154b9863
commit 903258942c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 786 additions and 453 deletions

1
Cargo.lock generated
View file

@ -203,6 +203,7 @@ dependencies = [
"once_cell", "once_cell",
"owo-colors", "owo-colors",
"tracing-error", "tracing-error",
"url",
] ]
[[package]] [[package]]

View file

@ -28,7 +28,7 @@ async-trait = { version = "0.1.57", default-features = false }
atty = { version = "0.2.14", default-features = false, optional = true } atty = { version = "0.2.14", default-features = false, optional = true }
bytes = { version = "1.2.1", default-features = false, features = ["std", "serde"] } bytes = { version = "1.2.1", default-features = false, features = ["std", "serde"] }
clap = { version = "4", features = ["std", "color", "usage", "help", "error-context", "suggestions", "derive", "env"], optional = true } clap = { version = "4", features = ["std", "color", "usage", "help", "error-context", "suggestions", "derive", "env"], optional = true }
color-eyre = { version = "0.6.2", default-features = false, features = [ "track-caller", "tracing-error", "capture-spantrace", "color-spantrace" ], optional = true } color-eyre = { version = "0.6.2", default-features = false, features = [ "track-caller", "issue-url", "tracing-error", "capture-spantrace", "color-spantrace" ], optional = true }
eyre = { version = "0.6.8", default-features = false, features = [ "track-caller" ], optional = true } eyre = { version = "0.6.8", default-features = false, features = [ "track-caller" ], optional = true }
glob = { version = "0.3.0", default-features = false } glob = { version = "0.3.0", default-features = false }
nix = { version = "0.26.0", default-features = false, features = ["user", "fs", "process", "term"] } nix = { version = "0.26.0", default-features = false, features = ["user", "fs", "process", "term"] }

View file

@ -70,9 +70,11 @@ impl AddUserToGroup {
command.arg(&this.groupname); command.arg(&this.groupname);
command.stdout(Stdio::piped()); command.stdout(Stdio::piped());
command.stderr(Stdio::piped()); command.stderr(Stdio::piped());
let command_str = format!("{:?}", command.as_std()); tracing::trace!("Executing `{:?}`", command.as_std());
tracing::trace!("Executing `{command_str}`"); let output = command
let output = command.output().await.map_err(ActionError::Command)?; .output()
.await
.map_err(|e| ActionError::command(&command, e))?;
match output.status.code() { match output.status.code() {
Some(0) => { Some(0) => {
// yes {user} is a member of {groupname} // yes {user} is a member of {groupname}
@ -96,18 +98,7 @@ impl AddUserToGroup {
}, },
_ => { _ => {
// Some other issue // Some other issue
return Err(ActionError::Command(std::io::Error::new( return Err(ActionError::command_output(&command, output));
std::io::ErrorKind::Other,
format!(
"Command `{command_str}` failed{}, stderr:\n{}\n",
if let Some(code) = output.status.code() {
format!(" status {code}")
} else {
"".to_string()
},
String::from_utf8_lossy(&output.stderr),
),
)));
}, },
}; };
}, },
@ -118,8 +109,7 @@ impl AddUserToGroup {
.arg(&this.name) .arg(&this.name)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
let output_str = String::from_utf8(output.stdout)?; let output_str = String::from_utf8(output.stdout)?;
let user_in_group = output_str.split(" ").any(|v| v == &this.groupname); let user_in_group = output_str.split(" ").any(|v| v == &this.groupname);
@ -138,6 +128,9 @@ impl AddUserToGroup {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "add_user_to_group")] #[typetag::serde(name = "add_user_to_group")]
impl Action for AddUserToGroup { impl Action for AddUserToGroup {
fn action_tag() -> crate::action::ActionTag {
crate::action::ActionTag("add_user_to_group")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!( format!(
"Add user `{}` (UID {}) to group `{}` (GID {})", "Add user `{}` (UID {}) to group `{}` (GID {})",
@ -194,8 +187,7 @@ impl Action for AddUserToGroup {
.arg(&name) .arg(&name)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
execute_command( execute_command(
Command::new("/usr/sbin/dseditgroup") Command::new("/usr/sbin/dseditgroup")
.process_group(0) .process_group(0)
@ -207,8 +199,7 @@ impl Action for AddUserToGroup {
.arg(groupname) .arg(groupname)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
}, },
_ => { _ => {
execute_command( execute_command(
@ -218,8 +209,7 @@ impl Action for AddUserToGroup {
.args([&name.to_string(), &groupname.to_string()]) .args([&name.to_string(), &groupname.to_string()])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
}, },
} }
@ -262,8 +252,7 @@ impl Action for AddUserToGroup {
.arg(&name) .arg(&name)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
}, },
_ => { _ => {
execute_command( execute_command(
@ -273,8 +262,7 @@ impl Action for AddUserToGroup {
.args([&name.to_string(), &groupname.to_string()]) .args([&name.to_string(), &groupname.to_string()])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
}, },
}; };

View file

@ -99,6 +99,9 @@ impl CreateDirectory {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "create_directory")] #[typetag::serde(name = "create_directory")]
impl Action for CreateDirectory { impl Action for CreateDirectory {
fn action_tag() -> crate::action::ActionTag {
crate::action::ActionTag("create_directory")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!("Create directory `{}`", self.path.display()) format!("Create directory `{}`", self.path.display())
} }

View file

@ -10,7 +10,7 @@ use tokio::{
io::{AsyncReadExt, AsyncWriteExt}, io::{AsyncReadExt, AsyncWriteExt},
}; };
use crate::action::{Action, ActionDescription, ActionError, StatefulAction}; use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction};
/** Create a file at the given location with the provided `buf`, /** Create a file at the given location with the provided `buf`,
optionally with an owning user, group, and mode. optionally with an owning user, group, and mode.
@ -134,6 +134,9 @@ impl CreateFile {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "create_file")] #[typetag::serde(name = "create_file")]
impl Action for CreateFile { impl Action for CreateFile {
fn action_tag() -> ActionTag {
ActionTag("create_file")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!("Create or overwrite file `{}`", self.path.display()) format!("Create or overwrite file `{}`", self.path.display())
} }

View file

@ -2,7 +2,7 @@ use nix::unistd::Group;
use tokio::process::Command; use tokio::process::Command;
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::ActionError; use crate::action::{ActionError, ActionTag};
use crate::execute_command; use crate::execute_command;
use crate::action::{Action, ActionDescription, StatefulAction}; use crate::action::{Action, ActionDescription, StatefulAction};
@ -45,6 +45,9 @@ impl CreateGroup {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "create_group")] #[typetag::serde(name = "create_group")]
impl Action for CreateGroup { impl Action for CreateGroup {
fn action_tag() -> ActionTag {
ActionTag("create_group")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!("Create group `{}` (GID {})", self.name, self.gid) format!("Create group `{}` (GID {})", self.name, self.gid)
} }
@ -93,8 +96,7 @@ impl Action for CreateGroup {
]) ])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
}, },
_ => { _ => {
execute_command( execute_command(
@ -103,8 +105,7 @@ impl Action for CreateGroup {
.args(["-g", &gid.to_string(), "--system", &name]) .args(["-g", &gid.to_string(), "--system", &name])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
}, },
}; };
@ -138,8 +139,7 @@ impl Action for CreateGroup {
.args([".", "-delete", &format!("/Groups/{name}")]) .args([".", "-delete", &format!("/Groups/{name}")])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
if !output.status.success() {} if !output.status.success() {}
}, },
_ => { _ => {
@ -149,8 +149,7 @@ impl Action for CreateGroup {
.arg(&name) .arg(&name)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(ActionError::Command)?;
}, },
}; };

View file

@ -1,6 +1,6 @@
use nix::unistd::{chown, Group, User}; use nix::unistd::{chown, Group, User};
use crate::action::{Action, ActionDescription, ActionError, StatefulAction}; use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction};
use rand::Rng; use rand::Rng;
use std::{ use std::{
io::SeekFrom, io::SeekFrom,
@ -140,6 +140,9 @@ impl CreateOrInsertIntoFile {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "create_or_insert_into_file")] #[typetag::serde(name = "create_or_insert_into_file")]
impl Action for CreateOrInsertIntoFile { impl Action for CreateOrInsertIntoFile {
fn action_tag() -> ActionTag {
ActionTag("create_or_insert_into_file")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!("Create or insert file `{}`", self.path.display()) format!("Create or insert file `{}`", self.path.display())
} }

View file

@ -2,7 +2,7 @@ use nix::unistd::User;
use tokio::process::Command; use tokio::process::Command;
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::ActionError; use crate::action::{ActionError, ActionTag};
use crate::execute_command; use crate::execute_command;
use crate::action::{Action, ActionDescription, StatefulAction}; use crate::action::{Action, ActionDescription, StatefulAction};
@ -60,6 +60,9 @@ impl CreateUser {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "create_user")] #[typetag::serde(name = "create_user")]
impl Action for CreateUser { impl Action for CreateUser {
fn action_tag() -> ActionTag {
ActionTag("create_user")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!( format!(
"Create user `{}` (UID {}) in group `{}` (GID {})", "Create user `{}` (UID {}) in group `{}` (GID {})",
@ -110,8 +113,7 @@ impl Action for CreateUser {
.args([".", "-create", &format!("/Users/{name}")]) .args([".", "-create", &format!("/Users/{name}")])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
execute_command( execute_command(
Command::new("/usr/bin/dscl") Command::new("/usr/bin/dscl")
.process_group(0) .process_group(0)
@ -124,8 +126,7 @@ impl Action for CreateUser {
]) ])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
execute_command( execute_command(
Command::new("/usr/bin/dscl") Command::new("/usr/bin/dscl")
.process_group(0) .process_group(0)
@ -138,8 +139,7 @@ impl Action for CreateUser {
]) ])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
execute_command( execute_command(
Command::new("/usr/bin/dscl") Command::new("/usr/bin/dscl")
.process_group(0) .process_group(0)
@ -152,8 +152,7 @@ impl Action for CreateUser {
]) ])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
execute_command( execute_command(
Command::new("/usr/bin/dscl") Command::new("/usr/bin/dscl")
.process_group(0) .process_group(0)
@ -166,16 +165,14 @@ impl Action for CreateUser {
]) ])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
execute_command( execute_command(
Command::new("/usr/bin/dscl") Command::new("/usr/bin/dscl")
.process_group(0) .process_group(0)
.args([".", "-create", &format!("/Users/{name}"), "IsHidden", "1"]) .args([".", "-create", &format!("/Users/{name}"), "IsHidden", "1"])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
}, },
_ => { _ => {
execute_command( execute_command(
@ -202,8 +199,7 @@ impl Action for CreateUser {
]) ])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
}, },
} }
@ -251,7 +247,7 @@ impl Action for CreateUser {
let output = command let output = command
.output() .output()
.await .await
.map_err(|e| ActionError::Command(e))?; .map_err(|e| ActionError::command(&command, e))?;
let stderr = String::from_utf8_lossy(&output.stderr); let stderr = String::from_utf8_lossy(&output.stderr);
match output.status.code() { match output.status.code() {
Some(0) => (), Some(0) => (),
@ -260,21 +256,9 @@ impl Action for CreateUser {
// These Macs cannot always delete users, as sometimes there is no graphical login // These Macs cannot always delete users, as sometimes there is no graphical login
tracing::warn!("Encountered an exit code 40 with -14120 error while removing user, this is likely because the initial executing user did not have a secure token, or that there was no graphical login session. To delete the user, log in graphically, then run `/usr/bin/dscl . -delete /Users/{name}"); tracing::warn!("Encountered an exit code 40 with -14120 error while removing user, this is likely because the initial executing user did not have a secure token, or that there was no graphical login session. To delete the user, log in graphically, then run `/usr/bin/dscl . -delete /Users/{name}");
}, },
status => { _ => {
let command_str = format!("{:?}", command.as_std());
// Something went wrong // Something went wrong
return Err(ActionError::Command(std::io::Error::new( return Err(ActionError::command_output(&command, output));
std::io::ErrorKind::Other,
format!(
"Command `{command_str}` failed{}, stderr:\n{}\n",
if let Some(status) = status {
format!(" {status}")
} else {
"".to_string()
},
stderr
),
)));
}, },
} }
}, },
@ -285,8 +269,7 @@ impl Action for CreateUser {
.args([&name.to_string()]) .args([&name.to_string()])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
}, },
}; };

View file

@ -4,7 +4,7 @@ use bytes::{Buf, Bytes};
use reqwest::Url; use reqwest::Url;
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::{Action, ActionDescription, ActionError, StatefulAction}; use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction};
/** /**
Fetch a URL to the given path Fetch a URL to the given path
@ -37,6 +37,9 @@ impl FetchAndUnpackNix {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "fetch_and_unpack_nix")] #[typetag::serde(name = "fetch_and_unpack_nix")]
impl Action for FetchAndUnpackNix { impl Action for FetchAndUnpackNix {
fn action_tag() -> ActionTag {
ActionTag("fetch_and_unpack_nix")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!("Fetch `{}` to `{}`", self.url, self.dest.display()) format!("Fetch `{}` to `{}`", self.url, self.dest.display())
} }

View file

@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::{Action, ActionDescription, ActionError, StatefulAction}; use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction};
pub(crate) const DEST: &str = "/nix/"; pub(crate) const DEST: &str = "/nix/";
@ -25,6 +25,9 @@ impl MoveUnpackedNix {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "mount_unpacked_nix")] #[typetag::serde(name = "mount_unpacked_nix")]
impl Action for MoveUnpackedNix { impl Action for MoveUnpackedNix {
fn action_tag() -> ActionTag {
ActionTag("move_unpacked_nix")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
"Move the downloaded Nix into `/nix`".to_string() "Move the downloaded Nix into `/nix`".to_string()
} }

View file

@ -1,7 +1,7 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::{ use crate::{
action::{ActionError, StatefulAction}, action::{ActionError, ActionTag, StatefulAction},
execute_command, set_env, ChannelValue, execute_command, set_env, ChannelValue,
}; };
@ -30,6 +30,9 @@ impl SetupDefaultProfile {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "setup_default_profile")] #[typetag::serde(name = "setup_default_profile")]
impl Action for SetupDefaultProfile { impl Action for SetupDefaultProfile {
fn action_tag() -> ActionTag {
ActionTag("setup_default_profile")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
"Setup the default Nix profile".to_string() "Setup the default Nix profile".to_string()
} }
@ -119,14 +122,14 @@ impl Action for SetupDefaultProfile {
ActionError::Custom(Box::new(SetupDefaultProfileError::NoRootHome)) ActionError::Custom(Box::new(SetupDefaultProfileError::NoRootHome))
})?, })?,
); );
let load_db_command_str = format!("{:?}", load_db_command.as_std());
tracing::trace!( tracing::trace!(
"Executing `{load_db_command_str}` with stdin from `{}`", "Executing `{:?}` with stdin from `{}`",
load_db_command.as_std(),
reginfo_path.display() reginfo_path.display()
); );
let mut handle = load_db_command let mut handle = load_db_command
.spawn() .spawn()
.map_err(|e| ActionError::Command(e))?; .map_err(|e| ActionError::command(&load_db_command, e))?;
let mut stdin = handle.stdin.take().unwrap(); let mut stdin = handle.stdin.take().unwrap();
stdin stdin
@ -146,20 +149,9 @@ impl Action for SetupDefaultProfile {
let output = handle let output = handle
.wait_with_output() .wait_with_output()
.await .await
.map_err(ActionError::Command)?; .map_err(|e| ActionError::command(&load_db_command, e))?;
if !output.status.success() { if !output.status.success() {
return Err(ActionError::Command(std::io::Error::new( return Err(ActionError::command_output(&load_db_command, output));
std::io::ErrorKind::Other,
format!(
"Command `{load_db_command_str}` failed{}, stderr:\n{}\n",
if let Some(code) = output.status.code() {
format!(" status {code}")
} else {
"".to_string()
},
String::from_utf8_lossy(&output.stderr)
),
)));
}; };
} }
@ -181,8 +173,7 @@ impl Action for SetupDefaultProfile {
nss_ca_cert_pkg.join("etc/ssl/certs/ca-bundle.crt"), nss_ca_cert_pkg.join("etc/ssl/certs/ca-bundle.crt"),
), /* This is apparently load bearing... */ ), /* This is apparently load bearing... */
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
// Install `nix` itself into the store // Install `nix` itself into the store
execute_command( execute_command(
@ -202,8 +193,7 @@ impl Action for SetupDefaultProfile {
nss_ca_cert_pkg.join("etc/ssl/certs/ca-bundle.crt"), nss_ca_cert_pkg.join("etc/ssl/certs/ca-bundle.crt"),
), /* This is apparently load bearing... */ ), /* This is apparently load bearing... */
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
set_env( set_env(
"NIX_SSL_CERT_FILE", "NIX_SSL_CERT_FILE",
@ -223,9 +213,7 @@ impl Action for SetupDefaultProfile {
); );
command.stdin(std::process::Stdio::null()); command.stdin(std::process::Stdio::null());
execute_command(&mut command) execute_command(&mut command).await?;
.await
.map_err(|e| ActionError::Command(e))?;
} }
Ok(()) Ok(())

View file

@ -3,7 +3,7 @@ use std::path::PathBuf;
use tokio::process::Command; use tokio::process::Command;
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::{ActionError, StatefulAction}; use crate::action::{ActionError, ActionTag, StatefulAction};
use crate::execute_command; use crate::execute_command;
use crate::action::{Action, ActionDescription}; use crate::action::{Action, ActionDescription};
@ -42,8 +42,11 @@ impl ConfigureInitService {
} }
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "configure_nix_daemon")] #[typetag::serde(name = "configure_init_service")]
impl Action for ConfigureInitService { impl Action for ConfigureInitService {
fn action_tag() -> ActionTag {
ActionTag("configure_init_service")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
match self.init { match self.init {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@ -58,7 +61,7 @@ impl Action for ConfigureInitService {
} }
fn tracing_span(&self) -> Span { fn tracing_span(&self) -> Span {
span!(tracing::Level::DEBUG, "configure_nix_daemon",) span!(tracing::Level::DEBUG, "configure_init_service",)
} }
fn execute_description(&self) -> Vec<ActionDescription> { fn execute_description(&self) -> Vec<ActionDescription> {
@ -118,8 +121,7 @@ impl Action for ConfigureInitService {
.arg(DARWIN_NIX_DAEMON_DEST) .arg(DARWIN_NIX_DAEMON_DEST)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(ActionError::Command)?;
if *start_daemon { if *start_daemon {
execute_command( execute_command(
@ -130,8 +132,7 @@ impl Action for ConfigureInitService {
.arg("system/org.nixos.nix-daemon") .arg("system/org.nixos.nix-daemon")
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(ActionError::Command)?;
} }
}, },
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@ -154,8 +155,7 @@ impl Action for ConfigureInitService {
.arg("--prefix=/nix/var/nix") .arg("--prefix=/nix/var/nix")
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(ActionError::Command)?;
execute_command( execute_command(
Command::new("systemctl") Command::new("systemctl")
@ -164,8 +164,7 @@ impl Action for ConfigureInitService {
.arg(SERVICE_SRC) .arg(SERVICE_SRC)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(ActionError::Command)?;
execute_command( execute_command(
Command::new("systemctl") Command::new("systemctl")
@ -174,8 +173,7 @@ impl Action for ConfigureInitService {
.arg(SOCKET_SRC) .arg(SOCKET_SRC)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(ActionError::Command)?;
if *start_daemon { if *start_daemon {
execute_command( execute_command(
@ -184,8 +182,7 @@ impl Action for ConfigureInitService {
.arg("daemon-reload") .arg("daemon-reload")
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(ActionError::Command)?;
execute_command( execute_command(
Command::new("systemctl") Command::new("systemctl")
@ -194,8 +191,7 @@ impl Action for ConfigureInitService {
.arg("--now") .arg("--now")
.arg(SOCKET_SRC), .arg(SOCKET_SRC),
) )
.await .await?;
.map_err(ActionError::Command)?;
} }
}, },
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
@ -244,8 +240,7 @@ impl Action for ConfigureInitService {
.arg("unload") .arg("unload")
.arg(DARWIN_NIX_DAEMON_DEST), .arg(DARWIN_NIX_DAEMON_DEST),
) )
.await .await?;
.map_err(ActionError::Command)?;
}, },
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
InitSystem::Systemd => { InitSystem::Systemd => {
@ -263,8 +258,7 @@ impl Action for ConfigureInitService {
.args(["stop", "nix-daemon.socket"]) .args(["stop", "nix-daemon.socket"])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(ActionError::Command)?;
} }
if socket_is_enabled { if socket_is_enabled {
@ -274,8 +268,7 @@ impl Action for ConfigureInitService {
.args(["disable", "nix-daemon.socket"]) .args(["disable", "nix-daemon.socket"])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(ActionError::Command)?;
} }
if service_is_active { if service_is_active {
@ -285,8 +278,7 @@ impl Action for ConfigureInitService {
.args(["stop", "nix-daemon.service"]) .args(["stop", "nix-daemon.service"])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(ActionError::Command)?;
} }
if service_is_enabled { if service_is_enabled {
@ -296,8 +288,7 @@ impl Action for ConfigureInitService {
.args(["disable", "nix-daemon.service"]) .args(["disable", "nix-daemon.service"])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(ActionError::Command)?;
} }
execute_command( execute_command(
@ -307,8 +298,7 @@ impl Action for ConfigureInitService {
.arg("--prefix=/nix/var/nix") .arg("--prefix=/nix/var/nix")
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(ActionError::Command)?;
tokio::fs::remove_file(TMPFILES_DEST) tokio::fs::remove_file(TMPFILES_DEST)
.await .await
@ -320,8 +310,7 @@ impl Action for ConfigureInitService {
.arg("daemon-reload") .arg("daemon-reload")
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(ActionError::Command)?;
}, },
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
InitSystem::None => { InitSystem::None => {
@ -342,12 +331,13 @@ pub enum ConfigureNixDaemonServiceError {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
async fn is_active(unit: &str) -> Result<bool, ActionError> { async fn is_active(unit: &str) -> Result<bool, ActionError> {
let output = Command::new("systemctl") let mut command = Command::new("systemctl");
.arg("is-active") command.arg("is-active");
.arg(unit) command.arg(unit);
let output = command
.output() .output()
.await .await
.map_err(ActionError::Command)?; .map_err(|e| ActionError::command(&command, e))?;
if String::from_utf8(output.stdout)?.starts_with("active") { if String::from_utf8(output.stdout)?.starts_with("active") {
tracing::trace!(%unit, "Is active"); tracing::trace!(%unit, "Is active");
Ok(true) Ok(true)
@ -359,12 +349,13 @@ async fn is_active(unit: &str) -> Result<bool, ActionError> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
async fn is_enabled(unit: &str) -> Result<bool, ActionError> { async fn is_enabled(unit: &str) -> Result<bool, ActionError> {
let output = Command::new("systemctl") let mut command = Command::new("systemctl");
.arg("is-enabled") command.arg("is-enabled");
.arg(unit) command.arg(unit);
let output = command
.output() .output()
.await .await
.map_err(ActionError::Command)?; .map_err(|e| ActionError::command(&command, e))?;
let stdout = String::from_utf8(output.stdout)?; let stdout = String::from_utf8(output.stdout)?;
if stdout.starts_with("enabled") || stdout.starts_with("linked") { if stdout.starts_with("enabled") || stdout.starts_with("linked") {
tracing::trace!(%unit, "Is enabled"); tracing::trace!(%unit, "Is enabled");

View file

@ -2,7 +2,7 @@ use crate::{
action::{ action::{
base::SetupDefaultProfile, base::SetupDefaultProfile,
common::{ConfigureShellProfile, PlaceChannelConfiguration, PlaceNixConfiguration}, common::{ConfigureShellProfile, PlaceChannelConfiguration, PlaceNixConfiguration},
Action, ActionDescription, ActionError, StatefulAction, Action, ActionDescription, ActionError, ActionTag, StatefulAction,
}, },
settings::CommonSettings, settings::CommonSettings,
}; };
@ -23,21 +23,30 @@ pub struct ConfigureNix {
impl ConfigureNix { impl ConfigureNix {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
pub async fn plan(settings: &CommonSettings) -> Result<StatefulAction<Self>, ActionError> { pub async fn plan(settings: &CommonSettings) -> Result<StatefulAction<Self>, ActionError> {
let setup_default_profile = SetupDefaultProfile::plan(settings.channels.clone()).await?; let setup_default_profile = SetupDefaultProfile::plan(settings.channels.clone())
.await
.map_err(|e| ActionError::Child(SetupDefaultProfile::action_tag(), Box::new(e)))?;
let configure_shell_profile = if settings.modify_profile { let configure_shell_profile = if settings.modify_profile {
Some(ConfigureShellProfile::plan().await?) Some(ConfigureShellProfile::plan().await.map_err(|e| {
ActionError::Child(ConfigureShellProfile::action_tag(), Box::new(e))
})?)
} else { } else {
None None
}; };
let place_channel_configuration = let place_channel_configuration =
PlaceChannelConfiguration::plan(settings.channels.clone(), settings.force).await?; PlaceChannelConfiguration::plan(settings.channels.clone(), settings.force)
.await
.map_err(|e| {
ActionError::Child(PlaceChannelConfiguration::action_tag(), Box::new(e))
})?;
let place_nix_configuration = PlaceNixConfiguration::plan( let place_nix_configuration = PlaceNixConfiguration::plan(
settings.nix_build_group_name.clone(), settings.nix_build_group_name.clone(),
settings.extra_conf.clone(), settings.extra_conf.clone(),
settings.force, settings.force,
) )
.await?; .await
.map_err(|e| ActionError::Child(PlaceNixConfiguration::action_tag(), Box::new(e)))?;
Ok(Self { Ok(Self {
place_channel_configuration, place_channel_configuration,
@ -52,6 +61,9 @@ impl ConfigureNix {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "configure_nix")] #[typetag::serde(name = "configure_nix")]
impl Action for ConfigureNix { impl Action for ConfigureNix {
fn action_tag() -> ActionTag {
ActionTag("configure_nix")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
"Configure Nix".to_string() "Configure Nix".to_string()
} }
@ -103,24 +115,39 @@ impl Action for ConfigureNix {
.try_execute() .try_execute()
.instrument(setup_default_profile_span) .instrument(setup_default_profile_span)
.await .await
.map_err(|e| {
ActionError::Child(setup_default_profile.action_tag(), Box::new(e))
})
}, },
async move { async move {
place_nix_configuration place_nix_configuration
.try_execute() .try_execute()
.instrument(place_nix_configuration_span) .instrument(place_nix_configuration_span)
.await .await
.map_err(|e| {
ActionError::Child(place_nix_configuration.action_tag(), Box::new(e))
})
}, },
async move { async move {
place_channel_configuration place_channel_configuration
.try_execute() .try_execute()
.instrument(place_channel_configuration_span) .instrument(place_channel_configuration_span)
.await .await
.map_err(|e| {
ActionError::Child(
place_channel_configuration.action_tag(),
Box::new(e),
)
})
}, },
async move { async move {
configure_shell_profile configure_shell_profile
.try_execute() .try_execute()
.instrument(configure_shell_profile_span) .instrument(configure_shell_profile_span)
.await .await
.map_err(|e| {
ActionError::Child(configure_shell_profile.action_tag(), Box::new(e))
})
}, },
)?; )?;
} else { } else {
@ -135,18 +162,30 @@ impl Action for ConfigureNix {
.try_execute() .try_execute()
.instrument(setup_default_profile_span) .instrument(setup_default_profile_span)
.await .await
.map_err(|e| {
ActionError::Child(setup_default_profile.action_tag(), Box::new(e))
})
}, },
async move { async move {
place_nix_configuration place_nix_configuration
.try_execute() .try_execute()
.instrument(place_nix_configuration_span) .instrument(place_nix_configuration_span)
.await .await
.map_err(|e| {
ActionError::Child(place_nix_configuration.action_tag(), Box::new(e))
})
}, },
async move { async move {
place_channel_configuration place_channel_configuration
.try_execute() .try_execute()
.instrument(place_channel_configuration_span) .instrument(place_channel_configuration_span)
.await .await
.map_err(|e| {
ActionError::Child(
place_channel_configuration.action_tag(),
Box::new(e),
)
})
}, },
)?; )?;
}; };
@ -175,19 +214,26 @@ impl Action for ConfigureNix {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
let Self { if let Some(configure_shell_profile) = &mut self.configure_shell_profile {
setup_default_profile, configure_shell_profile.try_revert().await.map_err(|e| {
place_nix_configuration, ActionError::Child(configure_shell_profile.action_tag(), Box::new(e))
place_channel_configuration, })?;
configure_shell_profile,
} = self;
if let Some(configure_shell_profile) = configure_shell_profile {
configure_shell_profile.try_revert().await?;
} }
place_channel_configuration.try_revert().await?; self.place_channel_configuration
place_nix_configuration.try_revert().await?; .try_revert()
setup_default_profile.try_revert().await?; .await
.map_err(|e| {
ActionError::Child(self.place_channel_configuration.action_tag(), Box::new(e))
})?;
self.place_nix_configuration
.try_revert()
.await
.map_err(|e| {
ActionError::Child(self.place_nix_configuration.action_tag(), Box::new(e))
})?;
self.setup_default_profile.try_revert().await.map_err(|e| {
ActionError::Child(self.setup_default_profile.action_tag(), Box::new(e))
})?;
Ok(()) Ok(())
} }

View file

@ -1,5 +1,5 @@
use crate::action::base::{create_or_insert_into_file, CreateDirectory, CreateOrInsertIntoFile}; use crate::action::base::{create_or_insert_into_file, CreateDirectory, CreateOrInsertIntoFile};
use crate::action::{Action, ActionDescription, ActionError, StatefulAction}; use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction};
use nix::unistd::User; use nix::unistd::User;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -166,6 +166,9 @@ impl ConfigureShellProfile {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "configure_shell_profile")] #[typetag::serde(name = "configure_shell_profile")]
impl Action for ConfigureShellProfile { impl Action for ConfigureShellProfile {
fn action_tag() -> ActionTag {
ActionTag("configure_shell_profile")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
"Configure the shell profiles".to_string() "Configure the shell profiles".to_string()
} }
@ -183,26 +186,29 @@ impl Action for ConfigureShellProfile {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn execute(&mut self) -> Result<(), ActionError> { async fn execute(&mut self) -> Result<(), ActionError> {
let Self { for create_directory in &mut self.create_directories {
create_or_insert_into_files,
create_directories,
} = self;
for create_directory in create_directories {
create_directory.try_execute().await?; create_directory.try_execute().await?;
} }
let mut set = JoinSet::new(); let mut set = JoinSet::new();
let mut errors = Vec::default(); let mut errors = Vec::default();
for (idx, create_or_insert_into_file) in create_or_insert_into_files.iter().enumerate() { for (idx, create_or_insert_into_file) in
self.create_or_insert_into_files.iter_mut().enumerate()
{
let span = tracing::Span::current().clone(); let span = tracing::Span::current().clone();
let mut create_or_insert_into_file_clone = create_or_insert_into_file.clone(); let mut create_or_insert_into_file_clone = create_or_insert_into_file.clone();
let _abort_handle = set.spawn(async move { let _abort_handle = set.spawn(async move {
create_or_insert_into_file_clone create_or_insert_into_file_clone
.try_execute() .try_execute()
.instrument(span) .instrument(span)
.await?; .await
.map_err(|e| {
ActionError::Child(
create_or_insert_into_file_clone.action_tag(),
Box::new(e),
)
})?;
Result::<_, ActionError>::Ok((idx, create_or_insert_into_file_clone)) Result::<_, ActionError>::Ok((idx, create_or_insert_into_file_clone))
}); });
} }
@ -210,18 +216,20 @@ impl Action for ConfigureShellProfile {
while let Some(result) = set.join_next().await { while let Some(result) = set.join_next().await {
match result { match result {
Ok(Ok((idx, create_or_insert_into_file))) => { Ok(Ok((idx, create_or_insert_into_file))) => {
create_or_insert_into_files[idx] = create_or_insert_into_file self.create_or_insert_into_files[idx] = create_or_insert_into_file
}, },
Ok(Err(e)) => errors.push(Box::new(e)), Ok(Err(e)) => errors.push(e),
Err(e) => return Err(e.into()), Err(e) => return Err(e)?,
}; };
} }
if !errors.is_empty() { if !errors.is_empty() {
if errors.len() == 1 { if errors.len() == 1 {
return Err(errors.into_iter().next().unwrap().into()); return Err(errors.into_iter().next().unwrap())?;
} else { } else {
return Err(ActionError::Children(errors)); return Err(ActionError::Children(
errors.into_iter().map(|v| Box::new(v)).collect(),
));
} }
} }
@ -237,15 +245,12 @@ impl Action for ConfigureShellProfile {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
let Self {
create_directories,
create_or_insert_into_files,
} = self;
let mut set = JoinSet::new(); let mut set = JoinSet::new();
let mut errors: Vec<Box<ActionError>> = Vec::default(); let mut errors: Vec<ActionError> = Vec::default();
for (idx, create_or_insert_into_file) in create_or_insert_into_files.iter().enumerate() { for (idx, create_or_insert_into_file) in
self.create_or_insert_into_files.iter_mut().enumerate()
{
let mut create_or_insert_file_clone = create_or_insert_into_file.clone(); let mut create_or_insert_file_clone = create_or_insert_into_file.clone();
let _abort_handle = set.spawn(async move { let _abort_handle = set.spawn(async move {
create_or_insert_file_clone.try_revert().await?; create_or_insert_file_clone.try_revert().await?;
@ -256,22 +261,27 @@ impl Action for ConfigureShellProfile {
while let Some(result) = set.join_next().await { while let Some(result) = set.join_next().await {
match result { match result {
Ok(Ok((idx, create_or_insert_into_file))) => { Ok(Ok((idx, create_or_insert_into_file))) => {
create_or_insert_into_files[idx] = create_or_insert_into_file self.create_or_insert_into_files[idx] = create_or_insert_into_file
}, },
Ok(Err(e)) => errors.push(Box::new(e)), Ok(Err(e)) => errors.push(e),
Err(e) => return Err(e.into()), Err(e) => return Err(e)?,
}; };
} }
for create_directory in create_directories { for create_directory in self.create_directories.iter_mut() {
create_directory.try_revert().await?; create_directory
.try_revert()
.await
.map_err(|e| ActionError::Child(create_directory.action_tag(), Box::new(e)))?;
} }
if !errors.is_empty() { if !errors.is_empty() {
if errors.len() == 1 { if errors.len() == 1 {
return Err(errors.into_iter().next().unwrap().into()); return Err(errors.into_iter().next().unwrap())?;
} else { } else {
return Err(ActionError::Children(errors)); return Err(ActionError::Children(
errors.into_iter().map(|v| Box::new(v)).collect(),
));
} }
} }

View file

@ -1,7 +1,7 @@
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::base::CreateDirectory; use crate::action::base::CreateDirectory;
use crate::action::{Action, ActionDescription, ActionError, StatefulAction}; use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction};
const PATHS: &[&str] = &[ const PATHS: &[&str] = &[
"/nix/var", "/nix/var",
@ -33,7 +33,11 @@ impl CreateNixTree {
let mut create_directories = Vec::default(); let mut create_directories = Vec::default();
for path in PATHS { for path in PATHS {
// We use `create_dir` over `create_dir_all` to ensure we always set permissions right // We use `create_dir` over `create_dir_all` to ensure we always set permissions right
create_directories.push(CreateDirectory::plan(path, None, None, 0o0755, false).await?) create_directories.push(
CreateDirectory::plan(path, None, None, 0o0755, false)
.await
.map_err(|e| ActionError::Child(CreateDirectory::action_tag(), Box::new(e)))?,
)
} }
Ok(Self { create_directories }.into()) Ok(Self { create_directories }.into())
@ -43,6 +47,9 @@ impl CreateNixTree {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "create_nix_tree")] #[typetag::serde(name = "create_nix_tree")]
impl Action for CreateNixTree { impl Action for CreateNixTree {
fn action_tag() -> ActionTag {
ActionTag("create_nix_tree")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
"Create a directory tree in `/nix`".to_string() "Create a directory tree in `/nix`".to_string()
} }
@ -68,11 +75,12 @@ impl Action for CreateNixTree {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn execute(&mut self) -> Result<(), ActionError> { async fn execute(&mut self) -> Result<(), ActionError> {
let Self { create_directories } = self;
// Just do sequential since parallelizing this will have little benefit // Just do sequential since parallelizing this will have little benefit
for create_directory in create_directories { for create_directory in self.create_directories.iter_mut() {
create_directory.try_execute().await? create_directory
.try_execute()
.await
.map_err(|e| ActionError::Child(create_directory.action_tag(), Box::new(e)))?
} }
Ok(()) Ok(())
@ -100,11 +108,12 @@ impl Action for CreateNixTree {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
let Self { create_directories } = self;
// Just do sequential since parallelizing this will have little benefit // Just do sequential since parallelizing this will have little benefit
for create_directory in create_directories.iter_mut().rev() { for create_directory in self.create_directories.iter_mut().rev() {
create_directory.try_revert().await? create_directory
.try_revert()
.await
.map_err(|e| ActionError::Child(create_directory.action_tag(), Box::new(e)))?
} }
Ok(()) Ok(())

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
action::{ action::{
base::{AddUserToGroup, CreateGroup, CreateUser}, base::{AddUserToGroup, CreateGroup, CreateUser},
Action, ActionDescription, ActionError, StatefulAction, Action, ActionDescription, ActionError, ActionTag, StatefulAction,
}, },
settings::CommonSettings, settings::CommonSettings,
}; };
@ -37,7 +37,8 @@ impl CreateUsersAndGroups {
settings.nix_build_group_name.clone(), settings.nix_build_group_name.clone(),
settings.nix_build_group_id, settings.nix_build_group_id,
) )
.await?, .await
.map_err(|e| ActionError::Child(CreateUser::action_tag(), Box::new(e)))?,
); );
add_users_to_groups.push( add_users_to_groups.push(
AddUserToGroup::plan( AddUserToGroup::plan(
@ -46,7 +47,8 @@ impl CreateUsersAndGroups {
settings.nix_build_group_name.clone(), settings.nix_build_group_name.clone(),
settings.nix_build_group_id, settings.nix_build_group_id,
) )
.await?, .await
.map_err(|e| ActionError::Child(AddUserToGroup::action_tag(), Box::new(e)))?,
); );
} }
Ok(Self { Ok(Self {
@ -66,6 +68,9 @@ impl CreateUsersAndGroups {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "create_users_and_group")] #[typetag::serde(name = "create_users_and_group")]
impl Action for CreateUsersAndGroups { impl Action for CreateUsersAndGroups {
fn action_tag() -> ActionTag {
ActionTag("create_users_and_group")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!( format!(
"Create build users (UID {}-{}) and group (GID {})", "Create build users (UID {}-{}) and group (GID {})",
@ -151,12 +156,18 @@ impl Action for CreateUsersAndGroups {
} }
| OperatingSystem::Darwin => { | OperatingSystem::Darwin => {
for create_user in create_users.iter_mut() { for create_user in create_users.iter_mut() {
create_user.try_execute().await?; create_user
.try_execute()
.await
.map_err(|e| ActionError::Child(create_user.action_tag(), Box::new(e)))?;
} }
}, },
_ => { _ => {
for create_user in create_users.iter_mut() { for create_user in create_users.iter_mut() {
create_user.try_execute().await?; create_user
.try_execute()
.await
.map_err(|e| ActionError::Child(create_user.action_tag(), Box::new(e)))?;
} }
// While we may be tempted to do something like this, it can break on many older OSes like Ubuntu 18.04: // While we may be tempted to do something like this, it can break on many older OSes like Ubuntu 18.04:
// ``` // ```
@ -194,7 +205,10 @@ impl Action for CreateUsersAndGroups {
}; };
for add_user_to_group in add_users_to_groups.iter_mut() { for add_user_to_group in add_users_to_groups.iter_mut() {
add_user_to_group.try_execute().await?; add_user_to_group
.try_execute()
.await
.map_err(|e| ActionError::Child(add_user_to_group.action_tag(), Box::new(e)))?;
} }
Ok(()) Ok(())
@ -260,7 +274,11 @@ impl Action for CreateUsersAndGroups {
let span = tracing::Span::current().clone(); let span = tracing::Span::current().clone();
let mut create_user_clone = create_user.clone(); let mut create_user_clone = create_user.clone();
let _abort_handle = set.spawn(async move { let _abort_handle = set.spawn(async move {
create_user_clone.try_revert().instrument(span).await?; create_user_clone
.try_revert()
.instrument(span)
.await
.map_err(|e| ActionError::Child(create_user_clone.action_tag(), Box::new(e)))?;
Result::<_, ActionError>::Ok((idx, create_user_clone)) Result::<_, ActionError>::Ok((idx, create_user_clone))
}); });
} }
@ -275,7 +293,7 @@ impl Action for CreateUsersAndGroups {
if !errors.is_empty() { if !errors.is_empty() {
if errors.len() == 1 { if errors.len() == 1 {
return Err(errors.into_iter().next().unwrap().into()); return Err(*errors.into_iter().next().unwrap());
} else { } else {
return Err(ActionError::Children(errors)); return Err(ActionError::Children(errors));
} }
@ -287,7 +305,10 @@ impl Action for CreateUsersAndGroups {
// } // }
// Create group // Create group
create_group.try_revert().await?; create_group
.try_revert()
.await
.map_err(|e| ActionError::Child(create_group.action_tag(), Box::new(e)))?;
Ok(()) Ok(())
} }

View file

@ -1,6 +1,6 @@
use crate::action::base::CreateFile; use crate::action::base::CreateFile;
use crate::action::ActionError;
use crate::action::{Action, ActionDescription, StatefulAction}; use crate::action::{Action, ActionDescription, StatefulAction};
use crate::action::{ActionError, ActionTag};
use crate::ChannelValue; use crate::ChannelValue;
use tracing::{span, Span}; use tracing::{span, Span};
@ -36,7 +36,8 @@ impl PlaceChannelConfiguration {
buf, buf,
force, force,
) )
.await?; .await
.map_err(|e| ActionError::Child(CreateFile::action_tag(), Box::new(e)))?;
Ok(Self { Ok(Self {
create_file, create_file,
channels, channels,
@ -48,6 +49,9 @@ impl PlaceChannelConfiguration {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "place_channel_configuration")] #[typetag::serde(name = "place_channel_configuration")]
impl Action for PlaceChannelConfiguration { impl Action for PlaceChannelConfiguration {
fn action_tag() -> ActionTag {
ActionTag("place_channel_configuration")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!( format!(
"Place channel configuration at `{}`", "Place channel configuration at `{}`",
@ -74,12 +78,10 @@ impl Action for PlaceChannelConfiguration {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn execute(&mut self) -> Result<(), ActionError> { async fn execute(&mut self) -> Result<(), ActionError> {
let Self { self.create_file
create_file, .try_execute()
channels: _, .await
} = self; .map_err(|e| ActionError::Child(self.create_file.action_tag(), Box::new(e)))?;
create_file.try_execute().await?;
Ok(()) Ok(())
} }
@ -96,12 +98,10 @@ impl Action for PlaceChannelConfiguration {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
let Self { self.create_file
create_file, .try_revert()
channels: _, .await
} = self; .map_err(|e| ActionError::Child(self.create_file.action_tag(), Box::new(e)))?;
create_file.try_revert().await?;
Ok(()) Ok(())
} }

View file

@ -1,7 +1,7 @@
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::base::{CreateDirectory, CreateFile}; use crate::action::base::{CreateDirectory, CreateFile};
use crate::action::{Action, ActionDescription, ActionError, StatefulAction}; use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction};
const NIX_CONF_FOLDER: &str = "/etc/nix"; const NIX_CONF_FOLDER: &str = "/etc/nix";
const NIX_CONF: &str = "/etc/nix/nix.conf"; const NIX_CONF: &str = "/etc/nix/nix.conf";
@ -41,9 +41,12 @@ impl PlaceNixConfiguration {
extra_conf = extra_conf.join("\n"), extra_conf = extra_conf.join("\n"),
version = env!("CARGO_PKG_VERSION"), version = env!("CARGO_PKG_VERSION"),
); );
let create_directory = let create_directory = CreateDirectory::plan(NIX_CONF_FOLDER, None, None, 0o0755, force)
CreateDirectory::plan(NIX_CONF_FOLDER, None, None, 0o0755, force).await?; .await
let create_file = CreateFile::plan(NIX_CONF, None, None, 0o0664, buf, force).await?; .map_err(|e| ActionError::Child(CreateDirectory::action_tag(), Box::new(e)))?;
let create_file = CreateFile::plan(NIX_CONF, None, None, 0o0664, buf, force)
.await
.map_err(|e| ActionError::Child(CreateFile::action_tag(), Box::new(e)))?;
Ok(Self { Ok(Self {
create_directory, create_directory,
create_file, create_file,
@ -55,6 +58,9 @@ impl PlaceNixConfiguration {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "place_nix_configuration")] #[typetag::serde(name = "place_nix_configuration")]
impl Action for PlaceNixConfiguration { impl Action for PlaceNixConfiguration {
fn action_tag() -> ActionTag {
ActionTag("place_nix_configuration")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!("Place the Nix configuration in `{NIX_CONF}`") format!("Place the Nix configuration in `{NIX_CONF}`")
} }
@ -75,13 +81,14 @@ impl Action for PlaceNixConfiguration {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn execute(&mut self) -> Result<(), ActionError> { async fn execute(&mut self) -> Result<(), ActionError> {
let Self { self.create_directory
create_file, .try_execute()
create_directory, .await
} = self; .map_err(|e| ActionError::Child(self.create_directory.action_tag(), Box::new(e)))?;
self.create_file
create_directory.try_execute().await?; .try_execute()
create_file.try_execute().await?; .await
.map_err(|e| ActionError::Child(self.create_file.action_tag(), Box::new(e)))?;
Ok(()) Ok(())
} }
@ -98,13 +105,14 @@ impl Action for PlaceNixConfiguration {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
let Self { self.create_file
create_file, .try_revert()
create_directory, .await
} = self; .map_err(|e| ActionError::Child(self.create_file.action_tag(), Box::new(e)))?;
self.create_directory
create_file.try_revert().await?; .try_revert()
create_directory.try_revert().await?; .await
.map_err(|e| ActionError::Child(self.create_directory.action_tag(), Box::new(e)))?;
Ok(()) Ok(())
} }

View file

@ -4,7 +4,7 @@ use super::{CreateNixTree, CreateUsersAndGroups};
use crate::{ use crate::{
action::{ action::{
base::{FetchAndUnpackNix, MoveUnpackedNix}, base::{FetchAndUnpackNix, MoveUnpackedNix},
Action, ActionDescription, ActionError, StatefulAction, Action, ActionDescription, ActionError, ActionTag, StatefulAction,
}, },
settings::CommonSettings, settings::CommonSettings,
}; };
@ -29,10 +29,15 @@ impl ProvisionNix {
PathBuf::from("/nix/temp-install-dir"), PathBuf::from("/nix/temp-install-dir"),
) )
.await?; .await?;
let create_users_and_group = CreateUsersAndGroups::plan(settings.clone()).await?; let create_users_and_group = CreateUsersAndGroups::plan(settings.clone())
let create_nix_tree = CreateNixTree::plan().await?; .await
let move_unpacked_nix = .map_err(|e| ActionError::Child(CreateUsersAndGroups::action_tag(), Box::new(e)))?;
MoveUnpackedNix::plan(PathBuf::from("/nix/temp-install-dir")).await?; let create_nix_tree = CreateNixTree::plan()
.await
.map_err(|e| ActionError::Child(CreateNixTree::action_tag(), Box::new(e)))?;
let move_unpacked_nix = MoveUnpackedNix::plan(PathBuf::from("/nix/temp-install-dir"))
.await
.map_err(|e| ActionError::Child(MoveUnpackedNix::action_tag(), Box::new(e)))?;
Ok(Self { Ok(Self {
fetch_nix, fetch_nix,
create_users_and_group, create_users_and_group,
@ -46,6 +51,9 @@ impl ProvisionNix {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "provision_nix")] #[typetag::serde(name = "provision_nix")]
impl Action for ProvisionNix { impl Action for ProvisionNix {
fn action_tag() -> ActionTag {
ActionTag("provision_nix")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
"Provision Nix".to_string() "Provision Nix".to_string()
} }
@ -73,25 +81,32 @@ impl Action for ProvisionNix {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn execute(&mut self) -> Result<(), ActionError> { async fn execute(&mut self) -> Result<(), ActionError> {
let Self {
fetch_nix,
create_nix_tree,
create_users_and_group,
move_unpacked_nix,
} = self;
// We fetch nix while doing the rest, then move it over. // We fetch nix while doing the rest, then move it over.
let mut fetch_nix_clone = fetch_nix.clone(); let mut fetch_nix_clone = self.fetch_nix.clone();
let fetch_nix_handle = tokio::task::spawn(async { let fetch_nix_handle = tokio::task::spawn(async {
fetch_nix_clone.try_execute().await?; fetch_nix_clone
.try_execute()
.await
.map_err(|e| ActionError::Child(fetch_nix_clone.action_tag(), Box::new(e)))?;
Result::<_, ActionError>::Ok(fetch_nix_clone) Result::<_, ActionError>::Ok(fetch_nix_clone)
}); });
create_users_and_group.try_execute().await?; self.create_users_and_group
create_nix_tree.try_execute().await?; .try_execute()
.await
.map_err(|e| {
ActionError::Child(self.create_users_and_group.action_tag(), Box::new(e))
})?;
self.create_nix_tree
.try_execute()
.await
.map_err(|e| ActionError::Child(self.create_nix_tree.action_tag(), Box::new(e)))?;
*fetch_nix = fetch_nix_handle.await.map_err(ActionError::Join)??; self.fetch_nix = fetch_nix_handle.await.map_err(ActionError::Join)??;
move_unpacked_nix.try_execute().await?; self.move_unpacked_nix
.try_execute()
.await
.map_err(|e| ActionError::Child(self.move_unpacked_nix.action_tag(), Box::new(e)))?;
Ok(()) Ok(())
} }
@ -114,31 +129,33 @@ impl Action for ProvisionNix {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
let Self {
fetch_nix,
create_nix_tree,
create_users_and_group,
move_unpacked_nix,
} = self;
// We fetch nix while doing the rest, then move it over. // We fetch nix while doing the rest, then move it over.
let mut fetch_nix_clone = fetch_nix.clone(); let mut fetch_nix_clone = self.fetch_nix.clone();
let fetch_nix_handle = tokio::task::spawn(async { let fetch_nix_handle = tokio::task::spawn(async {
fetch_nix_clone.try_revert().await?; fetch_nix_clone
.try_revert()
.await
.map_err(|e| ActionError::Child(fetch_nix_clone.action_tag(), Box::new(e)))?;
Result::<_, ActionError>::Ok(fetch_nix_clone) Result::<_, ActionError>::Ok(fetch_nix_clone)
}); });
if let Err(err) = create_users_and_group.try_revert().await { if let Err(err) = self.create_users_and_group.try_revert().await {
fetch_nix_handle.abort(); fetch_nix_handle.abort();
return Err(err); return Err(err);
} }
if let Err(err) = create_nix_tree.try_revert().await { if let Err(err) = self.create_nix_tree.try_revert().await {
fetch_nix_handle.abort(); fetch_nix_handle.abort();
return Err(err); return Err(err);
} }
*fetch_nix = fetch_nix_handle.await.map_err(ActionError::Join)??; self.fetch_nix = fetch_nix_handle
move_unpacked_nix.try_revert().await?; .await
.map_err(ActionError::Join)?
.map_err(|e| ActionError::Child(self.fetch_nix.action_tag(), Box::new(e)))?;
self.move_unpacked_nix
.try_revert()
.await
.map_err(|e| ActionError::Child(self.move_unpacked_nix.action_tag(), Box::new(e)))?;
Ok(()) Ok(())
} }

View file

@ -1,7 +1,7 @@
use tokio::process::Command; use tokio::process::Command;
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::{ActionError, ActionState, StatefulAction}; use crate::action::{ActionError, ActionState, ActionTag, StatefulAction};
use crate::execute_command; use crate::execute_command;
use crate::action::{Action, ActionDescription}; use crate::action::{Action, ActionDescription};
@ -22,12 +22,13 @@ impl StartSystemdUnit {
enable: bool, enable: bool,
) -> Result<StatefulAction<Self>, ActionError> { ) -> Result<StatefulAction<Self>, ActionError> {
let unit = unit.as_ref(); let unit = unit.as_ref();
let output = Command::new("systemctl") let mut command = Command::new("systemctl");
.arg("is-active") command.arg("is-active");
.arg(unit) command.arg(unit);
let output = command
.output() .output()
.await .await
.map_err(ActionError::Command)?; .map_err(|e| ActionError::command(&command, e))?;
let state = if output.status.success() { let state = if output.status.success() {
tracing::debug!("Starting systemd unit `{}` already complete", unit); tracing::debug!("Starting systemd unit `{}` already complete", unit);
@ -49,6 +50,9 @@ impl StartSystemdUnit {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "start_systemd_unit")] #[typetag::serde(name = "start_systemd_unit")]
impl Action for StartSystemdUnit { impl Action for StartSystemdUnit {
fn action_tag() -> ActionTag {
ActionTag("start_systemd_unit")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!("Enable (and start) the systemd unit {}", self.unit) format!("Enable (and start) the systemd unit {}", self.unit)
} }
@ -80,8 +84,7 @@ impl Action for StartSystemdUnit {
.arg(format!("{unit}")) .arg(format!("{unit}"))
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Custom(Box::new(StartSystemdUnitError::Command(e))))?;
}, },
false => { false => {
// TODO(@Hoverbear): Handle proxy vars // TODO(@Hoverbear): Handle proxy vars
@ -92,8 +95,7 @@ impl Action for StartSystemdUnit {
.arg(format!("{unit}")) .arg(format!("{unit}"))
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Custom(Box::new(StartSystemdUnitError::Command(e))))?;
}, },
} }
@ -119,8 +121,7 @@ impl Action for StartSystemdUnit {
.arg(format!("{unit}")) .arg(format!("{unit}"))
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Custom(Box::new(StartSystemdUnitError::Command(e))))?;
}; };
// We do both to avoid an error doing `disable --now` if the user did stop it already somehow. // We do both to avoid an error doing `disable --now` if the user did stop it already somehow.
@ -131,8 +132,7 @@ impl Action for StartSystemdUnit {
.arg(format!("{unit}")) .arg(format!("{unit}"))
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Custom(Box::new(StartSystemdUnitError::Command(e))))?;
Ok(()) Ok(())
} }

View file

@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
use tokio::process::Command; use tokio::process::Command;
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::{ActionError, StatefulAction}; use crate::action::{ActionError, ActionTag, StatefulAction};
use crate::execute_command; use crate::execute_command;
use crate::action::{Action, ActionDescription}; use crate::action::{Action, ActionDescription};
@ -27,8 +27,11 @@ impl BootstrapApfsVolume {
} }
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "bootstrap_volume")] #[typetag::serde(name = "bootstrap_apfs_volume")]
impl Action for BootstrapApfsVolume { impl Action for BootstrapApfsVolume {
fn action_tag() -> ActionTag {
ActionTag("bootstrap_apfs_volume")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!("Bootstrap and kickstart `{}`", self.path.display()) format!("Bootstrap and kickstart `{}`", self.path.display())
} }
@ -36,7 +39,7 @@ impl Action for BootstrapApfsVolume {
fn tracing_span(&self) -> Span { fn tracing_span(&self) -> Span {
span!( span!(
tracing::Level::DEBUG, tracing::Level::DEBUG,
"bootstrap_volume", "bootstrap_apfs_volume",
path = %self.path.display(), path = %self.path.display(),
) )
} }
@ -56,16 +59,14 @@ impl Action for BootstrapApfsVolume {
.arg(path) .arg(path)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
execute_command( execute_command(
Command::new("launchctl") Command::new("launchctl")
.process_group(0) .process_group(0)
.args(["kickstart", "-k", "system/org.nixos.darwin-store"]) .args(["kickstart", "-k", "system/org.nixos.darwin-store"])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
Ok(()) Ok(())
} }
@ -88,8 +89,7 @@ impl Action for BootstrapApfsVolume {
.arg(path) .arg(path)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
Ok(()) Ok(())
} }

View file

@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
use tokio::process::Command; use tokio::process::Command;
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::{ActionError, StatefulAction}; use crate::action::{ActionError, ActionTag, StatefulAction};
use crate::execute_command; use crate::execute_command;
use serde::Deserialize; use serde::Deserialize;
@ -25,8 +25,7 @@ impl CreateApfsVolume {
) -> Result<StatefulAction<Self>, ActionError> { ) -> Result<StatefulAction<Self>, ActionError> {
let output = let output =
execute_command(Command::new("/usr/sbin/diskutil").args(["apfs", "list", "-plist"])) execute_command(Command::new("/usr/sbin/diskutil").args(["apfs", "list", "-plist"]))
.await .await?;
.map_err(ActionError::Command)?;
let parsed: DiskUtilApfsListOutput = plist::from_bytes(&output.stdout)?; let parsed: DiskUtilApfsListOutput = plist::from_bytes(&output.stdout)?;
for container in parsed.containers { for container in parsed.containers {
@ -51,6 +50,9 @@ impl CreateApfsVolume {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "create_volume")] #[typetag::serde(name = "create_volume")]
impl Action for CreateApfsVolume { impl Action for CreateApfsVolume {
fn action_tag() -> ActionTag {
ActionTag("create_apfs_volume")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!( format!(
"Create an APFS volume on `{}` named `{}`", "Create an APFS volume on `{}` named `{}`",
@ -98,8 +100,7 @@ impl Action for CreateApfsVolume {
]) ])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
Ok(()) Ok(())
} }
@ -129,8 +130,7 @@ impl Action for CreateApfsVolume {
.args(["apfs", "deleteVolume", name]) .args(["apfs", "deleteVolume", name])
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
Ok(()) Ok(())
} }

View file

@ -1,7 +1,7 @@
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
action::{Action, ActionDescription, ActionError, StatefulAction}, action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction},
execute_command, execute_command,
}; };
use serde::Deserialize; use serde::Deserialize;
@ -60,6 +60,9 @@ impl CreateFstabEntry {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "create_fstab_entry")] #[typetag::serde(name = "create_fstab_entry")]
impl Action for CreateFstabEntry { impl Action for CreateFstabEntry {
fn action_tag() -> ActionTag {
ActionTag("create_fstab_entry")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!( format!(
"Add a UUID based entry for the APFS volume `{}` to `/etc/fstab`", "Add a UUID based entry for the APFS volume `{}` to `/etc/fstab`",
@ -178,8 +181,7 @@ async fn get_uuid_for_label(apfs_volume_label: &str) -> Result<Uuid, ActionError
.stdin(std::process::Stdio::null()) .stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped()), .stdout(std::process::Stdio::piped()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
let parsed: DiskUtilApfsInfoOutput = plist::from_bytes(&output.stdout)?; let parsed: DiskUtilApfsInfoOutput = plist::from_bytes(&output.stdout)?;

View file

@ -4,7 +4,7 @@ use crate::action::{
BootstrapApfsVolume, CreateApfsVolume, CreateSyntheticObjects, EnableOwnership, BootstrapApfsVolume, CreateApfsVolume, CreateSyntheticObjects, EnableOwnership,
EncryptApfsVolume, UnmountApfsVolume, EncryptApfsVolume, UnmountApfsVolume,
}, },
Action, ActionDescription, ActionError, StatefulAction, Action, ActionDescription, ActionError, ActionTag, StatefulAction,
}; };
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -53,17 +53,23 @@ impl CreateNixVolume {
create_or_insert_into_file::Position::End, create_or_insert_into_file::Position::End,
) )
.await .await
.map_err(|e| ActionError::Child(Box::new(e)))?; .map_err(|e| ActionError::Child(CreateOrInsertIntoFile::action_tag(), Box::new(e)))?;
let create_synthetic_objects = CreateSyntheticObjects::plan().await?; let create_synthetic_objects = CreateSyntheticObjects::plan()
.await
.map_err(|e| ActionError::Child(CreateSyntheticObjects::action_tag(), Box::new(e)))?;
let unmount_volume = UnmountApfsVolume::plan(disk, name.clone()).await?; let unmount_volume = UnmountApfsVolume::plan(disk, name.clone())
.await
.map_err(|e| ActionError::Child(UnmountApfsVolume::action_tag(), Box::new(e)))?;
let create_volume = CreateApfsVolume::plan(disk, name.clone(), case_sensitive).await?; let create_volume = CreateApfsVolume::plan(disk, name.clone(), case_sensitive)
.await
.map_err(|e| ActionError::Child(CreateApfsVolume::action_tag(), Box::new(e)))?;
let create_fstab_entry = CreateFstabEntry::plan(name.clone()) let create_fstab_entry = CreateFstabEntry::plan(name.clone())
.await .await
.map_err(|e| ActionError::Child(Box::new(e)))?; .map_err(|e| ActionError::Child(CreateFstabEntry::action_tag(), Box::new(e)))?;
let encrypt_volume = if encrypt { let encrypt_volume = if encrypt {
Some(EncryptApfsVolume::plan(disk, &name).await?) Some(EncryptApfsVolume::plan(disk, &name).await?)
@ -106,10 +112,16 @@ impl CreateNixVolume {
", mount_command.iter().map(|v| format!("<string>{v}</string>\n")).collect::<Vec<_>>().join("\n") ", mount_command.iter().map(|v| format!("<string>{v}</string>\n")).collect::<Vec<_>>().join("\n")
); );
let setup_volume_daemon = let setup_volume_daemon =
CreateFile::plan(NIX_VOLUME_MOUNTD_DEST, None, None, None, mount_plist, false).await?; CreateFile::plan(NIX_VOLUME_MOUNTD_DEST, None, None, None, mount_plist, false)
.await
.map_err(|e| ActionError::Child(CreateFile::action_tag(), Box::new(e)))?;
let bootstrap_volume = BootstrapApfsVolume::plan(NIX_VOLUME_MOUNTD_DEST).await?; let bootstrap_volume = BootstrapApfsVolume::plan(NIX_VOLUME_MOUNTD_DEST)
let enable_ownership = EnableOwnership::plan("/nix").await?; .await
.map_err(|e| ActionError::Child(BootstrapApfsVolume::action_tag(), Box::new(e)))?;
let enable_ownership = EnableOwnership::plan("/nix")
.await
.map_err(|e| ActionError::Child(EnableOwnership::action_tag(), Box::new(e)))?;
Ok(Self { Ok(Self {
disk: disk.to_path_buf(), disk: disk.to_path_buf(),
@ -133,6 +145,9 @@ impl CreateNixVolume {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "create_apfs_volume")] #[typetag::serde(name = "create_apfs_volume")]
impl Action for CreateNixVolume { impl Action for CreateNixVolume {
fn action_tag() -> ActionTag {
ActionTag("create_nix_volume")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!( format!(
"Create an APFS volume `{}` for Nix on `{}`", "Create an APFS volume `{}` for Nix on `{}`",
@ -159,44 +174,57 @@ impl Action for CreateNixVolume {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn execute(&mut self) -> Result<(), ActionError> { async fn execute(&mut self) -> Result<(), ActionError> {
let Self { self.create_or_append_synthetic_conf
disk: _, .try_execute()
name: _, .await
case_sensitive: _, .map_err(|e| {
encrypt: _, ActionError::Child(
create_or_append_synthetic_conf, self.create_or_append_synthetic_conf.action_tag(),
create_synthetic_objects, Box::new(e),
unmount_volume, )
create_volume, })?;
create_fstab_entry, self.create_synthetic_objects
encrypt_volume, .try_execute()
setup_volume_daemon, .await
bootstrap_volume, .map_err(|e| {
enable_ownership, ActionError::Child(self.create_synthetic_objects.action_tag(), Box::new(e))
} = self; })?;
self.unmount_volume.try_execute().await.ok(); // We actually expect this may fail.
create_or_append_synthetic_conf.try_execute().await?; self.create_volume
create_synthetic_objects.try_execute().await?; .try_execute()
unmount_volume.try_execute().await.ok(); // We actually expect this may fail. .await
create_volume.try_execute().await?; .map_err(|e| ActionError::Child(self.create_volume.action_tag(), Box::new(e)))?;
create_fstab_entry.try_execute().await?; self.create_fstab_entry
if let Some(encrypt_volume) = encrypt_volume { .try_execute()
encrypt_volume.try_execute().await?; .await
.map_err(|e| ActionError::Child(self.create_fstab_entry.action_tag(), Box::new(e)))?;
if let Some(encrypt_volume) = &mut self.encrypt_volume {
encrypt_volume
.try_execute()
.await
.map_err(|e| ActionError::Child(encrypt_volume.action_tag(), Box::new(e)))?;
} }
setup_volume_daemon.try_execute().await?; self.setup_volume_daemon
.try_execute()
.await
.map_err(|e| ActionError::Child(self.setup_volume_daemon.action_tag(), Box::new(e)))?;
bootstrap_volume.try_execute().await?; self.bootstrap_volume
.try_execute()
.await
.map_err(|e| ActionError::Child(self.bootstrap_volume.action_tag(), Box::new(e)))?;
let mut retry_tokens: usize = 50; let mut retry_tokens: usize = 50;
loop { loop {
tracing::trace!(%retry_tokens, "Checking for Nix Store existence"); tracing::trace!(%retry_tokens, "Checking for Nix Store existence");
let status = Command::new("/usr/sbin/diskutil") let mut command = Command::new("/usr/sbin/diskutil");
.args(["info", "/nix"]) command.args(["info", "/nix"]);
.stderr(std::process::Stdio::null()) command.stderr(std::process::Stdio::null());
.stdout(std::process::Stdio::null()) command.stdout(std::process::Stdio::null());
let status = command
.status() .status()
.await .await
.map_err(|e| ActionError::Command(e))?; .map_err(|e| ActionError::command(&command, e))?;
if status.success() || retry_tokens == 0 { if status.success() || retry_tokens == 0 {
break; break;
} else { } else {
@ -205,7 +233,10 @@ impl Action for CreateNixVolume {
tokio::time::sleep(Duration::from_millis(100)).await; tokio::time::sleep(Duration::from_millis(100)).await;
} }
enable_ownership.try_execute().await?; self.enable_ownership
.try_execute()
.await
.map_err(|e| ActionError::Child(self.enable_ownership.action_tag(), Box::new(e)))?;
Ok(()) Ok(())
} }
@ -222,36 +253,54 @@ impl Action for CreateNixVolume {
#[tracing::instrument(level = "debug", skip_all)] #[tracing::instrument(level = "debug", skip_all)]
async fn revert(&mut self) -> Result<(), ActionError> { async fn revert(&mut self) -> Result<(), ActionError> {
let Self { self.enable_ownership
disk: _, .try_revert()
name: _, .await
case_sensitive: _, .map_err(|e| ActionError::Child(self.enable_ownership.action_tag(), Box::new(e)))?;
encrypt: _, self.bootstrap_volume
create_or_append_synthetic_conf, .try_revert()
create_synthetic_objects, .await
unmount_volume, .map_err(|e| ActionError::Child(self.bootstrap_volume.action_tag(), Box::new(e)))?;
create_volume, self.setup_volume_daemon
create_fstab_entry, .try_revert()
encrypt_volume, .await
setup_volume_daemon, .map_err(|e| ActionError::Child(self.setup_volume_daemon.action_tag(), Box::new(e)))?;
bootstrap_volume, if let Some(encrypt_volume) = &mut self.encrypt_volume {
enable_ownership, encrypt_volume
} = self; .try_revert()
.await
enable_ownership.try_revert().await?; .map_err(|e| ActionError::Child(encrypt_volume.action_tag(), Box::new(e)))?;
bootstrap_volume.try_revert().await?;
setup_volume_daemon.try_revert().await?;
if let Some(encrypt_volume) = encrypt_volume {
encrypt_volume.try_revert().await?;
} }
create_fstab_entry.try_revert().await?; self.create_fstab_entry
.try_revert()
.await
.map_err(|e| ActionError::Child(self.create_fstab_entry.action_tag(), Box::new(e)))?;
unmount_volume.try_revert().await?; self.unmount_volume
create_volume.try_revert().await?; .try_revert()
.await
.map_err(|e| ActionError::Child(self.unmount_volume.action_tag(), Box::new(e)))?;
self.create_volume
.try_revert()
.await
.map_err(|e| ActionError::Child(self.create_volume.action_tag(), Box::new(e)))?;
// Purposefully not reversed // Purposefully not reversed
create_or_append_synthetic_conf.try_revert().await?; self.create_or_append_synthetic_conf
create_synthetic_objects.try_revert().await?; .try_revert()
.await
.map_err(|e| {
ActionError::Child(
self.create_or_append_synthetic_conf.action_tag(),
Box::new(e),
)
})?;
self.create_synthetic_objects
.try_revert()
.await
.map_err(|e| {
ActionError::Child(self.create_synthetic_objects.action_tag(), Box::new(e))
})?;
Ok(()) Ok(())
} }

View file

@ -3,7 +3,7 @@ use tracing::{span, Span};
use crate::execute_command; use crate::execute_command;
use crate::action::{Action, ActionDescription, ActionError, StatefulAction}; use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction};
/// Create the synthetic objects defined in `/etc/syntethic.conf` /// Create the synthetic objects defined in `/etc/syntethic.conf`
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
@ -19,6 +19,9 @@ impl CreateSyntheticObjects {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "create_synthetic_objects")] #[typetag::serde(name = "create_synthetic_objects")]
impl Action for CreateSyntheticObjects { impl Action for CreateSyntheticObjects {
fn action_tag() -> ActionTag {
ActionTag("create_synthetic_objects")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
"Create objects defined in `/etc/synthetic.conf`".to_string() "Create objects defined in `/etc/synthetic.conf`".to_string()
} }

View file

@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
use tokio::process::Command; use tokio::process::Command;
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::{ActionError, StatefulAction}; use crate::action::{ActionError, ActionTag, StatefulAction};
use crate::execute_command; use crate::execute_command;
use crate::action::{Action, ActionDescription}; use crate::action::{Action, ActionDescription};
@ -31,6 +31,9 @@ impl EnableOwnership {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "enable_ownership")] #[typetag::serde(name = "enable_ownership")]
impl Action for EnableOwnership { impl Action for EnableOwnership {
fn action_tag() -> ActionTag {
ActionTag("enable_ownership")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!("Enable ownership on {}", self.path.display()) format!("Enable ownership on {}", self.path.display())
} }
@ -59,8 +62,7 @@ impl Action for EnableOwnership {
.arg(&path) .arg(&path)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?
.map_err(ActionError::Command)?
.stdout; .stdout;
let the_plist: DiskUtilOutput = plist::from_reader(Cursor::new(buf))?; let the_plist: DiskUtilOutput = plist::from_reader(Cursor::new(buf))?;
@ -75,8 +77,7 @@ impl Action for EnableOwnership {
.arg(path) .arg(path)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(ActionError::Command)?;
} }
Ok(()) Ok(())

View file

@ -1,6 +1,7 @@
use crate::{ use crate::{
action::{ action::{
macos::NIX_VOLUME_MOUNTD_DEST, Action, ActionDescription, ActionError, StatefulAction, macos::NIX_VOLUME_MOUNTD_DEST, Action, ActionDescription, ActionError, ActionTag,
StatefulAction,
}, },
execute_command, execute_command,
}; };
@ -36,6 +37,9 @@ impl EncryptApfsVolume {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "encrypt_volume")] #[typetag::serde(name = "encrypt_volume")]
impl Action for EncryptApfsVolume { impl Action for EncryptApfsVolume {
fn action_tag() -> ActionTag {
ActionTag("encrypt_apfs_volume")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!( format!(
"Encrypt volume `{}` on disk `{}`", "Encrypt volume `{}` on disk `{}`",
@ -80,9 +84,7 @@ impl Action for EncryptApfsVolume {
let disk_str = disk.to_str().expect("Could not turn disk into string"); /* Should not reasonably ever fail */ let disk_str = disk.to_str().expect("Could not turn disk into string"); /* Should not reasonably ever fail */
execute_command(Command::new("/usr/sbin/diskutil").arg("mount").arg(&name)) execute_command(Command::new("/usr/sbin/diskutil").arg("mount").arg(&name)).await?;
.await
.map_err(ActionError::Command)?;
// Add the password to the user keychain so they can unlock it later. // Add the password to the user keychain so they can unlock it later.
execute_command( execute_command(
@ -112,8 +114,7 @@ impl Action for EncryptApfsVolume {
"/Library/Keychains/System.keychain", "/Library/Keychains/System.keychain",
]), ]),
) )
.await .await?;
.map_err(ActionError::Command)?;
// Encrypt the mounted volume // Encrypt the mounted volume
execute_command(Command::new("/usr/sbin/diskutil").process_group(0).args([ execute_command(Command::new("/usr/sbin/diskutil").process_group(0).args([
@ -125,8 +126,7 @@ impl Action for EncryptApfsVolume {
"-passphrase", "-passphrase",
password.as_str(), password.as_str(),
])) ]))
.await .await?;
.map_err(ActionError::Command)?;
execute_command( execute_command(
Command::new("/usr/sbin/diskutil") Command::new("/usr/sbin/diskutil")
@ -135,8 +135,7 @@ impl Action for EncryptApfsVolume {
.arg("force") .arg("force")
.arg(&name), .arg(&name),
) )
.await .await?;
.map_err(ActionError::Command)?;
Ok(()) Ok(())
} }
@ -178,8 +177,7 @@ impl Action for EncryptApfsVolume {
.as_str(), .as_str(),
]), ]),
) )
.await .await?;
.map_err(ActionError::Command)?;
Ok(()) Ok(())
} }

View file

@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
use tokio::process::Command; use tokio::process::Command;
use tracing::{span, Span}; use tracing::{span, Span};
use crate::action::{ActionError, StatefulAction}; use crate::action::{ActionError, ActionTag, StatefulAction};
use crate::execute_command; use crate::execute_command;
use crate::action::{Action, ActionDescription}; use crate::action::{Action, ActionDescription};
@ -31,6 +31,9 @@ impl UnmountApfsVolume {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "unmount_volume")] #[typetag::serde(name = "unmount_volume")]
impl Action for UnmountApfsVolume { impl Action for UnmountApfsVolume {
fn action_tag() -> ActionTag {
ActionTag("unmount_apfs_volume")
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
format!("Unmount the `{}` APFS volume", self.name) format!("Unmount the `{}` APFS volume", self.name)
} }
@ -59,8 +62,7 @@ impl Action for UnmountApfsVolume {
.arg(name) .arg(name)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
Ok(()) Ok(())
} }
@ -80,8 +82,7 @@ impl Action for UnmountApfsVolume {
.arg(name) .arg(name)
.stdin(std::process::Stdio::null()), .stdin(std::process::Stdio::null()),
) )
.await .await?;
.map_err(|e| ActionError::Command(e))?;
Ok(()) Ok(())
} }

View file

@ -68,6 +68,9 @@ impl MyAction {
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(name = "my_action")] #[typetag::serde(name = "my_action")]
impl Action for MyAction { impl Action for MyAction {
fn action_tag() -> nix_installer::action::ActionTag {
"my_action".into()
}
fn tracing_synopsis(&self) -> String { fn tracing_synopsis(&self) -> String {
"My action".to_string() "My action".to_string()
} }
@ -171,7 +174,7 @@ pub mod macos;
mod stateful; mod stateful;
pub use stateful::{ActionState, StatefulAction}; pub use stateful::{ActionState, StatefulAction};
use std::error::Error; use std::{error::Error, process::Output};
use tokio::task::JoinError; use tokio::task::JoinError;
use tracing::Span; use tracing::Span;
@ -185,6 +188,9 @@ use crate::error::HasExpectedErrors;
#[async_trait::async_trait] #[async_trait::async_trait]
#[typetag::serde(tag = "action")] #[typetag::serde(tag = "action")]
pub trait Action: Send + Sync + std::fmt::Debug + dyn_clone::DynClone { pub trait Action: Send + Sync + std::fmt::Debug + dyn_clone::DynClone {
fn action_tag() -> ActionTag
where
Self: Sized;
/// A synopsis of the action for tracing purposes /// A synopsis of the action for tracing purposes
fn tracing_synopsis(&self) -> String; fn tracing_synopsis(&self) -> String;
/// A tracing span suitable for the action /// A tracing span suitable for the action
@ -252,6 +258,27 @@ impl ActionDescription {
} }
} }
/// A 'tag' name an action has that corresponds to the one we serialize in [`typetag]`
pub struct ActionTag(&'static str);
impl std::fmt::Display for ActionTag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
impl std::fmt::Debug for ActionTag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
impl From<&'static str> for ActionTag {
fn from(value: &'static str) -> Self {
Self(value)
}
}
/// An error occurring during an action /// An error occurring during an action
#[non_exhaustive] #[non_exhaustive]
#[derive(thiserror::Error, Debug, strum::IntoStaticStr)] #[derive(thiserror::Error, Debug, strum::IntoStaticStr)]
@ -260,10 +287,10 @@ pub enum ActionError {
#[error(transparent)] #[error(transparent)]
Custom(Box<dyn std::error::Error + Send + Sync>), Custom(Box<dyn std::error::Error + Send + Sync>),
/// A child error /// A child error
#[error(transparent)] #[error("Child action `{0}`")]
Child(#[from] Box<ActionError>), Child(ActionTag, #[source] Box<ActionError>),
/// Several child errors /// Several child errors
#[error("Multiple errors: {}", .0.iter().map(|v| { #[error("Child action errors: {}", .0.iter().map(|v| {
if let Some(source) = v.source() { if let Some(source) = v.source() {
format!("{v} ({source})") format!("{v} ({source})")
} else { } else {
@ -341,8 +368,33 @@ pub enum ActionError {
#[error("Chowning path `{0}`")] #[error("Chowning path `{0}`")]
Chown(std::path::PathBuf, #[source] nix::errno::Errno), Chown(std::path::PathBuf, #[source] nix::errno::Errno),
/// Failed to execute command /// Failed to execute command
#[error("Failed to execute command")] #[error("Failed to execute command `{command}`",
Command(#[source] std::io::Error), command = .command,
)]
Command {
#[cfg(feature = "diagnostics")]
program: String,
command: String,
#[source]
error: std::io::Error,
},
#[error(
"Failed to execute command{maybe_status} `{command}`, stdout: {stdout}\nstderr: {stderr}\n",
command = .command,
stdout = String::from_utf8_lossy(&.output.stdout),
stderr = String::from_utf8_lossy(&.output.stderr),
maybe_status = if let Some(status) = .output.status.code() {
format!(" with status {status}")
} else {
"".to_string()
}
)]
CommandOutput {
#[cfg(feature = "diagnostics")]
program: String,
command: String,
output: Output,
},
#[error("Joining spawned async task")] #[error("Joining spawned async task")]
Join( Join(
#[source] #[source]
@ -360,6 +412,25 @@ pub enum ActionError {
Plist(#[from] plist::Error), Plist(#[from] plist::Error),
} }
impl ActionError {
pub fn command(command: &tokio::process::Command, error: std::io::Error) -> Self {
Self::Command {
#[cfg(feature = "diagnostics")]
program: command.as_std().get_program().to_string_lossy().into(),
command: format!("{:?}", command.as_std()),
error,
}
}
pub fn command_output(command: &tokio::process::Command, output: std::process::Output) -> Self {
Self::CommandOutput {
#[cfg(feature = "diagnostics")]
program: command.as_std().get_program().to_string_lossy().into(),
command: format!("{:?}", command.as_std()),
output,
}
}
}
impl HasExpectedErrors for ActionError { impl HasExpectedErrors for ActionError {
fn expected<'a>(&'a self) -> Option<Box<dyn std::error::Error + 'a>> { fn expected<'a>(&'a self) -> Option<Box<dyn std::error::Error + 'a>> {
match self { match self {
@ -370,3 +441,56 @@ impl HasExpectedErrors for ActionError {
} }
} }
} }
#[cfg(feature = "diagnostics")]
impl crate::diagnostics::ErrorDiagnostic for ActionError {
fn diagnostic(&self) -> String {
let static_str: &'static str = (self).into();
let context = match self {
Self::Child(action, _) => vec![action.to_string()],
Self::Read(path, _)
| Self::Open(path, _)
| Self::Write(path, _)
| Self::Flush(path, _)
| Self::SetPermissions(_, path, _)
| Self::GettingMetadata(path, _)
| Self::CreateDirectory(path, _)
| Self::PathWasNotFile(path) => {
vec![path.to_string_lossy().to_string()]
},
Self::Rename(first_path, second_path, _)
| Self::Copy(first_path, second_path, _)
| Self::Symlink(first_path, second_path, _) => {
vec![
first_path.to_string_lossy().to_string(),
second_path.to_string_lossy().to_string(),
]
},
Self::NoGroup(name) | Self::NoUser(name) => {
vec![name.clone()]
},
Self::Command {
program,
command: _,
error: _,
}
| Self::CommandOutput {
program,
command: _,
output: _,
} => {
vec![program.clone()]
},
_ => vec![],
};
return format!(
"{}({})",
static_str,
context
.iter()
.map(|v| format!("\"{v}\""))
.collect::<Vec<_>>()
.join(", ")
);
}
}

View file

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::{Instrument, Span}; use tracing::{Instrument, Span};
use super::{Action, ActionDescription, ActionError}; use super::{Action, ActionDescription, ActionError, ActionTag};
/// A wrapper around an [`Action`](crate::action::Action) which tracks the [`ActionState`] and /// A wrapper around an [`Action`](crate::action::Action) which tracks the [`ActionState`] and
/// handles some tracing output /// handles some tracing output
@ -24,6 +24,9 @@ where
} }
impl StatefulAction<Box<dyn Action>> { impl StatefulAction<Box<dyn Action>> {
pub fn inner_typetag_name(&self) -> &'static str {
self.action.typetag_name()
}
pub fn tracing_synopsis(&self) -> String { pub fn tracing_synopsis(&self) -> String {
self.action.tracing_synopsis() self.action.tracing_synopsis()
} }
@ -109,6 +112,12 @@ impl<A> StatefulAction<A>
where where
A: Action, A: Action,
{ {
pub fn tag() -> ActionTag {
A::action_tag()
}
pub fn action_tag(&self) -> ActionTag {
A::action_tag()
}
pub fn tracing_synopsis(&self) -> String { pub fn tracing_synopsis(&self) -> String {
self.action.tracing_synopsis() self.action.tracing_synopsis()
} }

View file

@ -4,8 +4,9 @@ use clap::Parser;
use nix_installer::cli::CommandExecute; use nix_installer::cli::CommandExecute;
#[tokio::main] #[tokio::main]
async fn main() -> color_eyre::Result<ExitCode> { async fn main() -> eyre::Result<ExitCode> {
color_eyre::config::HookBuilder::default() color_eyre::config::HookBuilder::default()
.issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new"))
.theme(if !atty::is(atty::Stream::Stderr) { .theme(if !atty::is(atty::Stream::Stderr) {
color_eyre::config::Theme::new() color_eyre::config::Theme::new()
} else { } else {

View file

@ -139,7 +139,7 @@ impl CommandExecute for Install {
eprintln!("{}", expected.red()); eprintln!("{}", expected.red());
return Ok(ExitCode::FAILURE); return Ok(ExitCode::FAILURE);
} }
return Err(err.into()) return Err(err)?;
} }
} }
}, },
@ -212,12 +212,12 @@ impl CommandExecute for Install {
let rx2 = tx.subscribe(); let rx2 = tx.subscribe();
let res = install_plan.uninstall(rx2).await; let res = install_plan.uninstall(rx2).await;
if let Err(e) = res { if let Err(err) = res {
if let Some(expected) = e.expected() { if let Some(expected) = err.expected() {
eprintln!("{}", expected.red()); eprintln!("{}", expected.red());
return Ok(ExitCode::FAILURE); return Ok(ExitCode::FAILURE);
} }
return Err(e.into()); return Err(err)?;
} else { } else {
println!( println!(
"\ "\
@ -233,7 +233,7 @@ impl CommandExecute for Install {
} }
let error = eyre!(err).wrap_err("Install failure"); let error = eyre!(err).wrap_err("Install failure");
return Err(error); return Err(error)?;
} }
}, },
Ok(_) => { Ok(_) => {

View file

@ -25,21 +25,19 @@ impl CommandExecute for Plan {
let planner = match planner { let planner = match planner {
Some(planner) => planner, Some(planner) => planner,
None => BuiltinPlanner::default() None => BuiltinPlanner::default().await?,
.await
.map_err(|e| eyre::eyre!(e))?,
}; };
let res = planner.plan().await; let res = planner.plan().await;
let install_plan = match res { let install_plan = match res {
Ok(plan) => plan, Ok(plan) => plan,
Err(e) => { Err(err) => {
if let Some(expected) = e.expected() { if let Some(expected) = err.expected() {
eprintln!("{}", expected.red()); eprintln!("{}", expected.red());
return Ok(ExitCode::FAILURE); return Ok(ExitCode::FAILURE);
} }
return Err(e.into()); return Err(err)?;
}, },
}; };

View file

@ -124,12 +124,12 @@ impl CommandExecute for Uninstall {
let (_tx, rx) = signal_channel().await?; let (_tx, rx) = signal_channel().await?;
let res = plan.uninstall(rx).await; let res = plan.uninstall(rx).await;
if let Err(e) = res { if let Err(err) = res {
if let Some(expected) = e.expected() { if let Some(expected) = err.expected() {
println!("{}", expected.red()); println!("{}", expected.red());
return Ok(ExitCode::FAILURE); return Ok(ExitCode::FAILURE);
} }
return Err(e.into()); return Err(err)?;
} }
// 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...

View file

@ -10,6 +10,10 @@ use std::time::Duration;
use os_release::OsRelease; use os_release::OsRelease;
use reqwest::Url; use reqwest::Url;
use crate::{
action::ActionError, planner::PlannerError, settings::InstallSettingsError, NixInstallerError,
};
/// The static of an action attempt /// The static of an action attempt
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub enum DiagnosticStatus { pub enum DiagnosticStatus {
@ -39,7 +43,7 @@ pub struct DiagnosticReport {
pub action: DiagnosticAction, pub action: DiagnosticAction,
pub status: DiagnosticStatus, pub status: DiagnosticStatus,
/// Generally this includes the [`strum::IntoStaticStr`] representation of the error, we take special care not to include parameters of the error (which may include secrets) /// Generally this includes the [`strum::IntoStaticStr`] representation of the error, we take special care not to include parameters of the error (which may include secrets)
pub failure_variant: Option<String>, pub failure_chain: Option<Vec<String>>,
} }
/// A preparation of data to be sent to the `endpoint`. /// A preparation of data to be sent to the `endpoint`.
@ -53,7 +57,8 @@ pub struct DiagnosticData {
triple: String, triple: String,
is_ci: bool, is_ci: bool,
endpoint: Option<Url>, endpoint: Option<Url>,
failure_variant: Option<String>, /// Generally this includes the [`strum::IntoStaticStr`] representation of the error, we take special care not to include parameters of the error (which may include secrets)
failure_chain: Option<Vec<String>>,
} }
impl DiagnosticData { impl DiagnosticData {
@ -73,12 +78,42 @@ impl DiagnosticData {
os_version, os_version,
triple: target_lexicon::HOST.to_string(), triple: target_lexicon::HOST.to_string(),
is_ci, is_ci,
failure_variant: None, failure_chain: None,
} }
} }
pub fn variant(mut self, variant: String) -> Self { pub fn failure(mut self, err: &NixInstallerError) -> Self {
self.failure_variant = Some(variant); let mut failure_chain = vec![];
let diagnostic = err.diagnostic();
failure_chain.push(diagnostic);
let mut walker: &dyn std::error::Error = &err;
while let Some(source) = walker.source() {
if let Some(downcasted) = source.downcast_ref::<ActionError>() {
let downcasted_diagnostic = downcasted.diagnostic();
failure_chain.push(downcasted_diagnostic);
}
if let Some(downcasted) = source.downcast_ref::<Box<ActionError>>() {
let downcasted_diagnostic = downcasted.diagnostic();
failure_chain.push(downcasted_diagnostic);
}
if let Some(downcasted) = source.downcast_ref::<PlannerError>() {
let downcasted_diagnostic = downcasted.diagnostic();
failure_chain.push(downcasted_diagnostic);
}
if let Some(downcasted) = source.downcast_ref::<InstallSettingsError>() {
let downcasted_diagnostic = downcasted.diagnostic();
failure_chain.push(downcasted_diagnostic);
}
if let Some(downcasted) = source.downcast_ref::<DiagnosticError>() {
let downcasted_diagnostic = downcasted.diagnostic();
failure_chain.push(downcasted_diagnostic);
}
walker = source;
}
self.failure_chain = Some(failure_chain);
self self
} }
@ -92,7 +127,7 @@ impl DiagnosticData {
triple, triple,
is_ci, is_ci,
endpoint: _, endpoint: _,
failure_variant: variant, failure_chain,
} = self; } = self;
DiagnosticReport { DiagnosticReport {
version: version.clone(), version: version.clone(),
@ -104,7 +139,7 @@ impl DiagnosticData {
is_ci: *is_ci, is_ci: *is_ci,
action, action,
status, status,
failure_variant: variant.clone(), failure_chain: failure_chain.clone(),
} }
} }
@ -153,7 +188,7 @@ impl DiagnosticData {
} }
#[non_exhaustive] #[non_exhaustive]
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug, strum::IntoStaticStr)]
pub enum DiagnosticError { pub enum DiagnosticError {
#[error("Unknown url scheme")] #[error("Unknown url scheme")]
UnknownUrlScheme, UnknownUrlScheme,
@ -172,3 +207,14 @@ pub enum DiagnosticError {
serde_json::Error, serde_json::Error,
), ),
} }
pub trait ErrorDiagnostic {
fn diagnostic(&self) -> String;
}
impl ErrorDiagnostic for DiagnosticError {
fn diagnostic(&self) -> String {
let static_str: &'static str = (self).into();
return static_str.to_string();
}
}

View file

@ -1,18 +1,18 @@
use std::path::PathBuf; use std::path::PathBuf;
use crate::{action::ActionError, planner::PlannerError, settings::InstallSettingsError}; use crate::{
action::{ActionError, ActionTag},
planner::PlannerError,
settings::InstallSettingsError,
};
/// An error occurring during a call defined in this crate /// An error occurring during a call defined in this crate
#[non_exhaustive] #[non_exhaustive]
#[derive(thiserror::Error, Debug, strum::IntoStaticStr)] #[derive(thiserror::Error, Debug, strum::IntoStaticStr)]
pub enum NixInstallerError { pub enum NixInstallerError {
/// An error originating from an [`Action`](crate::action::Action) /// An error originating from an [`Action`](crate::action::Action)
#[error("Error executing action")] #[error("Error executing action `{0}`")]
Action( Action(ActionTag, #[source] ActionError),
#[source]
#[from]
ActionError,
),
/// An error while writing the [`InstallPlan`](crate::InstallPlan) /// An error while writing the [`InstallPlan`](crate::InstallPlan)
#[error("Recording install receipt")] #[error("Recording install receipt")]
RecordingReceipt(PathBuf, #[source] std::io::Error), RecordingReceipt(PathBuf, #[source] std::io::Error),
@ -72,7 +72,7 @@ pub(crate) trait HasExpectedErrors: std::error::Error + Sized + Send + Sync {
impl HasExpectedErrors for NixInstallerError { impl HasExpectedErrors for NixInstallerError {
fn expected<'a>(&'a self) -> Option<Box<dyn std::error::Error + 'a>> { fn expected<'a>(&'a self) -> Option<Box<dyn std::error::Error + 'a>> {
match self { match self {
NixInstallerError::Action(action_error) => action_error.expected(), NixInstallerError::Action(_, action_error) => action_error.expected(),
NixInstallerError::RecordingReceipt(_, _) => None, NixInstallerError::RecordingReceipt(_, _) => None,
NixInstallerError::CopyingSelf(_) => None, NixInstallerError::CopyingSelf(_) => None,
NixInstallerError::SerializingReceipt(_) => None, NixInstallerError::SerializingReceipt(_) => None,
@ -85,3 +85,23 @@ impl HasExpectedErrors for NixInstallerError {
} }
} }
} }
#[cfg(feature = "diagnostics")]
impl crate::diagnostics::ErrorDiagnostic for NixInstallerError {
fn diagnostic(&self) -> String {
let static_str: &'static str = (self).into();
let context = match self {
Self::Action(action, _) => vec![action.to_string()],
_ => vec![],
};
return format!(
"{}({})",
static_str,
context
.iter()
.map(|v| format!("\"{v}\""))
.collect::<Vec<_>>()
.join(", ")
);
}
}

View file

@ -83,7 +83,7 @@ pub mod settings;
use std::{ffi::OsStr, process::Output}; use std::{ffi::OsStr, process::Output};
use action::Action; use action::{Action, ActionError};
pub use channel_value::ChannelValue; pub use channel_value::ChannelValue;
pub use error::NixInstallerError; pub use error::NixInstallerError;
@ -93,24 +93,15 @@ use planner::BuiltinPlanner;
use tokio::process::Command; use tokio::process::Command;
#[tracing::instrument(level = "debug", skip_all, fields(command = %format!("{:?}", command.as_std())))] #[tracing::instrument(level = "debug", skip_all, fields(command = %format!("{:?}", command.as_std())))]
async fn execute_command(command: &mut Command) -> Result<Output, std::io::Error> { async fn execute_command(command: &mut Command) -> Result<Output, ActionError> {
let command_str = format!("{:?}", command.as_std()); tracing::trace!("Executing `{:?}`", command.as_std());
tracing::trace!("Executing `{command_str}`"); let output = command
let output = command.output().await?; .output()
.await
.map_err(|e| ActionError::command(command, e))?;
match output.status.success() { match output.status.success() {
true => Ok(output), true => Ok(output),
false => Err(std::io::Error::new( false => Err(ActionError::command_output(command, output)),
std::io::ErrorKind::Other,
format!(
"Command `{command_str}` failed{}, stderr:\n{}\n",
if let Some(code) = output.status.code() {
format!(" status {code}")
} else {
"".to_string()
},
String::from_utf8_lossy(&output.stderr)
),
)),
} }
} }

View file

@ -154,18 +154,17 @@ impl InstallPlan {
} }
tracing::info!("Step: {}", action.tracing_synopsis()); tracing::info!("Step: {}", action.tracing_synopsis());
let typetag_name = action.inner_typetag_name();
if let Err(err) = action.try_execute().await { if let Err(err) = action.try_execute().await {
if let Err(err) = write_receipt(self.clone()).await { if let Err(err) = write_receipt(self.clone()).await {
tracing::error!("Error saving receipt: {:?}", err); tracing::error!("Error saving receipt: {:?}", err);
} }
let err = NixInstallerError::Action(typetag_name.into(), err);
#[cfg(feature = "diagnostics")] #[cfg(feature = "diagnostics")]
if let Some(diagnostic_data) = &self.diagnostic_data { if let Some(diagnostic_data) = &self.diagnostic_data {
diagnostic_data diagnostic_data
.clone() .clone()
.variant({ .failure(&err)
let x: &'static str = (&err).into();
x.to_string()
})
.send( .send(
crate::diagnostics::DiagnosticAction::Install, crate::diagnostics::DiagnosticAction::Install,
crate::diagnostics::DiagnosticStatus::Failure, crate::diagnostics::DiagnosticStatus::Failure,
@ -173,7 +172,7 @@ impl InstallPlan {
.await?; .await?;
} }
return Err(NixInstallerError::Action(err)); return Err(err);
} }
} }
@ -287,25 +286,24 @@ impl InstallPlan {
} }
tracing::info!("Revert: {}", action.tracing_synopsis()); tracing::info!("Revert: {}", action.tracing_synopsis());
let typetag_name = action.inner_typetag_name();
if let Err(err) = action.try_revert().await { if let Err(err) = action.try_revert().await {
if let Err(err) = write_receipt(self.clone()).await { if let Err(err) = write_receipt(self.clone()).await {
tracing::error!("Error saving receipt: {:?}", err); tracing::error!("Error saving receipt: {:?}", err);
} }
let err = NixInstallerError::Action(typetag_name.into(), err);
#[cfg(feature = "diagnostics")] #[cfg(feature = "diagnostics")]
if let Some(diagnostic_data) = &self.diagnostic_data { if let Some(diagnostic_data) = &self.diagnostic_data {
diagnostic_data diagnostic_data
.clone() .clone()
.variant({ .failure(&err)
let x: &'static str = (&err).into();
x.to_string()
})
.send( .send(
crate::diagnostics::DiagnosticAction::Uninstall, crate::diagnostics::DiagnosticAction::Uninstall,
crate::diagnostics::DiagnosticStatus::Failure, crate::diagnostics::DiagnosticStatus::Failure,
) )
.await?; .await?;
} }
return Err(NixInstallerError::Action(err)); return Err(err);
} }
} }

View file

@ -322,3 +322,11 @@ impl HasExpectedErrors for PlannerError {
} }
} }
} }
#[cfg(feature = "diagnostics")]
impl crate::diagnostics::ErrorDiagnostic for PlannerError {
fn diagnostic(&self) -> String {
let static_str: &'static str = (self).into();
return static_str.to_string();
}
}

View file

@ -562,7 +562,7 @@ impl InitSettings {
/// An error originating from a [`Planner::settings`](crate::planner::Planner::settings) /// An error originating from a [`Planner::settings`](crate::planner::Planner::settings)
#[non_exhaustive] #[non_exhaustive]
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug, strum::IntoStaticStr)]
pub enum InstallSettingsError { pub enum InstallSettingsError {
/// `nix-installer` does not support the architecture right now /// `nix-installer` does not support the architecture right now
#[error("`nix-installer` does not support the `{0}` architecture right now")] #[error("`nix-installer` does not support the `{0}` architecture right now")]
@ -584,3 +584,11 @@ pub enum InstallSettingsError {
#[error("No supported init system found")] #[error("No supported init system found")]
InitNotSupported, InitNotSupported,
} }
#[cfg(feature = "diagnostics")]
impl crate::diagnostics::ErrorDiagnostic for InstallSettingsError {
fn diagnostic(&self) -> String {
let static_str: &'static str = (self).into();
return static_str.to_string();
}
}

View file

@ -884,7 +884,7 @@
}, },
{ {
"action": { "action": {
"action": "configure_nix_daemon", "action": "configure_init_service",
"init": "Systemd", "init": "Systemd",
"start_daemon": true "start_daemon": true
}, },

View file

@ -906,7 +906,7 @@
}, },
{ {
"action": { "action": {
"action": "configure_nix_daemon", "action": "configure_init_service",
"init": "Systemd", "init": "Systemd",
"start_daemon": true "start_daemon": true
}, },

View file

@ -938,7 +938,7 @@
}, },
{ {
"action": { "action": {
"action": "configure_nix_daemon", "action": "configure_init_service",
"init": "Launchd", "init": "Launchd",
"start_daemon": true "start_daemon": true
}, },