diff --git a/internal/robustio/robustio.go b/internal/robustio/robustio.go index 0a559fc9b..7a2825c93 100644 --- a/internal/robustio/robustio.go +++ b/internal/robustio/robustio.go @@ -14,7 +14,10 @@ // but substantially reduce their rate of occurrence in practice. package robustio -import "time" +import ( + "os" + "time" +) // Rename is like os.Rename, but on Windows retries errors that may occur if the // file is concurrently read or overwritten. @@ -40,6 +43,13 @@ func RemoveAll(path string) error { return removeAll(path) } +// WriteFile is like os.WriteFile, but on Windows retries errors that may occur +// if the file or its parent directory is briefly inaccessible due to antivirus +// scanning or pending-delete state. +func WriteFile(filename string, data []byte, perm os.FileMode) error { + return writeFile(filename, data, perm) +} + // IsEphemeralError reports whether err is one of the errors that the functions // in this package attempt to mitigate. // diff --git a/internal/robustio/robustio_flaky.go b/internal/robustio/robustio_flaky.go index c56e36ca6..097b5f0f9 100644 --- a/internal/robustio/robustio_flaky.go +++ b/internal/robustio/robustio_flaky.go @@ -89,3 +89,11 @@ func removeAll(path string) error { return err, isEphemeralError(err) }) } + +// writeFile is like os.WriteFile, but retries ephemeral errors. +func writeFile(filename string, data []byte, perm os.FileMode) error { + return retry(func() (err error, mayRetry bool) { + err = os.WriteFile(filename, data, perm) + return err, isEphemeralError(err) + }) +} diff --git a/internal/robustio/robustio_other.go b/internal/robustio/robustio_other.go index da9a46e4f..3c4a71371 100644 --- a/internal/robustio/robustio_other.go +++ b/internal/robustio/robustio_other.go @@ -22,6 +22,10 @@ func removeAll(path string) error { return os.RemoveAll(path) } +func writeFile(filename string, data []byte, perm os.FileMode) error { + return os.WriteFile(filename, data, perm) +} + func isEphemeralError(err error) bool { return false } diff --git a/mod/modcache/fetch.go b/mod/modcache/fetch.go index 43630b8b7..964d3b49c 100644 --- a/mod/modcache/fetch.go +++ b/mod/modcache/fetch.go @@ -147,7 +147,12 @@ func (c *Cache) Fetch(ctx context.Context, mv module.Version) (module.SourceLoc, if err := os.MkdirAll(parentDir, 0777); err != nil { return module.SourceLoc{}, err } - if err := os.WriteFile(partialPath, nil, 0666); err != nil { + // Upstream Go's modfetch uses a plain os.WriteFile here, but we hit + // transient Windows ERROR_ACCESS_DENIED on this path more often, likely + // because each testscript test starts with a cold module cache on an + // antivirus-scanned drive. Retry via robustio to absorb those. + // See https://cuelang.org/issue/3413. + if err := robustio.WriteFile(partialPath, nil, 0666); err != nil { return module.SourceLoc{}, err } if err := modzip.Unzip(dir, mv, zipfile); err != nil {