playground: add sandbox and nacl base image Change-Id: If6c2893ef8df63a3005ec85075db2e9df0cbf993
diff --git a/README b/README deleted file mode 100644 index 61c25fd..0000000 --- a/README +++ /dev/null
@@ -1,4 +0,0 @@ -This subrepository holds the source for various packages and tools that support -the Go playground: https://play.golang.org/ - -To submit changes to this repository, see http://golang.org/doc/contribute.html.
diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf7f902 --- /dev/null +++ b/README.md
@@ -0,0 +1,26 @@ +# playground + +This subrepository holds the source for various packages and tools that support +the Go playground: https://play.golang.org/ + +## building + +``` +# build the golang:nacl base image +docker build -t golang:nacl gonacl/ +# build the sandbox server image +docker build -t sandbox sandbox/ +``` + +## running + +``` +# run the sandbox +docker run -d -p 8080:8080 sandbox +# get docker host ip, try boot2docker fallback on localhost. +DOCKER_HOST_IP=$(boot2docker ip || echo localhost) +# run go some code +cat /path/to/code.go | go run ./sandbox/client.go | curl --data @- $DOCKER_HOST_IP:8080/compile +``` + +To submit changes to this repository, see http://golang.org/doc/contribute.html.
diff --git a/sandbox/Dockerfile b/sandbox/Dockerfile new file mode 100644 index 0000000..ee0a0a0 --- /dev/null +++ b/sandbox/Dockerfile
@@ -0,0 +1,21 @@ +# Copyright 2014 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +FROM golang:1.4 + +# enable faketime +RUN apt-get update && apt-get install -yq --no-install-recommends patch +ADD enable-fake-time.patch /usr/src/go/ +RUN patch /usr/src/go/src/runtime/rt0_nacl_amd64p32.s /usr/src/go/enable-fake-time.patch + +# build go nacl tool chain +RUN cd /usr/src/go/src && GOOS=nacl GOARCH=amd64p32 ./make.bash --no-clean +RUN cd /usr/local/bin && curl -s -O https://storage.googleapis.com/gobuilder/sel_ldr_x86_64 && chmod +x sel_ldr_x86_64 + +# add and compile sandbox daemon +ADD . /go/src/sandbox/ +RUN go install sandbox + +EXPOSE 8080 +ENTRYPOINT ["/go/bin/sandbox"]
diff --git a/sandbox/client.go b/sandbox/client.go new file mode 100644 index 0000000..eee62f9 --- /dev/null +++ b/sandbox/client.go
@@ -0,0 +1,26 @@ +// +build ignore + +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "encoding/json" + "io/ioutil" + "log" + "os" +) + +func main() { + body, err := ioutil.ReadAll(os.Stdin) + if err != nil { + log.Fatalf("error reading stdin: %v", err) + } + json.NewEncoder(os.Stdout).Encode(struct { + Body string + }{ + Body: string(body), + }) +}
diff --git a/sandbox/enable-fake-time.patch b/sandbox/enable-fake-time.patch new file mode 100644 index 0000000..4e389b9 --- /dev/null +++ b/sandbox/enable-fake-time.patch
@@ -0,0 +1,11 @@ +--- rt0_nacl_amd64p32.s 2014-10-28 17:28:25.028188222 -0700 ++++ rt0_nacl_amd64p32-faketime.s 2014-10-28 17:28:06.363674896 -0700 +@@ -25,6 +25,6 @@ + + TEXT main(SB),NOSPLIT,$0 + // Uncomment for fake time like on Go Playground. +- //MOVQ $1257894000000000000, AX +- //MOVQ AX, runtime·faketime(SB) ++ MOVQ $1257894000000000000, AX ++ MOVQ AX, runtime·faketime(SB) + JMP runtime·rt0_go(SB)
diff --git a/sandbox/play.go b/sandbox/play.go new file mode 100644 index 0000000..8679945 --- /dev/null +++ b/sandbox/play.go
@@ -0,0 +1,119 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "time" + "unicode/utf8" +) + +type Event struct { + Message string + Delay time.Duration // time to wait before printing Message + Kind string +} + +// Decode takes an output string comprised of playback headers, and converts +// it to a sequence of Events. +// It sanitizes each Event's Message to ensure it is valid UTF-8. +// +// Playground programs precede all their writes with a header (described +// below) that describes the time the write occurred (in playground time) and +// the length of the data that will be written. If a non-header is +// encountered where a header is expected, the output is scanned for the next +// header and the intervening text string is added to the sequence an event +// occurring at the same time as the preceding event. +// +// A playback header has this structure: +// 4 bytes: "\x00\x00PB", a magic header +// 8 bytes: big-endian int64, unix time in nanoseconds +// 4 bytes: big-endian int32, length of the next write +// +func Decode(output []byte) (seq []Event, err error) { + var ( + magic = []byte{0, 0, 'P', 'B'} + headerLen = 8 + 4 + now = time.Unix(1257894000, 0) // go epoch + ) + + add := func(t time.Time, b []byte) { + e := Event{ + Message: string(sanitize(b)), + Delay: t.Sub(now), + } + if e.Delay == 0 && len(seq) > 0 { + // Merge this event with previous event, to avoid + // sending a lot of events for a big output with no + // significant timing information. + seq[len(seq)-1].Message += e.Message + } else { + seq = append(seq, e) + } + now = t + } + + for i := 0; i < len(output); { + if !bytes.HasPrefix(output[i:], magic) { + // Not a header; find next header. + j := bytes.Index(output[i:], magic) + if j < 0 { + // No more headers; bail. + add(now, output[i:]) + break + } + add(now, output[i:i+j]) + i += j + } + i += len(magic) + + // Decode header. + if len(output)-i < headerLen { + return nil, errors.New("short header") + } + header := output[i : i+headerLen] + nanos := int64(binary.BigEndian.Uint64(header[0:])) + t := time.Unix(0, nanos) + if t.Before(now) { + // Force timestamps to be monotonic. (This could + // be an encoding error, which we ignore now but will + // will likely be picked up when decoding the length.) + t = now + } + n := int(binary.BigEndian.Uint32(header[8:])) + if n < 0 { + return nil, fmt.Errorf("bad length: %v", n) + } + i += headerLen + + // Slurp output. + // Truncated output is OK (probably caused by sandbox limits). + end := i + n + if end > len(output) { + end = len(output) + } + add(t, output[i:end]) + i += n + } + return +} + +// sanitize scans b for invalid utf8 code points. If found, it reconstructs +// the slice replacing the invalid codes with \uFFFD, properly encoded. +func sanitize(b []byte) []byte { + if utf8.Valid(b) { + return b + } + var buf bytes.Buffer + for len(b) > 0 { + r, size := utf8.DecodeRune(b) + b = b[size:] + buf.WriteRune(r) + } + return buf.Bytes() +}
diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go new file mode 100644 index 0000000..67e69af --- /dev/null +++ b/sandbox/sandbox.go
@@ -0,0 +1,95 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Command sandbox is an HTTP server that takes requests containing go +// source files, and builds and executes them in a NaCl sanbox. +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "path" +) + +type Request struct { + Body string +} + +type Response struct { + Errors string + Events []Event +} + +func handleCompile(w http.ResponseWriter, r *http.Request) { + var codeRequest Request + if err := json.NewDecoder(r.Body).Decode(&codeRequest); err != nil { + http.Error(w, fmt.Sprintf("request error: %v", err), http.StatusBadRequest) + return + } + resp, err := compileAndRun(&codeRequest) + if err != nil { + http.Error(w, fmt.Sprintf("sandbox error: %v", err), http.StatusInternalServerError) + return + } + if err := json.NewEncoder(w).Encode(resp); err != nil { + http.Error(w, fmt.Sprintf("response error: %v", err), http.StatusInternalServerError) + return + } +} + +func compileAndRun(req *Request) (*Response, error) { + tmpDir, err := ioutil.TempDir("", "sandbox") + if err != nil { + return nil, fmt.Errorf("error creating temp directory: %v", err) + } + defer os.RemoveAll(tmpDir) + + in := path.Join(tmpDir, "main.go") + if err := ioutil.WriteFile(in, []byte(req.Body), 0400); err != nil { + return nil, fmt.Errorf("error creating temp file %q: %v", in, err) + } + exe := path.Join(tmpDir, "a.out") + cmd := exec.Command("go", "build", "-o", exe, in) + cmd.Env = []string{ + "GOOS=nacl", + "GOARCH=amd64p32", + } + if out, err := cmd.CombinedOutput(); err != nil { + if _, ok := err.(*exec.ExitError); ok { + // build error + return &Response{ + Errors: string(out), + }, nil + } + return nil, fmt.Errorf("error building go source: %v", err) + } + // TODO(proppy): restrict run time and memory use. + cmd = exec.Command("sel_ldr_x86_64", "-l", "/dev/null", "-S", "-e", exe) + out, err := cmd.CombinedOutput() + if err != nil { + if _, ok := err.(*exec.ExitError); !ok { + return nil, fmt.Errorf("error running sandbox: %v", err) + } + } + events, err := Decode(out) + if err != nil { + return nil, fmt.Errorf("error decoding events: %v", err) + } + return &Response{ + Events: events, + }, nil +} + +func main() { + http.HandleFunc("/compile", handleCompile) + http.HandleFunc("/_ah/health", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "ok") + }) + log.Fatal(http.ListenAndServe(":8080", nil)) +}