astrid-runtime/astrid · error
FUSE service stderr is unavailable
Error message
FUSE service stderr is unavailable
What it means
When spawning the detached FUSE service, the parent requires piped stderr to capture diagnostics; if child.stderr is unexpectedly absent after a successful spawn (Stdio::piped was requested), it kills the child, waits for its status, and fails with this error plus the exit status as context. This prevents an unsupervisable service that could fail silently.
Solutions
- Check the child's exit status in the error context to see why the process terminated.
- Verify the spawn code keeps .stderr(std::process::Stdio::piped()) before spawn().
- Retry the launch; transient spawn failures should succeed on a clean retry.
- Capture the killed child's status for diagnostics before treating it as a hard failure.
Example fix
// before
let mut child = command.spawn()?;
// after
let mut child = command.spawn()?;
let Some(mut stderr) = child.stderr.take() else {
let _ = child.kill().await;
let status = child.wait().await?;
return Err(anyhow::anyhow!("FUSE service stderr is unavailable")
.context(format!("detached FUSE service status: {status:?}")));
}; Defensive patterns
Strategy: try-catch
Validate before calling
// ensure the spawn config pipes stderr before spawning command.stderr(std::process::Stdio::piped()); command.stdout(std::process::Stdio::piped());
Type guard
fn take_stderr(child: &mut tokio::process::Child) -> Result<tokio::process::ChildStderr, anyhow::Error> {
child.stderr.take().ok_or_else(|| anyhow::anyhow!("FUSE service stderr is unavailable"))
} Try / catch
let mut child = command.spawn()?;
let Some(mut stderr) = child.stderr.take() else {
let _ = child.kill().await;
let status = child.wait().await;
return Err(anyhow::anyhow!("FUSE service stderr is unavailable")
.context(format!("detached FUSE service status: {status:?}")));
}; Prevention
- Keep .stderr(Stdio::piped()) on the spawn command; never replace it with null or inherit in launcher forks.
- Include the killed child's exit status in the error context for diagnosability.
- Retry transient spawn failures after cleaning up partial process state.
When it happens
Trigger: command.spawn() succeeds but child.stderr.take() returns None — an invariant violation since stderr was configured as Stdio::piped; observed on some process-spawn edge cases or when spawn code paths are altered.
Common situations: Custom forks of the launcher that drop the stderr pipe; platform quirks in tokio process spawning; race with an already-exited child in unusual runtimes.
Understand the failure class
Background: "open() failed", "failed to open file", "cannot create file" — what a file open error means and how to fix it — this error's family across 42 libraries.
Related errors
- detached FUSE stderr drain failed
- detached FUSE stderr sink handoff timed out
- read detached FUSE stderr during sink handoff
- absent migration source has a digest
- alice
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/1bfd6212051786a5.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-storage-provider-fuse/src/main.rs:597
}
}
async fn launch_service(launch: &ServiceLaunch, control_path: &Path) -> Result<ControlReady> {
use std::os::unix::process::CommandExt as _;
let executable = std::env::current_exe()?;
let mut command = tokio::process::Command::new(executable);
command
.arg(PUBLIC_SERVICE_ARGUMENT)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
command.as_std_mut().process_group(0);
let mut child = command.spawn()?;
let Some(mut stderr) = child.stderr.take() else {
let _ = child.kill().await;
let status = child.wait().await;
return Err(anyhow::anyhow!("FUSE service stderr is unavailable")
.context(format!("detached FUSE service status: {status:?}")));
};
let mut stderr_task = tokio::spawn(async move {
let mut bytes = Vec::with_capacity(MAX_FUSE_STDERR_BYTES + 1);
let mut buffer = [0_u8; 4096];
loop {
let read = stderr.read(&mut buffer).await?;
if read == 0 {
break;
}
let remaining = (MAX_FUSE_STDERR_BYTES + 1).saturating_sub(bytes.len());
bytes.extend_from_slice(&buffer[..read.min(remaining)]);
}
std::io::Result::Ok(bytes)
});
let startup = read_service_startup(&mut child, launch, control_path).await;
match startup {
Ok(ready) => {View on GitHub (pinned to affd8760f4)