diff --git a/runner/atomic_resume.go b/runner/atomic_resume.go new file mode 100644 index 00000000..172c4e39 --- /dev/null +++ b/runner/atomic_resume.go @@ -0,0 +1,71 @@ +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() + + 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 + } + + 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