@@ -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