diff --git a/internal/upgrade/syncdir_unix.go b/internal/upgrade/syncdir_unix.go new file mode 100644 index 0000000000000000000000000000000000000000..20e55938e63a07a71ba9b258250a2bc47b1aea82 --- /dev/null +++ b/internal/upgrade/syncdir_unix.go @@ -0,0 +1,17 @@ +// Copyright (C) 2024 OpenCloudOS +// License: GPL-3.0-or-later + +//go:build !windows + +package upgrade + +import "os" + +func syncDirectory(path string) error { + dir, err := os.Open(path) + if err != nil { + return err + } + defer dir.Close() + return dir.Sync() +} diff --git a/internal/upgrade/syncdir_windows.go b/internal/upgrade/syncdir_windows.go new file mode 100644 index 0000000000000000000000000000000000000000..aa5a7b1f35ffdd49818b91dcff23651e711e53ad --- /dev/null +++ b/internal/upgrade/syncdir_windows.go @@ -0,0 +1,9 @@ +// Copyright (C) 2024 OpenCloudOS +// License: GPL-3.0-or-later + +//go:build windows + +package upgrade + +// Windows 无法通过 os.File.Sync 可移植地同步目录句柄。 +func syncDirectory(string) error { return nil } diff --git a/internal/upgrade/upgrader.go b/internal/upgrade/upgrader.go index d818fe686934c4d9b38b8d28673319aa58daa3f4..e178fbf01bf75eb58e5e2f6edf2b202f94f2a36d 100644 --- a/internal/upgrade/upgrader.go +++ b/internal/upgrade/upgrader.go @@ -209,12 +209,25 @@ func (u *Upgrader) Execute(ctx context.Context, taskID string, action UpgradeAct defer cancel() } dlErr := u.client.DownloadChunks(dlCtx, action.MD5, action.ArtifactID, f) - // 在校验/重命名前关闭文件,使 Windows 风格的语义也能安全运行。 - _ = f.Close() if dlErr != nil { + _ = f.Close() _ = os.Remove(tmpPath) return u.reportErr(ctx, taskID, ReasonDownloadFailed, dlErr.Error()) } + if err := f.Chmod(0o755); err != nil { + _ = f.Close() + _ = os.Remove(tmpPath) + return u.reportErr(ctx, taskID, ReasonReplaceFailed, "chmod: "+err.Error()) + } + if err := f.Sync(); err != nil { + _ = f.Close() + _ = os.Remove(tmpPath) + return u.reportErr(ctx, taskID, ReasonReplaceFailed, "sync temp: "+err.Error()) + } + if err := f.Close(); err != nil { + _ = os.Remove(tmpPath) + return u.reportErr(ctx, taskID, ReasonReplaceFailed, "close temp: "+err.Error()) + } // ---------------- 3. Verify ---------------- got, err := fileMD5(tmpPath) @@ -229,14 +242,13 @@ func (u *Upgrader) Execute(ctx context.Context, taskID string, action UpgradeAct } // ---------------- 4. Replace ---------------- - if err := os.Chmod(tmpPath, 0o755); err != nil { - _ = os.Remove(tmpPath) - return u.reportErr(ctx, taskID, ReasonReplaceFailed, "chmod: "+err.Error()) - } if err := os.Rename(tmpPath, execPath); err != nil { _ = os.Remove(tmpPath) return u.reportErr(ctx, taskID, ReasonReplaceFailed, "rename: "+err.Error()) } + if err := syncDirectory(filepath.Dir(execPath)); err != nil { + return u.reportErr(ctx, taskID, ReasonReplaceFailed, "sync parent directory: "+err.Error()) + } // ---------------- 5. Report + Exit ---------------- now := time.Now().Unix() diff --git a/internal/upgrade/upgrader_test.go b/internal/upgrade/upgrader_test.go index bcb4f0e03421c07ecef7aae6134f7419c1490e91..f3017607d36ceda680fa2245730d9f20b1981448 100644 --- a/internal/upgrade/upgrader_test.go +++ b/internal/upgrade/upgrader_test.go @@ -346,10 +346,10 @@ func (g *gatedClient) DownloadChunks(ctx context.Context, _ string, _ string, w return err } -// waitForExit 轮询 exit 计数器最长 500ms。 +// waitForExit 给生产延迟留出一倍调度余量,避免回调与断言同在边界时误报。 func waitForExit(t *testing.T, c *atomic.Int32, want int32) { t.Helper() - deadline := time.Now().Add(500 * time.Millisecond) + deadline := time.Now().Add(2 * exitGracePeriod) for time.Now().Before(deadline) { if c.Load() >= want { return