Skip to content

Commit 55d96f9

Browse files
Bryan C. Millsgopherbot
Bryan C. Mills
authored andcommitted
cmd/go/internal/work: make NewBuilder safe for concurrent and repeated use
Ever since 'go build' was added (in CL 5483069), it has used an atexit handler to clean up working directories. At some point (prior to CL 95900044), Init was called multiple times per builder, registering potentially many atexit handlers that execute asynchronously and make debugging more difficult. The use of an AtExit handler also makes the Builder (and anything that uses it) prone to races: the base.AtExit API is not designed for concurrent use, but cmd/go is becoming increasingly concurrent over time. The AtExit handler also makes the Builder inappropriate to use within a unit-test, since the handlers do not run during the test function and accumulate over time. This change makes NewBuilder safe for concurrent use by registering the AtExit handler only once (during BuildInit, which was already not safe for concurrent use), and using a sync.Map to store the set of builders that need cleanup in case of an unclean exit. In addition, it causes the test variant of cmd/go to fail if any Builder instance leaks from a clean exit, helping to ensure that functions that create Builders do not leak them indefinitely, especially in tests. Updates #54423. Change-Id: Ia227b15b8fa53c33177c71271d756ac0858feebe Reviewed-on: https://go-review.googlesource.com/c/go/+/425254 Auto-Submit: Bryan Mills <bcmills@google.com> Reviewed-by: Than McIntosh <thanm@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Run-TryBot: Bryan Mills <bcmills@google.com>
1 parent 3083529 commit 55d96f9

File tree

10 files changed

+101
-24
lines changed

10 files changed

+101
-24
lines changed

src/cmd/go/internal/bug/bug.go

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"cmd/go/internal/cfg"
2323
"cmd/go/internal/envcmd"
2424
"cmd/go/internal/web"
25+
"cmd/go/internal/work"
2526
)
2627

2728
var CmdBug = &base.Command{
@@ -42,6 +43,8 @@ func runBug(ctx context.Context, cmd *base.Command, args []string) {
4243
if len(args) > 0 {
4344
base.Fatalf("go: bug takes no arguments")
4445
}
46+
work.BuildInit()
47+
4548
var buf bytes.Buffer
4649
buf.WriteString(bugHeader)
4750
printGoVersion(&buf)

src/cmd/go/internal/envcmd/env.go

+6
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ func ExtraEnvVars() []cfg.EnvVar {
175175
// but are costly to evaluate.
176176
func ExtraEnvVarsCostly() []cfg.EnvVar {
177177
b := work.NewBuilder("")
178+
defer func() {
179+
if err := b.Close(); err != nil {
180+
base.Fatalf("go: %v", err)
181+
}
182+
}()
178183

179184
cppflags, cflags, cxxflags, fflags, ldflags, err := b.CFlags(&load.Package{})
180185
if err != nil {
@@ -272,6 +277,7 @@ func runEnv(ctx context.Context, cmd *base.Command, args []string) {
272277
}
273278
}
274279
if needCostly {
280+
work.BuildInit()
275281
env = append(env, ExtraEnvVarsCostly()...)
276282
}
277283

src/cmd/go/internal/list/list.go

+6
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,12 @@ func runList(ctx context.Context, cmd *base.Command, args []string) {
690690
needStale := (listJson && listJsonFields.needAny("Stale", "StaleReason")) || strings.Contains(*listFmt, ".Stale")
691691
if needStale || *listExport || *listCompiled {
692692
b := work.NewBuilder("")
693+
defer func() {
694+
if err := b.Close(); err != nil {
695+
base.Fatalf("go: %v", err)
696+
}
697+
}()
698+
693699
b.IsCmdList = true
694700
b.NeedExport = *listExport
695701
b.NeedCompiledGoFiles = *listCompiled

src/cmd/go/internal/run/run.go

+5
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ func runRun(ctx context.Context, cmd *base.Command, args []string) {
9292

9393
work.BuildInit()
9494
b := work.NewBuilder("")
95+
defer func() {
96+
if err := b.Close(); err != nil {
97+
base.Fatalf("go: %v", err)
98+
}
99+
}()
95100
b.Print = printStderr
96101

97102
i := 0

src/cmd/go/internal/test/test.go

+8
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,11 @@ func runTest(ctx context.Context, cmd *base.Command, args []string) {
745745
}
746746

747747
b := work.NewBuilder("")
748+
defer func() {
749+
if err := b.Close(); err != nil {
750+
base.Fatalf("go: %v", err)
751+
}
752+
}()
748753

749754
if cfg.BuildI {
750755
fmt.Fprint(os.Stderr, "go: -i flag is deprecated\n")
@@ -808,6 +813,9 @@ func runTest(ctx context.Context, cmd *base.Command, args []string) {
808813
//
809814
// Maybe this has the effect of removing actions that were registered by the
810815
// call to CompileAction above?
816+
if err := b.Close(); err != nil {
817+
base.Fatalf("go: %v", err)
818+
}
811819
b = work.NewBuilder("")
812820
}
813821

src/cmd/go/internal/vet/vet.go

+5
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ func runVet(ctx context.Context, cmd *base.Command, args []string) {
9595
}
9696

9797
b := work.NewBuilder("")
98+
defer func() {
99+
if err := b.Close(); err != nil {
100+
base.Fatalf("go: %v", err)
101+
}
102+
}()
98103

99104
root := &work.Action{Mode: "go vet"}
100105
for _, p := range pkgs {

src/cmd/go/internal/work/action.go

+45-24
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
"fmt"
1717
"os"
1818
"path/filepath"
19-
"runtime"
2019
"strings"
2120
"sync"
2221
"time"
@@ -25,6 +24,7 @@ import (
2524
"cmd/go/internal/cache"
2625
"cmd/go/internal/cfg"
2726
"cmd/go/internal/load"
27+
"cmd/go/internal/robustio"
2828
"cmd/go/internal/trace"
2929
"cmd/internal/buildid"
3030
)
@@ -244,6 +244,8 @@ const (
244244
//
245245
// If workDir is the empty string, NewBuilder creates a WorkDir if needed
246246
// and arranges for it to be removed in case of an unclean exit.
247+
// The caller must Close the builder explicitly to clean up the WorkDir
248+
// before a clean exit.
247249
func NewBuilder(workDir string) *Builder {
248250
b := new(Builder)
249251

@@ -260,6 +262,9 @@ func NewBuilder(workDir string) *Builder {
260262
} else if cfg.BuildN {
261263
b.WorkDir = "$WORK"
262264
} else {
265+
if !buildInitStarted {
266+
panic("internal error: NewBuilder called before BuildInit")
267+
}
263268
tmp, err := os.MkdirTemp(cfg.Getenv("GOTMPDIR"), "go-build")
264269
if err != nil {
265270
base.Fatalf("go: creating work dir: %v", err)
@@ -273,32 +278,10 @@ func NewBuilder(workDir string) *Builder {
273278
tmp = abs
274279
}
275280
b.WorkDir = tmp
281+
builderWorkDirs.Store(b, b.WorkDir)
276282
if cfg.BuildX || cfg.BuildWork {
277283
fmt.Fprintf(os.Stderr, "WORK=%s\n", b.WorkDir)
278284
}
279-
if !cfg.BuildWork {
280-
workdir := b.WorkDir
281-
base.AtExit(func() {
282-
start := time.Now()
283-
for {
284-
err := os.RemoveAll(workdir)
285-
if err == nil {
286-
return
287-
}
288-
289-
// On some configurations of Windows, directories containing executable
290-
// files may be locked for a while after the executable exits (perhaps
291-
// due to antivirus scans?). It's probably worth a little extra latency
292-
// on exit to avoid filling up the user's temporary directory with leaked
293-
// files. (See golang.org/issue/30789.)
294-
if runtime.GOOS != "windows" || time.Since(start) >= 500*time.Millisecond {
295-
fmt.Fprintf(os.Stderr, "go: failed to remove work dir: %s\n", err)
296-
return
297-
}
298-
time.Sleep(5 * time.Millisecond)
299-
}
300-
})
301-
}
302285
}
303286

304287
if err := CheckGOOSARCHPair(cfg.Goos, cfg.Goarch); err != nil {
@@ -318,6 +301,44 @@ func NewBuilder(workDir string) *Builder {
318301
return b
319302
}
320303

304+
var builderWorkDirs sync.Map // *Builder → WorkDir
305+
306+
func (b *Builder) Close() error {
307+
wd, ok := builderWorkDirs.Load(b)
308+
if !ok {
309+
return nil
310+
}
311+
defer builderWorkDirs.Delete(b)
312+
313+
if b.WorkDir != wd.(string) {
314+
base.Errorf("go: internal error: Builder WorkDir unexpectedly changed from %s to %s", wd, b.WorkDir)
315+
}
316+
317+
if !cfg.BuildWork {
318+
if err := robustio.RemoveAll(b.WorkDir); err != nil {
319+
return err
320+
}
321+
}
322+
b.WorkDir = ""
323+
return nil
324+
}
325+
326+
func closeBuilders() {
327+
leakedBuilders := 0
328+
builderWorkDirs.Range(func(bi, _ any) bool {
329+
leakedBuilders++
330+
if err := bi.(*Builder).Close(); err != nil {
331+
base.Errorf("go: %v", err)
332+
}
333+
return true
334+
})
335+
336+
if leakedBuilders > 0 && base.GetExitStatus() == 0 {
337+
fmt.Fprintf(os.Stderr, "go: internal error: Builder leaked on successful exit\n")
338+
base.SetExitStatus(1)
339+
}
340+
}
341+
321342
func CheckGOOSARCHPair(goos, goarch string) error {
322343
if _, ok := cfg.OSArchSupportsCgo[goos+"/"+goarch]; !ok && cfg.BuildContext.Compiler == "gc" {
323344
return fmt.Errorf("unsupported GOOS/GOARCH pair %s/%s", goos, goarch)

src/cmd/go/internal/work/build.go

+10
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,11 @@ func runBuild(ctx context.Context, cmd *base.Command, args []string) {
404404
modload.InitWorkfile()
405405
BuildInit()
406406
b := NewBuilder("")
407+
defer func() {
408+
if err := b.Close(); err != nil {
409+
base.Fatalf("go: %v", err)
410+
}
411+
}()
407412

408413
pkgs := load.PackagesAndErrors(ctx, load.PackageOpts{AutoVCS: true}, args)
409414
load.CheckPackageErrors(pkgs)
@@ -728,6 +733,11 @@ func InstallPackages(ctx context.Context, patterns []string, pkgs []*load.Packag
728733
base.ExitIfErrors()
729734

730735
b := NewBuilder("")
736+
defer func() {
737+
if err := b.Close(); err != nil {
738+
base.Fatalf("go: %v", err)
739+
}
740+
}()
731741

732742
depMode := ModeBuild
733743
if cfg.BuildI {

src/cmd/go/internal/work/init.go

+8
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,15 @@ import (
2424
"sync"
2525
)
2626

27+
var buildInitStarted = false
28+
2729
func BuildInit() {
30+
if buildInitStarted {
31+
base.Fatalf("go: internal error: work.BuildInit called more than once")
32+
}
33+
buildInitStarted = true
34+
base.AtExit(closeBuilders)
35+
2836
modload.Init()
2937
instrumentInit()
3038
buildModeInit()

src/cmd/go/script_test.go

+5
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,11 @@ func (ts *testScript) cmdCc(want simpleStatus, args []string) {
577577
}
578578

579579
b := work.NewBuilder(ts.workdir)
580+
defer func() {
581+
if err := b.Close(); err != nil {
582+
ts.fatalf("%v", err)
583+
}
584+
}()
580585
ts.cmdExec(want, append(b.GccCmd(".", ""), args...))
581586
}
582587

0 commit comments

Comments
 (0)