gitbutlerapp/gitbutler · error
BUG: a signal has caused the tempfile to be removed, but we
Error message
BUG: a signal has caused the tempfile to be removed, but we didn't install a handler
What it means
`gix::tempfile::Handle::persist` returns `Ok(None)` when the tempfile no longer exists - normally because a signal handler removed it. but-utils did not install a signal handler for this handle, so this state means a signal (or another gix component's signal cleanup in the same process) destroyed the tempfile between write and rename (crates/but-utils/src/lib.rs:167).
Source
Thrown at crates/but-utils/src/lib.rs:167
contents: impl AsRef<[u8]>,
) -> std::io::Result<()> {
let mut temp_file = gix::tempfile::new(
file_path.as_ref().parent().unwrap(),
ContainingDirectory::CreateAllRaceProof(Retries::default()),
AutoRemove::Tempfile,
)?;
temp_file.write_all(contents.as_ref())?;
persist_tempfile(temp_file, file_path)
}
fn persist_tempfile(
tempfile: gix::tempfile::Handle<gix::tempfile::handle::Writable>,
to_path: impl AsRef<Path>,
) -> std::io::Result<()> {
match tempfile.persist(to_path) {
Ok(Some(_opened_file)) => Ok(()),
Ok(None) => {
unreachable!(
"BUG: a signal has caused the tempfile to be removed, but we didn't install a handler"
)
}
Err(err) => Err(err.error),
}
}
/// Reads and parses the state file.
///
/// If the file does not exist, it will be created.
pub fn read_toml_file_or_default<T: DeserializeOwned + Default>(path: &Path) -> Result<T> {
let mut file = match File::open(path) {
Ok(f) => f,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(T::default()),
Err(err) => return Err(err.into()),
};
let mut contents = String::new();
file.read_to_string(&mut contents)?;View on GitHub (pinned to caf1f223d3)
Solutions
- Retry the whole write operation: recreate the tempfile, rewrite contents, persist again - the destination file was never touched, so a retry is safe
- Ensure only one component in the process installs gix signal handlers, so stray cleanup does not drop handles it does not own
- If the signal was the user quitting, propagate the termination instead of looping
Example fix
// before
fn persist_tempfile(tempfile: gix::tempfile::Handle<gix::tempfile::handle::Writable>, to_path: impl AsRef<Path>) -> std::io::Result<()> {
match tempfile.persist(to_path) {
Ok(Some(_)) => Ok(()),
Ok(None) => unreachable!("BUG: a signal has caused the tempfile to be removed..."),
Err(err) => Err(err.error),
}
}
// after - signal removal is transient; caller recreates and retries
match tempfile.persist(to_path) {
Ok(Some(_)) => Ok(()),
Ok(None) => Err(std::io::Error::new(std::io::ErrorKind::Interrupted, "tempfile removed by signal before persist")),
Err(err) => Err(err.error),
}
// caller: on ErrorKind::Interrupted, rebuild the tempfile and write again Defensive patterns
Strategy: retry
Validate before calling
// Nothing to validate pre-call; the state is created by a signal between write and persist. // Reduce the window: write fast, and avoid running state writes during shutdown handlers.
Try / catch
// Treat Ok(None)/Interrupted as transient and redo the whole write
fn write_atomic_retry(path: &Path, contents: &[u8]) -> std::io::Result<()> {
for _ in 0..3 {
match persist_tempfile(make_and_write_tempfile(contents)?, path) {
Ok(()) => return Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
Err(std::io::Error::new(std::io::ErrorKind::Interrupted, "tempfile kept being removed by signals"))
} Prevention
- Retry the full create-write-persist sequence on Interrupted; the destination was never modified
- Let exactly one component in the process own gix signal-handler installation
- Defer graceful-shutdown signal handling until in-flight state writes complete
When it happens
Trigger: The process receives SIGINT/SIGTERM precisely while an atomic state-file write is in flight (tempfile created and written, persist not yet done); or an embedded gix layer registered signal cleanup that closed this handle.
Common situations: Ctrl-C / service stop during a config or state write; a long-running host process mixing gix components with differing signal-handling setups; container teardown signals.
Related errors
- Failed to parse diff header
- we never return these as the status iteration is configured
- BUG: we use 'matching' so there are no directories
- disabled
- BUG: this must have been deactivated
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/648d4f0abdbabe79.
Report an issue: GitHub.