From 1b80844b180213f55a81cbac57dbf679079830e0 Mon Sep 17 00:00:00 2001 From: adamscarmccoy-boop Date: Thu, 10 Sep 2026 02:29:56 -0400 Subject: [PATCH 1/2] fix(runner): use os.CreateTemp and mutex serialization for atomic resume on Windows --- runner/atomic_resume.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 runner/atomic_resume.go diff --git a/runner/atomic_resume.go b/runner/atomic_resume.go new file mode 100644 index 00000000..89da5dc6 --- /dev/null +++ b/runner/atomic_resume.go @@ -0,0 +1,36 @@ +package runner + +import ( + "os" + "path/filepath" + "sync" +) + +var atomicResumeMutex sync.Mutex + +func SaveAtomic(targetPath string, data []byte) error { + atomicResumeMutex.Lock() + defer atomicResumeMutex.Unlock() + + dir := filepath.Dir(targetPath) + tmpFile, err := os.CreateTemp(dir, "httpx-resume-*.tmp") + if err != nil { + return err + } + tmpName := tmpFile.Name() + defer os.Remove(tmpName) + + if _, err := tmpFile.Write(data); err != nil { + tmpFile.Close() + return err + } + if err := tmpFile.Sync(); err != nil { + tmpFile.Close() + return err + } + if err := tmpFile.Close(); err != nil { + return err + } + + return os.Rename(tmpName, targetPath) +} From f7076127731ad34cc66994e9d1531394dd82fc25 Mon Sep 17 00:00:00 2001 From: adamscarmccoy-boop Date: Fri, 18 Sep 2026 15:01:32 -0400 Subject: [PATCH 2/2] fix(runner): route SaveResumeConfig through SaveAtomic and add POSIX syncDir --- runner/atomic_resume.go | 37 ++++++++++++++++++++++++++++++++++++- runner/runner.go | 5 +---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/runner/atomic_resume.go b/runner/atomic_resume.go index 89da5dc6..172c4e39 100644 --- a/runner/atomic_resume.go +++ b/runner/atomic_resume.go @@ -3,11 +3,27 @@ package runner import ( "os" "path/filepath" + "runtime" "sync" + + "gopkg.in/yaml.v3" ) var atomicResumeMutex sync.Mutex +// syncDir attempts to fsync the directory containing targetPath on POSIX platforms. +func syncDir(dirPath string) error { + if runtime.GOOS == "windows" { + return nil + } + d, err := os.Open(dirPath) + if err != nil { + return err + } + defer d.Close() + return d.Sync() +} + func SaveAtomic(targetPath string, data []byte) error { atomicResumeMutex.Lock() defer atomicResumeMutex.Unlock() @@ -32,5 +48,24 @@ func SaveAtomic(targetPath string, data []byte) error { return err } - return os.Rename(tmpName, targetPath) + if err := os.Rename(tmpName, targetPath); err != nil { + return err + } + + return syncDir(dir) +} + +// SaveResumeConfigAtomic serializes the resume config and writes it using SaveAtomic. +func (r *Runner) SaveResumeConfigAtomic() error { + if r.options == nil || r.options.resumeCfg == nil { + return nil + } + var resumeCfg ResumeCfg + resumeCfg.Index = r.options.resumeCfg.currentIndex + resumeCfg.ResumeFrom = r.options.resumeCfg.current + data, err := yaml.Marshal(resumeCfg) + if err != nil { + return err + } + return SaveAtomic(DefaultResumeFile, data) } diff --git a/runner/runner.go b/runner/runner.go index ea0cac3f..ded51325 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -2939,10 +2939,7 @@ func extractPotentialFavIconsURLs(resp []byte) (candidates []string, baseHref st // SaveResumeConfig to file func (r *Runner) SaveResumeConfig() error { - var resumeCfg ResumeCfg - resumeCfg.Index = r.options.resumeCfg.currentIndex - resumeCfg.ResumeFrom = r.options.resumeCfg.current - return goconfig.Save(resumeCfg, DefaultResumeFile) + return r.SaveResumeConfigAtomic() } // JSON the result