Skip to content

Commit 058b88e

Browse files
committed
Fix ETXTBSY on Linux during update
On Linux, the kernel enforces ETXTBSY ("Text file busy") and refuses to open a file for writting if that file is being executed. The original code prior to this change used "fs::copy(...)" which opens the destination file for writting and this was being blocked by the ETXTBSY error. This patch fixes the issue by copying the new binary to a temp file in the same directory as the current executable and then uses "fs::rename" to atomically swap the directory entry (it only updates the directory mapping it doesn't open the file's content). This basically mimics what is already implemented for Windows in the update.rs module but adapted for Unix semantics. Fixes #8468 Signed-off-by: Lucas Alvares Gomes <lucasagomes@gmail.com>
1 parent b1eff5f commit 058b88e

File tree

1 file changed

+49
-6
lines changed

1 file changed

+49
-6
lines changed

crates/goose-cli/src/commands/update.rs

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,10 @@ fn find_binary(extract_dir: &Path, binary_name: &str) -> Option<PathBuf> {
428428
/// On Windows we must rename the running exe (Windows allows rename but not
429429
/// delete/overwrite of a locked file) then copy the new file in.
430430
///
431-
/// On Unix we can simply copy over the existing binary.
431+
/// On Unix, we write to a temp file in the same directory to avoid ETXTBSY
432+
/// ("Text file busy") on Linux, which blocks writing to a running executable.
433+
/// Renaming then atomically replaces the directory entry without affecting
434+
/// the file currently in use by the kernel.
432435
fn replace_binary(new_binary: &Path, current_exe: &Path) -> Result<()> {
433436
#[cfg(target_os = "windows")]
434437
{
@@ -462,18 +465,30 @@ fn replace_binary(new_binary: &Path, current_exe: &Path) -> Result<()> {
462465

463466
#[cfg(not(target_os = "windows"))]
464467
{
465-
// On Unix, copy the new binary over the existing one
466-
fs::copy(new_binary, current_exe)
467-
.with_context(|| format!("Failed to copy new binary to {}", current_exe.display()))?;
468+
// Write to a temp file and avoids ETXTBSY ("Text file busy") on Linux
469+
// where the kernel refuses to open write a running executable.
470+
let dest_dir = current_exe
471+
.parent()
472+
.context("Current executable has no parent directory")?;
473+
474+
let tmp_path = dest_dir.join(".old");
475+
476+
fs::copy(new_binary, &tmp_path)
477+
.with_context(|| format!("Failed to write update to {}", tmp_path.display()))?;
468478

469479
// Ensure the binary is executable
470480
#[cfg(unix)]
471481
{
472482
use std::os::unix::fs::PermissionsExt;
473-
let mut perms = fs::metadata(current_exe)?.permissions();
483+
let mut perms = fs::metadata(&tmp_path)?.permissions();
474484
perms.set_mode(0o755);
475-
fs::set_permissions(current_exe, perms)?;
485+
fs::set_permissions(&tmp_path, perms)?;
476486
}
487+
488+
fs::rename(&tmp_path, current_exe).with_context(|| {
489+
let _ = fs::remove_file(&tmp_path);
490+
format!("Failed to replace binary at {}", current_exe.display())
491+
})?;
477492
}
478493

479494
Ok(())
@@ -601,6 +616,34 @@ mod tests {
601616
assert_eq!(content, "new version");
602617
}
603618

619+
// On Unix the replacement must go through a rename so that it avoids
620+
// ETXTBSY on Linux.
621+
#[cfg(not(target_os = "windows"))]
622+
#[test]
623+
fn test_replace_binary_unix_no_leftover_tmp() {
624+
let tmp = tempdir().unwrap();
625+
let new_bin = tmp.path().join("new_goose");
626+
let current = tmp.path().join("goose");
627+
628+
fs::write(&new_bin, b"new version").unwrap();
629+
fs::write(&current, b"old version").unwrap();
630+
631+
replace_binary(&new_bin, &current).unwrap();
632+
633+
assert_eq!(fs::read_to_string(&current).unwrap(), "new version");
634+
635+
// No stray .old temp files should remain.
636+
let leftovers: Vec<_> = fs::read_dir(tmp.path())
637+
.unwrap()
638+
.flatten()
639+
.filter(|e| e.file_name() == ".old")
640+
.collect();
641+
assert!(
642+
leftovers.is_empty(),
643+
"stray temp files left behind: {leftovers:?}"
644+
);
645+
}
646+
604647
#[cfg(target_os = "windows")]
605648
#[test]
606649
fn test_replace_binary_windows_rename_away() {

0 commit comments

Comments
 (0)