golang.org/x/playground@v0.0.0-20230418134305-14ebe15bcd59/sandbox.go (about)

     1  // Copyright 2014 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // TODO(andybons): add logging
     6  // TODO(andybons): restrict memory use
     7  
     8  package main
     9  
    10  import (
    11  	"bytes"
    12  	"context"
    13  	"crypto/sha256"
    14  	"encoding/json"
    15  	"errors"
    16  	"fmt"
    17  	"go/ast"
    18  	"go/doc"
    19  	"go/parser"
    20  	"go/token"
    21  	"io"
    22  	"net"
    23  	"net/http"
    24  	"os"
    25  	"os/exec"
    26  	"path/filepath"
    27  	"runtime"
    28  	"strconv"
    29  	"strings"
    30  	"sync"
    31  	"time"
    32  	"unicode"
    33  	"unicode/utf8"
    34  
    35  	"cloud.google.com/go/compute/metadata"
    36  	"github.com/bradfitz/gomemcache/memcache"
    37  	"go.opencensus.io/stats"
    38  	"go.opencensus.io/tag"
    39  	"golang.org/x/playground/internal"
    40  	"golang.org/x/playground/internal/gcpdial"
    41  	"golang.org/x/playground/sandbox/sandboxtypes"
    42  )
    43  
    44  const (
    45  	// Time for 'go build' to download 3rd-party modules and compile.
    46  	maxBuildTime = 10 * time.Second
    47  	maxRunTime   = 5 * time.Second
    48  
    49  	// progName is the implicit program name written to the temp
    50  	// dir and used in compiler and vet errors.
    51  	progName     = "prog.go"
    52  	progTestName = "prog_test.go"
    53  )
    54  
    55  const (
    56  	goBuildTimeoutError = "timeout running go build"
    57  	runTimeoutError     = "timeout running program"
    58  )
    59  
    60  // internalErrors are strings found in responses that will not be cached
    61  // due to their non-deterministic nature.
    62  var internalErrors = []string{
    63  	"out of memory",
    64  	"cannot allocate memory",
    65  }
    66  
    67  type request struct {
    68  	Body    string
    69  	WithVet bool // whether client supports vet response in a /compile request (Issue 31970)
    70  }
    71  
    72  type response struct {
    73  	Errors      string
    74  	Events      []Event
    75  	Status      int
    76  	IsTest      bool
    77  	TestsFailed int
    78  
    79  	// VetErrors, if non-empty, contains any vet errors. It is
    80  	// only populated if request.WithVet was true.
    81  	VetErrors string `json:",omitempty"`
    82  	// VetOK reports whether vet ran & passed. It is only
    83  	// populated if request.WithVet was true. Only one of
    84  	// VetErrors or VetOK can be non-zero.
    85  	VetOK bool `json:",omitempty"`
    86  }
    87  
    88  // commandHandler returns an http.HandlerFunc.
    89  // This handler creates a *request, assigning the "Body" field a value
    90  // from the "body" form parameter or from the HTTP request body.
    91  // If there is no cached *response for the combination of cachePrefix and request.Body,
    92  // handler calls cmdFunc and in case of a nil error, stores the value of *response in the cache.
    93  // The handler returned supports Cross-Origin Resource Sharing (CORS) from any domain.
    94  func (s *server) commandHandler(cachePrefix string, cmdFunc func(context.Context, *request) (*response, error)) http.HandlerFunc {
    95  	return func(w http.ResponseWriter, r *http.Request) {
    96  		cachePrefix := cachePrefix // so we can modify it below
    97  		w.Header().Set("Access-Control-Allow-Origin", "*")
    98  		if r.Method == "OPTIONS" {
    99  			// This is likely a pre-flight CORS request.
   100  			return
   101  		}
   102  
   103  		var req request
   104  		// Until programs that depend on golang.org/x/tools/godoc/static/playground.js
   105  		// are updated to always send JSON, this check is in place.
   106  		if b := r.FormValue("body"); b != "" {
   107  			req.Body = b
   108  			req.WithVet, _ = strconv.ParseBool(r.FormValue("withVet"))
   109  		} else if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
   110  			s.log.Errorf("error decoding request: %v", err)
   111  			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   112  			return
   113  		}
   114  
   115  		if req.WithVet {
   116  			cachePrefix += "_vet" // "prog" -> "prog_vet"
   117  		}
   118  
   119  		resp := &response{}
   120  		key := cacheKey(cachePrefix, req.Body)
   121  		if err := s.cache.Get(key, resp); err != nil {
   122  			if !errors.Is(err, memcache.ErrCacheMiss) {
   123  				s.log.Errorf("s.cache.Get(%q, &response): %v", key, err)
   124  			}
   125  			resp, err = cmdFunc(r.Context(), &req)
   126  			if err != nil {
   127  				s.log.Errorf("cmdFunc error: %v", err)
   128  				http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   129  				return
   130  			}
   131  			if strings.Contains(resp.Errors, goBuildTimeoutError) || strings.Contains(resp.Errors, runTimeoutError) {
   132  				// TODO(golang.org/issue/38576) - This should be a http.StatusBadRequest,
   133  				// but the UI requires a 200 to parse the response. It's difficult to know
   134  				// if we've timed out because of an error in the code snippet, or instability
   135  				// on the playground itself. Either way, we should try to show the user the
   136  				// partial output of their program.
   137  				s.writeJSONResponse(w, resp, http.StatusOK)
   138  				return
   139  			}
   140  			for _, e := range internalErrors {
   141  				if strings.Contains(resp.Errors, e) {
   142  					s.log.Errorf("cmdFunc compilation error: %q", resp.Errors)
   143  					http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   144  					return
   145  				}
   146  			}
   147  			for _, el := range resp.Events {
   148  				if el.Kind != "stderr" {
   149  					continue
   150  				}
   151  				for _, e := range internalErrors {
   152  					if strings.Contains(el.Message, e) {
   153  						s.log.Errorf("cmdFunc runtime error: %q", el.Message)
   154  						http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   155  						return
   156  					}
   157  				}
   158  			}
   159  			if err := s.cache.Set(key, resp); err != nil {
   160  				s.log.Errorf("cache.Set(%q, resp): %v", key, err)
   161  			}
   162  		}
   163  
   164  		s.writeJSONResponse(w, resp, http.StatusOK)
   165  	}
   166  }
   167  
   168  func cacheKey(prefix, body string) string {
   169  	h := sha256.New()
   170  	io.WriteString(h, body)
   171  	return fmt.Sprintf("%s-%s-%x", prefix, runtime.Version(), h.Sum(nil))
   172  }
   173  
   174  // isTestFunc tells whether fn has the type of a testing, or fuzz function, or a TestMain func.
   175  func isTestFunc(fn *ast.FuncDecl) bool {
   176  	if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 ||
   177  		fn.Type.Params.List == nil ||
   178  		len(fn.Type.Params.List) != 1 ||
   179  		len(fn.Type.Params.List[0].Names) > 1 {
   180  		return false
   181  	}
   182  	ptr, ok := fn.Type.Params.List[0].Type.(*ast.StarExpr)
   183  	if !ok {
   184  		return false
   185  	}
   186  	// We can't easily check that the type is *testing.T or *testing.F
   187  	// because we don't know how testing has been imported,
   188  	// but at least check that it's *T (or *F) or *something.T (or *something.F).
   189  	if name, ok := ptr.X.(*ast.Ident); ok && (name.Name == "T" || name.Name == "F" || name.Name == "M") {
   190  		return true
   191  	}
   192  	if sel, ok := ptr.X.(*ast.SelectorExpr); ok && (sel.Sel.Name == "T" || sel.Sel.Name == "F" || sel.Sel.Name == "M") {
   193  		return true
   194  	}
   195  	return false
   196  }
   197  
   198  // isTest tells whether name looks like a test (or benchmark, or fuzz, according to prefix).
   199  // It is a Test (say) if there is a character after Test that is not a lower-case letter.
   200  // We don't want mistaken Testimony or erroneous Benchmarking.
   201  func isTest(name, prefix string) bool {
   202  	if !strings.HasPrefix(name, prefix) {
   203  		return false
   204  	}
   205  	if len(name) == len(prefix) { // "Test" is ok
   206  		return true
   207  	}
   208  	r, _ := utf8.DecodeRuneInString(name[len(prefix):])
   209  	return !unicode.IsLower(r)
   210  }
   211  
   212  // getTestProg returns source code that executes all valid tests and examples in src.
   213  // If the main function is present or there are no tests or examples, it returns nil.
   214  // getTestProg emulates the "go test" command as closely as possible.
   215  // Benchmarks are not supported because of sandboxing.
   216  func isTestProg(src []byte) bool {
   217  	fset := token.NewFileSet()
   218  	// Early bail for most cases.
   219  	f, err := parser.ParseFile(fset, progName, src, parser.ImportsOnly)
   220  	if err != nil || f.Name.Name != "main" {
   221  		return false
   222  	}
   223  
   224  	// Parse everything and extract test names.
   225  	f, err = parser.ParseFile(fset, progName, src, parser.ParseComments)
   226  	if err != nil {
   227  		return false
   228  	}
   229  
   230  	var hasTest bool
   231  	var hasFuzz bool
   232  	for _, d := range f.Decls {
   233  		n, ok := d.(*ast.FuncDecl)
   234  		if !ok {
   235  			continue
   236  		}
   237  		name := n.Name.Name
   238  		switch {
   239  		case name == "main":
   240  			// main declared as a method will not obstruct creation of our main function.
   241  			if n.Recv == nil {
   242  				return false
   243  			}
   244  		case name == "TestMain" && isTestFunc(n):
   245  			hasTest = true
   246  		case isTest(name, "Test") && isTestFunc(n):
   247  			hasTest = true
   248  		case isTest(name, "Fuzz") && isTestFunc(n):
   249  			hasFuzz = true
   250  		}
   251  	}
   252  
   253  	if hasTest || hasFuzz {
   254  		return true
   255  	}
   256  
   257  	return len(doc.Examples(f)) > 0
   258  }
   259  
   260  var failedTestPattern = "--- FAIL"
   261  
   262  // compileAndRun tries to build and run a user program.
   263  // The output of successfully ran program is returned in *response.Events.
   264  // If a program cannot be built or has timed out,
   265  // *response.Errors contains an explanation for a user.
   266  func compileAndRun(ctx context.Context, req *request) (*response, error) {
   267  	// TODO(andybons): Add semaphore to limit number of running programs at once.
   268  	tmpDir, err := os.MkdirTemp("", "sandbox")
   269  	if err != nil {
   270  		return nil, fmt.Errorf("error creating temp directory: %v", err)
   271  	}
   272  	defer os.RemoveAll(tmpDir)
   273  
   274  	br, err := sandboxBuild(ctx, tmpDir, []byte(req.Body), req.WithVet)
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  	if br.errorMessage != "" {
   279  		return &response{Errors: removeBanner(br.errorMessage)}, nil
   280  	}
   281  
   282  	execRes, err := sandboxRun(ctx, br.exePath, br.testParam)
   283  	if err != nil {
   284  		return nil, err
   285  	}
   286  	if execRes.Error != "" {
   287  		return &response{Errors: execRes.Error}, nil
   288  	}
   289  
   290  	rec := new(Recorder)
   291  	rec.Stdout().Write(execRes.Stdout)
   292  	rec.Stderr().Write(execRes.Stderr)
   293  	events, err := rec.Events()
   294  	if err != nil {
   295  		log.Printf("error decoding events: %v", err)
   296  		return nil, fmt.Errorf("error decoding events: %v", err)
   297  	}
   298  	var fails int
   299  	if br.testParam != "" {
   300  		// In case of testing the TestsFailed field contains how many tests have failed.
   301  		for _, e := range events {
   302  			fails += strings.Count(e.Message, failedTestPattern)
   303  		}
   304  	}
   305  	return &response{
   306  		Events:      events,
   307  		Status:      execRes.ExitCode,
   308  		IsTest:      br.testParam != "",
   309  		TestsFailed: fails,
   310  		VetErrors:   br.vetOut,
   311  		VetOK:       req.WithVet && br.vetOut == "",
   312  	}, nil
   313  }
   314  
   315  // buildResult is the output of a sandbox build attempt.
   316  type buildResult struct {
   317  	// goPath is a temporary directory if the binary was built with module support.
   318  	// TODO(golang.org/issue/25224) - Why is the module mode built so differently?
   319  	goPath string
   320  	// exePath is the path to the built binary.
   321  	exePath string
   322  	// testParam is set if tests should be run when running the binary.
   323  	testParam string
   324  	// errorMessage is an error message string to be returned to the user.
   325  	errorMessage string
   326  	// vetOut is the output of go vet, if requested.
   327  	vetOut string
   328  }
   329  
   330  // cleanup cleans up the temporary goPath created when building with module support.
   331  func (b *buildResult) cleanup() error {
   332  	if b.goPath != "" {
   333  		return os.RemoveAll(b.goPath)
   334  	}
   335  	return nil
   336  }
   337  
   338  // sandboxBuild builds a Go program and returns a build result that includes the build context.
   339  //
   340  // An error is returned if a non-user-correctable error has occurred.
   341  func sandboxBuild(ctx context.Context, tmpDir string, in []byte, vet bool) (br *buildResult, err error) {
   342  	start := time.Now()
   343  	defer func() {
   344  		status := "success"
   345  		if err != nil {
   346  			status = "error"
   347  		}
   348  		// Ignore error. The only error can be invalid tag key or value
   349  		// length, which we know are safe.
   350  		stats.RecordWithTags(ctx, []tag.Mutator{tag.Upsert(kGoBuildSuccess, status)},
   351  			mGoBuildLatency.M(float64(time.Since(start))/float64(time.Millisecond)))
   352  	}()
   353  
   354  	files, err := splitFiles(in)
   355  	if err != nil {
   356  		return &buildResult{errorMessage: err.Error()}, nil
   357  	}
   358  
   359  	br = new(buildResult)
   360  	defer br.cleanup()
   361  	var buildPkgArg = "."
   362  	if files.Num() == 1 && len(files.Data(progName)) > 0 {
   363  		src := files.Data(progName)
   364  		if isTestProg(src) {
   365  			br.testParam = "-test.v"
   366  			files.MvFile(progName, progTestName)
   367  		}
   368  	}
   369  
   370  	if !files.Contains("go.mod") {
   371  		files.AddFile("go.mod", []byte("module play\n"))
   372  	}
   373  
   374  	for f, src := range files.m {
   375  		// Before multi-file support we required that the
   376  		// program be in package main, so continue to do that
   377  		// for now. But permit anything in subdirectories to have other
   378  		// packages.
   379  		if !strings.Contains(f, "/") {
   380  			fset := token.NewFileSet()
   381  			f, err := parser.ParseFile(fset, f, src, parser.PackageClauseOnly)
   382  			if err == nil && f.Name.Name != "main" {
   383  				return &buildResult{errorMessage: "package name must be main"}, nil
   384  			}
   385  		}
   386  
   387  		in := filepath.Join(tmpDir, f)
   388  		if strings.Contains(f, "/") {
   389  			if err := os.MkdirAll(filepath.Dir(in), 0755); err != nil {
   390  				return nil, err
   391  			}
   392  		}
   393  		if err := os.WriteFile(in, src, 0644); err != nil {
   394  			return nil, fmt.Errorf("error creating temp file %q: %v", in, err)
   395  		}
   396  	}
   397  
   398  	br.exePath = filepath.Join(tmpDir, "a.out")
   399  	goCache := filepath.Join(tmpDir, "gocache")
   400  
   401  	// Copy the gocache directory containing .a files for std, so that we can
   402  	// avoid recompiling std during this build. Using -al (hard linking) is
   403  	// faster than actually copying the bytes.
   404  	//
   405  	// This is necessary as .a files are no longer included in GOROOT following
   406  	// https://go.dev/cl/432535.
   407  	if err := exec.Command("cp", "-al", "/gocache", goCache).Run(); err != nil {
   408  		return nil, fmt.Errorf("error copying GOCACHE: %v", err)
   409  	}
   410  
   411  	var goArgs []string
   412  	if br.testParam != "" {
   413  		goArgs = append(goArgs, "test", "-c")
   414  	} else {
   415  		goArgs = append(goArgs, "build")
   416  	}
   417  	goArgs = append(goArgs, "-o", br.exePath, "-tags=faketime")
   418  
   419  	cmd := exec.Command("/usr/local/go-faketime/bin/go", goArgs...)
   420  	cmd.Dir = tmpDir
   421  	cmd.Env = []string{"GOOS=linux", "GOARCH=amd64", "GOROOT=/usr/local/go-faketime"}
   422  	cmd.Env = append(cmd.Env, "GOCACHE="+goCache)
   423  	cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
   424  	// Create a GOPATH just for modules to be downloaded
   425  	// into GOPATH/pkg/mod.
   426  	cmd.Args = append(cmd.Args, "-modcacherw")
   427  	cmd.Args = append(cmd.Args, "-mod=mod")
   428  	br.goPath, err = os.MkdirTemp("", "gopath")
   429  	if err != nil {
   430  		log.Printf("error creating temp directory: %v", err)
   431  		return nil, fmt.Errorf("error creating temp directory: %v", err)
   432  	}
   433  	cmd.Env = append(cmd.Env, "GO111MODULE=on", "GOPROXY="+playgroundGoproxy())
   434  	cmd.Args = append(cmd.Args, buildPkgArg)
   435  	cmd.Env = append(cmd.Env, "GOPATH="+br.goPath)
   436  	out := &bytes.Buffer{}
   437  	cmd.Stderr, cmd.Stdout = out, out
   438  
   439  	if err := cmd.Start(); err != nil {
   440  		return nil, fmt.Errorf("error starting go build: %v", err)
   441  	}
   442  	ctx, cancel := context.WithTimeout(ctx, maxBuildTime)
   443  	defer cancel()
   444  	if err := internal.WaitOrStop(ctx, cmd, os.Interrupt, 250*time.Millisecond); err != nil {
   445  		if errors.Is(err, context.DeadlineExceeded) {
   446  			br.errorMessage = fmt.Sprintln(goBuildTimeoutError)
   447  		} else if ee := (*exec.ExitError)(nil); !errors.As(err, &ee) {
   448  			log.Printf("error building program: %v", err)
   449  			return nil, fmt.Errorf("error building go source: %v", err)
   450  		}
   451  		// Return compile errors to the user.
   452  		// Rewrite compiler errors to strip the tmpDir name.
   453  		br.errorMessage = br.errorMessage + strings.Replace(string(out.Bytes()), tmpDir+"/", "", -1)
   454  
   455  		// "go build", invoked with a file name, puts this odd
   456  		// message before any compile errors; strip it.
   457  		br.errorMessage = strings.Replace(br.errorMessage, "# command-line-arguments\n", "", 1)
   458  
   459  		return br, nil
   460  	}
   461  	const maxBinarySize = 100 << 20 // copied from sandbox backend; TODO: unify?
   462  	if fi, err := os.Stat(br.exePath); err != nil || fi.Size() == 0 || fi.Size() > maxBinarySize {
   463  		if err != nil {
   464  			return nil, fmt.Errorf("failed to stat binary: %v", err)
   465  		}
   466  		return nil, fmt.Errorf("invalid binary size %d", fi.Size())
   467  	}
   468  	if vet {
   469  		// TODO: do this concurrently with the execution to reduce latency.
   470  		br.vetOut, err = vetCheckInDir(ctx, tmpDir, br.goPath)
   471  		if err != nil {
   472  			return nil, fmt.Errorf("running vet: %v", err)
   473  		}
   474  	}
   475  	return br, nil
   476  }
   477  
   478  // sandboxRun runs a Go binary in a sandbox environment.
   479  func sandboxRun(ctx context.Context, exePath string, testParam string) (execRes sandboxtypes.Response, err error) {
   480  	start := time.Now()
   481  	defer func() {
   482  		status := "success"
   483  		if err != nil {
   484  			status = "error"
   485  		}
   486  		// Ignore error. The only error can be invalid tag key or value
   487  		// length, which we know are safe.
   488  		stats.RecordWithTags(ctx, []tag.Mutator{tag.Upsert(kGoBuildSuccess, status)},
   489  			mGoRunLatency.M(float64(time.Since(start))/float64(time.Millisecond)))
   490  	}()
   491  	exeBytes, err := os.ReadFile(exePath)
   492  	if err != nil {
   493  		return execRes, err
   494  	}
   495  	ctx, cancel := context.WithTimeout(ctx, maxRunTime)
   496  	defer cancel()
   497  	sreq, err := http.NewRequestWithContext(ctx, "POST", sandboxBackendURL(), bytes.NewReader(exeBytes))
   498  	if err != nil {
   499  		return execRes, fmt.Errorf("NewRequestWithContext %q: %w", sandboxBackendURL(), err)
   500  	}
   501  	sreq.Header.Add("Idempotency-Key", "1") // lets Transport do retries with a POST
   502  	if testParam != "" {
   503  		sreq.Header.Add("X-Argument", testParam)
   504  	}
   505  	sreq.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(exeBytes)), nil }
   506  	res, err := sandboxBackendClient().Do(sreq)
   507  	if err != nil {
   508  		if errors.Is(ctx.Err(), context.DeadlineExceeded) {
   509  			execRes.Error = runTimeoutError
   510  			return execRes, nil
   511  		}
   512  		return execRes, fmt.Errorf("POST %q: %w", sandboxBackendURL(), err)
   513  	}
   514  	defer res.Body.Close()
   515  	if res.StatusCode != http.StatusOK {
   516  		log.Printf("unexpected response from backend: %v", res.Status)
   517  		return execRes, fmt.Errorf("unexpected response from backend: %v", res.Status)
   518  	}
   519  	if err := json.NewDecoder(res.Body).Decode(&execRes); err != nil {
   520  		log.Printf("JSON decode error from backend: %v", err)
   521  		return execRes, errors.New("error parsing JSON from backend")
   522  	}
   523  	return execRes, nil
   524  }
   525  
   526  // playgroundGoproxy returns the GOPROXY environment config the playground should use.
   527  // It is fetched from the environment variable PLAY_GOPROXY. A missing or empty
   528  // value for PLAY_GOPROXY returns the default value of https://proxy.golang.org.
   529  func playgroundGoproxy() string {
   530  	proxypath := os.Getenv("PLAY_GOPROXY")
   531  	if proxypath != "" {
   532  		return proxypath
   533  	}
   534  	return "https://proxy.golang.org"
   535  }
   536  
   537  // healthCheck attempts to build a binary from the source in healthProg.
   538  // It returns any error returned from sandboxBuild, or nil if none is returned.
   539  func (s *server) healthCheck(ctx context.Context) error {
   540  	tmpDir, err := os.MkdirTemp("", "sandbox")
   541  	if err != nil {
   542  		return fmt.Errorf("error creating temp directory: %v", err)
   543  	}
   544  	defer os.RemoveAll(tmpDir)
   545  	br, err := sandboxBuild(ctx, tmpDir, []byte(healthProg), false)
   546  	if err != nil {
   547  		return err
   548  	}
   549  	if br.errorMessage != "" {
   550  		return errors.New(br.errorMessage)
   551  	}
   552  	return nil
   553  }
   554  
   555  // sandboxBackendURL returns the URL of the sandbox backend that
   556  // executes binaries. This backend is required for Go 1.14+ (where it
   557  // executes using gvisor, since Native Client support is removed).
   558  //
   559  // This function either returns a non-empty string or it panics.
   560  func sandboxBackendURL() string {
   561  	if v := os.Getenv("SANDBOX_BACKEND_URL"); v != "" {
   562  		return v
   563  	}
   564  	id, _ := metadata.ProjectID()
   565  	switch id {
   566  	case "golang-org":
   567  		return "http://sandbox.play-sandbox-fwd.il4.us-central1.lb.golang-org.internal/run"
   568  	}
   569  	panic(fmt.Sprintf("no SANDBOX_BACKEND_URL environment and no default defined for project %q", id))
   570  }
   571  
   572  var sandboxBackendOnce struct {
   573  	sync.Once
   574  	c *http.Client
   575  }
   576  
   577  func sandboxBackendClient() *http.Client {
   578  	sandboxBackendOnce.Do(initSandboxBackendClient)
   579  	return sandboxBackendOnce.c
   580  }
   581  
   582  // initSandboxBackendClient runs from a sync.Once and initializes
   583  // sandboxBackendOnce.c with the *http.Client we'll use to contact the
   584  // sandbox execution backend.
   585  func initSandboxBackendClient() {
   586  	id, _ := metadata.ProjectID()
   587  	switch id {
   588  	case "golang-org":
   589  		// For production, use a funky Transport dialer that
   590  		// contacts backend directly, without going through an
   591  		// internal load balancer, due to internal GCP
   592  		// reasons, which we might resolve later. This might
   593  		// be a temporary hack.
   594  		tr := http.DefaultTransport.(*http.Transport).Clone()
   595  		rigd := gcpdial.NewRegionInstanceGroupDialer("golang-org", "us-central1", "play-sandbox-rigm")
   596  		tr.DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) {
   597  			if addr == "sandbox.play-sandbox-fwd.il4.us-central1.lb.golang-org.internal:80" {
   598  				ip, err := rigd.PickIP(ctx)
   599  				if err != nil {
   600  					return nil, err
   601  				}
   602  				addr = net.JoinHostPort(ip, "80") // and fallthrough
   603  			}
   604  			var d net.Dialer
   605  			return d.DialContext(ctx, netw, addr)
   606  		}
   607  		sandboxBackendOnce.c = &http.Client{Transport: tr}
   608  	default:
   609  		sandboxBackendOnce.c = http.DefaultClient
   610  	}
   611  }
   612  
   613  // removeBanner remove package name banner
   614  func removeBanner(output string) string {
   615  	if strings.HasPrefix(output, "#") {
   616  		if nl := strings.Index(output, "\n"); nl != -1 {
   617  			output = output[nl+1:]
   618  		}
   619  	}
   620  	return output
   621  }
   622  
   623  const healthProg = `
   624  package main
   625  
   626  import "fmt"
   627  
   628  func main() { fmt.Print("ok") }
   629  `