Skip to content

Commit db6b66e

Browse files
author
Bryan C. Mills
committed
cmd/go: use lockedfile instead of renameio for go.mod and go.sum files
This change is based on the previous discussion in CL 202442. Fixes #34634 Change-Id: I1319aa26d5cfcd034bc576555787b3ca79968c38 Reviewed-on: https://go-review.googlesource.com/c/go/+/205637 Run-TryBot: Bryan C. Mills <[email protected]> Reviewed-by: Jay Conrod <[email protected]>
1 parent 3c29796 commit db6b66e

File tree

6 files changed

+209
-89
lines changed

6 files changed

+209
-89
lines changed

src/cmd/go/internal/lockedfile/lockedfile.go

+65
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,68 @@ func Write(name string, content io.Reader, perm os.FileMode) (err error) {
120120
}
121121
return err
122122
}
123+
124+
// Transform invokes t with the result of reading the named file, with its lock
125+
// still held.
126+
//
127+
// If t returns a nil error, Transform then writes the returned contents back to
128+
// the file, making a best effort to preserve existing contents on error.
129+
//
130+
// t must not modify the slice passed to it.
131+
func Transform(name string, t func([]byte) ([]byte, error)) (err error) {
132+
f, err := Edit(name)
133+
if err != nil {
134+
return err
135+
}
136+
defer f.Close()
137+
138+
old, err := ioutil.ReadAll(f)
139+
if err != nil {
140+
return err
141+
}
142+
143+
new, err := t(old)
144+
if err != nil {
145+
return err
146+
}
147+
148+
if len(new) > len(old) {
149+
// The overall file size is increasing, so write the tail first: if we're
150+
// about to run out of space on the disk, we would rather detect that
151+
// failure before we have overwritten the original contents.
152+
if _, err := f.WriteAt(new[len(old):], int64(len(old))); err != nil {
153+
// Make a best effort to remove the incomplete tail.
154+
f.Truncate(int64(len(old)))
155+
return err
156+
}
157+
}
158+
159+
// We're about to overwrite the old contents. In case of failure, make a best
160+
// effort to roll back before we close the file.
161+
defer func() {
162+
if err != nil {
163+
if _, err := f.WriteAt(old, 0); err == nil {
164+
f.Truncate(int64(len(old)))
165+
}
166+
}
167+
}()
168+
169+
if len(new) >= len(old) {
170+
if _, err := f.WriteAt(new[:len(old)], 0); err != nil {
171+
return err
172+
}
173+
} else {
174+
if _, err := f.WriteAt(new, 0); err != nil {
175+
return err
176+
}
177+
// The overall file size is decreasing, so shrink the file to its final size
178+
// after writing. We do this after writing (instead of before) so that if
179+
// the write fails, enough filesystem space will likely still be reserved
180+
// to contain the previous contents.
181+
if err := f.Truncate(int64(len(new))); err != nil {
182+
return err
183+
}
184+
}
185+
186+
return nil
187+
}

src/cmd/go/internal/modcmd/edit.go

+15-8
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ package modcmd
99
import (
1010
"bytes"
1111
"encoding/json"
12+
"errors"
1213
"fmt"
13-
"io/ioutil"
1414
"os"
1515
"strings"
1616

1717
"cmd/go/internal/base"
18+
"cmd/go/internal/lockedfile"
1819
"cmd/go/internal/modfetch"
1920
"cmd/go/internal/modload"
2021
"cmd/go/internal/work"
@@ -174,7 +175,7 @@ func runEdit(cmd *base.Command, args []string) {
174175
}
175176
}
176177

177-
data, err := ioutil.ReadFile(gomod)
178+
data, err := lockedfile.Read(gomod)
178179
if err != nil {
179180
base.Fatalf("go: %v", err)
180181
}
@@ -217,13 +218,19 @@ func runEdit(cmd *base.Command, args []string) {
217218
return
218219
}
219220

220-
unlock := modfetch.SideLock()
221-
defer unlock()
222-
lockedData, err := ioutil.ReadFile(gomod)
223-
if err == nil && !bytes.Equal(lockedData, data) {
224-
base.Fatalf("go: go.mod changed during editing; not overwriting")
221+
// Make a best-effort attempt to acquire the side lock, only to exclude
222+
// previous versions of the 'go' command from making simultaneous edits.
223+
if unlock, err := modfetch.SideLock(); err == nil {
224+
defer unlock()
225225
}
226-
if err := ioutil.WriteFile(gomod, out, 0666); err != nil {
226+
227+
err = lockedfile.Transform(gomod, func(lockedData []byte) ([]byte, error) {
228+
if !bytes.Equal(lockedData, data) {
229+
return nil, errors.New("go.mod changed during editing; not overwriting")
230+
}
231+
return out, nil
232+
})
233+
if err != nil {
227234
base.Fatalf("go: %v", err)
228235
}
229236
}

src/cmd/go/internal/modfetch/cache.go

+9-10
Original file line numberDiff line numberDiff line change
@@ -95,22 +95,21 @@ func lockVersion(mod module.Version) (unlock func(), err error) {
9595
return lockedfile.MutexAt(path).Lock()
9696
}
9797

98-
// SideLock locks a file within the module cache that that guards edits to files
99-
// outside the cache, such as go.sum and go.mod files in the user's working
100-
// directory. It returns a function that must be called to unlock the file.
101-
func SideLock() (unlock func()) {
98+
// SideLock locks a file within the module cache that that previously guarded
99+
// edits to files outside the cache, such as go.sum and go.mod files in the
100+
// user's working directory.
101+
// If err is nil, the caller MUST eventually call the unlock function.
102+
func SideLock() (unlock func(), err error) {
102103
if PkgMod == "" {
103104
base.Fatalf("go: internal error: modfetch.PkgMod not set")
104105
}
106+
105107
path := filepath.Join(PkgMod, "cache", "lock")
106108
if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
107-
base.Fatalf("go: failed to create cache directory %s: %v", filepath.Dir(path), err)
108-
}
109-
unlock, err := lockedfile.MutexAt(path).Lock()
110-
if err != nil {
111-
base.Fatalf("go: failed to lock file at %v", path)
109+
return nil, fmt.Errorf("failed to create cache directory: %w", err)
112110
}
113-
return unlock
111+
112+
return lockedfile.MutexAt(path).Lock()
114113
}
115114

116115
// A cachingRepo is a cache around an underlying Repo,

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

+35-49
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919

2020
"cmd/go/internal/base"
2121
"cmd/go/internal/cfg"
22+
"cmd/go/internal/lockedfile"
2223
"cmd/go/internal/par"
2324
"cmd/go/internal/renameio"
2425

@@ -296,7 +297,7 @@ func initGoSum() (bool, error) {
296297

297298
goSum.m = make(map[module.Version][]string)
298299
goSum.checked = make(map[modSum]bool)
299-
data, err := renameio.ReadFile(GoSumFile)
300+
data, err := lockedfile.Read(GoSumFile)
300301
if err != nil && !os.IsNotExist(err) {
301302
return false, err
302303
}
@@ -529,60 +530,45 @@ func WriteGoSum() {
529530
base.Fatalf("go: updates to go.sum needed, disabled by -mod=readonly")
530531
}
531532

532-
// We want to avoid races between creating the lockfile and deleting it, but
533-
// we also don't want to leave a permanent lockfile in the user's repository.
534-
//
535-
// On top of that, if we crash while writing go.sum, we don't want to lose the
536-
// sums that were already present in the file, so it's important that we write
537-
// the file by renaming rather than truncating — which means that we can't
538-
// lock the go.sum file itself.
539-
//
540-
// Instead, we'll lock a distinguished file in the cache directory: that will
541-
// only race if the user runs `go clean -modcache` concurrently with a command
542-
// that updates go.sum, and that's already racy to begin with.
543-
//
544-
// We'll end up slightly over-synchronizing go.sum writes if the user runs a
545-
// bunch of go commands that update sums in separate modules simultaneously,
546-
// but that's unlikely to matter in practice.
547-
548-
unlock := SideLock()
549-
defer unlock()
533+
// Make a best-effort attempt to acquire the side lock, only to exclude
534+
// previous versions of the 'go' command from making simultaneous edits.
535+
if unlock, err := SideLock(); err == nil {
536+
defer unlock()
537+
}
550538

551-
if !goSum.overwrite {
552-
// Re-read the go.sum file to incorporate any sums added by other processes
553-
// in the meantime.
554-
data, err := renameio.ReadFile(GoSumFile)
555-
if err != nil && !os.IsNotExist(err) {
556-
base.Fatalf("go: re-reading go.sum: %v", err)
539+
err := lockedfile.Transform(GoSumFile, func(data []byte) ([]byte, error) {
540+
if !goSum.overwrite {
541+
// Incorporate any sums added by other processes in the meantime.
542+
// Add only the sums that we actually checked: the user may have edited or
543+
// truncated the file to remove erroneous hashes, and we shouldn't restore
544+
// them without good reason.
545+
goSum.m = make(map[module.Version][]string, len(goSum.m))
546+
readGoSum(goSum.m, GoSumFile, data)
547+
for ms := range goSum.checked {
548+
addModSumLocked(ms.mod, ms.sum)
549+
goSum.dirty = true
550+
}
557551
}
558552

559-
// Add only the sums that we actually checked: the user may have edited or
560-
// truncated the file to remove erroneous hashes, and we shouldn't restore
561-
// them without good reason.
562-
goSum.m = make(map[module.Version][]string, len(goSum.m))
563-
readGoSum(goSum.m, GoSumFile, data)
564-
for ms := range goSum.checked {
565-
addModSumLocked(ms.mod, ms.sum)
566-
goSum.dirty = true
553+
var mods []module.Version
554+
for m := range goSum.m {
555+
mods = append(mods, m)
567556
}
568-
}
569-
570-
var mods []module.Version
571-
for m := range goSum.m {
572-
mods = append(mods, m)
573-
}
574-
module.Sort(mods)
575-
var buf bytes.Buffer
576-
for _, m := range mods {
577-
list := goSum.m[m]
578-
sort.Strings(list)
579-
for _, h := range list {
580-
fmt.Fprintf(&buf, "%s %s %s\n", m.Path, m.Version, h)
557+
module.Sort(mods)
558+
559+
var buf bytes.Buffer
560+
for _, m := range mods {
561+
list := goSum.m[m]
562+
sort.Strings(list)
563+
for _, h := range list {
564+
fmt.Fprintf(&buf, "%s %s %s\n", m.Path, m.Version, h)
565+
}
581566
}
582-
}
567+
return buf.Bytes(), nil
568+
})
583569

584-
if err := renameio.WriteFile(GoSumFile, buf.Bytes(), 0666); err != nil {
585-
base.Fatalf("go: writing go.sum: %v", err)
570+
if err != nil {
571+
base.Fatalf("go: updating go.sum: %v", err)
586572
}
587573

588574
goSum.checked = make(map[modSum]bool)

src/cmd/go/internal/modload/init.go

+28-22
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package modload
77
import (
88
"bytes"
99
"encoding/json"
10+
"errors"
1011
"fmt"
1112
"go/build"
1213
"internal/lazyregexp"
@@ -22,6 +23,7 @@ import (
2223
"cmd/go/internal/cache"
2324
"cmd/go/internal/cfg"
2425
"cmd/go/internal/load"
26+
"cmd/go/internal/lockedfile"
2527
"cmd/go/internal/modconv"
2628
"cmd/go/internal/modfetch"
2729
"cmd/go/internal/modfetch/codehost"
@@ -950,32 +952,36 @@ func WriteGoMod() {
950952
index = indexModFile(new, modFile, false)
951953
}()
952954

953-
unlock := modfetch.SideLock()
954-
defer unlock()
955-
956-
file := ModFilePath()
957-
old, err := renameio.ReadFile(file)
958-
if bytes.Equal(old, new) {
959-
// The go.mod file is already equal to new, possibly as the result of some
960-
// other process.
961-
return
955+
// Make a best-effort attempt to acquire the side lock, only to exclude
956+
// previous versions of the 'go' command from making simultaneous edits.
957+
if unlock, err := modfetch.SideLock(); err == nil {
958+
defer unlock()
962959
}
963960

964-
if index != nil && !bytes.Equal(old, index.data) {
965-
if err != nil {
966-
base.Fatalf("go: can't determine whether go.mod has changed: %v", err)
961+
errNoChange := errors.New("no update needed")
962+
963+
err = lockedfile.Transform(ModFilePath(), func(old []byte) ([]byte, error) {
964+
if bytes.Equal(old, new) {
965+
// The go.mod file is already equal to new, possibly as the result of some
966+
// other process.
967+
return nil, errNoChange
967968
}
968-
// The contents of the go.mod file have changed. In theory we could add all
969-
// of the new modules to the build list, recompute, and check whether any
970-
// module in *our* build list got bumped to a different version, but that's
971-
// a lot of work for marginal benefit. Instead, fail the command: if users
972-
// want to run concurrent commands, they need to start with a complete,
973-
// consistent module definition.
974-
base.Fatalf("go: updates to go.mod needed, but contents have changed")
975-
}
976969

977-
if err := renameio.WriteFile(file, new, 0666); err != nil {
978-
base.Fatalf("error writing go.mod: %v", err)
970+
if index != nil && !bytes.Equal(old, index.data) {
971+
// The contents of the go.mod file have changed. In theory we could add all
972+
// of the new modules to the build list, recompute, and check whether any
973+
// module in *our* build list got bumped to a different version, but that's
974+
// a lot of work for marginal benefit. Instead, fail the command: if users
975+
// want to run concurrent commands, they need to start with a complete,
976+
// consistent module definition.
977+
return nil, fmt.Errorf("existing contents have changed since last read")
978+
}
979+
980+
return new, nil
981+
})
982+
983+
if err != nil && err != errNoChange {
984+
base.Fatalf("go: updating go.mod: %v", err)
979985
}
980986
}
981987

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Regression test for golang.org/issue/34634: permissions for the go.sum and
2+
# go.mod files should be preserved when overwriting them.
3+
4+
env GO111MODULE=on
5+
[short] skip
6+
7+
# Skip platforms that do not have Unix-style file permissions.
8+
[windows] skip
9+
[plan9] skip
10+
11+
chmod 0640 go.mod
12+
chmod 0604 go.sum
13+
go mod edit -module=golang.org/issue/34634
14+
15+
go build .
16+
cmp go.mod go.mod.want
17+
cmp go.sum go.sum.want
18+
19+
go run .
20+
stdout 'go.mod: 0640'
21+
stdout 'go.sum: 0604'
22+
23+
-- read_perm.go --
24+
package main
25+
26+
import (
27+
"fmt"
28+
"os"
29+
_ "rsc.io/sampler"
30+
)
31+
32+
func main() {
33+
for _, name := range []string{"go.mod", "go.sum"} {
34+
fi, err := os.Stat(name)
35+
if err != nil {
36+
fmt.Fprintf(os.Stderr, "%s: %v\n", err)
37+
continue
38+
}
39+
fmt.Printf("%s: 0%o\n", name, fi.Mode().Perm())
40+
}
41+
}
42+
-- go.mod --
43+
module TODO
44+
45+
go 1.14
46+
-- go.sum --
47+
-- go.mod.want --
48+
module golang.org/issue/34634
49+
50+
go 1.14
51+
52+
require rsc.io/sampler v1.99.99
53+
-- go.sum.want --
54+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:pvCbr/wm8HzDD3fVywevekufpn6tCGPY3spdHeZJEsw=
55+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
56+
rsc.io/sampler v1.99.99 h1:iMG9lbEG/8MdeR4lgL+Q8IcwbLNw7ijW7fTiK8Miqts=
57+
rsc.io/sampler v1.99.99/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

0 commit comments

Comments
 (0)