fix(core): never write temp files in target directory (fixes t.TempDir cleanup) (#1777)

AtomicWriteFile now always writes the initial temp file to the system
temp directory (os.TempDir()), avoiding orphan .tmp-* files in the
target directory when background goroutines (e.g. session manager's
saveLocked) call it concurrently during tests.

If the atomic rename fails (EXDEV on cross-device), we fall back to
a direct os.WriteFile instead of creating another temp file in the
target directory, ensuring no orphan temp files are left behind.
This commit is contained in:
LING Tong authored and GitHub committed 2026-09-21 11:50:44 +08:00
1 parent 7e03efa707
commit a5c93d9ce1
1 file changed
+28 -17
+28 -17
View File
@@ -5,12 +5,26 @@ import (
"path/filepath"
)
// AtomicWriteFile writes data to a file atomically by first writing to a
// temporary file in the same directory, syncing, then renaming over the target.
// This prevents data loss / corruption on crash.
// AtomicWriteFile writes data to a file atomically when possible by first
// writing to a temporary file in the system temp directory, syncing, then
// renaming over the target. On the same filesystem the rename is fully
// atomic.
//
// When os.TempDir() and the target directory are on different filesystems
// the rename will fail with EXDEV; in that case we fall back to writing
// directly to the target path. We never create temp files in the target
// directory so that callers which write from background goroutines (e.g.
// the session manager's saveLocked) never leave orphan temp files behind —
// this avoids spurious "t.TempDir() cleanup: directory not empty" failures
// during parallel tests.
func AtomicWriteFile(path string, data []byte, perm os.FileMode) error {
dir := filepath.Dir(path)
tmp, err := os.CreateTemp(dir, ".tmp-*")
dstDir := filepath.Dir(path)
if err := os.MkdirAll(dstDir, 0o755); err != nil {
return err
}
// Write to system temp dir for atomic rename.
tmp, err := os.CreateTemp("", "cc-atomic-*")
if err != nil {
return err
}
@@ -34,17 +48,14 @@ func AtomicWriteFile(path string, data []byte, perm os.FileMode) error {
os.Remove(tmpPath)
return err
}
if err := os.Rename(tmpPath, path); err != nil {
// Rename can fail when the destination is a directory, the
// destination's filesystem differs from the temp dir's (rare given
// CreateTemp uses the same dir, but possible with bind mounts), or
// the destination is locked by another process on Windows. In any
// of those cases the temp file is now an orphaned `.tmp-*` we
// created — clean it up so repeated failures don't litter the
// directory and confuse later directory scans (e.g. cron / session
// stores that walk their parent dir).
os.Remove(tmpPath)
return err
// Try atomic rename (works when temp and target are on the same fs).
if err := os.Rename(tmpPath, path); err == nil {
return nil
}
return nil
// Rename failed (likely EXDEV on different mount points).
// Fall back to direct write — safe, no orphan temp files.
_ = os.Remove(tmpPath)
return os.WriteFile(path, data, perm)
}