Skip to content

Commit 688d9f6

Browse files
committed
playground: move build and run into functions
Based on a suggestion in CL 226697, this change moves the build and run steps from compileAndRun into their own functions. This will make it less likely to accidentally use the incorrect context again, which was the cause of golang/go#38052. Updates golang/go#25224 Change-Id: Id8399c2bd93fca7d61dced0431c8596f7998f3ee Reviewed-on: https://go-review.googlesource.com/c/playground/+/227352 Run-TryBot: Alexander Rakoczy <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]> Reviewed-by: Carlos Amedee <[email protected]>
1 parent 27e208b commit 688d9f6

File tree

1 file changed

+115
-67
lines changed

1 file changed

+115
-67
lines changed

sandbox.go

Lines changed: 115 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -328,24 +328,95 @@ func compileAndRun(ctx context.Context, req *request) (*response, error) {
328328
}
329329
defer os.RemoveAll(tmpDir)
330330

331-
files, err := splitFiles([]byte(req.Body))
331+
br, err := sandboxBuild(ctx, tmpDir, []byte(req.Body), req.WithVet)
332332
if err != nil {
333-
return &response{Errors: err.Error()}, nil
333+
return nil, err
334+
}
335+
if br.errorMessage != "" {
336+
return &response{Errors: br.errorMessage}, nil
337+
}
338+
339+
execRes, err := sandboxRun(ctx, br.exePath, br.testParam)
340+
if err != nil {
341+
return nil, err
342+
}
343+
if execRes.Error != "" {
344+
return &response{Errors: execRes.Error}, nil
345+
}
346+
347+
rec := new(Recorder)
348+
rec.Stdout().Write(execRes.Stdout)
349+
rec.Stderr().Write(execRes.Stderr)
350+
events, err := rec.Events()
351+
if err != nil {
352+
log.Printf("error decoding events: %v", err)
353+
return nil, fmt.Errorf("error decoding events: %v", err)
354+
}
355+
var fails int
356+
if br.testParam != "" {
357+
// In case of testing the TestsFailed field contains how many tests have failed.
358+
for _, e := range events {
359+
fails += strings.Count(e.Message, failedTestPattern)
360+
}
361+
}
362+
return &response{
363+
Events: events,
364+
Status: execRes.ExitCode,
365+
IsTest: br.testParam != "",
366+
TestsFailed: fails,
367+
VetErrors: br.vetOut,
368+
VetOK: req.WithVet && br.vetOut == "",
369+
}, nil
370+
}
371+
372+
// buildResult is the output of a sandbox build attempt.
373+
type buildResult struct {
374+
// goPath is a temporary directory if the binary was built with module support.
375+
// TODO(golang.org/issue/25224) - Why is the module mode built so differently?
376+
goPath string
377+
// exePath is the path to the built binary.
378+
exePath string
379+
// useModules is true if the binary was built with module support.
380+
useModules bool
381+
// testParam is set if tests should be run when running the binary.
382+
testParam string
383+
// errorMessage is an error message string to be returned to the user.
384+
errorMessage string
385+
// vetOut is the output of go vet, if requested.
386+
vetOut string
387+
}
388+
389+
// cleanup cleans up the temporary goPath created when building with module support.
390+
func (b *buildResult) cleanup() error {
391+
if b.useModules && b.goPath != "" {
392+
return os.RemoveAll(b.goPath)
393+
}
394+
return nil
395+
}
396+
397+
// sandboxBuild builds a Go program and returns a build result that includes the build context.
398+
//
399+
// An error is returned if a non-user-correctable error has occurred.
400+
func sandboxBuild(ctx context.Context, tmpDir string, in []byte, vet bool) (*buildResult, error) {
401+
files, err := splitFiles(in)
402+
if err != nil {
403+
return &buildResult{errorMessage: err.Error()}, nil
334404
}
335405

336-
var testParam string
406+
br := new(buildResult)
407+
defer br.cleanup()
337408
var buildPkgArg = "."
338409
if files.Num() == 1 && len(files.Data(progName)) > 0 {
339410
buildPkgArg = progName
340411
src := files.Data(progName)
341412
if code := getTestProg(src); code != nil {
342-
testParam = "-test.v"
413+
br.testParam = "-test.v"
343414
files.AddFile(progName, code)
344415
}
345416
}
346417

347-
useModules := allowModuleDownloads(files)
348-
if !files.Contains("go.mod") && useModules {
418+
br.useModules = allowModuleDownloads(files)
419+
if !files.Contains("go.mod") && br.useModules {
349420
files.AddFile("go.mod", []byte("module play\n"))
350421
}
351422

@@ -358,7 +429,7 @@ func compileAndRun(ctx context.Context, req *request) (*response, error) {
358429
fset := token.NewFileSet()
359430
f, err := parser.ParseFile(fset, f, src, parser.PackageClauseOnly)
360431
if err == nil && f.Name.Name != "main" {
361-
return &response{Errors: "package name must be main"}, nil
432+
return &buildResult{errorMessage: "package name must be main"}, nil
362433
}
363434
}
364435

@@ -373,69 +444,80 @@ func compileAndRun(ctx context.Context, req *request) (*response, error) {
373444
}
374445
}
375446

376-
exe := filepath.Join(tmpDir, "a.out")
447+
br.exePath = filepath.Join(tmpDir, "a.out")
377448
goCache := filepath.Join(tmpDir, "gocache")
378449

379-
buildCtx, cancel := context.WithTimeout(ctx, maxCompileTime)
450+
ctx, cancel := context.WithTimeout(ctx, maxCompileTime)
380451
defer cancel()
381-
cmd := exec.CommandContext(ctx, "/usr/local/go-faketime/bin/go", "build", "-o", exe, "-tags=faketime", buildPkgArg)
452+
cmd := exec.CommandContext(ctx, "/usr/local/go-faketime/bin/go", "build", "-o", br.exePath, "-tags=faketime")
382453
cmd.Dir = tmpDir
383-
var goPath string
384454
cmd.Env = []string{"GOOS=linux", "GOARCH=amd64", "GOROOT=/usr/local/go-faketime"}
385455
cmd.Env = append(cmd.Env, "GOCACHE="+goCache)
386-
if useModules {
456+
if br.useModules {
387457
// Create a GOPATH just for modules to be downloaded
388458
// into GOPATH/pkg/mod.
389-
goPath, err = ioutil.TempDir("", "gopath")
459+
cmd.Args = append(cmd.Args, "-modcacherw")
460+
br.goPath, err = ioutil.TempDir("", "gopath")
390461
if err != nil {
391462
log.Printf("error creating temp directory: %v", err)
392463
return nil, fmt.Errorf("error creating temp directory: %v", err)
393464
}
394-
defer os.RemoveAll(goPath)
395465
cmd.Env = append(cmd.Env, "GO111MODULE=on", "GOPROXY="+playgroundGoproxy())
396466
} else {
397-
goPath = os.Getenv("GOPATH") // contains old code.google.com/p/go-tour, etc
467+
br.goPath = os.Getenv("GOPATH") // contains old code.google.com/p/go-tour, etc
398468
cmd.Env = append(cmd.Env, "GO111MODULE=off") // in case it becomes on by default later
399469
}
400-
cmd.Env = append(cmd.Env, "GOPATH="+goPath)
470+
cmd.Args = append(cmd.Args, buildPkgArg)
471+
cmd.Env = append(cmd.Env, "GOPATH="+br.goPath)
401472
t0 := time.Now()
402473
if out, err := cmd.CombinedOutput(); err != nil {
403-
if buildCtx.Err() == context.DeadlineExceeded {
474+
if ctx.Err() == context.DeadlineExceeded {
404475
log.Printf("go build timed out after %v", time.Since(t0))
405-
return &response{Errors: goBuildTimeoutError}, nil
476+
return &buildResult{errorMessage: goBuildTimeoutError}, nil
406477
}
407478
if _, ok := err.(*exec.ExitError); ok {
408479
// Return compile errors to the user.
409480

410481
// Rewrite compiler errors to strip the tmpDir name.
411-
errs := strings.Replace(string(out), tmpDir+"/", "", -1)
482+
br.errorMessage = strings.Replace(string(out), tmpDir+"/", "", -1)
412483

413484
// "go build", invoked with a file name, puts this odd
414485
// message before any compile errors; strip it.
415-
errs = strings.Replace(errs, "# command-line-arguments\n", "", 1)
486+
br.errorMessage = strings.Replace(br.errorMessage, "# command-line-arguments\n", "", 1)
416487

417-
return &response{Errors: errs}, nil
488+
return br, nil
418489
}
419490
return nil, fmt.Errorf("error building go source: %v", err)
420491
}
421-
rec := new(Recorder)
422-
var exitCode int
423492
const maxBinarySize = 100 << 20 // copied from sandbox backend; TODO: unify?
424-
if fi, err := os.Stat(exe); err != nil || fi.Size() == 0 || fi.Size() > maxBinarySize {
493+
if fi, err := os.Stat(br.exePath); err != nil || fi.Size() == 0 || fi.Size() > maxBinarySize {
425494
if err != nil {
426495
return nil, fmt.Errorf("failed to stat binary: %v", err)
427496
}
428497
return nil, fmt.Errorf("invalid binary size %d", fi.Size())
429498
}
430-
exeBytes, err := ioutil.ReadFile(exe)
499+
if vet {
500+
// TODO: do this concurrently with the execution to reduce latency.
501+
br.vetOut, err = vetCheckInDir(tmpDir, br.goPath, br.useModules)
502+
if err != nil {
503+
return nil, fmt.Errorf("running vet: %v", err)
504+
}
505+
}
506+
return br, nil
507+
}
508+
509+
// sandboxRun runs a Go binary in a sandbox environment.
510+
func sandboxRun(ctx context.Context, exePath string, testParam string) (sandboxtypes.Response, error) {
511+
var execRes sandboxtypes.Response
512+
exeBytes, err := ioutil.ReadFile(exePath)
431513
if err != nil {
432-
return nil, err
514+
return execRes, err
433515
}
434-
runCtx, cancel := context.WithTimeout(ctx, maxRunTime)
516+
ctx, cancel := context.WithTimeout(ctx, maxRunTime)
435517
defer cancel()
436-
sreq, err := http.NewRequestWithContext(runCtx, "POST", sandboxBackendURL(), bytes.NewReader(exeBytes))
518+
sreq, err := http.NewRequestWithContext(ctx, "POST", sandboxBackendURL(), bytes.NewReader(exeBytes))
437519
if err != nil {
438-
return nil, fmt.Errorf("NewRequestWithContext %q: %w", sandboxBackendURL(), err)
520+
return execRes, fmt.Errorf("NewRequestWithContext %q: %w", sandboxBackendURL(), err)
439521
}
440522
sreq.Header.Add("Idempotency-Key", "1") // lets Transport do retries with a POST
441523
if testParam != "" {
@@ -444,52 +526,18 @@ func compileAndRun(ctx context.Context, req *request) (*response, error) {
444526
sreq.GetBody = func() (io.ReadCloser, error) { return ioutil.NopCloser(bytes.NewReader(exeBytes)), nil }
445527
res, err := sandboxBackendClient().Do(sreq)
446528
if err != nil {
447-
return nil, fmt.Errorf("POST %q: %w", sandboxBackendURL(), err)
529+
return execRes, fmt.Errorf("POST %q: %w", sandboxBackendURL(), err)
448530
}
449531
defer res.Body.Close()
450532
if res.StatusCode != http.StatusOK {
451533
log.Printf("unexpected response from backend: %v", res.Status)
452-
return nil, fmt.Errorf("unexpected response from backend: %v", res.Status)
534+
return execRes, fmt.Errorf("unexpected response from backend: %v", res.Status)
453535
}
454-
var execRes sandboxtypes.Response
455536
if err := json.NewDecoder(res.Body).Decode(&execRes); err != nil {
456537
log.Printf("JSON decode error from backend: %v", err)
457-
return nil, errors.New("error parsing JSON from backend")
458-
}
459-
if execRes.Error != "" {
460-
return &response{Errors: execRes.Error}, nil
461-
}
462-
exitCode = execRes.ExitCode
463-
rec.Stdout().Write(execRes.Stdout)
464-
rec.Stderr().Write(execRes.Stderr)
465-
events, err := rec.Events()
466-
if err != nil {
467-
log.Printf("error decoding events: %v", err)
468-
return nil, fmt.Errorf("error decoding events: %v", err)
469-
}
470-
var fails int
471-
if testParam != "" {
472-
// In case of testing the TestsFailed field contains how many tests have failed.
473-
for _, e := range events {
474-
fails += strings.Count(e.Message, failedTestPattern)
475-
}
538+
return execRes, errors.New("error parsing JSON from backend")
476539
}
477-
var vetOut string
478-
if req.WithVet {
479-
// TODO: do this concurrently with the execution to reduce latency.
480-
vetOut, err = vetCheckInDir(tmpDir, goPath, useModules)
481-
if err != nil {
482-
return nil, fmt.Errorf("running vet: %v", err)
483-
}
484-
}
485-
return &response{
486-
Events: events,
487-
Status: exitCode,
488-
IsTest: testParam != "",
489-
TestsFailed: fails,
490-
VetErrors: vetOut,
491-
VetOK: req.WithVet && vetOut == "",
492-
}, nil
540+
return execRes, nil
493541
}
494542

495543
// allowModuleDownloads reports whether the code snippet in src should be allowed

0 commit comments

Comments
 (0)