diff --git a/lib/crates/fabro-cli/src/commands/artifact/cp.rs b/lib/crates/fabro-cli/src/commands/artifact/cp.rs index dba10bd53..b1e8e7304 100644 --- a/lib/crates/fabro-cli/src/commands/artifact/cp.rs +++ b/lib/crates/fabro-cli/src/commands/artifact/cp.rs @@ -141,7 +141,8 @@ async fn write_artifact_file( dest_file: &Path, ) -> Result<()> { if let Some(parent) = dest_file.parent() { - std::fs::create_dir_all(parent)?; + std::fs::create_dir_all(parent) + .with_context(|| format!("creating directory {}", parent.display()))?; } let bytes = client .download_stage_artifact(run_id, &entry.stage_id, &entry.relative_path) diff --git a/lib/crates/fabro-cli/src/commands/graph.rs b/lib/crates/fabro-cli/src/commands/graph.rs index 12a873dc8..a16001cc5 100644 --- a/lib/crates/fabro-cli/src/commands/graph.rs +++ b/lib/crates/fabro-cli/src/commands/graph.rs @@ -1,6 +1,6 @@ use std::io::Write; -use anyhow::bail; +use anyhow::{Context, bail}; use fabro_api::types; use fabro_config::load::load_settings_user; use fabro_config::user::active_settings_path; @@ -62,7 +62,8 @@ pub(crate) async fn run( .await?; if let Some(ref output_path) = args.output { - std::fs::write(output_path, &rendered)?; + std::fs::write(output_path, &rendered) + .with_context(|| format!("writing rendered graph to {}", output_path.display()))?; if cli.output.format == OutputFormat::Json { print_json_pretty(&serde_json::json!({ "path": absolute_or_current(output_path), diff --git a/lib/crates/fabro-cli/src/commands/install.rs b/lib/crates/fabro-cli/src/commands/install.rs index dbf06315e..25fe3d6be 100644 --- a/lib/crates/fabro-cli/src/commands/install.rs +++ b/lib/crates/fabro-cli/src/commands/install.rs @@ -1326,10 +1326,9 @@ fn persist_server_env_secrets(storage_dir: &Path, secrets: &[(String, String)]) return Ok(()); } - envfile::merge_env_file( - &Storage::new(storage_dir).server_state().env_path(), - secrets.iter().cloned(), - )?; + let env_path = Storage::new(storage_dir).server_state().env_path(); + envfile::merge_env_file(&env_path, secrets.iter().cloned()) + .with_context(|| format!("merging server env secrets into {}", env_path.display()))?; Ok(()) } @@ -1364,14 +1363,18 @@ fn restore_optional_file(path: &Path, previous_contents: Option<&str>) -> Result match previous_contents { Some(contents) => { if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; + std::fs::create_dir_all(parent) + .with_context(|| format!("creating directory {}", parent.display()))?; } - std::fs::write(path, contents)?; + std::fs::write(path, contents) + .with_context(|| format!("restoring {}", path.display()))?; } None => match std::fs::remove_file(path) { Ok(()) => {} Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} - Err(err) => return Err(err.into()), + Err(err) => { + return Err(anyhow::Error::new(err).context(format!("removing {}", path.display()))); + } }, } @@ -1389,7 +1392,8 @@ fn persist_github_install_changes( let previous_vault = std::fs::read_to_string(&vault_path).ok(); let result = (|| -> Result<()> { - let mut server_env = envfile::read_env_file(&server_env_path)?; + let mut server_env = envfile::read_env_file(&server_env_path) + .with_context(|| format!("reading env file {}", server_env_path.display()))?; for key in &writes.server_env_remove { server_env.remove(*key); } @@ -1399,7 +1403,8 @@ fn persist_github_install_changes( if server_env.is_empty() { restore_optional_file(&server_env_path, None)?; } else { - envfile::write_env_file(&server_env_path, &server_env)?; + envfile::write_env_file(&server_env_path, &server_env) + .with_context(|| format!("writing env file {}", server_env_path.display()))?; } let mut vault = Vault::load(vault_path.clone()).map_err(anyhow::Error::from)?; @@ -1415,7 +1420,14 @@ fn persist_github_install_changes( .map_err(anyhow::Error::from)?; } - std::fs::write(writes.settings_write.path, writes.settings_write.contents)?; + std::fs::write(writes.settings_write.path, writes.settings_write.contents).with_context( + || { + format!( + "writing settings file {}", + writes.settings_write.path.display() + ) + }, + )?; Ok(()) })(); @@ -1467,7 +1479,8 @@ async fn persist_install_outputs_with_settings( persist_server_env_secrets(storage_dir, server_env_secrets)?; if let Some(write) = settings_write { - std::fs::write(write.path, write.contents)?; + std::fs::write(write.path, write.contents) + .with_context(|| format!("writing settings file {}", write.path.display()))?; } let persist_result = persist_vault_secrets_with( @@ -1482,8 +1495,10 @@ async fn persist_install_outputs_with_settings( if let Err(err) = persist_result { if let Some(write) = settings_write { match write.previous_contents { - Some(previous) => std::fs::write(write.path, previous)?, - None if write.path.exists() => std::fs::remove_file(write.path)?, + Some(previous) => std::fs::write(write.path, previous) + .with_context(|| format!("restoring settings file {}", write.path.display()))?, + None if write.path.exists() => std::fs::remove_file(write.path) + .with_context(|| format!("removing settings file {}", write.path.display()))?, None => {} } } @@ -1818,7 +1833,8 @@ async fn run_install_inner( ); fabro_util::printerr!(printer, ""); - std::fs::create_dir_all(&fabro_dir)?; + std::fs::create_dir_all(&fabro_dir) + .with_context(|| format!("creating fabro home directory {}", fabro_dir.display()))?; { let env_path = legacy_env::legacy_env_file_path(); diff --git a/lib/crates/fabro-cli/src/commands/server/record.rs b/lib/crates/fabro-cli/src/commands/server/record.rs index c98b886f9..560f9d9f7 100644 --- a/lib/crates/fabro-cli/src/commands/server/record.rs +++ b/lib/crates/fabro-cli/src/commands/server/record.rs @@ -25,7 +25,8 @@ pub(crate) struct ActiveServerRecord { pub(crate) fn write_server_record(path: &Path, record: &ServerRecord) -> Result<()> { if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; + std::fs::create_dir_all(parent) + .with_context(|| format!("creating server record directory {}", parent.display()))?; } std::fs::write(path, serde_json::to_string_pretty(record)?) .with_context(|| format!("Failed to write server metadata to {}", path.display())) diff --git a/lib/crates/fabro-cli/src/commands/server/start.rs b/lib/crates/fabro-cli/src/commands/server/start.rs index ed723fcef..ceab81a41 100644 --- a/lib/crates/fabro-cli/src/commands/server/start.rs +++ b/lib/crates/fabro-cli/src/commands/server/start.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use std::time::Duration; -use anyhow::{Result, anyhow, bail}; +use anyhow::{Context, Result, anyhow, bail}; use chrono::Utc; use fabro_config::user::load_settings_config; use fabro_config::{Storage, envfile, resolve_server_from_file}; @@ -174,13 +174,33 @@ fn load_or_create_local_dev_token(storage_dir: &Path, home: &Home) -> Result Result { } let secret = session_secret::generate_session_secret(); - envfile::merge_env_file(&server_env_path, [("SESSION_SECRET", secret.as_str())])?; + envfile::merge_env_file(&server_env_path, [("SESSION_SECRET", secret.as_str())]) + .with_context(|| format!("merging session secret into {}", server_env_path.display()))?; Ok(secret) } @@ -319,13 +340,17 @@ async fn execute_daemon( let server_state = Storage::new(storage_dir).server_state(); let log_path = server_state.log_path(); if let Some(parent) = log_path.parent() { - std::fs::create_dir_all(parent)?; + std::fs::create_dir_all(parent) + .with_context(|| format!("creating log directory {}", parent.display()))?; } let record_path = server_state.record_path(); - let log_file = std::fs::File::create(&log_path)?; - let stdout_log = log_file.try_clone()?; - let exe = std::env::current_exe()?; + let log_file = std::fs::File::create(&log_path) + .with_context(|| format!("creating server log file {}", log_path.display()))?; + let stdout_log = log_file + .try_clone() + .with_context(|| format!("cloning server log file handle for {}", log_path.display()))?; + let exe = std::env::current_exe().context("resolving current fabro executable path")?; let mut cmd = TokioCommand::new(&exe); cmd.args(["server", "__serve"]) @@ -376,7 +401,9 @@ async fn execute_daemon( #[cfg(unix)] fabro_proc::pre_exec_setsid(cmd.as_std_mut()); - let mut child = cmd.spawn()?; + let mut child = cmd + .spawn() + .with_context(|| format!("spawning fabro server subprocess {}", exe.display()))?; if let Ok(Some(status)) = child.try_wait() { record::remove_server_record(&record_path); @@ -464,13 +491,15 @@ fn print_dev_token(printer: Printer, home: &Home, token: &str) { async fn acquire_lock(storage_dir: &Path) -> Result { let lock_path = Storage::new(storage_dir).server_state().lock_path(); if let Some(parent) = lock_path.parent() { - std::fs::create_dir_all(parent)?; + std::fs::create_dir_all(parent) + .with_context(|| format!("creating server lock directory {}", parent.display()))?; } let lock_file = std::fs::OpenOptions::new() .create(true) .write(true) .truncate(false) - .open(&lock_path)?; + .open(&lock_path) + .with_context(|| format!("opening server lock file {}", lock_path.display()))?; let poll_interval = Duration::from_millis(50); let timeout = Duration::from_secs(5); diff --git a/lib/crates/fabro-cli/src/commands/store/dump.rs b/lib/crates/fabro-cli/src/commands/store/dump.rs index 67764a7d3..018e7775a 100644 --- a/lib/crates/fabro-cli/src/commands/store/dump.rs +++ b/lib/crates/fabro-cli/src/commands/store/dump.rs @@ -277,7 +277,9 @@ fn inspect_output_dir(path: &Path) -> Result { Ok(OutputDirState::ExistingEmpty) } Err(err) if err.kind() == ErrorKind::NotFound => Ok(OutputDirState::Missing), - Err(err) => Err(err.into()), + Err(err) => { + Err(anyhow::Error::new(err).context(format!("reading metadata for {}", path.display()))) + } } } diff --git a/lib/crates/fabro-cli/src/commands/upgrade.rs b/lib/crates/fabro-cli/src/commands/upgrade.rs index f47a40cac..001cff8ed 100644 --- a/lib/crates/fabro-cli/src/commands/upgrade.rs +++ b/lib/crates/fabro-cli/src/commands/upgrade.rs @@ -144,8 +144,10 @@ impl Backend { bail!("download failed: HTTP {}", resp.status()); } let bytes = resp.bytes().await?; - let mut file = fs::File::create(&dest)?; - file.write_all(&bytes)?; + let mut file = fs::File::create(&dest) + .with_context(|| format!("creating download file {}", dest.display()))?; + file.write_all(&bytes) + .with_context(|| format!("writing download to {}", dest.display()))?; } } Ok(dest) @@ -231,7 +233,8 @@ fn verify_checksum(path: &Path, expected_hex: &str) -> Result<()> { let mut file = std::io::BufReader::new( fs::File::open(path).with_context(|| format!("failed to open {}", path.display()))?, ); - std::io::copy(&mut file, &mut hasher)?; + std::io::copy(&mut file, &mut hasher) + .with_context(|| format!("reading {} for checksum", path.display()))?; let computed = format!("{:x}", hasher.finalize()); // The .sha256 file may contain "hash filename" or just "hash" let expected = expected_hex @@ -272,10 +275,12 @@ impl UpgradeCheckState { fn save(&self, path: &Path) -> Result<()> { if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; + fs::create_dir_all(parent) + .with_context(|| format!("creating directory {}", parent.display()))?; } let json = serde_json::to_string(self)?; - fs::write(path, json)?; + fs::write(path, json) + .with_context(|| format!("writing upgrade check state {}", path.display()))?; Ok(()) } } @@ -365,7 +370,10 @@ pub(crate) async fn run_upgrade( let tarball_name = format!("fabro-{triple}.tar.gz"); let checksum_name = format!("{tarball_name}.sha256"); - let current_exe = std::env::current_exe()?.canonicalize()?; + let current_exe = std::env::current_exe() + .context("resolving current fabro executable path")? + .canonicalize() + .context("canonicalizing current fabro executable path")?; let exe_dir = current_exe .parent() .context("could not determine executable directory")?; @@ -382,7 +390,8 @@ pub(crate) async fn run_upgrade( )?; // Verify SHA256 using streaming hash - let checksum_content = fs::read_to_string(&checksum_path)?; + let checksum_content = fs::read_to_string(&checksum_path) + .with_context(|| format!("reading checksum file {}", checksum_path.display()))?; verify_checksum(&tarball_path, &checksum_content)?; debug!("SHA256 checksum verified"); diff --git a/lib/crates/fabro-cli/src/user_config.rs b/lib/crates/fabro-cli/src/user_config.rs index 4074284d9..a64cf5c46 100644 --- a/lib/crates/fabro-cli/src/user_config.rs +++ b/lib/crates/fabro-cli/src/user_config.rs @@ -1,6 +1,6 @@ use std::path::{Path, PathBuf}; -use anyhow::{Result, bail}; +use anyhow::{Context, Result, bail}; pub(crate) use fabro_config::user::*; use fabro_types::settings::cli::CliTargetSettings; use fabro_types::settings::{CliSettings, SettingsLayer}; @@ -208,9 +208,12 @@ pub(crate) fn build_server_client_builder( let key_path = fabro_config::expand_tilde(&tls.key); let ca_path = fabro_config::expand_tilde(&tls.ca); - let cert_pem = std::fs::read(&cert_path)?; - let key_pem = std::fs::read(&key_path)?; - let ca_pem = std::fs::read(&ca_path)?; + let cert_pem = std::fs::read(&cert_path) + .with_context(|| format!("reading TLS client certificate {}", cert_path.display()))?; + let key_pem = std::fs::read(&key_path) + .with_context(|| format!("reading TLS client key {}", key_path.display()))?; + let ca_pem = std::fs::read(&ca_path) + .with_context(|| format!("reading TLS CA certificate {}", ca_path.display()))?; let mut identity_pem = cert_pem; identity_pem.push(b'\n'); diff --git a/lib/crates/fabro-devcontainer/src/compose.rs b/lib/crates/fabro-devcontainer/src/compose.rs index 6a29213a4..997d65efb 100644 --- a/lib/crates/fabro-devcontainer/src/compose.rs +++ b/lib/crates/fabro-devcontainer/src/compose.rs @@ -23,11 +23,15 @@ pub(crate) fn parse_compose( compose_path: &Path, service_name: &str, ) -> Result { - let contents = std::fs::read_to_string(compose_path) - .map_err(|e| format!("failed to read compose file: {e}"))?; + let contents = std::fs::read_to_string(compose_path).map_err(|e| { + format!( + "failed to read compose file {}: {e}", + compose_path.display() + ) + })?; - let doc: serde_yaml::Value = - serde_yaml::from_str(&contents).map_err(|e| format!("failed to parse YAML: {e}"))?; + let doc: serde_yaml::Value = serde_yaml::from_str(&contents) + .map_err(|e| format!("failed to parse YAML {}: {e}", compose_path.display()))?; let service = doc .get("services") diff --git a/lib/crates/fabro-devcontainer/src/features.rs b/lib/crates/fabro-devcontainer/src/features.rs index bd88e1ed7..8599e7cb6 100644 --- a/lib/crates/fabro-devcontainer/src/features.rs +++ b/lib/crates/fabro-devcontainer/src/features.rs @@ -564,9 +564,12 @@ pub(crate) async fn resolve_features( .as_nanos() ); let tmp_dir = std::env::temp_dir().join(unique_id); - fs::create_dir_all(&tmp_dir) - .await - .map_err(|e| DevcontainerError::Feature(format!("failed to create temp dir: {e}")))?; + fs::create_dir_all(&tmp_dir).await.map_err(|e| { + DevcontainerError::Feature(format!( + "failed to create temp dir {}: {e}", + tmp_dir.display() + )) + })?; // Collect feature IDs in a stable order let mut feature_ids: Vec = features.keys().cloned().collect(); diff --git a/lib/crates/fabro-interview/src/recording.rs b/lib/crates/fabro-interview/src/recording.rs index 8c7ef8f2b..9d803916a 100644 --- a/lib/crates/fabro-interview/src/recording.rs +++ b/lib/crates/fabro-interview/src/recording.rs @@ -53,7 +53,12 @@ impl RecordingInterviewer { /// Returns an error if serialization or file writing fails. pub fn save_to_file(&self, path: &Path) -> std::io::Result<()> { let json = self.to_json()?; - std::fs::write(path, json)?; + std::fs::write(path, json).map_err(|err| { + std::io::Error::new( + err.kind(), + format!("write interview recording {}: {err}", path.display()), + ) + })?; Ok(()) } @@ -62,7 +67,12 @@ impl RecordingInterviewer { /// # Errors /// Returns an error if file reading or deserialization fails. pub fn load_from_file(path: &Path) -> std::io::Result> { - let json = std::fs::read_to_string(path)?; + let json = std::fs::read_to_string(path).map_err(|err| { + std::io::Error::new( + err.kind(), + format!("read interview recording {}: {err}", path.display()), + ) + })?; Self::from_json(&json) } } diff --git a/lib/crates/fabro-llm/src/providers/common.rs b/lib/crates/fabro-llm/src/providers/common.rs index 2a227476e..6a3cae449 100644 --- a/lib/crates/fabro-llm/src/providers/common.rs +++ b/lib/crates/fabro-llm/src/providers/common.rs @@ -100,7 +100,9 @@ pub fn load_file_as_base64(path: &str) -> Result<(String, String), std::io::Erro format!("{home}/{rest}") }, ); - let data = std::fs::read(&expanded)?; + let data = std::fs::read(&expanded).map_err(|err| { + std::io::Error::new(err.kind(), format!("read attachment {expanded}: {err}")) + })?; let mime = mime_from_extension(&expanded).to_string(); let b64 = BASE64_STANDARD.encode(&data); Ok((b64, mime)) diff --git a/lib/crates/fabro-server/src/serve.rs b/lib/crates/fabro-server/src/serve.rs index 728b22e32..6f87f03e1 100644 --- a/lib/crates/fabro-server/src/serve.rs +++ b/lib/crates/fabro-server/src/serve.rs @@ -157,7 +157,8 @@ fn build_local_object_store_with_preference( return Ok(Arc::new(InMemory::new())); } - std::fs::create_dir_all(store_path)?; + std::fs::create_dir_all(store_path) + .with_context(|| format!("creating object store directory {}", store_path.display()))?; Ok(Arc::new(LocalFileSystem::new_with_prefix(store_path)?)) } @@ -318,7 +319,8 @@ where let bind_request = resolve_bind_request_from_settings(&effective_settings, args.bind.as_deref())?; let shared_settings = Arc::new(RwLock::new(effective_settings)); - std::fs::create_dir_all(&data_dir)?; + std::fs::create_dir_all(&data_dir) + .with_context(|| format!("creating data directory {}", data_dir.display()))?; let (auth_mode, max_concurrent_runs) = { let auth_mode = resolve_auth_mode_with_lookup(&resolved_server_settings, |name| { server_secrets.get(name) @@ -499,7 +501,9 @@ where #[cfg(debug_assertions)] let mut watch_web_child = if watch_web { - let web_dir = std::env::current_dir()?.join("apps/fabro-web"); + let web_dir = std::env::current_dir() + .context("reading current directory for --watch-web")? + .join("apps/fabro-web"); info!(dir = %web_dir.display(), "Starting bun run dev (--watch-web)"); #[expect( clippy::disallowed_methods, @@ -508,7 +512,8 @@ where let child = std::process::Command::new("bun") .args(["run", "dev"]) .current_dir(&web_dir) - .spawn()?; + .spawn() + .with_context(|| format!("spawning `bun run dev` in {}", web_dir.display()))?; Some(child) } else { None @@ -577,10 +582,12 @@ async fn bind_listener(requested: &BindRequest) -> anyhow::Result { if path.exists() { - std::fs::remove_file(path)?; + std::fs::remove_file(path) + .with_context(|| format!("removing stale unix socket {}", path.display()))?; } - let listener = UnixListener::bind(path)?; + let listener = UnixListener::bind(path) + .with_context(|| format!("binding unix socket {}", path.display()))?; Ok(BoundServerListener { listener: BoundListener::Unix(listener), bind: Bind::Unix(path.clone()), diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index 307977817..16b84edc5 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -6,6 +6,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, LazyLock, Mutex, RwLock}; use std::time::{Duration, Instant}; +use anyhow::Context as _; use axum::body::Body; #[cfg(test)] use axum::body::to_bytes; @@ -3363,8 +3364,8 @@ fn worker_command( mode: RunExecutionMode, run_dir: &std::path::Path, ) -> anyhow::Result { - let exe = - std::env::var_os("CARGO_BIN_EXE_fabro").map_or(std::env::current_exe()?, PathBuf::from); + let current_exe = std::env::current_exe().context("reading current executable path")?; + let exe = std::env::var_os("CARGO_BIN_EXE_fabro").map_or(current_exe, PathBuf::from); let storage_dir = state.server_storage_dir(); let server_target = current_server_target(&storage_dir)?; let artifact_upload_token = state @@ -4257,7 +4258,7 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { )); let mut child = match worker_command(state.as_ref(), run_id, execution_mode, &run_dir) - .and_then(|mut cmd| cmd.spawn().map_err(anyhow::Error::from)) + .and_then(|mut cmd| cmd.spawn().context("spawning run worker process")) { Ok(child) => child, Err(err) => { diff --git a/lib/crates/fabro-telemetry/src/panic.rs b/lib/crates/fabro-telemetry/src/panic.rs index e6f575411..e2fa7ac08 100644 --- a/lib/crates/fabro-telemetry/src/panic.rs +++ b/lib/crates/fabro-telemetry/src/panic.rs @@ -1,6 +1,7 @@ use std::panic::PanicHookInfo; use std::path::Path; +use anyhow::Context as _; use sentry::integrations::backtrace; use sentry::protocol::{Context, Event, Exception, Mechanism, OsContext, Values}; @@ -116,7 +117,8 @@ fn spawn_panic_sender(event: &Event<'static>) { pub fn capture(path: &Path) -> anyhow::Result<()> { let dsn = SENTRY_DSN.ok_or_else(|| anyhow::anyhow!("SENTRY_DSN not set at compile time"))?; - let json = std::fs::read(path)?; + let json = + std::fs::read(path).with_context(|| format!("read panic payload {}", path.display()))?; let event: Event<'static> = serde_json::from_slice(&json)?; let guard = sentry::init((dsn, sentry::ClientOptions::default())); diff --git a/lib/crates/fabro-telemetry/src/sender.rs b/lib/crates/fabro-telemetry/src/sender.rs index b4161a3cb..0c2b531a5 100644 --- a/lib/crates/fabro-telemetry/src/sender.rs +++ b/lib/crates/fabro-telemetry/src/sender.rs @@ -1,5 +1,6 @@ use std::path::Path; +use anyhow::Context; use base64::Engine; use base64::engine::general_purpose::STANDARD; use uuid::Uuid; @@ -122,7 +123,8 @@ pub async fn upload(path: &Path) -> anyhow::Result<()> { let write_key = SEGMENT_WRITE_KEY .ok_or_else(|| anyhow::anyhow!("SEGMENT_WRITE_KEY not set at compile time"))?; - let content = std::fs::read_to_string(path)?; + let content = std::fs::read_to_string(path) + .with_context(|| format!("read telemetry batch {}", path.display()))?; let Some(payload) = build_segment_batch(&content) else { return Ok(()); }; diff --git a/lib/crates/fabro-util/src/dev_token.rs b/lib/crates/fabro-util/src/dev_token.rs index 6013d2331..238a86f59 100644 --- a/lib/crates/fabro-util/src/dev_token.rs +++ b/lib/crates/fabro-util/src/dev_token.rs @@ -2,7 +2,7 @@ use std::fs; use std::io::Write as _; use std::path::Path; -use anyhow::{Result, anyhow}; +use anyhow::{Context, Result, anyhow}; use rand::TryRngCore; use rand::rngs::OsRng; @@ -52,7 +52,10 @@ pub fn load_or_create_dev_token(path: &Path) -> Result { return Err(anyhow!("invalid dev token format in {}", path.display())); } Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} - Err(err) => return Err(err.into()), + Err(err) => { + return Err(anyhow::Error::from(err)) + .with_context(|| format!("read dev token {}", path.display())); + } } let token = generate_dev_token(); @@ -70,7 +73,8 @@ pub fn write_dev_token(path: &Path, token: &str) -> Result<()> { fn atomic_write_private(path: &Path, contents: &str) -> Result<()> { if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; + fs::create_dir_all(parent) + .with_context(|| format!("create directory {}", parent.display()))?; } let temp_path = path.with_file_name(format!( @@ -81,7 +85,8 @@ fn atomic_write_private(path: &Path, contents: &str) -> Result<()> { rand::random::() )); write_private_file(&temp_path, contents)?; - fs::rename(&temp_path, path)?; + fs::rename(&temp_path, path) + .with_context(|| format!("rename {} to {}", temp_path.display(), path.display()))?; Ok(()) } @@ -96,13 +101,17 @@ fn write_private_file(path: &Path, contents: &str) -> Result<()> { .create(true) .truncate(true) .mode(0o600) - .open(path)? + .open(path) + .with_context(|| format!("open {} for writing", path.display()))? }; #[cfg(not(unix))] - let mut file = std::fs::File::create(path)?; + let mut file = + std::fs::File::create(path).with_context(|| format!("create {}", path.display()))?; - file.write_all(contents.as_bytes())?; - file.sync_all()?; + file.write_all(contents.as_bytes()) + .with_context(|| format!("write {}", path.display()))?; + file.sync_all() + .with_context(|| format!("sync {}", path.display()))?; Ok(()) } diff --git a/lib/crates/fabro-vault/src/lib.rs b/lib/crates/fabro-vault/src/lib.rs index 676249333..57e777581 100644 --- a/lib/crates/fabro-vault/src/lib.rs +++ b/lib/crates/fabro-vault/src/lib.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use std::fmt; use std::path::{Component, Path, PathBuf}; +use std::{fmt, io}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case")] @@ -77,7 +77,7 @@ impl Vault { let entries = match std::fs::read_to_string(&path) { Ok(contents) => serde_json::from_str(&contents)?, Err(err) if err.kind() == std::io::ErrorKind::NotFound => HashMap::new(), - Err(err) => return Err(err.into()), + Err(err) => return Err(io_context("read vault", &path, &err).into()), }; Ok(Self { path, entries }) @@ -231,7 +231,8 @@ impl Vault { .path .parent() .map_or_else(|| PathBuf::from("."), Path::to_path_buf); - std::fs::create_dir_all(&parent)?; + std::fs::create_dir_all(&parent) + .map_err(|err| io_context("create vault directory", &parent, &err))?; let file_name = self .path @@ -240,18 +241,32 @@ impl Vault { .unwrap_or("secrets.json"); let tmp_path = parent.join(format!(".{file_name}.tmp-{}", ulid::Ulid::new())); let json = serde_json::to_vec_pretty(&self.entries)?; - std::fs::write(&tmp_path, json)?; + std::fs::write(&tmp_path, json) + .map_err(|err| io_context("write vault temp file", &tmp_path, &err))?; set_private_permissions(&tmp_path)?; - std::fs::rename(&tmp_path, &self.path)?; + std::fs::rename(&tmp_path, &self.path).map_err(|err| { + io_context( + &format!("rename vault temp file to {}", self.path.display()), + &tmp_path, + &err, + ) + })?; Ok(()) } } +/// Wrap an `io::Error` with a human-readable verb and path so downstream +/// reporting shows which operation failed on which file. +fn io_context(op: &str, path: &Path, source: &io::Error) -> io::Error { + io::Error::new(source.kind(), format!("{op} {}: {source}", path.display())) +} + #[cfg(unix)] fn set_private_permissions(path: &Path) -> Result<(), Error> { use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?; + std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)) + .map_err(|err| io_context("set permissions on", path, &err))?; Ok(()) } diff --git a/lib/crates/fabro-workflow/src/artifact.rs b/lib/crates/fabro-workflow/src/artifact.rs index 3347c717e..b9757ca94 100644 --- a/lib/crates/fabro-workflow/src/artifact.rs +++ b/lib/crates/fabro-workflow/src/artifact.rs @@ -282,9 +282,16 @@ async fn materialize_blob_ref( let path = local_materialized_blob_path(run_dir, blob_id); if !path.exists() { if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; + std::fs::create_dir_all(parent).map_err(|err| { + Error::Io(format!( + "creating artifact blob directory {}: {err}", + parent.display() + )) + })?; } - std::fs::write(&path, &bytes)?; + std::fs::write(&path, &bytes).map_err(|err| { + Error::Io(format!("writing artifact blob {}: {err}", path.display())) + })?; } return Ok(format!("{ARTIFACT_POINTER_PREFIX}{}", path.display())); } diff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs index ba6a91751..2f4b29c5a 100644 --- a/lib/crates/fabro-workflow/src/operations/start.rs +++ b/lib/crates/fabro-workflow/src/operations/start.rs @@ -107,7 +107,12 @@ pub struct Started { /// Start a fresh workflow run. Errors if a checkpoint already exists (use /// `resume()` instead). pub async fn start(run_dir: &Path, services: StartServices) -> Result { - std::fs::create_dir_all(run_dir).map_err(|err| Error::Io(err.to_string()))?; + std::fs::create_dir_all(run_dir).map_err(|err| { + Error::Io(format!( + "creating run directory {}: {err}", + run_dir.display() + )) + })?; let state = services .run_store .state() diff --git a/lib/crates/fabro-workflow/src/pipeline/persist.rs b/lib/crates/fabro-workflow/src/pipeline/persist.rs index 523abf0b0..c1e935b49 100644 --- a/lib/crates/fabro-workflow/src/pipeline/persist.rs +++ b/lib/crates/fabro-workflow/src/pipeline/persist.rs @@ -13,7 +13,12 @@ pub(crate) fn persist( let (graph, source, diagnostics) = validated.into_parts(); options.run_record.graph = graph.clone(); - std::fs::create_dir_all(&options.run_dir)?; + std::fs::create_dir_all(&options.run_dir).map_err(|err| { + Error::Io(format!( + "creating run directory {}: {err}", + options.run_dir.display() + )) + })?; Ok(Persisted::new( graph, diff --git a/lib/crates/fabro-workflow/src/run_lookup.rs b/lib/crates/fabro-workflow/src/run_lookup.rs index 24036067a..56b3c4ff6 100644 --- a/lib/crates/fabro-workflow/src/run_lookup.rs +++ b/lib/crates/fabro-workflow/src/run_lookup.rs @@ -148,7 +148,10 @@ fn scan_orphan_runs(base: &Path) -> Result> { let entries = match std::fs::read_dir(base) { Ok(entries) => entries, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()), - Err(err) => return Err(err.into()), + Err(err) => { + return Err(anyhow::Error::new(err) + .context(format!("reading orphan runs directory {}", base.display()))); + } }; let mut runs = Vec::new();