mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-06 08:18:58 +00:00
fix: wrap propagated io errors with anyhow context across prod code
A recent CI flake surfaced as bare "error: No such file or directory
(os error 2)" with no chain, because the failing operation lived behind
a raw `?` on a `std::fs::` / `File::create` / `Command::spawn` call. The
error had no verb, no path, no hint at which step in server startup
broke. Retry loops were explicitly rejected -- the goal is to diagnose
the next occurrence, not mask it.
Wraps 50+ such sites across fabro-cli, fabro-server, fabro-workflow,
fabro-util, fabro-vault, fabro-telemetry, fabro-interview, fabro-llm,
and fabro-devcontainer with `.with_context(|| format!("<verb> {path}"))`
so anyhow's error chain carries both the operation and the path when
an io error escapes.
Where the enclosing function returns `io::Result` (fabro-util run_log,
fabro-interview recording, fabro-llm attachment loader), the error is
re-wrapped via `io::Error::new` to keep the signature stable. Where a
crate uses its own thiserror enum, either a new `io_context` helper
was added (fabro-vault) or the path was folded into the existing
`Error::Io(String)` message (fabro-workflow).
No retry loops. No behavior changes. Skipped sites documented:
`.ok()`-swallowed, `match ErrorKind::NotFound`, `let _ = ...`, typed
error variants that already carry the path, and test modules.
Verified: cargo build --workspace, cargo +nightly clippy --workspace
--all-targets -- -D warnings, cargo nextest run --workspace (3991/3991
pass).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2d8d72717d
commit
7bc3fe0dbc
22 changed files with 220 additions and 83 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
|
|
|
|||
|
|
@ -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<Str
|
|||
.and_then(|entries| entries.get("FABRO_DEV_TOKEN").cloned())
|
||||
.filter(|token| dev_token::validate_dev_token_format(token))
|
||||
{
|
||||
dev_token::write_dev_token(&home.dev_token_path(), &token)?;
|
||||
dev_token::write_dev_token(&storage.server_state().dev_token_path(), &token)?;
|
||||
dev_token::write_dev_token(&home.dev_token_path(), &token)
|
||||
.with_context(|| format!("writing dev token to {}", home.dev_token_path().display()))?;
|
||||
dev_token::write_dev_token(&storage.server_state().dev_token_path(), &token).with_context(
|
||||
|| {
|
||||
format!(
|
||||
"writing dev token to {}",
|
||||
storage.server_state().dev_token_path().display()
|
||||
)
|
||||
},
|
||||
)?;
|
||||
return Ok(token);
|
||||
}
|
||||
|
||||
let token = dev_token::load_or_create_dev_token(&home.dev_token_path())?;
|
||||
dev_token::write_dev_token(&storage.server_state().dev_token_path(), &token)?;
|
||||
let token = dev_token::load_or_create_dev_token(&home.dev_token_path()).with_context(|| {
|
||||
format!(
|
||||
"loading or creating dev token at {}",
|
||||
home.dev_token_path().display()
|
||||
)
|
||||
})?;
|
||||
dev_token::write_dev_token(&storage.server_state().dev_token_path(), &token).with_context(
|
||||
|| {
|
||||
format!(
|
||||
"writing dev token to {}",
|
||||
storage.server_state().dev_token_path().display()
|
||||
)
|
||||
},
|
||||
)?;
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
|
|
@ -207,7 +227,8 @@ fn load_or_create_local_session_secret(storage_dir: &Path) -> Result<String> {
|
|||
}
|
||||
|
||||
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<std::fs::File> {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -277,7 +277,9 @@ fn inspect_output_dir(path: &Path) -> Result<OutputDirState> {
|
|||
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())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -23,11 +23,15 @@ pub(crate) fn parse_compose(
|
|||
compose_path: &Path,
|
||||
service_name: &str,
|
||||
) -> Result<ComposeServiceSpec, String> {
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -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<String> = features.keys().cloned().collect();
|
||||
|
|
|
|||
|
|
@ -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<Vec<(Question, Answer)>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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<BoundServerLis
|
|||
match requested {
|
||||
BindRequest::Unix(path) => {
|
||||
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()),
|
||||
|
|
|
|||
|
|
@ -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<Command> {
|
||||
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<AppState>, 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) => {
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
|
|
|
|||
|
|
@ -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(());
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<String> {
|
|||
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::<u64>()
|
||||
));
|
||||
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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Started, Error> {
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -148,7 +148,10 @@ fn scan_orphan_runs(base: &Path) -> Result<Vec<RunInfo>> {
|
|||
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();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue