From aa3140286d784c8a0c9540ab472ef43d7ee38b70 Mon Sep 17 00:00:00 2001 From: marshmallow Date: Wed, 20 Aug 2025 17:18:22 +1000 Subject: [PATCH] switch to miette + rehaul errors (#209) --- CHANGELOG.md | 2 + Cargo.lock | 105 +++++++++- Cargo.toml | 2 + doc/.vitepress/config.ts | 1 + doc/default.nix | 4 +- doc/package.nix | 2 + doc/reference/errors.md | 9 + wire/cli/Cargo.toml | 3 +- wire/cli/default.nix | 7 + wire/cli/src/apply.rs | 38 ++-- wire/cli/src/cli.rs | 2 +- wire/cli/src/main.rs | 7 +- wire/lib/Cargo.toml | 7 + wire/lib/build.rs | 202 +++++++++++++++++++ wire/lib/src/errors.rs | 299 ++++++++++++++++++++++++++++ wire/lib/src/hive/mod.rs | 34 +++- wire/lib/src/hive/node.rs | 13 +- wire/lib/src/hive/steps/activate.rs | 36 ++-- wire/lib/src/hive/steps/evaluate.rs | 2 +- wire/lib/src/hive/steps/keys.rs | 54 +++-- wire/lib/src/lib.rs | 86 +------- wire/lib/src/nix.rs | 33 ++- 22 files changed, 773 insertions(+), 175 deletions(-) create mode 100644 doc/reference/errors.md create mode 100644 wire/lib/build.rs create mode 100644 wire/lib/src/errors.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index eaba761..6534599 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `--reboot`. Wire will wait for the node to reconnect after rebooting. Wire will refuse to reboot localhost. Keys post-activation will be applied after rebooting! +- Most errors now have error codes and documentation links. ### Changed @@ -20,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 dry-activate). - Nix logs with the `Talkative` and `Chatty` level have been moved to `tracing_level::TRACE`. +- Error messages have been greatly improved. ## [0.4.0] - 2025-07-10 diff --git a/Cargo.lock b/Cargo.lock index e059ac5..d4efdad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + [[package]] name = "beef" version = "0.5.2" @@ -540,6 +549,12 @@ dependencies = [ "libc", ] +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -597,14 +612,18 @@ dependencies = [ "futures", "gethostname", "im", + "itertools", "key_agent", + "miette", "nix", + "proc-macro2", "prost", "regex", "serde", "serde-query", "serde_json", "serde_repr", + "syn 2.0.105", "tempdir", "thiserror", "tokio", @@ -670,6 +689,36 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -760,6 +809,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owo-colors" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" + [[package]] name = "parking_lot" version = "0.12.4" @@ -1197,6 +1252,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + [[package]] name = "syn" version = "1.0.109" @@ -1242,6 +1318,26 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "terminal_size" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.2.1", +] + [[package]] name = "thiserror" version = "2.0.14" @@ -1389,6 +1485,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-width" version = "0.1.14" @@ -1725,7 +1827,6 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" name = "wire" version = "0.5.0" dependencies = [ - "anyhow", "clap", "clap-markdown", "clap-num", @@ -1738,8 +1839,10 @@ dependencies = [ "indicatif", "itertools", "lib", + "miette", "serde", "serde_json", + "thiserror", "tokio", "tracing", "tracing-indicatif", diff --git a/Cargo.toml b/Cargo.toml index 9f6dacd..7f9b8a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,5 @@ im = { version = "15.1.0", features = ["serde"] } anyhow = "1.0.98" prost = "0.14.1" nix = { version = "0.30.1", features = ["user"] } +miette = { version = "7.6.0", features = ["fancy"] } +thiserror = "2.0.12" diff --git a/doc/.vitepress/config.ts b/doc/.vitepress/config.ts index 0fa0d7b..f302ff6 100644 --- a/doc/.vitepress/config.ts +++ b/doc/.vitepress/config.ts @@ -67,6 +67,7 @@ export default withMermaid( { text: "CLI", link: "/reference/cli" }, { text: "Meta Options", link: "/reference/meta" }, { text: "Module Options", link: "/reference/module" }, + { text: "Error Codes", link: "/reference/errors" }, ], }, ], diff --git a/doc/default.nix b/doc/default.nix index 670a521..feb707c 100644 --- a/doc/default.nix +++ b/doc/default.nix @@ -6,6 +6,8 @@ ... }: { - packages.docs = pkgs.callPackage ./package.nix { inherit (self'.packages) wire-small; }; + packages.docs = pkgs.callPackage ./package.nix { + inherit (self'.packages) wire-small wire-dignostics-md; + }; }; } diff --git a/doc/package.nix b/doc/package.nix index 5bc283c..239eec7 100644 --- a/doc/package.nix +++ b/doc/package.nix @@ -3,6 +3,7 @@ nixosOptionsDoc, runCommand, wire-small, + wire-dignostics-md, nix, nodejs, pnpm, @@ -54,6 +55,7 @@ stdenv.mkDerivation (finalAttrs: { }; patchPhase = '' cat ${optionsDoc} >> ./reference/module.md + cat ${wire-dignostics-md} >> ./reference/errors.md wire inspect --markdown-help > ./reference/cli.md ''; buildPhase = "pnpm run build > build.log 2>&1"; diff --git a/doc/reference/errors.md b/doc/reference/errors.md new file mode 100644 index 0000000..b246057 --- /dev/null +++ b/doc/reference/errors.md @@ -0,0 +1,9 @@ +--- +comment: true +title: Error Codes +description: Most error codes and their associated documentation. +--- + +# {{ $frontmatter.title }} + +{{ $frontmatter.description }} diff --git a/wire/cli/Cargo.toml b/wire/cli/Cargo.toml index 8327ed3..ed125da 100644 --- a/wire/cli/Cargo.toml +++ b/wire/cli/Cargo.toml @@ -17,7 +17,8 @@ tracing-log = { workspace = true } tracing-subscriber = { workspace = true } lib = { path = "../lib" } serde_json = { workspace = true } -anyhow = { workspace = true } +miette = { workspace = true } +thiserror = { workspace = true } indicatif = "0.18.0" enum-display-derive = "0.1.1" im = { workspace = true } diff --git a/wire/cli/default.nix b/wire/cli/default.nix index c4e59ff..333c151 100644 --- a/wire/cli/default.nix +++ b/wire/cli/default.nix @@ -55,6 +55,13 @@ ''; meta.mainProgram = "wire"; }; + + wire-dignostics-md = self'.packages.wire-unwrapped.overrideAttrs { + DIAGNOSTICS_MD_OUTPUT = "/build/source"; + installPhase = '' + mv /build/source/DIAGNOSTICS.md $out + ''; + }; }; }; } diff --git a/wire/cli/src/apply.rs b/wire/cli/src/apply.rs index 2e3427d..7ae0fdb 100644 --- a/wire/cli/src/apply.rs +++ b/wire/cli/src/apply.rs @@ -1,25 +1,38 @@ -use anyhow::anyhow; use futures::{FutureExt, StreamExt}; use indicatif::ProgressStyle; use itertools::{Either, Itertools}; -use lib::SubCommandModifiers; use lib::hive::Hive; -use lib::hive::node::{Context, GoalExecutor, StepState}; +use lib::hive::node::{Context, GoalExecutor, Name, StepState}; +use lib::{SubCommandModifiers, errors::HiveLibError}; +use miette::{Diagnostic, Result}; use std::collections::HashSet; -use std::fmt::Write; use std::path::PathBuf; +use thiserror::Error; use tracing::{Span, error, info, instrument}; use tracing_indicatif::span_ext::IndicatifSpanExt; use crate::cli::{ApplyArgs, ApplyTarget}; +#[derive(Debug, Error, Diagnostic)] +#[error("node {} failed to apply", .0)] +struct NodeError( + Name, + #[source] + #[diagnostic_source] + HiveLibError, +); + +#[derive(Debug, Error, Diagnostic)] +#[error("{} node(s) failed to apply.", .0.len())] +struct NodeErrors(#[related] Vec); + #[instrument(skip_all, fields(goal = %args.goal, on = %args.on.iter().join(", ")))] pub async fn apply( hive: &mut Hive, args: ApplyArgs, path: PathBuf, modifiers: SubCommandModifiers, -) -> Result<(), anyhow::Error> { +) -> Result<()> { let header_span = Span::current(); header_span.pb_set_style(&ProgressStyle::default_bar()); header_span.pb_set_length(1); @@ -97,16 +110,13 @@ pub async fn apply( std::mem::drop(header_span); if !errors.is_empty() { - return Err(anyhow!( - "{} node(s) failed to apply. {}", - errors.len(), + return Err(NodeErrors( errors - .iter() - .fold(String::new(), |mut output, (name, error)| { - let _ = write!(output, "\n\n{name}: {error}"); - output - }) - )); + .into_iter() + .map(|(name, error)| NodeError(name.clone(), error)) + .collect(), + ) + .into()); } Ok(()) diff --git a/wire/cli/src/cli.rs b/wire/cli/src/cli.rs index fba6319..b7cace3 100644 --- a/wire/cli/src/cli.rs +++ b/wire/cli/src/cli.rs @@ -141,7 +141,7 @@ pub enum Goal { } impl TryFrom for HiveGoal { - type Error = anyhow::Error; + type Error = miette::Error; fn try_from(value: Goal) -> Result { match value { diff --git a/wire/cli/src/main.rs b/wire/cli/src/main.rs index bd6f6a3..d1005e1 100644 --- a/wire/cli/src/main.rs +++ b/wire/cli/src/main.rs @@ -2,13 +2,14 @@ #![allow(clippy::missing_panics_doc)] use crate::cli::Cli; use crate::cli::ToSubCommandModifiers; -use anyhow::Ok; use clap::CommandFactory; use clap::Parser; use clap_complete::generate; use clap_verbosity_flag::{Verbosity, WarnLevel}; use indicatif::style::ProgressStyle; use lib::hive::Hive; +use miette::IntoDiagnostic; +use miette::Result; use tracing::warn; use tracing_indicatif::IndicatifLayer; use tracing_log::AsTrace; @@ -27,7 +28,7 @@ mod cli; static ALLOC: dhat::Alloc = dhat::Alloc; #[tokio::main] -async fn main() -> Result<(), anyhow::Error> { +async fn main() -> Result<()> { #[cfg(feature = "dhat-heap")] let _profiler = dhat::Profiler::new_heap(); @@ -56,7 +57,7 @@ async fn main() -> Result<(), anyhow::Error> { cli::Commands::Inspect { online: _, json } => println!("{}", { let hive = Hive::new_from_path(args.path.as_path(), modifiers).await?; if json { - serde_json::to_string(&hive)? + serde_json::to_string(&hive).into_diagnostic()? } else { warn!("use --json to output something scripting suitable"); format!("{hive:#?}") diff --git a/wire/lib/Cargo.toml b/wire/lib/Cargo.toml index aaf1799..dba27dd 100644 --- a/wire/lib/Cargo.toml +++ b/wire/lib/Cargo.toml @@ -25,6 +25,13 @@ prost = { workspace = true } gethostname = "1.0.2" async-trait = "0.1.88" nix = { workspace = true } +miette = { workspace = true } [dev-dependencies] tempdir = "0.3" + +[build-dependencies] +miette = { workspace = true } +syn = "2.0.104" +proc-macro2 = "1.0.95" +itertools = "0.14.0" diff --git a/wire/lib/build.rs b/wire/lib/build.rs new file mode 100644 index 0000000..5ce6c6e --- /dev/null +++ b/wire/lib/build.rs @@ -0,0 +1,202 @@ +use miette::{Context, IntoDiagnostic as _, Result, miette}; +use std::{ + env, + fmt::{self, Display, Formatter}, + fs::{self}, + path::Path, +}; + +use itertools::Itertools; +use proc_macro2::TokenTree; +use syn::{Expr, Item, ItemEnum, Lit, Meta, MetaList, MetaNameValue, parse_file}; + +macro_rules! p { + ($($tokens: tt)*) => { + println!("cargo::warning={}", format!($($tokens)*)) + } +} + +#[derive(Debug)] +struct DerviedError { + code: Option, + help: Option, + message: Option, + doc_string: String, +} + +impl Display for DerviedError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "## `{code}` {{#{code}}} + +{doc} +{message} +{help}", + doc = self.doc_string, + code = self.code.as_ref().unwrap(), + help = match &self.help { + Some(help) => format!( + " +::: tip HELP +{help} +:::" + ), + None => "".to_string(), + }, + message = match &self.message { + Some(message) => format!( + " +```txt [message] +{message} +```" + ), + None => "".to_string(), + } + ) + } +} + +impl DerviedError { + fn get_error(&mut self, list: &MetaList) -> Result<(), miette::Error> { + if list.path.segments.last().unwrap().ident != "error" { + return Err(miette!("Not an error")); + } + + self.message = Some( + list.tokens + .clone() + .into_iter() + .filter(|tok| matches!(tok, TokenTree::Literal(tok) if tok.to_string().starts_with("\""))) + .map(|tok| tok.to_string()) + .join(""), + ); + + Err(miette!("No error msg found")) + } + + fn update_diagnostic(&mut self, list: &MetaList) -> Result<(), miette::Error> { + if list.path.segments.last().unwrap().ident != "diagnostic" { + return Err(miette!("Not a diagnostic")); + } + + let vec: Vec<_> = list.tokens.clone().into_iter().collect(); + + // Find `diagnostic(code(x::y::z))` + let code: Option = if let Some((_, TokenTree::Group(group))) = + vec.iter().tuple_windows().find(|(ident, group)| { + matches!(ident, TokenTree::Ident(ident) if ident == "code") + && matches!(group, TokenTree::Group(..)) + }) { + Some(group.stream().to_string().replace(" ", "")) + } else { + None + }; + + // Find `diagnostic(help("hi"))` + let help: Option = if let Some((_, TokenTree::Group(group))) = + vec.iter().tuple_windows().find(|(ident, group)| { + matches!(ident, TokenTree::Ident(ident) if ident == "help") + && matches!(group, TokenTree::Group(..)) + }) { + Some(group.stream().to_string()) + } else { + None + }; + + if let Some(code) = code { + self.code = Some(code); + self.help = help; + return Ok(()); + } + + Err(miette!("Had no code.")) + } + + fn update_from_list(&mut self, list: MetaList) { + let _ = self.get_error(&list); + let _ = self.update_diagnostic(&list); + } + + fn update_from_namevalue(&mut self, list: MetaNameValue) -> Result<(), miette::Error> { + if list.path.segments.last().unwrap().ident != "doc" { + return Err(miette!("Not a doc string")); + } + + if let Expr::Lit(lit) = list.value { + if let Lit::Str(str) = lit.lit { + self.doc_string + .push_str(&format!("{}\n\n", &str.value()[1..])); + } + } + + Ok(()) + } +} + +fn main() -> Result<()> { + println!("cargo:rerun-if-changed=src/errors.rs"); + + let manifest_dir = env::var("CARGO_MANIFEST_DIR").into_diagnostic()?; + let md_out_dir = if let Ok(path) = env::var("DIAGNOSTICS_MD_OUTPUT") { + path + } else { + return Ok(()); + }; + + let src_path = Path::new(&manifest_dir).join("src/errors.rs"); + let src = fs::read_to_string(&src_path) + .into_diagnostic() + .wrap_err("reading errors.rs")?; + + let syntax_tree = parse_file(&src) + .into_diagnostic() + .wrap_err("parsing errors.rs")?; + let mut entries: Vec = Vec::new(); + + for item in &syntax_tree.items { + if let Item::Enum(ItemEnum { variants, .. }) = item { + for variant in variants { + let mut entry = DerviedError { + code: None, + help: None, + message: None, + doc_string: String::new(), + }; + + for attribute in variant.attrs.clone() { + match attribute.meta { + Meta::List(list) => { + entry.update_from_list(list); + } + Meta::NameValue(nv) => { + let _ = entry.update_from_namevalue(nv); + } + _ => {} + } + } + + if entry.code.is_some() { + entries.push(entry); + } + } + } + } + + fs::create_dir_all(Path::new(&md_out_dir)) + .into_diagnostic() + .wrap_err("creating target directory")?; + fs::write( + Path::new(&md_out_dir).join("DIAGNOSTICS.md"), + entries.iter().map(|x| x.to_string()).join("\n\n"), + ) + .into_diagnostic() + .wrap_err("writing DIAGNOSTICS.md")?; + + p!( + "wrote to {:?}", + Path::new(&md_out_dir).join("DIAGNOSTICS.md") + ); + + Ok(()) +} diff --git a/wire/lib/src/errors.rs b/wire/lib/src/errors.rs new file mode 100644 index 0000000..c8ddcce --- /dev/null +++ b/wire/lib/src/errors.rs @@ -0,0 +1,299 @@ +use std::{num::ParseIntError, path::PathBuf, process::ExitStatus}; + +use miette::{Diagnostic, SourceSpan}; +use thiserror::Error; + +use crate::{ + format_error_lines, + hive::node::{Name, SwitchToConfigurationGoal}, + nix_log::{NixLog, Trace}, +}; + +#[cfg(debug_assertions)] +const DOCS_URL: &str = "http://localhost:5173/reference/errors.html"; +#[cfg(not(debug_assertions))] +const DOCS_URL: &str = "https://wire.althaea.zone/reference/errors.html"; + +#[derive(Debug, Diagnostic, Error)] +pub enum KeyError { + #[diagnostic( + code(wire::Key::File), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("error reading file")] + File(#[source] std::io::Error), + + #[diagnostic( + code(wire::Key::SpawningCommand), + help("Ensure wire has the correct $PATH for this command"), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("error spawning key command")] + CommandSpawnError { + #[source] + error: std::io::Error, + + #[source_code] + command: String, + + #[label(primary, "Program ran")] + command_span: Option, + }, + + #[diagnostic( + code(wire::Key::Resolving), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("Error resolving key command child process")] + CommandResolveError { + #[source] + error: std::io::Error, + + #[source_code] + command: String, + }, + + #[diagnostic( + code(wire::Key::CommandExit), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("key command failed with status {}: {}", .0,.1)] + CommandError(ExitStatus, String), + + #[diagnostic( + code(wire::Key::Empty), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("Command list empty")] + Empty, + + #[diagnostic( + code(wire::Key::ParseKeyPermissions), + help("Refer to the documentation for the format of key file permissions."), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("Failed to parse key permissions")] + ParseKeyPermissions(#[source] ParseIntError), +} + +#[derive(Debug, Diagnostic, Error)] +pub enum KeyAgentError { + #[diagnostic( + code(wire::KeyAgent::SpawningAgent), + help("Please create an issue!"), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("Error spawning key agent")] + SpawningAgent(#[source] std::io::Error), + + #[diagnostic( + code(wire::KeyAgent::Resolving), + help("Please create an issue!"), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("Error resolving key agent child process")] + ResolvingError(#[source] std::io::Error), + + #[diagnostic( + code(wire::KeyAgent::Fail), + help("If you suspect the reason is wire's fault, please create an issue!"), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("failed to push keys (last 20 lines):\n{lines}", lines = format_error_lines(.1))] + AgentFailed(Name, Vec), +} + +#[derive(Debug, Diagnostic, Error)] +pub enum ActivationError { + #[diagnostic( + code(wire::Activation::SwitchToConfiguration), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("failed to run switch-to-configuration {0} on node {1} (last 20 lines):\n{lines}", lines = format_error_lines(.2))] + SwitchToConfigurationError(SwitchToConfigurationGoal, Name, Vec), + + #[diagnostic( + code(wire::Activation::Elevate), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("failed to elevate")] + FailedToElevate(#[source] std::io::Error), + + #[diagnostic( + code(wire::Activation::NixEnv), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("failed to run nix-env on node {0} (last 20 lines):\n{lines}", lines = format_error_lines(.1))] + NixEnvError(Name, Vec), +} + +#[derive(Debug, Diagnostic, Error)] +pub enum NetworkError { + #[diagnostic( + code(wire::Network::HostUnreachable), + help( + "If you failed due to a fault in DNS, note that a node can have multiple targets defined." + ), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("Cannot reach host {0}")] + HostUnreachable(String), + + #[diagnostic( + code(wire::Network::HostUnreachableAfterReboot), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("Cannot reach host {0} after reboot")] + HostUnreachableAfterReboot(String), + + #[diagnostic( + code(wire::Network::HostsExhausted), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("Ran out of contactable hosts")] + HostsExhausted, +} + +#[derive(Debug, Diagnostic, Error)] +pub enum HiveInitializationError { + #[diagnostic( + code(wire::HiveInit::NoHiveFound), + help( + "Double check the path is correct. You can adjust the hive path with `--path` when the hive lies outside of the CWD." + ), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("No hive could be found in {}", .0.display())] + NoHiveFound(PathBuf), + + #[diagnostic( + code(wire::HiveInit::NixEval), + help("Check your hive is syntactically valid."), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("failed to evaluate your hive! last 20 lines:\n{}", format_error_lines(.0))] + NixEvalError(Vec), + + #[diagnostic( + code(wire::HiveInit::Parse), + help("Please create an issue!"), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("Failed to parse internal wire json.")] + ParseEvaluateError(#[source] serde_json::Error), + + #[diagnostic( + code(wire::HiveInit::NodeDoesNotExist), + help("Please create an issue!"), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("node {0} not exist in hive")] + NodeDoesNotExist(String), +} + +#[derive(Debug, Diagnostic, Error)] +pub enum NixChildError { + #[diagnostic( + code(wire::NixChild::JoiningTasks), + help("This should never happen, please create an issue!"), + url("{DOCS_URL}#{}", self.code().unwrap()) + + )] + #[error("Could not join nix logging task")] + JoinError(#[source] tokio::task::JoinError), + + #[diagnostic( + code(wire::NixChild::NoHandle), + help("This should never happen, please create an issue!"), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("There was no handle to io on the child process")] + NoHandle, + + #[diagnostic( + code(wire::NixChild::SpawnFailed), + help("Please run wire under a host with nix installed."), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("failed to execute nix")] + SpawnFailed(#[source] tokio::io::Error), + + #[diagnostic( + code(wire::NixChild::Resolving), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("Error resolving nix child process")] + ResolveError(#[source] std::io::Error), +} + +#[derive(Debug, Diagnostic, Error)] +pub enum HiveLibError { + #[error(transparent)] + #[diagnostic(transparent)] + HiveInitializationError(HiveInitializationError), + + #[error(transparent)] + #[diagnostic(transparent)] + NetworkError(NetworkError), + + #[error(transparent)] + #[diagnostic(transparent)] + ActivationError(ActivationError), + + #[error("Failed to apply key {}", .0)] + KeyError( + String, + #[source] + #[diagnostic_source] + KeyError, + ), + + #[error("Wire key-agent failed")] + KeyAgentError( + #[source] + #[diagnostic_source] + KeyAgentError, + ), + + #[error(transparent)] + #[diagnostic(transparent)] + NixChildError(NixChildError), + + #[diagnostic( + code(wire::EvaluateNode), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error( + "failed to evaluate node {0} (filtered logs, run with -vvv to see all):\n{log}", + log = .1.iter().filter(|l| l.is_error()).map(std::string::ToString::to_string).collect::>().join("\n")) + ] + NixEvalInternalError(Name, Vec), + + #[diagnostic( + code(wire::BuildNode), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("failed to build node {0} (last 20 lines):\n{lines}", lines = format_error_lines(.1))] + NixBuildError(Name, Vec), + + #[diagnostic( + code(wire::CopyPath), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error( + "failed to copy path {path} to node {name} (filtered logs, run with -vvv to see all):\n{log}", + log = logs.iter().filter(|l| l.is_error()).map(std::string::ToString::to_string).collect::>().join("\n")) + ] + NixCopyError { + name: Name, + path: String, + logs: Vec, + }, + + #[diagnostic( + code(wire::BufferOperation), + url("{DOCS_URL}#{}", self.code().unwrap()) + )] + #[error("an operation failed in regards to buffers")] + BufferOperationError(#[source] tokio::io::Error), +} diff --git a/wire/lib/src/hive/mod.rs b/wire/lib/src/hive/mod.rs index 2a98c8a..01a1e57 100644 --- a/wire/lib/src/hive/mod.rs +++ b/wire/lib/src/hive/mod.rs @@ -7,6 +7,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use tracing::{debug, error, info, instrument, trace}; +use crate::errors::{HiveInitializationError, NixChildError}; use crate::nix::{EvalGoal, get_eval_command}; use crate::{HiveLibError, SubCommandModifiers}; pub mod node; @@ -49,7 +50,7 @@ impl Hive { let command = get_eval_command(path, &EvalGoal::Inspect, modifiers)? .output() .await - .map_err(HiveLibError::NixExecError)?; + .map_err(|err| HiveLibError::NixChildError(NixChildError::ResolveError(err)))?; let stdout = String::from_utf8_lossy(&command.stdout); let stderr = String::from_utf8_lossy(&command.stderr); @@ -57,17 +58,22 @@ impl Hive { debug!("Output of nix eval: {stdout}"); if command.status.success() { - let hive: Hive = - serde_json::from_str(&stdout).map_err(HiveLibError::ParseEvaluateError)?; + let hive: Hive = serde_json::from_str(&stdout).map_err(|err| { + HiveLibError::HiveInitializationError(HiveInitializationError::ParseEvaluateError( + err, + )) + })?; return Ok(hive); } - Err(HiveLibError::NixEvalError( - stderr - .split('\n') - .map(std::string::ToString::to_string) - .collect(), + Err(HiveLibError::HiveInitializationError( + HiveInitializationError::NixEvalError( + stderr + .split('\n') + .map(std::string::ToString::to_string) + .collect(), + ), )) } @@ -80,7 +86,9 @@ impl Hive { self.nodes .get_mut(&Name(Arc::from(node.clone()))) - .ok_or(HiveLibError::NodeDoesNotExist(node.to_string()))? + .ok_or(HiveLibError::HiveInitializationError( + HiveInitializationError::NodeDoesNotExist(node.to_string()), + ))? .build_remotely = false; } @@ -234,7 +242,9 @@ mod tests { assert!(matches!( Hive::new_from_path(&path, SubCommandModifiers::default()).await, - Err(HiveLibError::NixEvalError(..)) + Err(HiveLibError::HiveInitializationError( + HiveInitializationError::NixEvalError(..) + )) )); } @@ -244,7 +254,9 @@ mod tests { assert!(matches!( Hive::new_from_path(&path, SubCommandModifiers::default()).await, - Err(HiveLibError::NixEvalError(..)) + Err(HiveLibError::HiveInitializationError( + HiveInitializationError::NixEvalError(..) + )) )); } } diff --git a/wire/lib/src/hive/node.rs b/wire/lib/src/hive/node.rs index ff864e1..d3f7369 100644 --- a/wire/lib/src/hive/node.rs +++ b/wire/lib/src/hive/node.rs @@ -10,6 +10,7 @@ use tracing::{Instrument, Span, error, info, instrument, trace}; use tracing_indicatif::span_ext::IndicatifSpanExt; use crate::SubCommandModifiers; +use crate::errors::NetworkError; use crate::hive::steps::keys::{Key, KeysStep, PushKeyAgentStep, UploadKeyAt}; use crate::hive::steps::ping::PingStep; use crate::nix::StreamTracing; @@ -46,7 +47,7 @@ impl Target { pub fn get_preffered_host(&self) -> Result<&Arc, HiveLibError> { self.hosts .get(self.current_host) - .ok_or(HiveLibError::HostsExhausted) + .ok_or(HiveLibError::NetworkError(NetworkError::HostsExhausted)) } pub fn host_failed(&mut self) { @@ -129,9 +130,9 @@ impl Node { return Ok(()); } - Err(HiveLibError::HostUnreachable( + Err(HiveLibError::NetworkError(NetworkError::HostUnreachable( self.target.get_preffered_host()?.to_string(), - )) + ))) } } @@ -273,7 +274,11 @@ pub async fn push(node: &Node, name: &Name, push: Push<'_>) -> Result<(), HiveLi let (status, _stdout, stderr_vec) = command.execute(true).in_current_span().await?; if !status.success() { - return Err(HiveLibError::NixCopyError(name.clone(), stderr_vec)); + return Err(HiveLibError::NixCopyError { + name: name.clone(), + logs: stderr_vec, + path: push.to_string(), + }); } Ok(()) diff --git a/wire/lib/src/hive/steps/activate.rs b/wire/lib/src/hive/steps/activate.rs index 0a298a1..9e8d695 100644 --- a/wire/lib/src/hive/steps/activate.rs +++ b/wire/lib/src/hive/steps/activate.rs @@ -7,6 +7,7 @@ use tracing_indicatif::suspend_tracing_indicatif; use crate::{ HiveLibError, create_ssh_command, + errors::{ActivationError, NetworkError}, hive::node::{Context, ExecuteStep, Goal, SwitchToConfigurationGoal, should_apply_locally}, nix::StreamTracing, }; @@ -19,13 +20,13 @@ impl Display for SwitchToConfigurationStep { } } -pub(crate) fn get_elevation(reason: &str) -> Result { +pub(crate) fn get_elevation(reason: &str) -> Result { info!("Attempting to elevate for {reason}."); suspend_tracing_indicatif(|| { let mut command = std::process::Command::new("sudo"); command.arg("-v").output() }) - .map_err(HiveLibError::FailedToElevate) + .map_err(ActivationError::FailedToElevate) } pub async fn wait_for_ping(ctx: &Context<'_>) -> Result<(), HiveLibError> { @@ -46,9 +47,9 @@ pub async fn wait_for_ping(ctx: &Context<'_>) -> Result<(), HiveLibError> { } } - Err(HiveLibError::HostUnreachable( + Err(HiveLibError::NetworkError(NetworkError::HostUnreachable( ctx.node.target.get_preffered_host()?.to_string(), - )) + ))) } #[async_trait] @@ -75,7 +76,7 @@ impl ExecuteStep for SwitchToConfigurationStep { if should_apply_locally(ctx.node.allow_local_deployment, &ctx.name.to_string()) { // Refresh sudo timeout warn!("Running nix-env ON THIS MACHINE for node {0}", ctx.name); - get_elevation("nix-env")?; + get_elevation("nix-env").map_err(HiveLibError::ActivationError)?; let mut command = Command::new("sudo"); command.arg("nix-env"); command @@ -96,7 +97,10 @@ impl ExecuteStep for SwitchToConfigurationStep { .filter(|s| !s.is_empty()) .collect(); - return Err(HiveLibError::NixEnvError(ctx.name.clone(), stderr)); + return Err(HiveLibError::ActivationError(ActivationError::NixEnvError( + ctx.name.clone(), + stderr, + ))); } info!("Set system profile"); @@ -113,7 +117,7 @@ impl ExecuteStep for SwitchToConfigurationStep { "Running switch-to-configuration {goal:?} ON THIS MACHINE for node {0}", ctx.name ); - get_elevation("switch-to-configuration")?; + get_elevation("switch-to-configuration").map_err(HiveLibError::ActivationError)?; let mut command = Command::new("sudo"); command.arg(cmd); command @@ -167,8 +171,10 @@ impl ExecuteStep for SwitchToConfigurationStep { host = ctx.node.target.get_preffered_host()? ); - return Err(HiveLibError::HostUnreachableAfterReboot( - ctx.node.target.get_preffered_host()?.to_string(), + return Err(HiveLibError::NetworkError( + NetworkError::HostUnreachableAfterReboot( + ctx.node.target.get_preffered_host()?.to_string(), + ), )); } @@ -188,10 +194,8 @@ impl ExecuteStep for SwitchToConfigurationStep { if matches!(goal, SwitchToConfigurationGoal::DryActivate) || should_apply_locally(ctx.node.allow_local_deployment, &ctx.name.to_string()) { - return Err(HiveLibError::SwitchToConfigurationError( - *goal, - ctx.name.clone(), - stderr, + return Err(HiveLibError::ActivationError( + ActivationError::SwitchToConfigurationError(*goal, ctx.name.clone(), stderr), )); } @@ -205,10 +209,8 @@ impl ExecuteStep for SwitchToConfigurationStep { host = ctx.node.target.get_preffered_host()? ); - return Err(HiveLibError::SwitchToConfigurationError( - *goal, - ctx.name.clone(), - stderr, + return Err(HiveLibError::ActivationError( + ActivationError::SwitchToConfigurationError(*goal, ctx.name.clone(), stderr), )); } } diff --git a/wire/lib/src/hive/steps/evaluate.rs b/wire/lib/src/hive/steps/evaluate.rs index 8cbab5c..ab3cb45 100644 --- a/wire/lib/src/hive/steps/evaluate.rs +++ b/wire/lib/src/hive/steps/evaluate.rs @@ -48,6 +48,6 @@ impl ExecuteStep for Step { return Ok(()); } - Err(HiveLibError::NixEvalInteralError(ctx.name.clone(), stderr)) + Err(HiveLibError::NixEvalInternalError(ctx.name.clone(), stderr)) } } diff --git a/wire/lib/src/hive/steps/keys.rs b/wire/lib/src/hive/steps/keys.rs index f5db91f..05126e1 100644 --- a/wire/lib/src/hive/steps/keys.rs +++ b/wire/lib/src/hive/steps/keys.rs @@ -5,40 +5,22 @@ use serde::{Deserialize, Serialize}; use std::env; use std::fmt::Display; use std::io::Cursor; +use std::path::PathBuf; use std::pin::Pin; -use std::process::{ExitStatus, Stdio}; +use std::process::Stdio; use std::str::from_utf8; -use std::{num::ParseIntError, path::PathBuf}; -use thiserror::Error; use tokio::io::{AsyncReadExt as _, AsyncWriteExt}; use tokio::process::Command; use tokio::{fs::File, io::AsyncRead}; use tracing::{debug, info, trace, warn}; +use crate::errors::{KeyAgentError, KeyError}; use crate::hive::node::{ Context, ExecuteStep, Goal, Push, SwitchToConfigurationGoal, push, should_apply_locally, }; use crate::hive::steps::activate::get_elevation; use crate::{HiveLibError, create_ssh_command}; -#[derive(Debug, Error)] -pub enum KeyError { - #[error("error reading file")] - File(#[source] std::io::Error), - - #[error("error spawning command")] - CommandSpawnError(#[source] std::io::Error), - - #[error("key command failed with status {}: {}", .0,.1)] - CommandError(ExitStatus, String), - - #[error("Command list empty")] - Empty, - - #[error("Failed to parse key permissions")] - ParseKeyPermissions(#[source] ParseIntError), -} - #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)] #[serde(tag = "t", content = "c")] pub enum Source { @@ -105,10 +87,17 @@ async fn create_reader( .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() - .map_err(KeyError::CommandSpawnError)? + .map_err(|err| KeyError::CommandSpawnError { + error: err, + command: args.join(" "), + command_span: Some((0..args.first().unwrap().len()).into()), + })? .wait_with_output() .await - .map_err(KeyError::CommandSpawnError)?; + .map_err(|err| KeyError::CommandResolveError { + error: err, + command: args.join(" "), + })?; if output.status.success() { return Ok(Box::pin(Cursor::new(output.stdout))); @@ -214,13 +203,16 @@ impl ExecuteStep for KeysStep { self.filter == UploadKeyAt::NoFilter || (self.filter != UploadKeyAt::NoFilter && key.upload_at != self.filter) }) - .map(|key| async move { process_key(key).await }); + .map(|key| async move { + process_key(key) + .await + .map_err(|err| HiveLibError::KeyError(key.name.clone(), err)) + }); let (keys, bufs): (Vec, Vec>) = join_all(futures) .await .into_iter() - .collect::, KeyError>>() - .map_err(HiveLibError::KeyError)? + .collect::, HiveLibError>>()? .into_iter() .unzip(); @@ -233,7 +225,7 @@ impl ExecuteStep for KeysStep { let mut command = if should_apply_locally(ctx.node.allow_local_deployment, &ctx.name.to_string()) { warn!("Placing keys locally for node {0}", ctx.name); - get_elevation("wire key agent")?; + get_elevation("wire key agent").map_err(HiveLibError::ActivationError)?; Command::new("sudo") } else { create_ssh_command(&ctx.node.target, true)? @@ -248,7 +240,7 @@ impl ExecuteStep for KeysStep { .stderr(Stdio::piped()) .stdin(Stdio::piped()) .spawn() - .map_err(HiveLibError::SpawnFailed)?; + .map_err(|err| HiveLibError::KeyAgentError(KeyAgentError::SpawningAgent(err)))?; // take() stdin so it will be dropped out of block if let Some(mut stdin) = child.stdin.take() { @@ -260,7 +252,7 @@ impl ExecuteStep for KeysStep { let output = child .wait_with_output() .await - .map_err(HiveLibError::SpawnFailed)?; + .map_err(|err| HiveLibError::KeyAgentError(KeyAgentError::ResolvingError(err)))?; if output.status.success() { info!("Successfully pushed keys to {}", ctx.name); @@ -271,13 +263,13 @@ impl ExecuteStep for KeysStep { let stderr = String::from_utf8_lossy(&output.stderr); - Err(HiveLibError::KeyCommandError( + Err(HiveLibError::KeyAgentError(KeyAgentError::AgentFailed( ctx.name.clone(), stderr .split('\n') .map(std::string::ToString::to_string) .collect(), - )) + ))) } } diff --git a/wire/lib/src/lib.rs b/wire/lib/src/lib.rs index 78e9ee9..fc811c1 100644 --- a/wire/lib/src/lib.rs +++ b/wire/lib/src/lib.rs @@ -4,14 +4,10 @@ clippy::must_use_candidate, clippy::missing_panics_doc )] -use hive::{ - node::{Name, SwitchToConfigurationGoal, Target}, - steps::keys::KeyError, -}; -use nix_log::{NixLog, Trace}; -use std::path::PathBuf; -use thiserror::Error; -use tokio::{process::Command, task::JoinError}; +use hive::node::Target; +use tokio::process::Command; + +use crate::errors::HiveLibError; pub mod hive; mod nix; @@ -23,6 +19,8 @@ mod test_macros; #[cfg(test)] mod test_support; +pub mod errors; + fn create_ssh_command(target: &Target, sudo: bool) -> Result { let mut command = Command::new("ssh"); @@ -49,78 +47,6 @@ fn format_error_lines(lines: &[String]) -> String { .join("\n") } -#[derive(Debug, Error)] -pub enum HiveLibError { - #[error("no hive could be found in {}", .0.display())] - NoHiveFound(PathBuf), - - #[error("failed to execute nix command")] - NixExecError(#[source] tokio::io::Error), - - #[error("failed to evaluate your hive! is it valid? (last 20 lines):\n{}", format_error_lines(.0))] - NixEvalError(Vec), - - #[error( - "failed to evaluate node {0} (filtered logs, run with -vvv to see all):\n{log}", - log = .1.iter().filter(|l| l.is_error()).map(std::string::ToString::to_string).collect::>().join("\n")) - ] - NixEvalInteralError(Name, Vec), - - #[error( - "failed to copy drv to node {0} (filtered logs, run with -vvv to see all):\n{log}", - log = .1.iter().filter(|l| l.is_error()).map(std::string::ToString::to_string).collect::>().join("\n")) - ] - NixCopyError(Name, Vec), - - #[error("failed to build node {0} (last 20 lines):\n{lines}", lines = format_error_lines(.1))] - NixBuildError(Name, Vec), - - #[error("failed to run switch-to-configuration {0} on node {1} (last 20 lines):\n{lines}", lines = format_error_lines(.2))] - SwitchToConfigurationError(SwitchToConfigurationGoal, Name, Vec), - - #[error("failed to run nix-env on node {0} (last 20 lines):\n{lines}", lines = format_error_lines(.1))] - NixEnvError(Name, Vec), - - #[error("failed to push keys to {0} (last 20 lines):\n{lines}", lines = format_error_lines(.1))] - KeyCommandError(Name, Vec), - - #[error("failed to push a key")] - KeyError(#[source] KeyError), - - #[error("node {0} not exist in hive")] - NodeDoesNotExist(String), - - #[error("failed to execute command")] - SpawnFailed(#[source] tokio::io::Error), - - #[error("failed to join task")] - JoinError(#[source] JoinError), - - #[error("there was no handle to io on the child process")] - NoHandle, - - #[error("failed to parse nix log \"{0}\"")] - ParseLogError(String, #[source] serde_json::Error), - - #[error("failed to parse internal wire json. please create an issue!")] - ParseEvaluateError(#[source] serde_json::Error), - - #[error("an operation failed in regards to buffers")] - BufferOperationError(#[source] tokio::io::Error), - - #[error("failed to elevate")] - FailedToElevate(#[source] std::io::Error), - - #[error("Cannot reach host {0}")] - HostUnreachable(String), - - #[error("Cannot reach host {0} after reboot")] - HostUnreachableAfterReboot(String), - - #[error("Ran out of contactable hosts")] - HostsExhausted, -} - #[derive(Debug, Default, Clone, Copy)] pub struct SubCommandModifiers { pub show_trace: bool, diff --git a/wire/lib/src/nix.rs b/wire/lib/src/nix.rs index 3f846f0..45d5b1e 100644 --- a/wire/lib/src/nix.rs +++ b/wire/lib/src/nix.rs @@ -7,6 +7,7 @@ use tokio::io::{AsyncBufReadExt, AsyncRead}; use tracing::{Instrument, Span, error, info, trace}; use tracing_indicatif::span_ext::IndicatifSpanExt; +use crate::errors::{HiveInitializationError, NixChildError}; use crate::hive::find_hive; use crate::hive::node::Name; use crate::nix_log::{Action, Internal, NixLog, Trace}; @@ -47,8 +48,10 @@ pub fn get_eval_command( ) -> Result { assert!(check_nix_available(), "nix is not available on this system"); - let canon_path = find_hive(&path.canonicalize().unwrap()) - .ok_or(HiveLibError::NoHiveFound(path.to_path_buf()))?; + let canon_path = + find_hive(&path.canonicalize().unwrap()).ok_or(HiveLibError::HiveInitializationError( + HiveInitializationError::NoHiveFound(path.to_path_buf()), + ))?; let mut command = tokio::process::Command::new("nix"); command.args(["--extra-experimental-features", "nix-command"]); @@ -92,7 +95,7 @@ where while let Some(line) = io_reader .next_line() .await - .map_err(HiveLibError::SpawnFailed)? + .map_err(|err| HiveLibError::NixChildError(NixChildError::SpawnFailed(err)))? { let log = serde_json::from_str::(line.strip_prefix("@nix ").unwrap_or(&line)) .map(NixLog::Internal) @@ -142,19 +145,29 @@ impl StreamTracing for tokio::process::Command { .stderr(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .spawn() - .map_err(HiveLibError::SpawnFailed)?; + .map_err(|err| HiveLibError::NixChildError(NixChildError::SpawnFailed(err)))?; - let stdout_handle = child.stdout.take().ok_or(HiveLibError::NoHandle)?; - let stderr_handle = child.stderr.take().ok_or(HiveLibError::NoHandle)?; + let stdout_handle = child + .stdout + .take() + .ok_or(HiveLibError::NixChildError(NixChildError::NoHandle))?; + let stderr_handle = child + .stderr + .take() + .ok_or(HiveLibError::NixChildError(NixChildError::NoHandle))?; let stderr_task = tokio::spawn(handle_io(stderr_handle, log_stderr).in_current_span()); let stdout_task = tokio::spawn(handle_io(stdout_handle, false)); - let handle = - tokio::spawn(async move { child.wait().await.map_err(HiveLibError::SpawnFailed) }); + let handle = tokio::spawn(async move { + child + .wait() + .await + .map_err(|err| HiveLibError::NixChildError(NixChildError::SpawnFailed(err))) + }); - let (result, stdout, stderr) = - tokio::try_join!(handle, stdout_task, stderr_task).map_err(HiveLibError::JoinError)?; + let (result, stdout, stderr) = tokio::try_join!(handle, stdout_task, stderr_task) + .map_err(|err| HiveLibError::NixChildError(NixChildError::JoinError(err)))?; Ok((result?, stdout?, stderr?)) } -- 2.51.2