{"record":{"id":"648d4f0abdbabe79","repo":"gitbutlerapp/gitbutler","slug":"bug-a-signal-has-caused-the-tempfile-to-be-remove","errorCode":null,"errorMessage":"BUG: a signal has caused the tempfile to be removed, but we didn't install a handler","messagePattern":"BUG: a signal has caused the tempfile to be removed, but we didn't install a handler","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-utils/src/lib.rs","lineNumber":167,"sourceCode":"    contents: impl AsRef<[u8]>,\n) -> std::io::Result<()> {\n    let mut temp_file = gix::tempfile::new(\n        file_path.as_ref().parent().unwrap(),\n        ContainingDirectory::CreateAllRaceProof(Retries::default()),\n        AutoRemove::Tempfile,\n    )?;\n    temp_file.write_all(contents.as_ref())?;\n    persist_tempfile(temp_file, file_path)\n}\n\nfn persist_tempfile(\n    tempfile: gix::tempfile::Handle<gix::tempfile::handle::Writable>,\n    to_path: impl AsRef<Path>,\n) -> std::io::Result<()> {\n    match tempfile.persist(to_path) {\n        Ok(Some(_opened_file)) => Ok(()),\n        Ok(None) => {\n            unreachable!(\n                \"BUG: a signal has caused the tempfile to be removed, but we didn't install a handler\"\n            )\n        }\n        Err(err) => Err(err.error),\n    }\n}\n\n/// Reads and parses the state file.\n///\n/// If the file does not exist, it will be created.\npub fn read_toml_file_or_default<T: DeserializeOwned + Default>(path: &Path) -> Result<T> {\n    let mut file = match File::open(path) {\n        Ok(f) => f,\n        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(T::default()),\n        Err(err) => return Err(err.into()),\n    };\n    let mut contents = String::new();\n    file.read_to_string(&mut contents)?;","sourceCodeStart":149,"sourceCodeEnd":185,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-utils/src/lib.rs#L149-L185","documentation":"`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).","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nfn persist_tempfile(tempfile: gix::tempfile::Handle<gix::tempfile::handle::Writable>, to_path: impl AsRef<Path>) -> std::io::Result<()> {\n    match tempfile.persist(to_path) {\n        Ok(Some(_)) => Ok(()),\n        Ok(None) => unreachable!(\"BUG: a signal has caused the tempfile to be removed...\"),\n        Err(err) => Err(err.error),\n    }\n}\n\n// after - signal removal is transient; caller recreates and retries\nmatch tempfile.persist(to_path) {\n    Ok(Some(_)) => Ok(()),\n    Ok(None) => Err(std::io::Error::new(std::io::ErrorKind::Interrupted, \"tempfile removed by signal before persist\")),\n    Err(err) => Err(err.error),\n}\n// caller: on ErrorKind::Interrupted, rebuild the tempfile and write again","handlingStrategy":"retry","validationCode":"// Nothing to validate pre-call; the state is created by a signal between write and persist.\n// Reduce the window: write fast, and avoid running state writes during shutdown handlers.","typeGuard":null,"tryCatchPattern":"// Treat Ok(None)/Interrupted as transient and redo the whole write\nfn write_atomic_retry(path: &Path, contents: &[u8]) -> std::io::Result<()> {\n    for _ in 0..3 {\n        match persist_tempfile(make_and_write_tempfile(contents)?, path) {\n            Ok(()) => return Ok(()),\n            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,\n            Err(e) => return Err(e),\n        }\n    }\n    Err(std::io::Error::new(std::io::ErrorKind::Interrupted, \"tempfile kept being removed by signals\"))\n}","preventionTips":["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"],"tags":["rust","gix","tempfile","signal","atomic-write","interrupted","but-utils"],"backgroundTag":"write-interrupted-by-signal","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}