Skip to content

Commit 507a88c

Browse files
author
Jay Conrod
committed
cmd/go/internal/modfetch: always extract module directories in place
Previously by default, we extracted modules to a temporary directory, then renamed it into place. This failed with ERROR_ACCESS_DENIED on Windows if another process (usually an anti-virus scanner) opened files in the temporary directory. Since Go 1.15, users have been able to set GODEBUG=modcacheunzipinplace=1 to opt into new behavior: we extract modules at their final location, and we create and later delete a .partial file to prevent the directory from being used if we crash. .partial files are recognized by Go 1.14.2 and later. With this change, the new behavior is the only behavior. modcacheunzipinplace is no longer recognized. Fixes #36568 Change-Id: Iff19fca5cd6eaa3597975a69fa05c4cb1b834bd6 Reviewed-on: https://go-review.googlesource.com/c/go/+/258798 Run-TryBot: Jay Conrod <[email protected]> TryBot-Result: Go Bot <[email protected]> Trust: Jay Conrod <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]>
1 parent f4cbf34 commit 507a88c

File tree

4 files changed

+38
-108
lines changed

4 files changed

+38
-108
lines changed

src/cmd/go/internal/modfetch/fetch.go

+31-74
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,9 @@ func download(ctx context.Context, mod module.Version) (dir string, err error) {
6363
ctx, span := trace.StartSpan(ctx, "modfetch.download "+mod.String())
6464
defer span.Done()
6565

66-
// If the directory exists, and no .partial file exists, the module has
67-
// already been completely extracted. .partial files may be created when a
68-
// module zip directory is extracted in place instead of being extracted to a
69-
// temporary directory and renamed.
7066
dir, err = DownloadDir(mod)
7167
if err == nil {
68+
// The directory has already been completely extracted (no .partial file exists).
7269
return dir, nil
7370
} else if dir == "" || !errors.Is(err, os.ErrNotExist) {
7471
return "", err
@@ -88,17 +85,21 @@ func download(ctx context.Context, mod module.Version) (dir string, err error) {
8885
}
8986
defer unlock()
9087

88+
ctx, span = trace.StartSpan(ctx, "unzip "+zipfile)
89+
defer span.Done()
90+
9191
// Check whether the directory was populated while we were waiting on the lock.
9292
_, dirErr := DownloadDir(mod)
9393
if dirErr == nil {
9494
return dir, nil
9595
}
9696
_, dirExists := dirErr.(*DownloadDirPartialError)
9797

98-
// Clean up any remaining temporary directories from previous runs, as well
99-
// as partially extracted diectories created by future versions of cmd/go.
100-
// This is only safe to do because the lock file ensures that their writers
101-
// are no longer active.
98+
// Clean up any remaining temporary directories created by old versions
99+
// (before 1.16), as well as partially extracted directories (indicated by
100+
// DownloadDirPartialError, usually because of a .partial file). This is only
101+
// safe to do because the lock file ensures that their writers are no longer
102+
// active.
102103
parentDir := filepath.Dir(dir)
103104
tmpPrefix := filepath.Base(dir) + ".tmp-"
104105
if old, err := filepath.Glob(filepath.Join(parentDir, tmpPrefix+"*")); err == nil {
@@ -116,88 +117,44 @@ func download(ctx context.Context, mod module.Version) (dir string, err error) {
116117
if err != nil {
117118
return "", err
118119
}
119-
if err := os.Remove(partialPath); err != nil && !os.IsNotExist(err) {
120-
return "", err
121-
}
122120

123-
// Extract the module zip directory.
121+
// Extract the module zip directory at its final location.
124122
//
125-
// By default, we extract to a temporary directory, then atomically rename to
126-
// its final location. We use the existence of the source directory to signal
127-
// that it has been extracted successfully (see DownloadDir). If someone
128-
// deletes the entire directory (e.g., as an attempt to prune out file
129-
// corruption), the module cache will still be left in a recoverable
130-
// state.
123+
// To prevent other processes from reading the directory if we crash,
124+
// create a .partial file before extracting the directory, and delete
125+
// the .partial file afterward (all while holding the lock).
131126
//
132-
// Unfortunately, os.Rename may fail with ERROR_ACCESS_DENIED on Windows if
133-
// another process opens files in the temporary directory. This is partially
134-
// mitigated by using robustio.Rename, which retries os.Rename for a short
135-
// time.
127+
// Before Go 1.16, we extracted to a temporary directory with a random name
128+
// then renamed it into place with os.Rename. On Windows, this failed with
129+
// ERROR_ACCESS_DENIED when another process (usually an anti-virus scanner)
130+
// opened files in the temporary directory.
136131
//
137-
// To avoid this error completely, if unzipInPlace is set, we instead create a
138-
// .partial file (indicating the directory isn't fully extracted), then we
139-
// extract the directory at its final location, then we delete the .partial
140-
// file. This is not the default behavior because older versions of Go may
141-
// simply stat the directory to check whether it exists without looking for a
142-
// .partial file. If multiple versions run concurrently, the older version may
143-
// assume a partially extracted directory is complete.
144-
// TODO(golang.org/issue/36568): when these older versions are no longer
145-
// supported, remove the old default behavior and the unzipInPlace flag.
132+
// Go 1.14.2 and higher respect .partial files. Older versions may use
133+
// partially extracted directories. 'go mod verify' can detect this,
134+
// and 'go clean -modcache' can fix it.
146135
if err := os.MkdirAll(parentDir, 0777); err != nil {
147136
return "", err
148137
}
149-
150-
ctx, span = trace.StartSpan(ctx, "unzip "+zipfile)
151-
if unzipInPlace {
152-
if err := ioutil.WriteFile(partialPath, nil, 0666); err != nil {
153-
return "", err
154-
}
155-
if err := modzip.Unzip(dir, mod, zipfile); err != nil {
156-
fmt.Fprintf(os.Stderr, "-> %s\n", err)
157-
if rmErr := RemoveAll(dir); rmErr == nil {
158-
os.Remove(partialPath)
159-
}
160-
return "", err
161-
}
162-
if err := os.Remove(partialPath); err != nil {
163-
return "", err
164-
}
165-
} else {
166-
tmpDir, err := ioutil.TempDir(parentDir, tmpPrefix)
167-
if err != nil {
168-
return "", err
169-
}
170-
if err := modzip.Unzip(tmpDir, mod, zipfile); err != nil {
171-
fmt.Fprintf(os.Stderr, "-> %s\n", err)
172-
RemoveAll(tmpDir)
173-
return "", err
174-
}
175-
if err := robustio.Rename(tmpDir, dir); err != nil {
176-
RemoveAll(tmpDir)
177-
return "", err
138+
if err := ioutil.WriteFile(partialPath, nil, 0666); err != nil {
139+
return "", err
140+
}
141+
if err := modzip.Unzip(dir, mod, zipfile); err != nil {
142+
fmt.Fprintf(os.Stderr, "-> %s\n", err)
143+
if rmErr := RemoveAll(dir); rmErr == nil {
144+
os.Remove(partialPath)
178145
}
146+
return "", err
147+
}
148+
if err := os.Remove(partialPath); err != nil {
149+
return "", err
179150
}
180-
defer span.Done()
181151

182152
if !cfg.ModCacheRW {
183-
// Make dir read-only only *after* renaming it.
184-
// os.Rename was observed to fail for read-only directories on macOS.
185153
makeDirsReadOnly(dir)
186154
}
187155
return dir, nil
188156
}
189157

190-
var unzipInPlace bool
191-
192-
func init() {
193-
for _, f := range strings.Split(os.Getenv("GODEBUG"), ",") {
194-
if f == "modcacheunzipinplace=1" {
195-
unzipInPlace = true
196-
break
197-
}
198-
}
199-
}
200-
201158
var downloadZipCache par.Cache
202159

203160
// DownloadZip downloads the specific module version to the

src/cmd/go/testdata/script/mod_concurrent_unzipinplace.txt

-17
This file was deleted.

src/cmd/go/testdata/script/mod_download_concurrent_read.txt

+7-16
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,18 @@
11
# This test simulates a process watching for changes and reading files in
22
# module cache as a module is extracted.
33
#
4-
# By default, we unzip a downloaded module into a temporary directory with a
5-
# random name, then rename the directory into place. On Windows, this fails
6-
# with ERROR_ACCESS_DENIED if another process (e.g., antivirus) opens files
7-
# in the directory.
4+
# Before Go 1.16, we extracted each module zip to a temporary directory with
5+
# a random name, then renamed that into place with os.Rename. On Windows,
6+
# this failed with ERROR_ACCESS_DENIED when another process (usually an
7+
# anti-virus scanner) opened files in the temporary directory. This test
8+
# simulates that behavior, verifying golang.org/issue/36568.
89
#
9-
# Setting GODEBUG=modcacheunzipinplace=1 opts into new behavior: a downloaded
10-
# module is unzipped in place. A .partial file is created elsewhere to indicate
11-
# that the extraction is incomplete.
12-
#
13-
# Verifies golang.org/issue/36568.
10+
# Since 1.16, we extract to the final directory, but we create a .partial file
11+
# so that if we crash, other processes know the directory is incomplete.
1412

1513
[!windows] skip
1614
[short] skip
1715

18-
# Control case: check that the default behavior fails.
19-
# This is commented out to avoid flakiness. We can't reproduce the failure
20-
# 100% of the time.
21-
# ! go run downloader.go
22-
23-
# Experiment: check that the new behavior does not fail.
24-
env GODEBUG=modcacheunzipinplace=1
2516
go run downloader.go
2617

2718
-- go.mod --

src/cmd/go/testdata/script/mod_download_partial.txt

-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ rm $GOPATH/pkg/mod/rsc.io/[email protected]/go.mod
4646

4747
# 'go mod download' should not leave behind a directory or a .partial file
4848
# if there is an error extracting the zip file.
49-
env GODEBUG=modcacheunzipinplace=1
5049
rm $GOPATH/pkg/mod/rsc.io/[email protected]
5150
cp empty $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.zip
5251
! go mod download

0 commit comments

Comments
 (0)