├── .gitignore ├── screenshot.png ├── src ├── static │ ├── gopher.png │ ├── favicon.ico │ ├── godoc.css │ ├── jquery-linedtextarea.js │ ├── playground-embed.js │ ├── style.css │ └── playground.js ├── AUTHORS ├── CONTRIBUTORS ├── fake_fs.lst ├── examples │ ├── min.gotip.txt │ ├── README.md │ ├── multi.txt │ ├── sleep.txt │ ├── fib.txt │ ├── clear.txt │ ├── index-dev.txt │ ├── pi.txt │ ├── http.txt │ ├── sieve.txt │ ├── test.txt │ ├── image.txt │ ├── peano.txt │ ├── tree.txt │ ├── life.txt │ └── solitaire.txt ├── enable-fake-time.patch ├── go.mod ├── client.go ├── sandbox │ ├── sandboxtypes │ │ └── types.go │ ├── metrics.go │ ├── sandbox_test.go │ └── sandbox.go ├── version.go ├── store.go ├── logger.go ├── internal │ ├── internal_test.go │ └── internal.go ├── PATENTS ├── LICENSE ├── cache.go ├── play_test.go ├── main.go ├── fmt.go ├── edit.go ├── README.md ├── metrics.go ├── examples.go ├── vet.go ├── server.go ├── share.go ├── txtar.go ├── txtar_test.go ├── sandbox_test.go ├── fmt_test.go ├── play.go ├── edit.html ├── go.sum ├── tests.go └── server_test.go ├── .dockerignore ├── pull-images.sh ├── make-images.sh ├── init-script.sh ├── docker-compose.yml ├── docker ├── Dockerfile.sandbox ├── Dockerfile.actuator └── Dockerfile.web └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | *.exe -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ypingcn/golang-playground/next/screenshot.png -------------------------------------------------------------------------------- /src/static/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ypingcn/golang-playground/next/src/static/gopher.png -------------------------------------------------------------------------------- /src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ypingcn/golang-playground/next/src/static/favicon.ico -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | docker 4 | .dockerignore 5 | .gitignore 6 | docker-compose.yml 7 | make-images.sh 8 | README.md 9 | screenshot.png -------------------------------------------------------------------------------- /src/AUTHORS: -------------------------------------------------------------------------------- 1 | # This source code refers to The Go Authors for copyright purposes. 2 | # The master list of authors is in the main Go distribution, 3 | # visible at http://tip.golang.org/AUTHORS. 4 | -------------------------------------------------------------------------------- /src/CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This source code was written by the Go contributors. 2 | # The master list of contributors is in the main Go distribution, 3 | # visible at http://tip.golang.org/CONTRIBUTORS. 4 | -------------------------------------------------------------------------------- /pull-images.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # docker pull ypingcn/golang-playground:web-1.23.4 4 | # docker pull ypingcn/golang-playground:sandbox-1.23.4 5 | # docker pull ypingcn/golang-playground:actuator-1.23.4 6 | docker pull memcached:1.6.15-alpine 7 | -------------------------------------------------------------------------------- /src/fake_fs.lst: -------------------------------------------------------------------------------- 1 | etc src=/etc 2 | resolv.conf src=misc/nacl/testdata/empty 3 | group src=misc/nacl/testdata/group 4 | passwd src=misc/nacl/testdata/empty 5 | hosts src=misc/nacl/testdata/hosts 6 | usr src=/usr 7 | local 8 | go 9 | lib 10 | time 11 | zoneinfo.zip 12 | -------------------------------------------------------------------------------- /make-images.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker build -t ypingcn/golang-playground:web-1.23.4 -f docker/Dockerfile.web . 4 | docker build -t ypingcn/golang-playground:sandbox-1.23.4 -f docker/Dockerfile.sandbox . 5 | docker build -t ypingcn/golang-playground:actuator-1.23.4 -f docker/Dockerfile.actuator . 6 | -------------------------------------------------------------------------------- /src/examples/min.gotip.txt: -------------------------------------------------------------------------------- 1 | // Title: Generic min 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "constraints" 7 | ) 8 | 9 | func min[P constraints.Ordered](x, y P) P { 10 | if x < y { 11 | return x 12 | } else { 13 | return y 14 | } 15 | } 16 | 17 | func main() { 18 | fmt.Println(min(42, 24)) 19 | } 20 | -------------------------------------------------------------------------------- /src/examples/README.md: -------------------------------------------------------------------------------- 1 | # Playground Examples 2 | 3 | Add examples to the playground by adding files to this directory with the 4 | `.txt` file extension. 5 | 6 | Each example must start with a line beginning with "// Title:", specifying the 7 | title of the example in the selection menu. This title line will be stripped 8 | from the example before serving. 9 | -------------------------------------------------------------------------------- /src/examples/multi.txt: -------------------------------------------------------------------------------- 1 | // Title: Multiple files 2 | package main 3 | 4 | import ( 5 | "play.ground/foo" 6 | ) 7 | 8 | func main() { 9 | foo.Bar() 10 | } 11 | 12 | -- go.mod -- 13 | module play.ground 14 | 15 | -- foo/foo.go -- 16 | package foo 17 | 18 | import "fmt" 19 | 20 | func Bar() { 21 | fmt.Println("This function lives in an another file!") 22 | } 23 | -------------------------------------------------------------------------------- /src/examples/sleep.txt: -------------------------------------------------------------------------------- 1 | // Title: Sleep 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "math/rand" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | for i := 0; i < 10; i++ { 12 | dur := time.Duration(rand.Intn(1000)) * time.Millisecond 13 | fmt.Printf("Sleeping for %v\n", dur) 14 | // Sleep for a random duration between 0-1000ms 15 | time.Sleep(dur) 16 | } 17 | fmt.Println("Done!") 18 | } 19 | -------------------------------------------------------------------------------- /src/examples/fib.txt: -------------------------------------------------------------------------------- 1 | // Title: Fibonacci Closure 2 | package main 3 | 4 | import "fmt" 5 | 6 | // fib returns a function that returns 7 | // successive Fibonacci numbers. 8 | func fib() func() int { 9 | a, b := 0, 1 10 | return func() int { 11 | a, b = b, a+b 12 | return a 13 | } 14 | } 15 | 16 | func main() { 17 | f := fib() 18 | // Function calls are evaluated left-to-right. 19 | fmt.Println(f(), f(), f(), f(), f()) 20 | } 21 | -------------------------------------------------------------------------------- /src/enable-fake-time.patch: -------------------------------------------------------------------------------- 1 | --- rt0_nacl_amd64p32.s 2014-10-28 17:28:25.028188222 -0700 2 | +++ rt0_nacl_amd64p32-faketime.s 2014-10-28 17:28:06.363674896 -0700 3 | @@ -25,6 +25,6 @@ 4 | 5 | TEXT main(SB),NOSPLIT,$0 6 | // Uncomment for fake time like on Go Playground. 7 | - //MOVQ $1257894000000000000, AX 8 | - //MOVQ AX, runtime·faketime(SB) 9 | + MOVQ $1257894000000000000, AX 10 | + MOVQ AX, runtime·faketime(SB) 11 | JMP runtime·rt0_go(SB) 12 | -------------------------------------------------------------------------------- /src/examples/clear.txt: -------------------------------------------------------------------------------- 1 | // Title: Clear 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | const col = 30 12 | // Clear the screen by printing \x0c. 13 | bar := fmt.Sprintf("\x0c[%%-%vs]", col) 14 | for i := 0; i < col; i++ { 15 | fmt.Printf(bar, strings.Repeat("=", i)+">") 16 | time.Sleep(100 * time.Millisecond) 17 | } 18 | fmt.Printf(bar+" Done!", strings.Repeat("=", col)) 19 | } 20 | -------------------------------------------------------------------------------- /src/examples/index-dev.txt: -------------------------------------------------------------------------------- 1 | // Title: Generic index 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | ) 7 | 8 | // The index function returns the index of the first occurrence of v in s, 9 | // or -1 if not present. 10 | func index[E comparable](s []E, v E) int { 11 | for i, vs := range s { 12 | if v == vs { 13 | return i 14 | } 15 | } 16 | return -1 17 | } 18 | 19 | func main() { 20 | s := []int{1, 3, 5, 2, 4} 21 | fmt.Println(index(s, 3)) 22 | fmt.Println(index(s, 6)) 23 | } 24 | -------------------------------------------------------------------------------- /init-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | git config --global url."git@$GIT_HOST:".insteadof https://$GIT_HOST/ && \ 3 | git config --global url."ssh://git@$GIT_HOST:".insteadof https://$GIT_HOST/ 4 | 5 | mkdir -p -m 0600 ~/.ssh && ssh-keyscan $GIT_HOST >> ~/.ssh/known_hosts 6 | 7 | touch ~/.netrc && \ 8 | echo "machine $NETRC_MACHINE" >> ~/.netrc && \ 9 | echo "login $NETRC_LOGIN" >> ~/.netrc && \ 10 | echo "password $NETRC_TOKEN" >> ~/.netrc && \ 11 | chmod 0600 ~/.netrc 12 | 13 | /app/playground -------------------------------------------------------------------------------- /src/go.mod: -------------------------------------------------------------------------------- 1 | module golang.org/x/playground 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d 7 | github.com/google/go-cmp v0.5.8 8 | go.opencensus.io v0.23.0 9 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 10 | golang.org/x/tools v0.1.11 11 | ) 12 | 13 | require ( 14 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 15 | golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect 16 | golang.org/x/sys v0.0.0-20220624220833-87e55d714810 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /src/client.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | // Copyright 2014 The Go Authors. All rights reserved. 5 | // Use of this source code is governed by a BSD-style 6 | // license that can be found in the LICENSE file. 7 | 8 | package main 9 | 10 | import ( 11 | "encoding/json" 12 | "io/ioutil" 13 | "log" 14 | "os" 15 | ) 16 | 17 | func main() { 18 | body, err := ioutil.ReadAll(os.Stdin) 19 | if err != nil { 20 | log.Fatalf("error reading stdin: %v", err) 21 | } 22 | json.NewEncoder(os.Stdout).Encode(struct { 23 | Body string 24 | }{ 25 | Body: string(body), 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /src/examples/pi.txt: -------------------------------------------------------------------------------- 1 | // Title: Concurrent pi 2 | // Concurrent computation of pi. 3 | // See https://goo.gl/la6Kli. 4 | // 5 | // This demonstrates Go's ability to handle 6 | // large numbers of concurrent processes. 7 | // It is an unreasonable way to calculate pi. 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "math" 13 | ) 14 | 15 | func main() { 16 | fmt.Println(pi(5000)) 17 | } 18 | 19 | // pi launches n goroutines to compute an 20 | // approximation of pi. 21 | func pi(n int) float64 { 22 | ch := make(chan float64) 23 | for k := 0; k < n; k++ { 24 | go term(ch, float64(k)) 25 | } 26 | f := 0.0 27 | for k := 0; k < n; k++ { 28 | f += <-ch 29 | } 30 | return f 31 | } 32 | 33 | func term(ch chan float64, k float64) { 34 | ch <- 4 * math.Pow(-1, k) / (2*k + 1) 35 | } 36 | -------------------------------------------------------------------------------- /src/examples/http.txt: -------------------------------------------------------------------------------- 1 | // Title: HTTP server 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "log" 8 | "net" 9 | "net/http" 10 | "os" 11 | ) 12 | 13 | func main() { 14 | http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { 15 | fmt.Fprint(w, "Hello, playground") 16 | }) 17 | 18 | log.Println("Starting server...") 19 | l, err := net.Listen("tcp", "localhost:8080") 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | go func() { 24 | log.Fatal(http.Serve(l, nil)) 25 | }() 26 | 27 | log.Println("Sending request...") 28 | res, err := http.Get("http://localhost:8080/hello") 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | log.Println("Reading response...") 34 | if _, err := io.Copy(os.Stdout, res.Body); err != nil { 35 | log.Fatal(err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/sandbox/sandboxtypes/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 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 | // The sandboxtypes package contains the shared types 6 | // to communicate between the different sandbox components. 7 | package sandboxtypes 8 | 9 | // Response is the response from the x/playground/sandbox backend to 10 | // the x/playground frontend. 11 | // 12 | // The stdout/stderr are base64 encoded which isn't ideal but is good 13 | // enough for now. Maybe we'll move to protobufs later. 14 | type Response struct { 15 | // Error, if non-empty, means we failed to run the binary. 16 | // It's meant to be user-visible. 17 | Error string `json:"error,omitempty"` 18 | 19 | ExitCode int `json:"exitCode"` 20 | Stdout []byte `json:"stdout"` 21 | Stderr []byte `json:"stderr"` 22 | } 23 | -------------------------------------------------------------------------------- /src/examples/sieve.txt: -------------------------------------------------------------------------------- 1 | // Title: Concurrent Prime Sieve 2 | // A concurrent prime sieve 3 | 4 | package main 5 | 6 | import "fmt" 7 | 8 | // Send the sequence 2, 3, 4, ... to channel 'ch'. 9 | func Generate(ch chan<- int) { 10 | for i := 2; ; i++ { 11 | ch <- i // Send 'i' to channel 'ch'. 12 | } 13 | } 14 | 15 | // Copy the values from channel 'in' to channel 'out', 16 | // removing those divisible by 'prime'. 17 | func Filter(in <-chan int, out chan<- int, prime int) { 18 | for { 19 | i := <-in // Receive value from 'in'. 20 | if i%prime != 0 { 21 | out <- i // Send 'i' to 'out'. 22 | } 23 | } 24 | } 25 | 26 | // The prime sieve: Daisy-chain Filter processes. 27 | func main() { 28 | ch := make(chan int) // Create a new channel. 29 | go Generate(ch) // Launch Generate goroutine. 30 | for i := 0; i < 10; i++ { 31 | prime := <-ch 32 | fmt.Println(prime) 33 | ch1 := make(chan int) 34 | go Filter(ch, ch1, prime) 35 | ch = ch1 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 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 | package main 6 | 7 | import ( 8 | "fmt" 9 | "go/build" 10 | "net/http" 11 | "runtime" 12 | ) 13 | 14 | func (s *server) handleVersion(w http.ResponseWriter, req *http.Request) { 15 | w.Header().Set("Access-Control-Allow-Origin", "*") 16 | 17 | tag := build.Default.ReleaseTags[len(build.Default.ReleaseTags)-1] 18 | var maj, min int 19 | if _, err := fmt.Sscanf(tag, "go%d.%d", &maj, &min); err != nil { 20 | code := http.StatusInternalServerError 21 | http.Error(w, http.StatusText(code), code) 22 | return 23 | } 24 | 25 | version := struct { 26 | Version, Release, Name string 27 | }{ 28 | Version: runtime.Version(), 29 | Release: tag, 30 | } 31 | 32 | version.Name = fmt.Sprintf("Go %d.%d", maj, min) 33 | 34 | s.writeJSONResponse(w, version, http.StatusOK) 35 | } 36 | -------------------------------------------------------------------------------- /src/examples/test.txt: -------------------------------------------------------------------------------- 1 | // Title: Test 2 | package main 3 | 4 | import ( 5 | "testing" 6 | ) 7 | 8 | // LastIndex returns the index of the last instance of x in list, or 9 | // -1 if x is not present. The loop condition has a fault that 10 | // causes somes tests to fail. Change it to i >= 0 to see them pass. 11 | func LastIndex(list []int, x int) int { 12 | for i := len(list) - 1; i >= 0; i-- { 13 | if list[i] == x { 14 | return i 15 | } 16 | } 17 | return -1 18 | } 19 | 20 | func TestLastIndex(t *testing.T) { 21 | tests := []struct { 22 | list []int 23 | x int 24 | want int 25 | }{ 26 | {list: []int{1}, x: 1, want: 0}, 27 | {list: []int{1, 1}, x: 1, want: 1}, 28 | {list: []int{2, 1}, x: 2, want: 0}, 29 | {list: []int{1, 2, 1, 1}, x: 2, want: 1}, 30 | {list: []int{1, 1, 1, 2, 2, 1}, x: 3, want: -1}, 31 | {list: []int{3, 1, 2, 2, 1, 1}, x: 3, want: 0}, 32 | } 33 | for _, tt := range tests { 34 | if got := LastIndex(tt.list, tt.x); got != tt.want { 35 | t.Errorf("LastIndex(%v, %v) = %v, want %v", tt.list, tt.x, got, tt.want) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | sandbox: 5 | image: ypingcn/golang-playground:sandbox-1.23.4 6 | restart: always 7 | command: -mode=server -listen=0.0.0.0:80 -workers=1 -untrusted-container=ypingcn/golang-playground:actuator-1.23.4 8 | volumes: 9 | - /var/run/docker.sock:/var/run/docker.sock:ro 10 | networks: 11 | - playground 12 | # depends_on: 13 | # - memcached 14 | 15 | web: 16 | image: ypingcn/golang-playground:web-1.23.4 17 | restart: always 18 | environment: 19 | - SANDBOX_BACKEND_URL=http://sandbox:/run 20 | - MEMCACHED_ADDR=memcached:11211 21 | - GONOPROXY= 22 | - GONOSUMDB= 23 | - GOPRIVATE= 24 | - GIT_HOST= 25 | - NETRC_MACHINE= 26 | - NETRC_LOGIN= 27 | - NETRC_TOKEN= 28 | ports: 29 | - 8061:8080 30 | depends_on: 31 | - sandbox 32 | networks: 33 | - playground 34 | 35 | memcached: 36 | image: memcached:1.6.15-alpine 37 | command: memcached -m 64 38 | networks: 39 | - playground 40 | networks: 41 | playground: -------------------------------------------------------------------------------- /docker/Dockerfile.sandbox: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION=1.23.4 2 | FROM golang:${GO_VERSION}-alpine3.21 AS build-sandbox 3 | # base deps 4 | RUN apk update && apk add tzdata 5 | # download golang deps 6 | ENV GO111MODULE=on 7 | ENV GOPROXY="https://goproxy.cn" 8 | COPY src/go.mod /go/src/playground/go.mod 9 | COPY src/go.sum /go/src/playground/go.sum 10 | WORKDIR /go/src/playground 11 | RUN go mod download 12 | # build golang binaries 13 | COPY ./src /go/src/playground 14 | WORKDIR /go/src/playground/sandbox 15 | RUN CGO_ENABLED=0 go build -ldflags "-w -s" . 16 | 17 | 18 | 19 | FROM alpine:3.21 20 | LABEL maintainer="ypingcn@outlook.com" 21 | RUN echo '' > /etc/apk/repositories && \ 22 | echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.21/main" >> /etc/apk/repositories && \ 23 | echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.21/community" >> /etc/apk/repositories && \ 24 | echo "Asia/Shanghai" > /etc/timezone 25 | RUN apk add --no-cache docker-cli git openssl 26 | COPY --from=build-sandbox /go/src/playground/sandbox/sandbox /bin/play-sandbox 27 | COPY --from=build-sandbox /usr/share/zoneinfo /usr/share/zoneinfo 28 | ENTRYPOINT ["/bin/play-sandbox"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Playground(Golang 游乐场) 2 | 3 | 基于 [Golang 官方项目](https://github.com/golang/playground)调整而来,让你可以在本地快速启动一套 Golang Web Playground,来快速验证各种 Golang 代码片段。 4 | 5 | ![](./screenshot.png) 6 | 7 | ## 项目特点 8 | 9 | - 支持完全离线运行,无需“联网”,不需担心有任何信息泄漏的风险(比如包含密钥的程序)。 10 | - 支持使用容器进行快速启动,不锁定任何公有云或者复杂的运行环境。 11 | - 和官方程序一样,使用沙盒方式运行 Golang 程序,确保运行程序安全,无副作用。 12 | - 和官方程序一样,使用 `faketime` “模块”,让程序能够提供确定性的输出,让程序复现和结果缓存变的更加容易。 13 | - 合并了来自 `go.dev` 的默认示例,并进行了适当的界面“汉化”。 14 | - 大幅精简程序模块和依赖,减少不必要的资源消耗。 15 | 16 | ## 快速开始 17 | 18 | 想要运行程序,**首先**需要先安装 Docker 19 | 20 | **然后**,执行下面的命令或者项目中的程序(`bash make-images.sh`),构建必要的镜像文件: 21 | 22 | ```bash 23 | docker pull ypingcn/golang-playground:web-1.23.4 24 | docker pull ypingcn/golang-playground:sandbox-1.23.4 25 | docker pull ypingcn/golang-playground:actuator-1.23.4 26 | docker pull memcached:1.6.15-alpine 27 | ``` 28 | 29 | 执行命令获取依赖的镜像文件(`bash make-images.sh`) 30 | 31 | ```bash 32 | docker pull memcached:1.6.15-alpine 33 | ``` 34 | 35 | **接着**,在镜像获取完毕之后,如需使用私有仓库中的代码,请编辑 web 服务中`GOPRIVATE`、`GONOPROXY`和`GONOSUMDB`三个环境变量,将私有仓库地址添加到其中。 36 | 37 | 检查`docker_web_init_script.sh`脚本中的参数,申请账号后更新相关参数。 38 | 39 | **最后**,使用 `docker-compose up -d` 或 `docker compose up -d`,启动程序。打开浏览器,访问 `http://localhost:8080`,就可以开始 Golang 之旅啦。 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/static/godoc.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | body { 5 | color: black; 6 | padding: 0; 7 | margin: 0; 8 | width: 100%; 9 | height: 100%; 10 | } 11 | a { 12 | color: #009; 13 | } 14 | #wrap { 15 | padding: 5px; 16 | margin: 0; 17 | 18 | position: absolute; 19 | top: 50px; 20 | bottom: 0; 21 | left: 0; 22 | right: 50%; 23 | 24 | background: #FFD; 25 | } 26 | #code, pre, .lines { 27 | font-family: Menlo, Courier New, monospace; 28 | font-size: 11pt; 29 | } 30 | #code { 31 | color: black; 32 | background: inherit; 33 | 34 | width: 100%; 35 | height: 100%; 36 | padding: 0; margin: 0; 37 | border: none; 38 | outline: none; 39 | resize: none; 40 | wrap: off; 41 | float: right; 42 | } 43 | #output { 44 | position: absolute; 45 | top: 50px; 46 | bottom: 0; 47 | left: 50%; 48 | right: 0; 49 | padding: 8px; 50 | font-size: 14pt; 51 | } 52 | #banner { 53 | position: absolute; 54 | left: 0; 55 | right: 0; 56 | top: 0; 57 | height: 50px; 58 | } 59 | #head { 60 | float: left; 61 | padding: 8px; 62 | 63 | font-size: 30px; 64 | font-family: Georgia, serif; 65 | } 66 | .lines { 67 | float: left; 68 | overflow: hidden; 69 | text-align: right; 70 | } 71 | .lines div { 72 | padding-right: 5px; 73 | color: lightgray; 74 | } 75 | -------------------------------------------------------------------------------- /src/store.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 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 | package main 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "sync" 11 | ) 12 | 13 | type store interface { 14 | PutSnippet(ctx context.Context, id string, snip *snippet) error 15 | GetSnippet(ctx context.Context, id string, snip *snippet) error 16 | } 17 | 18 | // inMemStore is a store backed by a map that should only be used for testing. 19 | type inMemStore struct { 20 | sync.RWMutex 21 | m map[string]*snippet // key -> snippet 22 | } 23 | 24 | var ErrNoSuchEntity = errors.New("datastore: no such entity") 25 | 26 | func (s *inMemStore) PutSnippet(_ context.Context, id string, snip *snippet) error { 27 | s.Lock() 28 | if s.m == nil { 29 | s.m = map[string]*snippet{} 30 | } 31 | b := make([]byte, len(snip.Body)) 32 | copy(b, snip.Body) 33 | s.m[id] = &snippet{Body: b} 34 | s.Unlock() 35 | return nil 36 | } 37 | 38 | func (s *inMemStore) GetSnippet(_ context.Context, id string, snip *snippet) error { 39 | 40 | s.RLock() 41 | defer s.RUnlock() 42 | v, ok := s.m[id] 43 | if !ok { 44 | return ErrNoSuchEntity 45 | } 46 | *snip = *v 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /src/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 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 | package main 6 | 7 | import ( 8 | stdlog "log" 9 | "os" 10 | ) 11 | 12 | type logger interface { 13 | Printf(format string, args ...interface{}) 14 | Errorf(format string, args ...interface{}) 15 | Fatalf(format string, args ...interface{}) 16 | } 17 | 18 | // stdLogger implements the logger interface using the log package. 19 | // There is no need to specify a date/time prefix since stdout and stderr 20 | // are logged in StackDriver with those values already present. 21 | type stdLogger struct { 22 | stderr *stdlog.Logger 23 | stdout *stdlog.Logger 24 | } 25 | 26 | func newStdLogger() *stdLogger { 27 | return &stdLogger{ 28 | stdout: stdlog.New(os.Stdout, "", 0), 29 | stderr: stdlog.New(os.Stderr, "", 0), 30 | } 31 | } 32 | 33 | func (l *stdLogger) Printf(format string, args ...interface{}) { 34 | l.stdout.Printf(format, args...) 35 | } 36 | 37 | func (l *stdLogger) Errorf(format string, args ...interface{}) { 38 | l.stderr.Printf(format, args...) 39 | } 40 | 41 | func (l *stdLogger) Fatalf(format string, args ...interface{}) { 42 | l.stderr.Fatalf(format, args...) 43 | } 44 | -------------------------------------------------------------------------------- /src/internal/internal_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 | package internal 6 | 7 | import ( 8 | "context" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestPeriodicallyDo(t *testing.T) { 14 | ctx, cancel := context.WithCancel(context.Background()) 15 | didWork := make(chan time.Time, 2) 16 | done := make(chan interface{}) 17 | go func() { 18 | PeriodicallyDo(ctx, 100*time.Millisecond, func(ctx context.Context, t time.Time) { 19 | select { 20 | case didWork <- t: 21 | default: 22 | // No need to assert that we can't send, we just care that we sent. 23 | } 24 | }) 25 | close(done) 26 | }() 27 | 28 | select { 29 | case <-time.After(5 * time.Second): 30 | t.Error("PeriodicallyDo() never called f, wanted at least one call") 31 | case <-didWork: 32 | // PeriodicallyDo called f successfully. 33 | } 34 | 35 | select { 36 | case <-done: 37 | t.Errorf("PeriodicallyDo() finished early, wanted it to still be looping") 38 | case <-didWork: 39 | cancel() 40 | } 41 | 42 | select { 43 | case <-time.After(time.Second): 44 | t.Fatal("PeriodicallyDo() never returned, wanted return after context cancellation") 45 | case <-done: 46 | // PeriodicallyDo successfully returned. 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/PATENTS: -------------------------------------------------------------------------------- 1 | Additional IP Rights Grant (Patents) 2 | 3 | "This implementation" means the copyrightable works distributed by 4 | Google as part of the Go project. 5 | 6 | Google hereby grants to You a perpetual, worldwide, non-exclusive, 7 | no-charge, royalty-free, irrevocable (except as stated in this section) 8 | patent license to make, have made, use, offer to sell, sell, import, 9 | transfer and otherwise run, modify and propagate the contents of this 10 | implementation of Go, where such license applies only to those patent 11 | claims, both currently owned or controlled by Google and acquired in 12 | the future, licensable by Google that are necessarily infringed by this 13 | implementation of Go. This grant does not include claims that would be 14 | infringed only as a consequence of further modification of this 15 | implementation. If you or your agent or exclusive licensee institute or 16 | order or agree to the institution of patent litigation against any 17 | entity (including a cross-claim or counterclaim in a lawsuit) alleging 18 | that this implementation of Go or any code incorporated within this 19 | implementation of Go constitutes direct or contributory patent 20 | infringement, or inducement of patent infringement, then any patent 21 | rights granted to you under this License for this implementation of Go 22 | shall terminate as of the date such litigation is filed. 23 | -------------------------------------------------------------------------------- /docker/Dockerfile.actuator: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION=1.23.4 2 | FROM golang:${GO_VERSION}-alpine3.21 AS build-sandbox 3 | # base deps 4 | RUN apk update && apk add tzdata 5 | # download golang deps 6 | ENV GO111MODULE=on 7 | ENV GOPROXY="https://goproxy.cn" 8 | COPY src/go.mod /go/src/playground/go.mod 9 | COPY src/go.sum /go/src/playground/go.sum 10 | WORKDIR /go/src/playground 11 | RUN go mod download 12 | # build golang binaries 13 | COPY ./src /go/src/playground 14 | WORKDIR /go/src/playground/sandbox 15 | RUN CGO_ENABLED=0 go build -ldflags "-w -s" . 16 | 17 | 18 | 19 | FROM alpine:3.21 AS build-actuator 20 | LABEL maintainer="ypingcn@outlook.com" 21 | RUN echo '' > /etc/apk/repositories && \ 22 | echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.21/main" >> /etc/apk/repositories && \ 23 | echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.21/community" >> /etc/apk/repositories && \ 24 | echo "Asia/Shanghai" > /etc/timezone 25 | RUN apk add --no-cache docker-cli 26 | COPY --from=build-sandbox /go/src/playground/sandbox/sandbox /bin/play-sandbox 27 | COPY --from=build-sandbox /usr/share/zoneinfo /usr/share/zoneinfo 28 | ENTRYPOINT ["/bin/play-sandbox"] 29 | 30 | # FROM busybox:glibc 31 | # LABEL maintainer="ypingcn@outlook.com" 32 | # COPY --from=build-actuator /bin/play-sandbox /bin/play-sandbox 33 | # COPY --from=build-actuator /usr/share/zoneinfo /usr/share/zoneinfo 34 | # ENTRYPOINT ["/bin/play-sandbox"] -------------------------------------------------------------------------------- /src/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /src/cache.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 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 | package main 6 | 7 | import ( 8 | "bytes" 9 | "encoding/gob" 10 | 11 | "github.com/bradfitz/gomemcache/memcache" 12 | ) 13 | 14 | // responseCache is a common interface for cache implementations. 15 | type responseCache interface { 16 | // Set sets the value for a key. 17 | Set(key string, v interface{}) error 18 | // Get sets v to the value stored for a key. 19 | Get(key string, v interface{}) error 20 | } 21 | 22 | // gobCache stores and retrieves values using a memcache client using the gob 23 | // encoding package. It does not currently allow for expiration of items. 24 | // With a nil gobCache, Set is a no-op and Get will always return memcache.ErrCacheMiss. 25 | type gobCache struct { 26 | client *memcache.Client 27 | } 28 | 29 | func newGobCache(addr string) *gobCache { 30 | return &gobCache{memcache.New(addr)} 31 | } 32 | 33 | func (c *gobCache) Set(key string, v interface{}) error { 34 | if c == nil { 35 | return nil 36 | } 37 | var buf bytes.Buffer 38 | if err := gob.NewEncoder(&buf).Encode(v); err != nil { 39 | return err 40 | } 41 | return c.client.Set(&memcache.Item{Key: key, Value: buf.Bytes()}) 42 | } 43 | 44 | func (c *gobCache) Get(key string, v interface{}) error { 45 | if c == nil { 46 | return memcache.ErrCacheMiss 47 | } 48 | item, err := c.client.Get(key) 49 | if err != nil { 50 | return err 51 | } 52 | return gob.NewDecoder(bytes.NewBuffer(item.Value)).Decode(v) 53 | } 54 | -------------------------------------------------------------------------------- /docker/Dockerfile.web: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION=1.23.4 2 | FROM golang:${GO_VERSION}-alpine3.21 AS build-playground 3 | LABEL maintainer="ypingcn@outlook.com" 4 | # download golang deps 5 | ENV GO111MODULE=on 6 | ENV GOPROXY="https://goproxy.cn" 7 | COPY src/go.mod /go/src/playground/go.mod 8 | COPY src/go.sum /go/src/playground/go.sum 9 | WORKDIR /go/src/playground 10 | RUN go mod download 11 | # build golang binaries 12 | COPY ./src /go/src/playground 13 | RUN CGO_ENABLED=0 go build -ldflags "-w -s" . 14 | 15 | 16 | 17 | FROM golang:${GO_VERSION}-alpine3.21 18 | COPY --from=build-playground /usr/local/go /usr/local/go-faketime 19 | 20 | ENV CGO_ENABLED=0 21 | ENV GOPATH=/go 22 | ENV GOROOT=/usr/local/go-faketime 23 | ARG GO_VERSION 24 | ENV GO_VERSION=${GO_VERSION} 25 | ENV PATH="/go/bin:/usr/local/go-faketime/bin:${PATH}" 26 | 27 | RUN echo '' > /etc/apk/repositories && \ 28 | echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.21/main" >> /etc/apk/repositories && \ 29 | echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.21/community" >> /etc/apk/repositories 30 | RUN apk add --no-cache git openssh-client 31 | 32 | WORKDIR /usr/local/go-faketime 33 | RUN ./bin/go install --tags=faketime std 34 | RUN ./bin/go vet --tags=faketime std || true 35 | 36 | RUN mkdir /app 37 | COPY --from=build-playground /go/src/playground/playground /app 38 | COPY init-script.sh /app/init-script.sh 39 | RUN dos2unix /app/init-script.sh 40 | RUN chmod +x /app/init-script.sh 41 | COPY src/edit.html /app 42 | COPY src/static /app/static 43 | COPY src/examples /app/examples 44 | WORKDIR /app 45 | 46 | EXPOSE 8080 47 | ENTRYPOINT ["/app/init-script.sh"] -------------------------------------------------------------------------------- /src/play_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 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 | package main 6 | 7 | import ( 8 | "encoding/binary" 9 | "reflect" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestDecode(t *testing.T) { 15 | r := new(Recorder) 16 | stdout := r.Stdout() 17 | stderr := r.Stderr() 18 | 19 | stdout.Write([]byte("head")) 20 | stdout.Write(pbWrite(0, "one")) 21 | stdout.Write(pbWrite(0, "two")) 22 | 23 | stderr.Write(pbWrite(1*time.Second, "three")) 24 | stderr.Write(pbWrite(2*time.Second, "five")) 25 | stdout.Write(pbWrite(2*time.Second-time.Nanosecond, "four")) 26 | stderr.Write(pbWrite(2*time.Second, "six")) 27 | 28 | stdout.Write([]byte("middle")) 29 | stdout.Write(pbWrite(3*time.Second, "seven")) 30 | stdout.Write([]byte("tail")) 31 | 32 | want := []Event{ 33 | {"headonetwo", "stdout", 0}, 34 | {"three", "stderr", time.Second}, 35 | {"fourmiddle", "stdout", time.Second - time.Nanosecond}, 36 | {"fivesix", "stderr", time.Nanosecond}, 37 | {"seventail", "stdout", time.Second}, 38 | } 39 | 40 | got, err := r.Events() 41 | if err != nil { 42 | t.Fatalf("Decode: %v", err) 43 | } 44 | if !reflect.DeepEqual(got, want) { 45 | t.Errorf("got: \n%v,\nwant \n%v", got, want) 46 | } 47 | } 48 | 49 | func pbWrite(offset time.Duration, s string) []byte { 50 | out := make([]byte, 16) 51 | out[2] = 'P' 52 | out[3] = 'B' 53 | binary.BigEndian.PutUint64(out[4:], uint64(epoch.Add(offset).UnixNano())) 54 | binary.BigEndian.PutUint32(out[12:], uint32(len(s))) 55 | return append(out, s...) 56 | } 57 | -------------------------------------------------------------------------------- /src/examples/image.txt: -------------------------------------------------------------------------------- 1 | // Title: Display image 2 | package main 3 | 4 | import ( 5 | "bytes" 6 | "encoding/base64" 7 | "fmt" 8 | "image" 9 | "image/png" 10 | ) 11 | 12 | var favicon = []byte{ 13 | 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 14 | 0x10, 0x00, 0x00, 0x00, 0x0f, 0x04, 0x03, 0x00, 0x00, 0x00, 0x1f, 0x5d, 0x52, 0x1c, 0x00, 0x00, 0x00, 0x0f, 0x50, 15 | 0x4c, 0x54, 0x45, 0x7a, 0xdf, 0xfd, 0xfd, 0xff, 0xfc, 0x39, 0x4d, 0x52, 0x19, 0x16, 0x15, 0xc3, 0x8d, 0x76, 0xc7, 16 | 0x36, 0x2c, 0xf5, 0x00, 0x00, 0x00, 0x40, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x95, 0xc9, 0xd1, 0x0d, 0xc0, 0x20, 17 | 0x0c, 0x03, 0xd1, 0x23, 0x5d, 0xa0, 0x49, 0x17, 0x20, 0x4c, 0xc0, 0x10, 0xec, 0x3f, 0x53, 0x8d, 0xc2, 0x02, 0x9c, 18 | 0xfc, 0xf1, 0x24, 0xe3, 0x31, 0x54, 0x3a, 0xd1, 0x51, 0x96, 0x74, 0x1c, 0xcd, 0x18, 0xed, 0x9b, 0x9a, 0x11, 0x85, 19 | 0x24, 0xea, 0xda, 0xe0, 0x99, 0x14, 0xd6, 0x3a, 0x68, 0x6f, 0x41, 0xdd, 0xe2, 0x07, 0xdb, 0xb5, 0x05, 0xca, 0xdb, 20 | 0xb2, 0x9a, 0xdd, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, 21 | } 22 | 23 | // displayImage renders an image to the playground's console by 24 | // base64-encoding the encoded image and printing it to stdout 25 | // with the prefix "IMAGE:". 26 | func displayImage(m image.Image) { 27 | var buf bytes.Buffer 28 | err := png.Encode(&buf, m) 29 | if err != nil { 30 | panic(err) 31 | } 32 | fmt.Println("IMAGE:" + base64.StdEncoding.EncodeToString(buf.Bytes())) 33 | } 34 | 35 | func main() { 36 | m, err := png.Decode(bytes.NewReader(favicon)) 37 | if err != nil { 38 | panic(err) 39 | } 40 | displayImage(m) 41 | } 42 | -------------------------------------------------------------------------------- /src/static/jquery-linedtextarea.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from jQuery Lined Textarea Plugin 3 | * http://alan.blog-city.com/jquerylinedtextarea.htm 4 | * 5 | * Released under the MIT License: 6 | * http://www.opensource.org/licenses/mit-license.php 7 | */ 8 | (function($) { 9 | $.fn.linedtextarea = function() { 10 | /* 11 | * Helper function to make sure the line numbers are always kept up to 12 | * the current system 13 | */ 14 | var fillOutLines = function(linesDiv, h, lineNo) { 15 | while (linesDiv.height() < h) { 16 | linesDiv.append("
" + lineNo + "
"); 17 | lineNo++; 18 | } 19 | return lineNo; 20 | }; 21 | 22 | return this.each(function() { 23 | var lineNo = 1; 24 | var textarea = $(this); 25 | 26 | /* Wrap the text area in the elements we need */ 27 | textarea.wrap("
"); 28 | textarea.width("97%"); 29 | textarea.parent().prepend("
"); 30 | var linesDiv = textarea.parent().find(".lines"); 31 | 32 | var scroll = function(tn) { 33 | var domTextArea = $(this)[0]; 34 | var scrollTop = domTextArea.scrollTop; 35 | var clientHeight = domTextArea.clientHeight; 36 | linesDiv.css({ 37 | 'margin-top' : (-scrollTop) + "px" 38 | }); 39 | lineNo = fillOutLines(linesDiv, scrollTop + clientHeight, 40 | lineNo); 41 | }; 42 | /* React to the scroll event */ 43 | textarea.scroll(scroll); 44 | $(window).resize(function() { textarea.scroll(); }); 45 | /* We call scroll once to add the line numbers */ 46 | textarea.scroll(); 47 | }); 48 | }; 49 | 50 | })(jQuery); 51 | -------------------------------------------------------------------------------- /src/examples/peano.txt: -------------------------------------------------------------------------------- 1 | // Title: Peano Integers 2 | // Peano integers are represented by a linked 3 | // list whose nodes contain no data 4 | // (the nodes are the data). 5 | // http://en.wikipedia.org/wiki/Peano_axioms 6 | 7 | // This program demonstrates that Go's automatic 8 | // stack management can handle heavily recursive 9 | // computations. 10 | 11 | package main 12 | 13 | import "fmt" 14 | 15 | // Number is a pointer to a Number 16 | type Number *Number 17 | 18 | // The arithmetic value of a Number is the 19 | // count of the nodes comprising the list. 20 | // (See the count function below.) 21 | 22 | // ------------------------------------- 23 | // Peano primitives 24 | 25 | func zero() *Number { 26 | return nil 27 | } 28 | 29 | func isZero(x *Number) bool { 30 | return x == nil 31 | } 32 | 33 | func add1(x *Number) *Number { 34 | e := new(Number) 35 | *e = x 36 | return e 37 | } 38 | 39 | func sub1(x *Number) *Number { 40 | return *x 41 | } 42 | 43 | func add(x, y *Number) *Number { 44 | if isZero(y) { 45 | return x 46 | } 47 | return add(add1(x), sub1(y)) 48 | } 49 | 50 | func mul(x, y *Number) *Number { 51 | if isZero(x) || isZero(y) { 52 | return zero() 53 | } 54 | return add(mul(x, sub1(y)), x) 55 | } 56 | 57 | func fact(n *Number) *Number { 58 | if isZero(n) { 59 | return add1(zero()) 60 | } 61 | return mul(fact(sub1(n)), n) 62 | } 63 | 64 | // ------------------------------------- 65 | // Helpers to generate/count Peano integers 66 | 67 | func gen(n int) *Number { 68 | if n > 0 { 69 | return add1(gen(n - 1)) 70 | } 71 | return zero() 72 | } 73 | 74 | func count(x *Number) int { 75 | if isZero(x) { 76 | return 0 77 | } 78 | return count(sub1(x)) + 1 79 | } 80 | 81 | // ------------------------------------- 82 | // Print i! for i in [0,9] 83 | 84 | func main() { 85 | for i := 0; i <= 9; i++ { 86 | f := count(fact(gen(i))) 87 | fmt.Println(i, "! =", f) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 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 | package main 6 | 7 | import ( 8 | "flag" 9 | "net/http" 10 | "os" 11 | ) 12 | 13 | var log = newStdLogger() 14 | 15 | var ( 16 | runtests = flag.Bool("runtests", false, "Run integration tests instead of Playground server.") 17 | backendURL = flag.String("backend-url", "", "URL for sandbox backend that runs Go binaries.") 18 | ) 19 | 20 | func main() { 21 | flag.Parse() 22 | s, err := newServer(func(s *server) error { 23 | s.db = &inMemStore{} 24 | if caddr := os.Getenv("MEMCACHED_ADDR"); caddr != "" { 25 | s.cache = newGobCache(caddr) 26 | log.Printf("Use Memcached caching results") 27 | } else { 28 | s.cache = (*gobCache)(nil) // Use a no-op cache implementation. 29 | log.Printf("NOT caching calc results") 30 | } 31 | s.log = log 32 | execpath, _ := os.Executable() 33 | if execpath != "" { 34 | if fi, _ := os.Stat(execpath); fi != nil { 35 | s.modtime = fi.ModTime() 36 | } 37 | } 38 | eh, err := newExamplesHandler(s.modtime) 39 | if err != nil { 40 | return err 41 | } 42 | s.examples = eh 43 | return nil 44 | }) 45 | if err != nil { 46 | log.Fatalf("Error creating server: %v", err) 47 | } 48 | 49 | if *runtests { 50 | s.test() 51 | return 52 | } 53 | if *backendURL != "" { 54 | // TODO(golang.org/issue/25224) - Remove environment variable and use a flag. 55 | os.Setenv("SANDBOX_BACKEND_URL", *backendURL) 56 | } 57 | 58 | port := os.Getenv("PORT") 59 | if port == "" { 60 | port = "8080" 61 | } 62 | 63 | // Get the backend dialer warmed up. This starts 64 | // RegionInstanceGroupDialer queries and health checks. 65 | go sandboxBackendClient() 66 | 67 | log.Printf("Listening on :%v ...", port) 68 | log.Fatalf("Error listening on :%v: %v", port, http.ListenAndServe(":"+port, s)) 69 | } 70 | -------------------------------------------------------------------------------- /src/static/playground-embed.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 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 | // opts is an object with these keys 6 | // codeEl - code editor element 7 | // embedEl - embed checkbox element 8 | // embedLabelEl - embed label element, containing embedEl 9 | // embedHTMLEl - embed HTML text input element 10 | function playgroundEmbed(opts) { 11 | if (opts.codeEl === null || opts.embedEl === null || opts.embedLabelEl === null || opts.embedHTMLEl === null) { 12 | return; 13 | } 14 | 15 | var code = $(opts.codeEl); 16 | var embed = $(opts.embedEl); 17 | var embedLabel = $(opts.embedLabelEl); 18 | 19 | function inIFrame(){ 20 | return window.self !== window.top; 21 | } 22 | embedLabel.hide(); 23 | if (inIFrame()) { 24 | $("body").addClass("embedded"); 25 | return; 26 | } 27 | 28 | function origin(href) { 29 | return (""+href).split("/").slice(0, 3).join("/"); 30 | } 31 | 32 | function inputChanged() { 33 | embedLabel.hide(); 34 | } 35 | 36 | if (window.history && window.history.pushState && window.addEventListener) { 37 | code[0].addEventListener('input', inputChanged); 38 | } 39 | 40 | var embedHTML = $(opts.embedHTMLEl).hide(); 41 | var embedding = false; 42 | embed.change(function() { 43 | if (embedding) return; 44 | embedding = true; 45 | var embeddingData = code.val(); 46 | $.ajax("/share", { 47 | processData: false, 48 | data: embeddingData, 49 | type: "POST", 50 | complete: function(xhr) { 51 | embedding = false; 52 | if (xhr.status != 200) { 53 | alert("Server error; try again."); 54 | return; 55 | } 56 | if (embedHTML) { 57 | var path = "/p/" + xhr.responseText; 58 | var url = origin(window.location) + path; 59 | if (embed.prop('checked')){ 60 | url = ""; 61 | } 62 | embedHTML.show().val(url).focus().select(); 63 | } 64 | } 65 | }); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /src/fmt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 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 | package main 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "go/format" 11 | "net/http" 12 | "path" 13 | 14 | "golang.org/x/mod/modfile" 15 | "golang.org/x/tools/imports" 16 | ) 17 | 18 | type fmtResponse struct { 19 | Body string 20 | Error string 21 | } 22 | 23 | func (s *server) handleFmt(w http.ResponseWriter, r *http.Request) { 24 | w.Header().Set("Access-Control-Allow-Origin", "*") 25 | if r.Method == "OPTIONS" { 26 | // This is likely a pre-flight CORS request. 27 | return 28 | } 29 | w.Header().Set("Content-Type", "application/json") 30 | 31 | fs, err := splitFiles([]byte(r.FormValue("body"))) 32 | if err != nil { 33 | json.NewEncoder(w).Encode(fmtResponse{Error: err.Error()}) 34 | return 35 | } 36 | 37 | fixImports := r.FormValue("imports") != "" 38 | for _, f := range fs.files { 39 | switch { 40 | case path.Ext(f) == ".go": 41 | var out []byte 42 | var err error 43 | in := fs.Data(f) 44 | if fixImports { 45 | // TODO: pass options to imports.Process so it 46 | // can find symbols in sibling files. 47 | out, err = imports.Process(f, in, nil) 48 | } else { 49 | out, err = format.Source(in) 50 | } 51 | if err != nil { 52 | errMsg := err.Error() 53 | if !fixImports { 54 | // Unlike imports.Process, format.Source does not prefix 55 | // the error with the file path. So, do it ourselves here. 56 | errMsg = fmt.Sprintf("%v:%v", f, errMsg) 57 | } 58 | json.NewEncoder(w).Encode(fmtResponse{Error: errMsg}) 59 | return 60 | } 61 | fs.AddFile(f, out) 62 | case path.Base(f) == "go.mod": 63 | out, err := formatGoMod(f, fs.Data(f)) 64 | if err != nil { 65 | json.NewEncoder(w).Encode(fmtResponse{Error: err.Error()}) 66 | return 67 | } 68 | fs.AddFile(f, out) 69 | } 70 | } 71 | 72 | s.writeJSONResponse(w, fmtResponse{Body: string(fs.Format())}, http.StatusOK) 73 | } 74 | 75 | func formatGoMod(file string, data []byte) ([]byte, error) { 76 | f, err := modfile.Parse(file, data, nil) 77 | if err != nil { 78 | return nil, err 79 | } 80 | return f.Format() 81 | } 82 | -------------------------------------------------------------------------------- /src/edit.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 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 | package main 6 | 7 | import ( 8 | "fmt" 9 | "html/template" 10 | "net/http" 11 | "runtime" 12 | "strings" 13 | ) 14 | 15 | var editTemplate = template.Must(template.ParseFiles("edit.html")) 16 | 17 | type editData struct { 18 | Snippet *snippet 19 | Share bool 20 | GoVersion string 21 | Examples []example 22 | } 23 | 24 | func (s *server) handleEdit(w http.ResponseWriter, r *http.Request) { 25 | w.Header().Set("Access-Control-Allow-Origin", "*") 26 | if r.Method == "OPTIONS" { 27 | // This is likely a pre-flight CORS request. 28 | return 29 | } 30 | 31 | // Serve 404 for /foo. 32 | if r.URL.Path != "/" && !strings.HasPrefix(r.URL.Path, "/p/") { 33 | http.NotFound(w, r) 34 | return 35 | } 36 | 37 | snip := &snippet{Body: []byte(s.examples.hello())} 38 | if strings.HasPrefix(r.URL.Path, "/p/") { 39 | if !allowShare(r) { 40 | w.WriteHeader(http.StatusUnavailableForLegalReasons) 41 | w.Write([]byte(`

Unavailable For Legal Reasons

Viewing and/or sharing code snippets is not available in your country for legal reasons. This message might also appear if your country is misdetected. If you believe this is an error, please file an issue.

`)) 42 | return 43 | } 44 | id := r.URL.Path[3:] 45 | serveText := false 46 | if strings.HasSuffix(id, ".go") { 47 | id = id[:len(id)-3] 48 | serveText = true 49 | } 50 | 51 | if err := s.db.GetSnippet(r.Context(), id, snip); err != nil { 52 | http.Error(w, "Snippet not found", http.StatusNotFound) 53 | return 54 | } 55 | if serveText { 56 | if r.FormValue("download") == "true" { 57 | w.Header().Set( 58 | "Content-Disposition", fmt.Sprintf(`attachment; filename="%s.go"`, id), 59 | ) 60 | } 61 | w.Header().Set("Content-type", "text/plain; charset=utf-8") 62 | w.Write(snip.Body) 63 | return 64 | } 65 | } 66 | 67 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 68 | data := &editData{ 69 | Snippet: snip, 70 | Share: allowShare(r), 71 | GoVersion: runtime.Version(), 72 | Examples: s.examples.examples, 73 | } 74 | if err := editTemplate.Execute(w, data); err != nil { 75 | s.log.Errorf("editTemplate.Execute(w, %+v): %v", data, err) 76 | return 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/examples/tree.txt: -------------------------------------------------------------------------------- 1 | // Title: Tree Comparison 2 | // Go's concurrency primitives make it easy to 3 | // express concurrent concepts, such as 4 | // this binary tree comparison. 5 | // 6 | // Trees may be of different shapes, 7 | // but have the same contents. For example: 8 | // 9 | // 4 6 10 | // 2 6 4 7 11 | // 1 3 5 7 2 5 12 | // 1 3 13 | // 14 | // This program compares a pair of trees by 15 | // walking each in its own goroutine, 16 | // sending their contents through a channel 17 | // to a third goroutine that compares them. 18 | 19 | package main 20 | 21 | import ( 22 | "fmt" 23 | "math/rand" 24 | ) 25 | 26 | // A Tree is a binary tree with integer values. 27 | type Tree struct { 28 | Left *Tree 29 | Value int 30 | Right *Tree 31 | } 32 | 33 | // Walk traverses a tree depth-first, 34 | // sending each Value on a channel. 35 | func Walk(t *Tree, ch chan int) { 36 | if t == nil { 37 | return 38 | } 39 | Walk(t.Left, ch) 40 | ch <- t.Value 41 | Walk(t.Right, ch) 42 | } 43 | 44 | // Walker launches Walk in a new goroutine, 45 | // and returns a read-only channel of values. 46 | func Walker(t *Tree) <-chan int { 47 | ch := make(chan int) 48 | go func() { 49 | Walk(t, ch) 50 | close(ch) 51 | }() 52 | return ch 53 | } 54 | 55 | // Compare reads values from two Walkers 56 | // that run simultaneously, and returns true 57 | // if t1 and t2 have the same contents. 58 | func Compare(t1, t2 *Tree) bool { 59 | c1, c2 := Walker(t1), Walker(t2) 60 | for { 61 | v1, ok1 := <-c1 62 | v2, ok2 := <-c2 63 | if !ok1 || !ok2 { 64 | return ok1 == ok2 65 | } 66 | if v1 != v2 { 67 | break 68 | } 69 | } 70 | return false 71 | } 72 | 73 | // New returns a new, random binary tree 74 | // holding the values 1k, 2k, ..., nk. 75 | func New(n, k int) *Tree { 76 | var t *Tree 77 | for _, v := range rand.Perm(n) { 78 | t = insert(t, (1+v)*k) 79 | } 80 | return t 81 | } 82 | 83 | func insert(t *Tree, v int) *Tree { 84 | if t == nil { 85 | return &Tree{nil, v, nil} 86 | } 87 | if v < t.Value { 88 | t.Left = insert(t.Left, v) 89 | return t 90 | } 91 | t.Right = insert(t.Right, v) 92 | return t 93 | } 94 | 95 | func main() { 96 | t1 := New(100, 1) 97 | fmt.Println(Compare(t1, New(100, 1)), "Same Contents") 98 | fmt.Println(Compare(t1, New(99, 1)), "Differing Sizes") 99 | fmt.Println(Compare(t1, New(100, 2)), "Differing Values") 100 | fmt.Println(Compare(t1, New(101, 2)), "Dissimilar") 101 | } 102 | -------------------------------------------------------------------------------- /src/internal/internal.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 | package internal 6 | 7 | import ( 8 | "context" 9 | "os" 10 | "os/exec" 11 | "time" 12 | ) 13 | 14 | // WaitOrStop waits for the already-started command cmd by calling its Wait method. 15 | // 16 | // If cmd does not return before ctx is done, WaitOrStop sends it the given interrupt signal. 17 | // If killDelay is positive, WaitOrStop waits that additional period for Wait to return before sending os.Kill. 18 | func WaitOrStop(ctx context.Context, cmd *exec.Cmd, interrupt os.Signal, killDelay time.Duration) error { 19 | if cmd.Process == nil { 20 | panic("WaitOrStop called with a nil cmd.Process — missing Start call?") 21 | } 22 | if interrupt == nil { 23 | panic("WaitOrStop requires a non-nil interrupt signal") 24 | } 25 | 26 | errc := make(chan error) 27 | go func() { 28 | select { 29 | case errc <- nil: 30 | return 31 | case <-ctx.Done(): 32 | } 33 | 34 | err := cmd.Process.Signal(interrupt) 35 | if err == nil { 36 | err = ctx.Err() // Report ctx.Err() as the reason we interrupted. 37 | } else if err.Error() == "os: process already finished" { 38 | errc <- nil 39 | return 40 | } 41 | 42 | if killDelay > 0 { 43 | timer := time.NewTimer(killDelay) 44 | select { 45 | // Report ctx.Err() as the reason we interrupted the process... 46 | case errc <- ctx.Err(): 47 | timer.Stop() 48 | return 49 | // ...but after killDelay has elapsed, fall back to a stronger signal. 50 | case <-timer.C: 51 | } 52 | 53 | // Wait still hasn't returned. 54 | // Kill the process harder to make sure that it exits. 55 | // 56 | // Ignore any error: if cmd.Process has already terminated, we still 57 | // want to send ctx.Err() (or the error from the Interrupt call) 58 | // to properly attribute the signal that may have terminated it. 59 | _ = cmd.Process.Kill() 60 | } 61 | 62 | errc <- err 63 | }() 64 | 65 | waitErr := cmd.Wait() 66 | if interruptErr := <-errc; interruptErr != nil { 67 | return interruptErr 68 | } 69 | return waitErr 70 | } 71 | 72 | // PeriodicallyDo calls f every period until the provided context is cancelled. 73 | func PeriodicallyDo(ctx context.Context, period time.Duration, f func(context.Context, time.Time)) { 74 | ticker := time.NewTicker(period) 75 | defer ticker.Stop() 76 | for { 77 | select { 78 | case <-ctx.Done(): 79 | return 80 | case now := <-ticker.C: 81 | f(ctx, now) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # playground 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/golang.org/x/playground.svg)](https://pkg.go.dev/golang.org/x/playground) 4 | 5 | This subrepository holds the source for the Go playground: 6 | https://play.golang.org/ 7 | 8 | ## Building 9 | 10 | ```bash 11 | # build the image 12 | docker build -t golang/playground . 13 | ``` 14 | 15 | ## Running 16 | 17 | ```bash 18 | docker run --name=play --rm -p 8080:8080 golang/playground & 19 | # run some Go code 20 | cat /path/to/code.go | go run client.go | curl -s --upload-file - localhost:8080/compile 21 | ``` 22 | 23 | To run the "gotip" version of the playground, set `GOTIP=true` 24 | in your environment (via `-e GOTIP=true` if using `docker run`). 25 | 26 | ## Deployment 27 | 28 | ### Deployment Triggers 29 | 30 | Playground releases automatically triggered when new Go repository tags are pushed to GitHub, or when master is pushed 31 | on the playground repository. 32 | 33 | For details, see [deploy/go_trigger.yaml](deploy/go_trigger.yaml), 34 | [deploy/playground_trigger.yaml](deploy/playground_trigger.yaml), 35 | and [deploy/deploy.json](deploy/deploy.json). 36 | 37 | Changes to the trigger configuration can be made to the YAML files, or in the GCP UI, which should be kept in sync 38 | using the `push-cloudbuild-triggers` and `pull-cloudbuild-triggers` make targets. 39 | 40 | ### Deploy via Cloud Build 41 | 42 | The Cloud Build configuration will always build and deploy with the latest supported release of Go. 43 | 44 | ```bash 45 | gcloud --project=golang-org builds submit --config deploy/deploy.json . 46 | ``` 47 | 48 | To deploy the "Go tip" version of the playground, which uses the latest 49 | development build, use `deploy_gotip.json` instead: 50 | 51 | ```bash 52 | gcloud --project=golang-org builds submit --config deploy/deploy_gotip.json . 53 | ``` 54 | 55 | ### Deploy via gcloud app deploy 56 | 57 | Building the playground Docker container takes more than the default 10 minute time limit of cloud build, so increase 58 | its timeout first (note, `app/cloud_build_timeout` is a global configuration value): 59 | 60 | ```bash 61 | gcloud config set app/cloud_build_timeout 1200 # 20 mins 62 | ``` 63 | 64 | Alternatively, to avoid Cloud Build and build locally: 65 | 66 | ```bash 67 | make docker 68 | docker tag golang/playground:latest gcr.io/golang-org/playground:latest 69 | docker push gcr.io/golang-org/playground:latest 70 | gcloud --project=golang-org --account=you@google.com app deploy app.yaml --image-url=gcr.io/golang-org/playground:latest 71 | ``` 72 | 73 | Then: 74 | 75 | ```bash 76 | gcloud --project=golang-org --account=you@google.com app deploy app.yaml 77 | ``` 78 | 79 | ## Contributing 80 | 81 | To submit changes to this repository, see 82 | https://golang.org/doc/contribute.html. 83 | -------------------------------------------------------------------------------- /src/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 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 | package main 6 | 7 | import ( 8 | "go.opencensus.io/stats" 9 | "go.opencensus.io/stats/view" 10 | "go.opencensus.io/tag" 11 | ) 12 | 13 | var ( 14 | BuildLatencyDistribution = view.Distribution(1, 5, 10, 15, 20, 25, 50, 75, 100, 125, 150, 200, 250, 300, 400, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 5500, 6000, 7000, 8000, 9000, 10000, 20000, 30000) 15 | kGoBuildSuccess = tag.MustNewKey("go-playground/frontend/go_build_success") 16 | kGoRunSuccess = tag.MustNewKey("go-playground/frontend/go_run_success") 17 | kGoVetSuccess = tag.MustNewKey("go-playground/frontend/go_vet_success") 18 | mGoBuildLatency = stats.Float64("go-playground/frontend/go_build_latency", "", stats.UnitMilliseconds) 19 | mGoRunLatency = stats.Float64("go-playground/frontend/go_run_latency", "", stats.UnitMilliseconds) 20 | mGoVetLatency = stats.Float64("go-playground/frontend/go_vet_latency", "", stats.UnitMilliseconds) 21 | 22 | goBuildCount = &view.View{ 23 | Name: "go-playground/frontend/go_build_count", 24 | Description: "Number of snippets built", 25 | Measure: mGoBuildLatency, 26 | TagKeys: []tag.Key{kGoBuildSuccess}, 27 | Aggregation: view.Count(), 28 | } 29 | goBuildLatency = &view.View{ 30 | Name: "go-playground/frontend/go_build_latency", 31 | Description: "Latency distribution of building snippets", 32 | Measure: mGoBuildLatency, 33 | Aggregation: BuildLatencyDistribution, 34 | } 35 | goRunCount = &view.View{ 36 | Name: "go-playground/frontend/go_run_count", 37 | Description: "Number of snippets run", 38 | Measure: mGoRunLatency, 39 | TagKeys: []tag.Key{kGoRunSuccess}, 40 | Aggregation: view.Count(), 41 | } 42 | goRunLatency = &view.View{ 43 | Name: "go-playground/frontend/go_run_latency", 44 | Description: "Latency distribution of running snippets", 45 | Measure: mGoRunLatency, 46 | Aggregation: BuildLatencyDistribution, 47 | } 48 | goVetCount = &view.View{ 49 | Name: "go-playground/frontend/go_vet_count", 50 | Description: "Number of vet runs", 51 | Measure: mGoVetLatency, 52 | TagKeys: []tag.Key{kGoVetSuccess}, 53 | Aggregation: view.Count(), 54 | } 55 | goVetLatency = &view.View{ 56 | Name: "go-playground/sandbox/go_vet_latency", 57 | Description: "Latency distribution of vet runs", 58 | Measure: mGoVetLatency, 59 | Aggregation: BuildLatencyDistribution, 60 | } 61 | ) 62 | 63 | // views should contain all measurements. All *view.View added to this 64 | // slice will be registered and exported to the metric service. 65 | var views = []*view.View{ 66 | goBuildCount, 67 | goBuildLatency, 68 | goRunCount, 69 | goRunLatency, 70 | goVetCount, 71 | goVetLatency, 72 | } 73 | -------------------------------------------------------------------------------- /src/examples/life.txt: -------------------------------------------------------------------------------- 1 | // Title: Conway's Game of Life 2 | // An implementation of Conway's Game of Life. 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "math/rand" 9 | "time" 10 | ) 11 | 12 | // Field represents a two-dimensional field of cells. 13 | type Field struct { 14 | s [][]bool 15 | w, h int 16 | } 17 | 18 | // NewField returns an empty field of the specified width and height. 19 | func NewField(w, h int) *Field { 20 | s := make([][]bool, h) 21 | for i := range s { 22 | s[i] = make([]bool, w) 23 | } 24 | return &Field{s: s, w: w, h: h} 25 | } 26 | 27 | // Set sets the state of the specified cell to the given value. 28 | func (f *Field) Set(x, y int, b bool) { 29 | f.s[y][x] = b 30 | } 31 | 32 | // Alive reports whether the specified cell is alive. 33 | // If the x or y coordinates are outside the field boundaries they are wrapped 34 | // toroidally. For instance, an x value of -1 is treated as width-1. 35 | func (f *Field) Alive(x, y int) bool { 36 | x += f.w 37 | x %= f.w 38 | y += f.h 39 | y %= f.h 40 | return f.s[y][x] 41 | } 42 | 43 | // Next returns the state of the specified cell at the next time step. 44 | func (f *Field) Next(x, y int) bool { 45 | // Count the adjacent cells that are alive. 46 | alive := 0 47 | for i := -1; i <= 1; i++ { 48 | for j := -1; j <= 1; j++ { 49 | if (j != 0 || i != 0) && f.Alive(x+i, y+j) { 50 | alive++ 51 | } 52 | } 53 | } 54 | // Return next state according to the game rules: 55 | // exactly 3 neighbors: on, 56 | // exactly 2 neighbors: maintain current state, 57 | // otherwise: off. 58 | return alive == 3 || alive == 2 && f.Alive(x, y) 59 | } 60 | 61 | // Life stores the state of a round of Conway's Game of Life. 62 | type Life struct { 63 | a, b *Field 64 | w, h int 65 | } 66 | 67 | // NewLife returns a new Life game state with a random initial state. 68 | func NewLife(w, h int) *Life { 69 | a := NewField(w, h) 70 | for i := 0; i < (w * h / 4); i++ { 71 | a.Set(rand.Intn(w), rand.Intn(h), true) 72 | } 73 | return &Life{ 74 | a: a, b: NewField(w, h), 75 | w: w, h: h, 76 | } 77 | } 78 | 79 | // Step advances the game by one instant, recomputing and updating all cells. 80 | func (l *Life) Step() { 81 | // Update the state of the next field (b) from the current field (a). 82 | for y := 0; y < l.h; y++ { 83 | for x := 0; x < l.w; x++ { 84 | l.b.Set(x, y, l.a.Next(x, y)) 85 | } 86 | } 87 | // Swap fields a and b. 88 | l.a, l.b = l.b, l.a 89 | } 90 | 91 | // String returns the game board as a string. 92 | func (l *Life) String() string { 93 | var buf bytes.Buffer 94 | for y := 0; y < l.h; y++ { 95 | for x := 0; x < l.w; x++ { 96 | b := byte(' ') 97 | if l.a.Alive(x, y) { 98 | b = '*' 99 | } 100 | buf.WriteByte(b) 101 | } 102 | buf.WriteByte('\n') 103 | } 104 | return buf.String() 105 | } 106 | 107 | func main() { 108 | l := NewLife(40, 15) 109 | for i := 0; i < 300; i++ { 110 | l.Step() 111 | fmt.Print("\x0c", l) // Clear screen and print field. 112 | time.Sleep(time.Second / 30) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/examples/solitaire.txt: -------------------------------------------------------------------------------- 1 | // Title: Peg Solitaire Solver 2 | // This program solves the (English) peg 3 | // solitaire board game. 4 | // http://en.wikipedia.org/wiki/Peg_solitaire 5 | 6 | package main 7 | 8 | import "fmt" 9 | 10 | const N = 11 + 1 // length of a row (+1 for \n) 11 | 12 | // The board must be surrounded by 2 illegal 13 | // fields in each direction so that move() 14 | // doesn't need to check the board boundaries. 15 | // Periods represent illegal fields, 16 | // ● are pegs, and ○ are holes. 17 | 18 | var board = []rune( 19 | `........... 20 | ........... 21 | ....●●●.... 22 | ....●●●.... 23 | ..●●●●●●●.. 24 | ..●●●○●●●.. 25 | ..●●●●●●●.. 26 | ....●●●.... 27 | ....●●●.... 28 | ........... 29 | ........... 30 | `) 31 | 32 | // center is the position of the center hole if 33 | // there is a single one; otherwise it is -1. 34 | var center int 35 | 36 | func init() { 37 | n := 0 38 | for pos, field := range board { 39 | if field == '○' { 40 | center = pos 41 | n++ 42 | } 43 | } 44 | if n != 1 { 45 | center = -1 // no single hole 46 | } 47 | } 48 | 49 | var moves int // number of times move is called 50 | 51 | // move tests if there is a peg at position pos that 52 | // can jump over another peg in direction dir. If the 53 | // move is valid, it is executed and move returns true. 54 | // Otherwise, move returns false. 55 | func move(pos, dir int) bool { 56 | moves++ 57 | if board[pos] == '●' && board[pos+dir] == '●' && board[pos+2*dir] == '○' { 58 | board[pos] = '○' 59 | board[pos+dir] = '○' 60 | board[pos+2*dir] = '●' 61 | return true 62 | } 63 | return false 64 | } 65 | 66 | // unmove reverts a previously executed valid move. 67 | func unmove(pos, dir int) { 68 | board[pos] = '●' 69 | board[pos+dir] = '●' 70 | board[pos+2*dir] = '○' 71 | } 72 | 73 | // solve tries to find a sequence of moves such that 74 | // there is only one peg left at the end; if center is 75 | // >= 0, that last peg must be in the center position. 76 | // If a solution is found, solve prints the board after 77 | // each move in a backward fashion (i.e., the last 78 | // board position is printed first, all the way back to 79 | // the starting board position). 80 | func solve() bool { 81 | var last, n int 82 | for pos, field := range board { 83 | // try each board position 84 | if field == '●' { 85 | // found a peg 86 | for _, dir := range [...]int{-1, -N, +1, +N} { 87 | // try each direction 88 | if move(pos, dir) { 89 | // a valid move was found and executed, 90 | // see if this new board has a solution 91 | if solve() { 92 | unmove(pos, dir) 93 | fmt.Println(string(board)) 94 | return true 95 | } 96 | unmove(pos, dir) 97 | } 98 | } 99 | last = pos 100 | n++ 101 | } 102 | } 103 | // tried each possible move 104 | if n == 1 && (center < 0 || last == center) { 105 | // there's only one peg left 106 | fmt.Println(string(board)) 107 | return true 108 | } 109 | // no solution found for this board 110 | return false 111 | } 112 | 113 | func main() { 114 | if !solve() { 115 | fmt.Println("no solution found") 116 | } 117 | fmt.Println(moves, "moves tried") 118 | } 119 | -------------------------------------------------------------------------------- /src/examples.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 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 | package main 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "sort" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | // examplesHandler serves example content out of the examples directory. 18 | type examplesHandler struct { 19 | modtime time.Time 20 | examples []example 21 | } 22 | 23 | type example struct { 24 | Title string 25 | Path string 26 | Content string 27 | } 28 | 29 | func (h *examplesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 30 | w.Header().Set("Access-Control-Allow-Origin", "*") 31 | for _, e := range h.examples { 32 | if e.Path == req.URL.Path { 33 | http.ServeContent(w, req, e.Path, h.modtime, strings.NewReader(e.Content)) 34 | return 35 | } 36 | } 37 | http.NotFound(w, req) 38 | } 39 | 40 | // hello returns the hello text for this instance, which depends on the Go version. 41 | func (h *examplesHandler) hello() string { 42 | return h.examples[0].Content 43 | } 44 | 45 | // newExamplesHandler reads from the examples directory, returning a handler to 46 | // serve their content. 47 | // 48 | // Examples must start with a line beginning "// Title:" that sets their title. 49 | // 50 | // modtime is used for content caching headers. 51 | func newExamplesHandler(modtime time.Time) (*examplesHandler, error) { 52 | const dir = "examples" 53 | entries, err := os.ReadDir(dir) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | var examples []example 59 | for _, entry := range entries { 60 | name := entry.Name() 61 | 62 | // Read examples ending in .txt 63 | prefix := "" // if non-empty, this is a relevant example file 64 | if strings.HasSuffix(name, ".txt") { 65 | prefix = strings.TrimSuffix(name, ".txt") 66 | } 67 | 68 | if prefix == "" { 69 | continue 70 | } 71 | 72 | data, err := os.ReadFile(filepath.Join(dir, name)) 73 | if err != nil { 74 | return nil, err 75 | } 76 | content := string(data) 77 | 78 | // Extract the magic "// Title:" comment specifying the example's title. 79 | nl := strings.IndexByte(content, '\n') 80 | const titlePrefix = "// Title:" 81 | if nl == -1 || !strings.HasPrefix(content, titlePrefix) { 82 | return nil, fmt.Errorf("malformed example for %q: must start with a title line beginning %q", name, titlePrefix) 83 | } 84 | title := strings.TrimPrefix(content[:nl], titlePrefix) 85 | title = strings.TrimSpace(title) 86 | 87 | examples = append(examples, example{ 88 | Title: title, 89 | Path: name, 90 | Content: content[nl+1:], 91 | }) 92 | } 93 | 94 | // Sort by title, before prepending the hello example (we always want Hello 95 | // to be first). 96 | sort.Slice(examples, func(i, j int) bool { 97 | return examples[i].Title < examples[j].Title 98 | }) 99 | 100 | examples = append([]example{ 101 | {"Hello, playground", "hello.txt", hello}, 102 | }, examples...) 103 | return &examplesHandler{ 104 | modtime: modtime, 105 | examples: examples, 106 | }, nil 107 | } 108 | 109 | const hello = `package main 110 | 111 | import ( 112 | "fmt" 113 | ) 114 | 115 | func main() { 116 | fmt.Println("Hello, playground") 117 | } 118 | ` 119 | -------------------------------------------------------------------------------- /src/vet.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 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 | package main 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "io/ioutil" 11 | "os" 12 | "os/exec" 13 | "path/filepath" 14 | "strings" 15 | "time" 16 | 17 | "go.opencensus.io/stats" 18 | "go.opencensus.io/tag" 19 | ) 20 | 21 | // vetCheck runs the "vet" tool on the source code in req.Body. 22 | // In case of no errors it returns an empty, non-nil *response. 23 | // Otherwise &response.Errors contains found errors. 24 | // 25 | // Deprecated: this is the handler for the legacy /vet endpoint; use 26 | // the /compile (compileAndRun) handler instead with the WithVet 27 | // boolean set. This code path doesn't support modules and only exists 28 | // as a temporary compatibility bridge to older javascript clients. 29 | func vetCheck(ctx context.Context, req *request) (*response, error) { 30 | tmpDir, err := ioutil.TempDir("", "vet") 31 | if err != nil { 32 | return nil, fmt.Errorf("error creating temp directory: %v", err) 33 | } 34 | defer os.RemoveAll(tmpDir) 35 | 36 | in := filepath.Join(tmpDir, progName) 37 | if err := ioutil.WriteFile(in, []byte(req.Body), 0400); err != nil { 38 | return nil, fmt.Errorf("error creating temp file %q: %v", in, err) 39 | } 40 | vetOutput, err := vetCheckInDir(ctx, tmpDir, os.Getenv("GOPATH")) 41 | if err != nil { 42 | // This is about errors running vet, not vet returning output. 43 | return nil, err 44 | } 45 | return &response{Errors: vetOutput}, nil 46 | } 47 | 48 | // vetCheckInDir runs go vet in the provided directory, using the 49 | // provided GOPATH value. The returned error is only about whether 50 | // go vet was able to run, not whether vet reported problem. The 51 | // returned value is ("", nil) if vet successfully found nothing, 52 | // and (non-empty, nil) if vet ran and found issues. 53 | func vetCheckInDir(ctx context.Context, dir, goPath string) (output string, execErr error) { 54 | start := time.Now() 55 | defer func() { 56 | status := "success" 57 | if execErr != nil { 58 | status = "error" 59 | } 60 | // Ignore error. The only error can be invalid tag key or value 61 | // length, which we know are safe. 62 | stats.RecordWithTags(ctx, []tag.Mutator{tag.Upsert(kGoVetSuccess, status)}, 63 | mGoVetLatency.M(float64(time.Since(start))/float64(time.Millisecond))) 64 | }() 65 | 66 | cmd := exec.Command("go", "vet", "--tags=faketime", "--mod=mod") 67 | cmd.Dir = dir 68 | // Linux go binary is not built with CGO_ENABLED=0. 69 | // Prevent vet to compile packages in cgo mode. 70 | // See #26307. 71 | cmd.Env = append(os.Environ(), "CGO_ENABLED=0", "GOPATH="+goPath) 72 | cmd.Env = append(cmd.Env, 73 | "GO111MODULE=on", 74 | "GOPROXY="+playgroundGoproxy(), 75 | ) 76 | out, err := cmd.CombinedOutput() 77 | if err == nil { 78 | return "", nil 79 | } 80 | if _, ok := err.(*exec.ExitError); !ok { 81 | return "", fmt.Errorf("error vetting go source: %v", err) 82 | } 83 | 84 | // Rewrite compiler errors to refer to progName 85 | // instead of '/tmp/sandbox1234/main.go'. 86 | errs := strings.Replace(string(out), dir, "", -1) 87 | 88 | // Remove vet's package name banner. 89 | if strings.HasPrefix(errs, "#") { 90 | if nl := strings.Index(errs, "\n"); nl != -1 { 91 | errs = errs[nl+1:] 92 | } 93 | } 94 | return errs, nil 95 | } 96 | -------------------------------------------------------------------------------- /src/static/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | body { 5 | color: black; 6 | padding: 0; 7 | margin: 0; 8 | width: 100%; 9 | height: 100%; 10 | } 11 | a { 12 | color: #009; 13 | } 14 | #wrap, 15 | #about { 16 | padding: 5px; 17 | margin: 0; 18 | 19 | position: absolute; 20 | top: 50px; 21 | bottom: 25%; 22 | left: 0; 23 | right: 0; 24 | 25 | background: #FFD; 26 | } 27 | #about { 28 | display: none; 29 | z-index: 1; 30 | padding: 10px 40px; 31 | font-size: 16px; 32 | font-family: sans-serif; 33 | overflow: auto; 34 | } 35 | #about p { 36 | max-width: 520px; 37 | } 38 | #about ul { 39 | max-width: 480px; 40 | } 41 | #about li { 42 | margin-bottom: 1em; 43 | } 44 | #code, #output, pre, .lines { 45 | /* The default monospace font on OS X is ugly, so specify Menlo 46 | * instead. On other systems the default monospace font will be used. */ 47 | font-family: Menlo, monospace; 48 | font-size: 11pt; 49 | } 50 | 51 | #code { 52 | color: black; 53 | background: inherit; 54 | 55 | width: 100%; 56 | height: 100%; 57 | padding: 0; margin: 0; 58 | border: none; 59 | outline: none; 60 | resize: none; 61 | wrap: off; 62 | float: right; 63 | } 64 | #output { 65 | position: absolute; 66 | top: 75%; 67 | bottom: 0; 68 | left: 0; 69 | right: 0; 70 | padding: 8px; 71 | } 72 | #output .system, #output .loading { 73 | color: #999; 74 | } 75 | #output .stderr, #output .error { 76 | color: #900; 77 | } 78 | #output pre { 79 | margin: 0; 80 | } 81 | #banner { 82 | display: flex; 83 | flex-wrap: wrap; 84 | align-items: center; 85 | position: absolute; 86 | left: 0; 87 | right: 0; 88 | top: 0; 89 | height: 50px; 90 | background-color: #E0EBF5; 91 | } 92 | #banner > * { 93 | margin-top: 10px; 94 | margin-bottom: 10px; 95 | margin-right: 5px; 96 | border-radius: 5px; 97 | box-sizing: border-box; 98 | height: 30px; 99 | } 100 | #head { 101 | padding-left: 10px; 102 | padding-right: 20px; 103 | padding-top: 5px; 104 | font-size: 20px; 105 | font-family: sans-serif; 106 | } 107 | #aboutButton { 108 | margin-left: auto; 109 | margin-right: 15px; 110 | } 111 | input[type=button], 112 | #importsBox { 113 | height: 30px; 114 | border: 1px solid #375EAB; 115 | font-size: 16px; 116 | font-family: sans-serif; 117 | background: #375EAB; 118 | color: white; 119 | position: static; 120 | top: 1px; 121 | border-radius: 5px; 122 | -webkit-appearance: none; 123 | } 124 | #importsBox { 125 | padding: 0.25em 7px; 126 | } 127 | #importsBox input { 128 | flex: none; 129 | height: 11px; 130 | width: 11px; 131 | margin: 0 5px 0 0; 132 | } 133 | #importsBox label { 134 | display: flex; 135 | align-items: center; 136 | line-height: 1.2; 137 | } 138 | #shareURL { 139 | width: 280px; 140 | font-size: 16px; 141 | border: 1px solid #ccc; 142 | background: #eee; 143 | color: black; 144 | } 145 | #embedLabel { 146 | font-family: sans-serif; 147 | padding-top: 5px; 148 | } 149 | #banner > select { 150 | font-size: 0.875rem; 151 | border: 0.0625rem solid #375EAB; 152 | } 153 | .lines { 154 | float: left; 155 | overflow: hidden; 156 | text-align: right; 157 | } 158 | .lines div { 159 | padding-right: 5px; 160 | color: lightgray; 161 | } 162 | .lineerror { 163 | color: red; 164 | background: #FDD; 165 | } 166 | .exit { 167 | color: lightgray; 168 | } 169 | 170 | .embedded #banner { 171 | display: none; 172 | } 173 | .embedded #wrap { 174 | top: 0; 175 | } 176 | -------------------------------------------------------------------------------- /src/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 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 | package main 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "net/http" 13 | "time" 14 | ) 15 | 16 | type server struct { 17 | mux *http.ServeMux 18 | db store 19 | log logger 20 | cache responseCache 21 | examples *examplesHandler 22 | 23 | // When the executable was last modified. Used for caching headers of compiled assets. 24 | modtime time.Time 25 | } 26 | 27 | func newServer(options ...func(s *server) error) (*server, error) { 28 | s := &server{mux: http.NewServeMux()} 29 | for _, o := range options { 30 | if err := o(s); err != nil { 31 | return nil, err 32 | } 33 | } 34 | if s.db == nil { 35 | return nil, fmt.Errorf("must provide an option func that specifies a datastore") 36 | } 37 | if s.log == nil { 38 | return nil, fmt.Errorf("must provide an option func that specifies a logger") 39 | } 40 | if s.examples == nil { 41 | return nil, fmt.Errorf("must provide an option func that sets the examples handler") 42 | } 43 | s.init() 44 | return s, nil 45 | } 46 | 47 | func (s *server) init() { 48 | s.mux.HandleFunc("/", s.handleEdit) 49 | s.mux.HandleFunc("/fmt", s.handleFmt) 50 | s.mux.HandleFunc("/version", s.handleVersion) 51 | s.mux.HandleFunc("/vet", s.commandHandler("vet", vetCheck)) 52 | s.mux.HandleFunc("/compile", s.commandHandler("prog", compileAndRun)) 53 | s.mux.HandleFunc("/share", s.handleShare) 54 | s.mux.HandleFunc("/favicon.ico", handleFavicon) 55 | s.mux.HandleFunc("/_ah/health", s.handleHealthCheck) 56 | 57 | staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))) 58 | s.mux.Handle("/static/", staticHandler) 59 | s.mux.Handle("/doc/play/", http.StripPrefix("/doc/play/", s.examples)) 60 | } 61 | 62 | func handleFavicon(w http.ResponseWriter, r *http.Request) { 63 | http.ServeFile(w, r, "./static/favicon.ico") 64 | } 65 | 66 | func (s *server) handleHealthCheck(w http.ResponseWriter, r *http.Request) { 67 | if err := s.healthCheck(r.Context()); err != nil { 68 | http.Error(w, "Health check failed: "+err.Error(), http.StatusInternalServerError) 69 | return 70 | } 71 | fmt.Fprint(w, "ok") 72 | } 73 | 74 | func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 75 | if r.Header.Get("X-Forwarded-Proto") == "http" { 76 | r.URL.Scheme = "https" 77 | r.URL.Host = r.Host 78 | http.Redirect(w, r, r.URL.String(), http.StatusFound) 79 | return 80 | } 81 | if r.Header.Get("X-Forwarded-Proto") == "https" { 82 | w.Header().Set("Strict-Transport-Security", "max-age=31536000; preload") 83 | } 84 | s.mux.ServeHTTP(w, r) 85 | } 86 | 87 | // writeJSONResponse JSON-encodes resp and writes to w with the given HTTP 88 | // status. 89 | func (s *server) writeJSONResponse(w http.ResponseWriter, resp interface{}, status int) { 90 | w.Header().Set("Content-Type", "application/json") 91 | var buf bytes.Buffer 92 | if err := json.NewEncoder(&buf).Encode(resp); err != nil { 93 | s.log.Errorf("error encoding response: %v", err) 94 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 95 | return 96 | } 97 | w.WriteHeader(status) 98 | if _, err := io.Copy(w, &buf); err != nil { 99 | s.log.Errorf("io.Copy(w, &buf): %v", err) 100 | return 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/share.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 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 | package main 6 | 7 | import ( 8 | "bytes" 9 | "crypto/sha256" 10 | "encoding/base64" 11 | "fmt" 12 | "io" 13 | "net/http" 14 | ) 15 | 16 | const ( 17 | // This salt is not meant to be kept secret (it’s checked in after all). It’s 18 | // a tiny bit of paranoia to avoid whatever problems a collision may cause. 19 | salt = "Go playground salt\n" 20 | 21 | maxSnippetSize = 64 * 1024 22 | ) 23 | 24 | type snippet struct { 25 | // Body []byte `datastore:",noindex"` // golang.org/issues/23253 26 | Body []byte 27 | } 28 | 29 | func (s *snippet) ID() string { 30 | h := sha256.New() 31 | io.WriteString(h, salt) 32 | h.Write(s.Body) 33 | sum := h.Sum(nil) 34 | b := make([]byte, base64.URLEncoding.EncodedLen(len(sum))) 35 | base64.URLEncoding.Encode(b, sum) 36 | // Web sites don’t always linkify a trailing underscore, making it seem like 37 | // the link is broken. If there is an underscore at the end of the substring, 38 | // extend it until there is not. 39 | hashLen := 11 40 | for hashLen <= len(b) && b[hashLen-1] == '_' { 41 | hashLen++ 42 | } 43 | return string(b)[:hashLen] 44 | } 45 | 46 | func (s *server) handleShare(w http.ResponseWriter, r *http.Request) { 47 | w.Header().Set("Access-Control-Allow-Origin", "*") 48 | 49 | if !allowShare(r) { 50 | w.WriteHeader(http.StatusUnavailableForLegalReasons) 51 | w.Write([]byte(`

Unavailable For Legal Reasons

Viewing and/or sharing code snippets is not available in your country for legal reasons. This message might also appear if your country is misdetected. If you believe this is an error, please file an issue.

`)) 52 | return 53 | } 54 | 55 | if r.Method == "OPTIONS" { 56 | // This is likely a pre-flight CORS request. 57 | return 58 | } 59 | 60 | // GET Method to get origin snippet 61 | if r.Method == "GET" { 62 | params := r.URL.Query() 63 | id := params.Get("id") 64 | 65 | snip := &snippet{Body: []byte("")} 66 | 67 | if err := s.db.GetSnippet(r.Context(), id, snip); err != nil { 68 | if err == ErrNoSuchEntity { 69 | http.Error(w, "Not found", http.StatusNotFound) 70 | return 71 | } 72 | s.log.Errorf("getting Snippet: %v", err) 73 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 74 | return 75 | } 76 | fmt.Fprint(w, string(snip.Body)) 77 | return 78 | } 79 | 80 | if r.Method != "POST" { 81 | http.Error(w, "Requires POST", http.StatusMethodNotAllowed) 82 | return 83 | } 84 | 85 | var body bytes.Buffer 86 | _, err := io.Copy(&body, io.LimitReader(r.Body, maxSnippetSize+1)) 87 | r.Body.Close() 88 | if err != nil { 89 | s.log.Errorf("reading Body: %v", err) 90 | http.Error(w, "Server Error", http.StatusInternalServerError) 91 | return 92 | } 93 | if body.Len() > maxSnippetSize { 94 | http.Error(w, "Snippet is too large", http.StatusRequestEntityTooLarge) 95 | return 96 | } 97 | 98 | snip := &snippet{Body: body.Bytes()} 99 | id := snip.ID() 100 | if err := s.db.PutSnippet(r.Context(), id, snip); err != nil { 101 | s.log.Errorf("putting Snippet: %v", err) 102 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 103 | return 104 | } 105 | 106 | fmt.Fprint(w, id) 107 | } 108 | 109 | func allowShare(r *http.Request) bool { 110 | // if r.Header.Get("X-AppEngine-Country") == "CN" { 111 | // return false 112 | // } 113 | return true 114 | } 115 | -------------------------------------------------------------------------------- /src/txtar.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 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 | package main 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "fmt" 11 | "path" 12 | "strings" 13 | 14 | "golang.org/x/tools/txtar" 15 | ) 16 | 17 | // fileSet is a set of files. 18 | // The zero value for fileSet is an empty set ready to use. 19 | type fileSet struct { 20 | files []string // filenames in user-provided order 21 | m map[string][]byte // filename -> source 22 | noHeader bool // whether the prog.go entry was implicit 23 | } 24 | 25 | // Data returns the content of the named file. 26 | // The fileSet retains ownership of the returned slice. 27 | func (fs *fileSet) Data(filename string) []byte { return fs.m[filename] } 28 | 29 | // Num returns the number of files in the set. 30 | func (fs *fileSet) Num() int { return len(fs.m) } 31 | 32 | // Contains reports whether fs contains the given filename. 33 | func (fs *fileSet) Contains(filename string) bool { 34 | _, ok := fs.m[filename] 35 | return ok 36 | } 37 | 38 | // AddFile adds a file to fs. If fs already contains filename, its 39 | // contents are replaced. 40 | func (fs *fileSet) AddFile(filename string, src []byte) { 41 | had := fs.Contains(filename) 42 | if fs.m == nil { 43 | fs.m = make(map[string][]byte) 44 | } 45 | fs.m[filename] = src 46 | if !had { 47 | fs.files = append(fs.files, filename) 48 | } 49 | } 50 | 51 | // Format returns fs formatted as a txtar archive. 52 | func (fs *fileSet) Format() []byte { 53 | a := new(txtar.Archive) 54 | if fs.noHeader { 55 | a.Comment = fs.m[progName] 56 | } 57 | for i, f := range fs.files { 58 | if i == 0 && f == progName && fs.noHeader { 59 | continue 60 | } 61 | a.Files = append(a.Files, txtar.File{Name: f, Data: fs.m[f]}) 62 | } 63 | return txtar.Format(a) 64 | } 65 | 66 | // splitFiles splits the user's input program src into 1 or more 67 | // files, splitting it based on boundaries as specified by the "txtar" 68 | // format. It returns an error if any filenames are bogus or 69 | // duplicates. The implicit filename for the txtar comment (the lines 70 | // before any txtar separator line) are named "prog.go". It is an 71 | // error to have an explicit file named "prog.go" in addition to 72 | // having the implicit "prog.go" file (non-empty comment section). 73 | // 74 | // The filenames are validated to only be relative paths, not too 75 | // long, not too deep, not have ".." elements, not have backslashes or 76 | // low ASCII binary characters, and to be in path.Clean canonical 77 | // form. 78 | // 79 | // splitFiles takes ownership of src. 80 | func splitFiles(src []byte) (*fileSet, error) { 81 | fs := new(fileSet) 82 | a := txtar.Parse(src) 83 | if v := bytes.TrimSpace(a.Comment); len(v) > 0 { 84 | fs.noHeader = true 85 | fs.AddFile(progName, a.Comment) 86 | } 87 | const limitNumFiles = 20 // arbitrary 88 | numFiles := len(a.Files) + fs.Num() 89 | if numFiles > limitNumFiles { 90 | return nil, fmt.Errorf("too many files in txtar archive (%v exceeds limit of %v)", numFiles, limitNumFiles) 91 | } 92 | for _, f := range a.Files { 93 | if len(f.Name) > 200 { // arbitrary limit 94 | return nil, errors.New("file name too long") 95 | } 96 | if strings.IndexFunc(f.Name, isBogusFilenameRune) != -1 { 97 | return nil, fmt.Errorf("invalid file name %q", f.Name) 98 | } 99 | if f.Name != path.Clean(f.Name) || path.IsAbs(f.Name) { 100 | return nil, fmt.Errorf("invalid file name %q", f.Name) 101 | } 102 | parts := strings.Split(f.Name, "/") 103 | if len(parts) > 10 { // arbitrary limit 104 | return nil, fmt.Errorf("file name %q too deep", f.Name) 105 | } 106 | for _, part := range parts { 107 | if part == "." || part == ".." { 108 | return nil, fmt.Errorf("invalid file name %q", f.Name) 109 | } 110 | } 111 | if fs.Contains(f.Name) { 112 | return nil, fmt.Errorf("duplicate file name %q", f.Name) 113 | } 114 | fs.AddFile(f.Name, f.Data) 115 | } 116 | return fs, nil 117 | } 118 | 119 | // isBogusFilenameRune reports whether r should be rejected if it 120 | // appears in a txtar section's filename. 121 | func isBogusFilenameRune(r rune) bool { return r == '\\' || r < ' ' } 122 | -------------------------------------------------------------------------------- /src/txtar_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 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 | package main 6 | 7 | import ( 8 | "fmt" 9 | "reflect" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func newFileSet(kv ...string) *fileSet { 15 | fs := new(fileSet) 16 | if kv[0] == "prog.go!implicit" { 17 | fs.noHeader = true 18 | kv[0] = "prog.go" 19 | } 20 | for len(kv) > 0 { 21 | fs.AddFile(kv[0], []byte(kv[1])) 22 | kv = kv[2:] 23 | } 24 | return fs 25 | } 26 | 27 | func TestSplitFiles(t *testing.T) { 28 | for _, tt := range []struct { 29 | name string 30 | in string 31 | want *fileSet 32 | wantErr string 33 | }{ 34 | { 35 | name: "classic", 36 | in: "package main", 37 | want: newFileSet("prog.go!implicit", "package main\n"), 38 | }, 39 | { 40 | name: "implicit prog.go", 41 | in: "package main\n-- two.go --\nsecond", 42 | want: newFileSet( 43 | "prog.go!implicit", "package main\n", 44 | "two.go", "second\n", 45 | ), 46 | }, 47 | { 48 | name: "basic txtar", 49 | in: "-- main.go --\npackage main\n-- foo.go --\npackage main\n", 50 | want: newFileSet( 51 | "main.go", "package main\n", 52 | "foo.go", "package main\n", 53 | ), 54 | }, 55 | { 56 | name: "reject dotdot 1", 57 | in: "-- ../foo --\n", 58 | wantErr: `invalid file name "../foo"`, 59 | }, 60 | { 61 | name: "reject dotdot 2", 62 | in: "-- .. --\n", 63 | wantErr: `invalid file name ".."`, 64 | }, 65 | { 66 | name: "reject dotdot 3", 67 | in: "-- bar/../foo --\n", 68 | wantErr: `invalid file name "bar/../foo"`, 69 | }, 70 | { 71 | name: "reject long", 72 | in: "-- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx --\n", 73 | wantErr: `file name too long`, 74 | }, 75 | { 76 | name: "reject deep", 77 | in: "-- x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x --\n", 78 | wantErr: `file name "x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x" too deep`, 79 | }, 80 | { 81 | name: "reject abs", 82 | in: "-- /etc/passwd --\n", 83 | wantErr: `invalid file name "/etc/passwd"`, 84 | }, 85 | { 86 | name: "reject backslash", 87 | in: "-- foo\\bar --\n", 88 | wantErr: `invalid file name "foo\\bar"`, 89 | }, 90 | { 91 | name: "reject binary null", 92 | in: "-- foo\x00bar --\n", 93 | wantErr: `invalid file name "foo\x00bar"`, 94 | }, 95 | { 96 | name: "reject binary low", 97 | in: "-- foo\x1fbar --\n", 98 | wantErr: `invalid file name "foo\x1fbar"`, 99 | }, 100 | { 101 | name: "reject dup", 102 | in: "-- foo.go --\n-- foo.go --\n", 103 | wantErr: `duplicate file name "foo.go"`, 104 | }, 105 | { 106 | name: "reject implicit dup", 107 | in: "package main\n-- prog.go --\n", 108 | wantErr: `duplicate file name "prog.go"`, 109 | }, 110 | { 111 | name: "skip leading whitespace comment", 112 | in: "\n \n\n \n\n-- f.go --\ncontents", 113 | want: newFileSet("f.go", "contents\n"), 114 | }, 115 | { 116 | name: "reject many files", 117 | in: strings.Repeat("-- x.go --\n", 50), 118 | wantErr: `too many files in txtar archive (50 exceeds limit of 20)`, 119 | }, 120 | } { 121 | got, err := splitFiles([]byte(tt.in)) 122 | var gotErr string 123 | if err != nil { 124 | gotErr = err.Error() 125 | } 126 | if gotErr != tt.wantErr { 127 | if tt.wantErr == "" { 128 | t.Errorf("%s: unexpected error: %v", tt.name, err) 129 | continue 130 | } 131 | t.Errorf("%s: error = %#q; want error %#q", tt.name, err, tt.wantErr) 132 | continue 133 | } 134 | if err != nil { 135 | continue 136 | } 137 | if !reflect.DeepEqual(got, tt.want) { 138 | t.Errorf("%s: wrong files\n got:\n%s\nwant:\n%s", tt.name, filesAsString(got), filesAsString(tt.want)) 139 | } 140 | } 141 | } 142 | 143 | func filesAsString(fs *fileSet) string { 144 | var sb strings.Builder 145 | for i, f := range fs.files { 146 | var implicit string 147 | if i == 0 && f == progName && fs.noHeader { 148 | implicit = " (implicit)" 149 | } 150 | fmt.Fprintf(&sb, "[file %q%s]: %q\n", f, implicit, fs.Data(f)) 151 | } 152 | return sb.String() 153 | } 154 | -------------------------------------------------------------------------------- /src/sandbox_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 | package main 6 | 7 | import ( 8 | "go/token" 9 | "os" 10 | "os/exec" 11 | "reflect" 12 | "runtime" 13 | "strings" 14 | "testing" 15 | ) 16 | 17 | // TestIsTest verifies that the isTest helper function matches 18 | // exactly (and only) the names of functions recognized as tests. 19 | func TestIsTest(t *testing.T) { 20 | cmd := exec.Command(os.Args[0], "-test.list=.") 21 | out, err := cmd.CombinedOutput() 22 | if err != nil { 23 | t.Fatalf("%s: %v\n%s", strings.Join(cmd.Args, " "), err, out) 24 | } 25 | t.Logf("%s:\n%s", strings.Join(cmd.Args, " "), out) 26 | 27 | isTestFunction := map[string]bool{} 28 | lines := strings.Split(string(out), "\n") 29 | for _, line := range lines { 30 | isTestFunction[strings.TrimSpace(line)] = true 31 | } 32 | 33 | for _, tc := range []struct { 34 | prefix string 35 | f interface{} 36 | want bool 37 | }{ 38 | {"Test", Test, true}, 39 | {"Test", TestIsTest, true}, 40 | {"Test", Test1IsATest, true}, 41 | {"Test", TestÑIsATest, true}, 42 | 43 | {"Test", TestisNotATest, false}, 44 | 45 | {"Example", Example, true}, 46 | {"Example", ExampleTest, true}, 47 | {"Example", Example_isAnExample, true}, 48 | {"Example", ExampleTest_isAnExample, true}, 49 | 50 | // Example_noOutput has a valid example function name but lacks an output 51 | // declaration, but the isTest function operates only on the test name 52 | // so it cannot detect that the function is not a test. 53 | 54 | {"Example", Example1IsAnExample, true}, 55 | {"Example", ExampleisNotAnExample, false}, 56 | 57 | {"Benchmark", Benchmark, true}, 58 | {"Benchmark", BenchmarkNop, true}, 59 | {"Benchmark", Benchmark1IsABenchmark, true}, 60 | 61 | {"Benchmark", BenchmarkisNotABenchmark, false}, 62 | } { 63 | name := nameOf(t, tc.f) 64 | t.Run(name, func(t *testing.T) { 65 | if tc.want != isTestFunction[name] { 66 | t.Fatalf(".want (%v) is inconsistent with -test.list", tc.want) 67 | } 68 | if !strings.HasPrefix(name, tc.prefix) { 69 | t.Fatalf("%q is not a prefix of %v", tc.prefix, name) 70 | } 71 | 72 | got := isTest(name, tc.prefix) 73 | if got != tc.want { 74 | t.Errorf(`isTest(%q, %q) = %v; want %v`, name, tc.prefix, got, tc.want) 75 | } 76 | }) 77 | } 78 | } 79 | 80 | // nameOf returns the runtime-reported name of function f. 81 | func nameOf(t *testing.T, f interface{}) string { 82 | t.Helper() 83 | 84 | v := reflect.ValueOf(f) 85 | if v.Kind() != reflect.Func { 86 | t.Fatalf("%v is not a function", f) 87 | } 88 | 89 | rf := runtime.FuncForPC(v.Pointer()) 90 | if rf == nil { 91 | t.Fatalf("%v.Pointer() is not a known function", f) 92 | } 93 | 94 | fullName := rf.Name() 95 | parts := strings.Split(fullName, ".") 96 | 97 | name := parts[len(parts)-1] 98 | if !token.IsIdentifier(name) { 99 | t.Fatalf("%q is not a valid identifier", name) 100 | } 101 | return name 102 | } 103 | 104 | // TestisNotATest is not a test function, despite appearances. 105 | // 106 | // Please ignore any lint or vet warnings for this function. 107 | func TestisNotATest(t *testing.T) { 108 | panic("This is not a valid test function.") 109 | } 110 | 111 | // Test11IsATest is a valid test function. 112 | func Test1IsATest(t *testing.T) { 113 | } 114 | 115 | // Test is a test with a minimal name. 116 | func Test(t *testing.T) { 117 | } 118 | 119 | // TestÑIsATest is a test with an interesting Unicode name. 120 | func TestÑIsATest(t *testing.T) { 121 | } 122 | 123 | func Example() { 124 | // Output: 125 | } 126 | 127 | func ExampleTest() { 128 | // This is an example for the function Test. 129 | // ❤ recursion. 130 | Test(nil) 131 | 132 | // Output: 133 | } 134 | 135 | func Example1IsAnExample() { 136 | // Output: 137 | } 138 | 139 | // ExampleisNotAnExample is not an example function, despite appearances. 140 | // 141 | // Please ignore any lint or vet warnings for this function. 142 | func ExampleisNotAnExample() { 143 | panic("This is not a valid example function.") 144 | 145 | // Output: 146 | // None. (This is not really an example function.) 147 | } 148 | 149 | func Example_isAnExample() { 150 | // Output: 151 | } 152 | 153 | func ExampleTest_isAnExample() { 154 | Test(nil) 155 | 156 | // Output: 157 | } 158 | 159 | func Example_noOutput() { 160 | // No output declared: should be compiled but not run. 161 | } 162 | 163 | func Benchmark(b *testing.B) { 164 | for i := 0; i < b.N; i++ { 165 | } 166 | } 167 | 168 | func BenchmarkNop(b *testing.B) { 169 | for i := 0; i < b.N; i++ { 170 | } 171 | } 172 | 173 | func Benchmark1IsABenchmark(b *testing.B) { 174 | for i := 0; i < b.N; i++ { 175 | } 176 | } 177 | 178 | // BenchmarkisNotABenchmark is not a benchmark function, despite appearances. 179 | // 180 | // Please ignore any lint or vet warnings for this function. 181 | func BenchmarkisNotABenchmark(b *testing.B) { 182 | panic("This is not a valid benchmark function.") 183 | } 184 | -------------------------------------------------------------------------------- /src/fmt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 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 | package main 6 | 7 | import ( 8 | "encoding/json" 9 | "net/http" 10 | "net/http/httptest" 11 | "net/url" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | func TestHandleFmt(t *testing.T) { 17 | s, err := newServer(testingOptions(t)) 18 | if err != nil { 19 | t.Fatalf("newServer(testingOptions(t)): %v", err) 20 | } 21 | 22 | for _, tt := range []struct { 23 | name string 24 | method string 25 | body string 26 | imports bool 27 | want string 28 | wantErr string 29 | }{ 30 | { 31 | name: "OPTIONS no-op", 32 | method: http.MethodOptions, 33 | }, 34 | { 35 | name: "classic", 36 | method: http.MethodPost, 37 | body: " package main\n func main( ) { }\n", 38 | want: "package main\n\nfunc main() {}\n", 39 | }, 40 | { 41 | name: "classic_goimports", 42 | method: http.MethodPost, 43 | body: " package main\nvar _ = fmt.Printf", 44 | imports: true, 45 | want: "package main\n\nimport \"fmt\"\n\nvar _ = fmt.Printf\n", 46 | }, 47 | { 48 | name: "single_go_with_header", 49 | method: http.MethodPost, 50 | body: "-- prog.go --\n package main", 51 | want: "-- prog.go --\npackage main\n", 52 | }, 53 | { 54 | name: "multi_go_with_header", 55 | method: http.MethodPost, 56 | body: "-- prog.go --\n package main\n\n\n-- two.go --\n package main\n var X = 5", 57 | want: "-- prog.go --\npackage main\n-- two.go --\npackage main\n\nvar X = 5\n", 58 | }, 59 | { 60 | name: "multi_go_without_header", 61 | method: http.MethodPost, 62 | body: " package main\n\n\n-- two.go --\n package main\n var X = 5", 63 | want: "package main\n-- two.go --\npackage main\n\nvar X = 5\n", 64 | }, 65 | { 66 | name: "single_go.mod_with_header", 67 | method: http.MethodPost, 68 | body: "-- go.mod --\n module \"foo\" ", 69 | want: "-- go.mod --\nmodule foo\n", 70 | }, 71 | { 72 | name: "multi_go.mod_with_header", 73 | method: http.MethodPost, 74 | body: "-- a/go.mod --\n module foo\n\n\n-- b/go.mod --\n module \"bar\"", 75 | want: "-- a/go.mod --\nmodule foo\n-- b/go.mod --\nmodule bar\n", 76 | }, 77 | { 78 | name: "only_format_go_and_go.mod", 79 | method: http.MethodPost, 80 | body: " package main \n\n\n" + 81 | "-- go.mod --\n module foo \n\n\n" + 82 | "-- plain.txt --\n plain text \n\n\n", 83 | want: "package main\n-- go.mod --\nmodule foo\n-- plain.txt --\n plain text \n\n\n", 84 | }, 85 | { 86 | name: "error_gofmt", 87 | method: http.MethodPost, 88 | body: "package 123\n", 89 | wantErr: "prog.go:1:9: expected 'IDENT', found 123", 90 | }, 91 | { 92 | name: "error_gofmt_with_header", 93 | method: http.MethodPost, 94 | body: "-- dir/one.go --\npackage 123\n", 95 | wantErr: "dir/one.go:1:9: expected 'IDENT', found 123", 96 | }, 97 | { 98 | name: "error_goimports", 99 | method: http.MethodPost, 100 | body: "package 123\n", 101 | imports: true, 102 | wantErr: "prog.go:1:9: expected 'IDENT', found 123", 103 | }, 104 | { 105 | name: "error_goimports_with_header", 106 | method: http.MethodPost, 107 | body: "-- dir/one.go --\npackage 123\n", 108 | imports: true, 109 | wantErr: "dir/one.go:1:9: expected 'IDENT', found 123", 110 | }, 111 | { 112 | name: "error_go.mod", 113 | method: http.MethodPost, 114 | body: "-- go.mod --\n123\n", 115 | wantErr: "go.mod:1: unknown directive: 123", 116 | }, 117 | { 118 | name: "error_go.mod_with_header", 119 | method: http.MethodPost, 120 | body: "-- dir/go.mod --\n123\n", 121 | wantErr: "dir/go.mod:1: unknown directive: 123", 122 | }, 123 | } { 124 | t.Run(tt.name, func(t *testing.T) { 125 | rec := httptest.NewRecorder() 126 | form := url.Values{} 127 | form.Set("body", tt.body) 128 | if tt.imports { 129 | form.Set("imports", "true") 130 | } 131 | req := httptest.NewRequest("POST", "/fmt", strings.NewReader(form.Encode())) 132 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 133 | s.handleFmt(rec, req) 134 | resp := rec.Result() 135 | if resp.StatusCode != 200 { 136 | t.Fatalf("code = %v", resp.Status) 137 | } 138 | corsHeader := "Access-Control-Allow-Origin" 139 | if got, want := resp.Header.Get(corsHeader), "*"; got != want { 140 | t.Errorf("Header %q: got %q; want %q", corsHeader, got, want) 141 | } 142 | if ct := resp.Header.Get("Content-Type"); ct != "application/json" { 143 | t.Fatalf("Content-Type = %q; want application/json", ct) 144 | } 145 | var got fmtResponse 146 | if err := json.NewDecoder(resp.Body).Decode(&got); err != nil { 147 | t.Fatal(err) 148 | } 149 | if got.Body != tt.want { 150 | t.Errorf("wrong output\n got: %q\nwant: %q\n", got.Body, tt.want) 151 | } 152 | if got.Error != tt.wantErr { 153 | t.Errorf("wrong error\n got err: %q\nwant err: %q\n", got.Error, tt.wantErr) 154 | } 155 | }) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/sandbox/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 | package main 6 | 7 | import ( 8 | "go.opencensus.io/plugin/ochttp" 9 | "go.opencensus.io/stats" 10 | "go.opencensus.io/stats/view" 11 | "go.opencensus.io/tag" 12 | ) 13 | 14 | var ( 15 | kContainerCreateSuccess = tag.MustNewKey("go-playground/sandbox/container_create_success") 16 | mContainers = stats.Int64("go-playground/sandbox/container_count", "number of sandbox containers", stats.UnitDimensionless) 17 | mUnwantedContainers = stats.Int64("go-playground/sandbox/unwanted_container_count", "number of sandbox containers that are unexpectedly running", stats.UnitDimensionless) 18 | mMaxContainers = stats.Int64("go-playground/sandbox/max_container_count", "target number of sandbox containers", stats.UnitDimensionless) 19 | mContainerCreateLatency = stats.Float64("go-playground/sandbox/container_create_latency", "", stats.UnitMilliseconds) 20 | 21 | containerCount = &view.View{ 22 | Name: "go-playground/sandbox/container_count", 23 | Description: "Number of running sandbox containers", 24 | TagKeys: nil, 25 | Measure: mContainers, 26 | Aggregation: view.LastValue(), 27 | } 28 | unwantedContainerCount = &view.View{ 29 | Name: "go-playground/sandbox/unwanted_container_count", 30 | Description: "Number of running sandbox containers that are not being tracked by the sandbox", 31 | TagKeys: nil, 32 | Measure: mUnwantedContainers, 33 | Aggregation: view.LastValue(), 34 | } 35 | maxContainerCount = &view.View{ 36 | Name: "go-playground/sandbox/max_container_count", 37 | Description: "Maximum number of containers to create", 38 | TagKeys: nil, 39 | Measure: mMaxContainers, 40 | Aggregation: view.LastValue(), 41 | } 42 | containerCreateCount = &view.View{ 43 | Name: "go-playground/sandbox/container_create_count", 44 | Description: "Number of containers created", 45 | Measure: mContainerCreateLatency, 46 | TagKeys: []tag.Key{kContainerCreateSuccess}, 47 | Aggregation: view.Count(), 48 | } 49 | containerCreationLatency = &view.View{ 50 | Name: "go-playground/sandbox/container_create_latency", 51 | Description: "Latency distribution of container creation", 52 | Measure: mContainerCreateLatency, 53 | Aggregation: ochttp.DefaultLatencyDistribution, 54 | } 55 | ) 56 | 57 | // Customizations of ochttp views. Views are updated as follows: 58 | // - The views are prefixed with go-playground-sandbox. 59 | // - ochttp.KeyServerRoute is added as a tag to label metrics per-route. 60 | var ( 61 | ServerRequestCountView = &view.View{ 62 | Name: "go-playground-sandbox/http/server/request_count", 63 | Description: "Count of HTTP requests started", 64 | Measure: ochttp.ServerRequestCount, 65 | TagKeys: []tag.Key{ochttp.KeyServerRoute}, 66 | Aggregation: view.Count(), 67 | } 68 | ServerRequestBytesView = &view.View{ 69 | Name: "go-playground-sandbox/http/server/request_bytes", 70 | Description: "Size distribution of HTTP request body", 71 | Measure: ochttp.ServerRequestBytes, 72 | TagKeys: []tag.Key{ochttp.KeyServerRoute}, 73 | Aggregation: ochttp.DefaultSizeDistribution, 74 | } 75 | ServerResponseBytesView = &view.View{ 76 | Name: "go-playground-sandbox/http/server/response_bytes", 77 | Description: "Size distribution of HTTP response body", 78 | Measure: ochttp.ServerResponseBytes, 79 | TagKeys: []tag.Key{ochttp.KeyServerRoute}, 80 | Aggregation: ochttp.DefaultSizeDistribution, 81 | } 82 | ServerLatencyView = &view.View{ 83 | Name: "go-playground-sandbox/http/server/latency", 84 | Description: "Latency distribution of HTTP requests", 85 | Measure: ochttp.ServerLatency, 86 | TagKeys: []tag.Key{ochttp.KeyServerRoute}, 87 | Aggregation: ochttp.DefaultLatencyDistribution, 88 | } 89 | ServerRequestCountByMethod = &view.View{ 90 | Name: "go-playground-sandbox/http/server/request_count_by_method", 91 | Description: "Server request count by HTTP method", 92 | TagKeys: []tag.Key{ochttp.Method, ochttp.KeyServerRoute}, 93 | Measure: ochttp.ServerRequestCount, 94 | Aggregation: view.Count(), 95 | } 96 | ServerResponseCountByStatusCode = &view.View{ 97 | Name: "go-playground-sandbox/http/server/response_count_by_status_code", 98 | Description: "Server response count by status code", 99 | TagKeys: []tag.Key{ochttp.StatusCode, ochttp.KeyServerRoute}, 100 | Measure: ochttp.ServerLatency, 101 | Aggregation: view.Count(), 102 | } 103 | ) 104 | 105 | // views should contain all measurements. All *view.View added to this 106 | // slice will be registered and exported to the metric service. 107 | var views = []*view.View{ 108 | containerCount, 109 | unwantedContainerCount, 110 | maxContainerCount, 111 | containerCreateCount, 112 | containerCreationLatency, 113 | ServerRequestCountView, 114 | ServerRequestBytesView, 115 | ServerResponseBytesView, 116 | ServerLatencyView, 117 | ServerRequestCountByMethod, 118 | ServerResponseCountByStatusCode, 119 | } 120 | -------------------------------------------------------------------------------- /src/play.go: -------------------------------------------------------------------------------- 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 | package main 6 | 7 | import ( 8 | "bytes" 9 | "encoding/binary" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "sync" 14 | "time" 15 | "unicode/utf8" 16 | ) 17 | 18 | // When sandbox time begins. 19 | var epoch = time.Unix(1257894000, 0) 20 | 21 | // Recorder records the standard and error outputs of a sandbox program 22 | // (comprised of playback headers) and converts it to a sequence of Events. 23 | // It sanitizes each Event's Message to ensure it is valid UTF-8. 24 | // 25 | // Playground programs precede all their writes with a header (described 26 | // below) that describes the time the write occurred (in playground time) and 27 | // the length of the data that will be written. If a non-header is 28 | // encountered where a header is expected, the output is scanned for the next 29 | // header and the intervening text string is added to the sequence an event 30 | // occurring at the same time as the preceding event. 31 | // 32 | // A playback header has this structure: 33 | // 34 | // 4 bytes: "\x00\x00PB", a magic header 35 | // 8 bytes: big-endian int64, unix time in nanoseconds 36 | // 4 bytes: big-endian int32, length of the next write 37 | type Recorder struct { 38 | stdout, stderr recorderWriter 39 | } 40 | 41 | func (r *Recorder) Stdout() io.Writer { return &r.stdout } 42 | func (r *Recorder) Stderr() io.Writer { return &r.stderr } 43 | 44 | type recorderWriter struct { 45 | mu sync.Mutex 46 | writes []byte 47 | } 48 | 49 | func (w *recorderWriter) bytes() []byte { 50 | w.mu.Lock() 51 | defer w.mu.Unlock() 52 | return w.writes[0:len(w.writes):len(w.writes)] 53 | } 54 | 55 | func (w *recorderWriter) Write(b []byte) (n int, err error) { 56 | w.mu.Lock() 57 | defer w.mu.Unlock() 58 | w.writes = append(w.writes, b...) 59 | return len(b), nil 60 | } 61 | 62 | type Event struct { 63 | Message string 64 | Kind string // "stdout" or "stderr" 65 | Delay time.Duration // time to wait before printing Message 66 | } 67 | 68 | func (r *Recorder) Events() ([]Event, error) { 69 | stdout, stderr := r.stdout.bytes(), r.stderr.bytes() 70 | 71 | evOut, err := decode("stdout", stdout) 72 | if err != nil { 73 | return nil, err 74 | } 75 | evErr, err := decode("stderr", stderr) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | events := sortedMerge(evOut, evErr) 81 | 82 | var ( 83 | out []Event 84 | now = epoch 85 | ) 86 | 87 | for _, e := range events { 88 | delay := e.time.Sub(now) 89 | if delay < 0 { 90 | delay = 0 91 | } 92 | out = append(out, Event{ 93 | Message: string(sanitize(e.msg)), 94 | Kind: e.kind, 95 | Delay: delay, 96 | }) 97 | if delay > 0 { 98 | now = e.time 99 | } 100 | } 101 | return out, nil 102 | } 103 | 104 | type event struct { 105 | msg []byte 106 | kind string 107 | time time.Time 108 | } 109 | 110 | func decode(kind string, output []byte) ([]event, error) { 111 | var ( 112 | magic = []byte{0, 0, 'P', 'B'} 113 | headerLen = 8 + 4 114 | last = epoch 115 | events []event 116 | ) 117 | add := func(t time.Time, b []byte) { 118 | var prev *event 119 | if len(events) > 0 { 120 | prev = &events[len(events)-1] 121 | } 122 | if prev != nil && t.Equal(prev.time) { 123 | // Merge this event with previous event, to avoid 124 | // sending a lot of events for a big output with no 125 | // significant timing information. 126 | prev.msg = append(prev.msg, b...) 127 | } else { 128 | e := event{msg: b, kind: kind, time: t} 129 | events = append(events, e) 130 | } 131 | last = t 132 | } 133 | for i := 0; i < len(output); { 134 | if !bytes.HasPrefix(output[i:], magic) { 135 | // Not a header; find next header. 136 | j := bytes.Index(output[i:], magic) 137 | if j < 0 { 138 | // No more headers; bail. 139 | add(last, output[i:]) 140 | break 141 | } 142 | add(last, output[i:i+j]) 143 | i += j 144 | } 145 | i += len(magic) 146 | 147 | // Decode header. 148 | if len(output)-i < headerLen { 149 | return nil, errors.New("short header") 150 | } 151 | header := output[i : i+headerLen] 152 | nanos := int64(binary.BigEndian.Uint64(header[0:])) 153 | t := time.Unix(0, nanos) 154 | if t.Before(last) { 155 | // Force timestamps to be monotonic. (This could 156 | // be an encoding error, which we ignore now but will 157 | // will likely be picked up when decoding the length.) 158 | t = last 159 | } 160 | n := int(binary.BigEndian.Uint32(header[8:])) 161 | if n < 0 { 162 | return nil, fmt.Errorf("bad length: %v", n) 163 | } 164 | i += headerLen 165 | 166 | // Slurp output. 167 | // Truncated output is OK (probably caused by sandbox limits). 168 | end := i + n 169 | if end > len(output) { 170 | end = len(output) 171 | } 172 | add(t, output[i:end]) 173 | i += n 174 | } 175 | return events, nil 176 | } 177 | 178 | // Sorted merge of two slices of events into one slice. 179 | func sortedMerge(a, b []event) []event { 180 | if len(a) == 0 { 181 | return b 182 | } 183 | if len(b) == 0 { 184 | return a 185 | } 186 | 187 | sorted := make([]event, 0, len(a)+len(b)) 188 | i, j := 0, 0 189 | for i < len(a) && j < len(b) { 190 | if a[i].time.Before(b[j].time) { 191 | sorted = append(sorted, a[i]) 192 | i++ 193 | } else { 194 | sorted = append(sorted, b[j]) 195 | j++ 196 | } 197 | } 198 | sorted = append(sorted, a[i:]...) 199 | sorted = append(sorted, b[j:]...) 200 | return sorted 201 | } 202 | 203 | // sanitize scans b for invalid utf8 code points. If found, it reconstructs 204 | // the slice replacing the invalid codes with \uFFFD, properly encoded. 205 | func sanitize(b []byte) []byte { 206 | if utf8.Valid(b) { 207 | return b 208 | } 209 | var buf bytes.Buffer 210 | for len(b) > 0 { 211 | r, size := utf8.DecodeRune(b) 212 | b = b[size:] 213 | buf.WriteRune(r) 214 | } 215 | return buf.Bytes() 216 | } 217 | -------------------------------------------------------------------------------- /src/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The Go Playground 5 | 6 | 7 | 8 | 9 | 10 | 73 | 74 | 75 | 100 |
101 | 102 |
103 |
104 | 105 |
106 |

About the Playground

107 | 108 |

109 | The Go Playground is a web service that runs on 110 | golang.org's servers. 111 | The service receives a Go program, vets, compiles, links, and 112 | runs the program inside a sandbox, then returns the output. 113 |

114 | 115 |

116 | If the program contains tests or examples 117 | and no main function, the service runs the tests. 118 | Benchmarks will likely not be supported since the program runs in a sandboxed 119 | environment with limited resources. 120 |

121 | 122 |

123 | There are limitations to the programs that can be run in the playground: 124 |

125 | 126 | 145 | 146 |

147 | The article "Inside 148 | the Go Playground" describes how the playground is implemented. 149 | The source code is available at 150 | https://go.googlesource.com/playground. 151 |

152 | 153 |

154 | The playground uses the latest stable release of Go.
155 | The current version is {{.GoVersion}}. 156 |

157 | 158 |

159 | The playground service is used by more than just the official Go project 160 | (Go by Example is one other instance) 161 | and we are happy for you to use it on your own site. 162 | All we ask is that you 163 | contact us first (note this is a public mailing list), 164 | use a unique user agent in your requests (so we can identify you), 165 | and that your service is of benefit to the Go community. 166 |

167 | 168 |

169 | Any requests for content removal should be directed to 170 | security@golang.org. 171 | Please include the URL and the reason for the request. 172 |

173 |
174 | 175 | 176 | -------------------------------------------------------------------------------- /src/sandbox/sandbox_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 | package main 6 | 7 | import ( 8 | "bytes" 9 | "io" 10 | "strings" 11 | "testing" 12 | "testing/iotest" 13 | 14 | "github.com/google/go-cmp/cmp" 15 | ) 16 | 17 | func TestLimitedWriter(t *testing.T) { 18 | cases := []struct { 19 | desc string 20 | lw *limitedWriter 21 | in []byte 22 | want []byte 23 | wantN int64 24 | wantRemaining int64 25 | err error 26 | }{ 27 | { 28 | desc: "simple", 29 | lw: &limitedWriter{dst: &bytes.Buffer{}, n: 10}, 30 | in: []byte("hi"), 31 | want: []byte("hi"), 32 | wantN: 2, 33 | wantRemaining: 8, 34 | }, 35 | { 36 | desc: "writing nothing", 37 | lw: &limitedWriter{dst: &bytes.Buffer{}, n: 10}, 38 | in: []byte(""), 39 | want: []byte(""), 40 | wantN: 0, 41 | wantRemaining: 10, 42 | }, 43 | { 44 | desc: "writing exactly enough", 45 | lw: &limitedWriter{dst: &bytes.Buffer{}, n: 6}, 46 | in: []byte("enough"), 47 | want: []byte("enough"), 48 | wantN: 6, 49 | wantRemaining: 0, 50 | err: nil, 51 | }, 52 | { 53 | desc: "writing too much", 54 | lw: &limitedWriter{dst: &bytes.Buffer{}, n: 10}, 55 | in: []byte("this is much longer than 10"), 56 | want: []byte("this is mu"), 57 | wantN: 10, 58 | wantRemaining: -1, 59 | err: errTooMuchOutput, 60 | }, 61 | } 62 | for _, c := range cases { 63 | t.Run(c.desc, func(t *testing.T) { 64 | n, err := io.Copy(c.lw, iotest.OneByteReader(bytes.NewReader(c.in))) 65 | if err != c.err { 66 | t.Errorf("c.lw.Write(%q) = %d, %q, wanted %d, %q", c.in, n, err, c.wantN, c.err) 67 | } 68 | if n != c.wantN { 69 | t.Errorf("c.lw.Write(%q) = %d, %q, wanted %d, %q", c.in, n, err, c.wantN, c.err) 70 | } 71 | if c.lw.n != c.wantRemaining { 72 | t.Errorf("c.lw.n = %d, wanted %d", c.lw.n, c.wantRemaining) 73 | } 74 | if string(c.lw.dst.Bytes()) != string(c.want) { 75 | t.Errorf("c.lw.dst.Bytes() = %q, wanted %q", c.lw.dst.Bytes(), c.want) 76 | } 77 | }) 78 | } 79 | } 80 | 81 | func TestSwitchWriter(t *testing.T) { 82 | cases := []struct { 83 | desc string 84 | sw *switchWriter 85 | in []byte 86 | want1 []byte 87 | want2 []byte 88 | wantN int64 89 | wantFound bool 90 | err error 91 | }{ 92 | { 93 | desc: "not found", 94 | sw: &switchWriter{switchAfter: []byte("UNIQUE")}, 95 | in: []byte("hi"), 96 | want1: []byte("hi"), 97 | want2: []byte(""), 98 | wantN: 2, 99 | wantFound: false, 100 | }, 101 | { 102 | desc: "writing nothing", 103 | sw: &switchWriter{switchAfter: []byte("UNIQUE")}, 104 | in: []byte(""), 105 | want1: []byte(""), 106 | want2: []byte(""), 107 | wantN: 0, 108 | wantFound: false, 109 | }, 110 | { 111 | desc: "writing exactly switchAfter", 112 | sw: &switchWriter{switchAfter: []byte("UNIQUE")}, 113 | in: []byte("UNIQUE"), 114 | want1: []byte("UNIQUE"), 115 | want2: []byte(""), 116 | wantN: 6, 117 | wantFound: true, 118 | }, 119 | { 120 | desc: "writing before and after switchAfter", 121 | sw: &switchWriter{switchAfter: []byte("UNIQUE")}, 122 | in: []byte("this is before UNIQUE and this is after"), 123 | want1: []byte("this is before UNIQUE"), 124 | want2: []byte(" and this is after"), 125 | wantN: 39, 126 | wantFound: true, 127 | }, 128 | } 129 | for _, c := range cases { 130 | t.Run(c.desc, func(t *testing.T) { 131 | dst1, dst2 := &bytes.Buffer{}, &bytes.Buffer{} 132 | c.sw.dst1, c.sw.dst2 = dst1, dst2 133 | n, err := io.Copy(c.sw, iotest.OneByteReader(bytes.NewReader(c.in))) 134 | if err != c.err { 135 | t.Errorf("c.sw.Write(%q) = %d, %q, wanted %d, %q", c.in, n, err, c.wantN, c.err) 136 | } 137 | if n != c.wantN { 138 | t.Errorf("c.sw.Write(%q) = %d, %q, wanted %d, %q", c.in, n, err, c.wantN, c.err) 139 | } 140 | if c.sw.found != c.wantFound { 141 | t.Errorf("c.sw.found = %v, wanted %v", c.sw.found, c.wantFound) 142 | } 143 | if string(dst1.Bytes()) != string(c.want1) { 144 | t.Errorf("dst1.Bytes() = %q, wanted %q", dst1.Bytes(), c.want1) 145 | } 146 | if string(dst2.Bytes()) != string(c.want2) { 147 | t.Errorf("dst2.Bytes() = %q, wanted %q", dst2.Bytes(), c.want2) 148 | } 149 | }) 150 | } 151 | } 152 | 153 | func TestSwitchWriterMultipleWrites(t *testing.T) { 154 | dst1, dst2 := &bytes.Buffer{}, &bytes.Buffer{} 155 | sw := &switchWriter{ 156 | dst1: dst1, 157 | dst2: dst2, 158 | switchAfter: []byte("GOPHER"), 159 | } 160 | n, err := io.Copy(sw, iotest.OneByteReader(strings.NewReader("this is before GO"))) 161 | if err != nil || n != 17 { 162 | t.Errorf("sw.Write(%q) = %d, %q, wanted %d, no error", "this is before GO", n, err, 17) 163 | } 164 | if sw.found { 165 | t.Errorf("sw.found = %v, wanted %v", sw.found, false) 166 | } 167 | if string(dst1.Bytes()) != "this is before GO" { 168 | t.Errorf("dst1.Bytes() = %q, wanted %q", dst1.Bytes(), "this is before GO") 169 | } 170 | if string(dst2.Bytes()) != "" { 171 | t.Errorf("dst2.Bytes() = %q, wanted %q", dst2.Bytes(), "") 172 | } 173 | n, err = io.Copy(sw, iotest.OneByteReader(strings.NewReader("PHER and this is after"))) 174 | if err != nil || n != 22 { 175 | t.Errorf("sw.Write(%q) = %d, %q, wanted %d, no error", "this is before GO", n, err, 22) 176 | } 177 | if !sw.found { 178 | t.Errorf("sw.found = %v, wanted %v", sw.found, true) 179 | } 180 | if string(dst1.Bytes()) != "this is before GOPHER" { 181 | t.Errorf("dst1.Bytes() = %q, wanted %q", dst1.Bytes(), "this is before GOPHEr") 182 | } 183 | if string(dst2.Bytes()) != " and this is after" { 184 | t.Errorf("dst2.Bytes() = %q, wanted %q", dst2.Bytes(), " and this is after") 185 | } 186 | } 187 | 188 | func TestParseDockerContainers(t *testing.T) { 189 | cases := []struct { 190 | desc string 191 | output string 192 | want []dockerContainer 193 | wantErr bool 194 | }{ 195 | { 196 | desc: "normal output (container per line)", 197 | output: `{"Command":"\"/usr/local/bin/play…\"","CreatedAt":"2020-04-23 17:44:02 -0400 EDT","ID":"f7f170fde076","Image":"gcr.io/golang-org/playground-sandbox-gvisor:latest","Labels":"","LocalVolumes":"0","Mounts":"","Names":"play_run_a02cfe67","Networks":"none","Ports":"","RunningFor":"8 seconds ago","Size":"0B","Status":"Up 7 seconds"} 198 | {"Command":"\"/usr/local/bin/play…\"","CreatedAt":"2020-04-23 17:44:02 -0400 EDT","ID":"af872e55a773","Image":"gcr.io/golang-org/playground-sandbox-gvisor:latest","Labels":"","LocalVolumes":"0","Mounts":"","Names":"play_run_0a69c3e8","Networks":"none","Ports":"","RunningFor":"8 seconds ago","Size":"0B","Status":"Up 7 seconds"}`, 199 | want: []dockerContainer{ 200 | {ID: "f7f170fde076", Image: "gcr.io/golang-org/playground-sandbox-gvisor:latest", Names: "play_run_a02cfe67"}, 201 | {ID: "af872e55a773", Image: "gcr.io/golang-org/playground-sandbox-gvisor:latest", Names: "play_run_0a69c3e8"}, 202 | }, 203 | wantErr: false, 204 | }, 205 | { 206 | desc: "empty output", 207 | wantErr: false, 208 | }, 209 | { 210 | desc: "malformatted output", 211 | output: `xyzzy{}`, 212 | wantErr: true, 213 | }, 214 | } 215 | for _, tc := range cases { 216 | t.Run(tc.desc, func(t *testing.T) { 217 | cs, err := parseDockerContainers([]byte(tc.output)) 218 | if (err != nil) != tc.wantErr { 219 | t.Errorf("parseDockerContainers(_) = %v, %v, wantErr: %v", cs, err, tc.wantErr) 220 | } 221 | if diff := cmp.Diff(tc.want, cs); diff != "" { 222 | t.Errorf("parseDockerContainers() mismatch (-want +got):\n%s", diff) 223 | } 224 | }) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw= 4 | github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= 5 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 6 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 7 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 10 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 11 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 12 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 13 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 14 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 15 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 16 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 17 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 18 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 19 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 20 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 21 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 22 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 23 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 24 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 25 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 26 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 27 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 28 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 29 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 30 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 31 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 32 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 33 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 34 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 35 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 38 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 39 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 40 | go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= 41 | go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= 42 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 43 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 44 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 45 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 46 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 47 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 48 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= 49 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 50 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 51 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 52 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 53 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 54 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 55 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 56 | golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= 57 | golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 58 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 59 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 60 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 61 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 62 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 63 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 64 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 65 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20220624220833-87e55d714810 h1:rHZQSjJdAI4Xf5Qzeh2bBc5YJIkPFVM6oDtMFYmgws0= 67 | golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 68 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 69 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 70 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 71 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 72 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 73 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 74 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 75 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 76 | golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= 77 | golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= 78 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 79 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 80 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 81 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 82 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 83 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 84 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 85 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 86 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 87 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 88 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 89 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 90 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 91 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 92 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 93 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 94 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 95 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 96 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 97 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 98 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 99 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 100 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 101 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 102 | -------------------------------------------------------------------------------- /src/tests.go: -------------------------------------------------------------------------------- 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 | // Test tests are linked into the main binary and are run as part of 6 | // the Docker build step. 7 | 8 | package main 9 | 10 | import ( 11 | "context" 12 | "fmt" 13 | stdlog "log" 14 | "net" 15 | "os" 16 | "reflect" 17 | "strings" 18 | "time" 19 | ) 20 | 21 | type compileTest struct { 22 | name string // test name 23 | prog, want, errors string 24 | wantFunc func(got string) error // alternative to want 25 | withVet bool 26 | wantEvents []Event 27 | wantVetErrors string 28 | } 29 | 30 | func (s *server) test() { 31 | if _, err := net.ResolveIPAddr("ip", "sandbox_dev.sandnet."); err != nil { 32 | log.Fatalf("sandbox_dev.sandnet not available") 33 | } 34 | os.Setenv("DEBUG_FORCE_GVISOR", "1") 35 | os.Setenv("SANDBOX_BACKEND_URL", "http://sandbox_dev.sandnet/run") 36 | s.runTests() 37 | } 38 | 39 | func (s *server) runTests() { 40 | if err := s.healthCheck(context.Background()); err != nil { 41 | stdlog.Fatal(err) 42 | } 43 | 44 | failed := false 45 | for i, t := range tests { 46 | stdlog.Printf("testing case %d (%q)...\n", i, t.name) 47 | resp, err := compileAndRun(context.Background(), &request{Body: t.prog, WithVet: t.withVet}) 48 | if err != nil { 49 | stdlog.Fatal(err) 50 | } 51 | if t.wantEvents != nil { 52 | if !reflect.DeepEqual(resp.Events, t.wantEvents) { 53 | stdlog.Printf("resp.Events = %q, want %q", resp.Events, t.wantEvents) 54 | failed = true 55 | } 56 | continue 57 | } 58 | if t.errors != "" { 59 | if resp.Errors != t.errors { 60 | stdlog.Printf("resp.Errors = %q, want %q", resp.Errors, t.errors) 61 | failed = true 62 | } 63 | continue 64 | } 65 | if resp.Errors != "" { 66 | stdlog.Printf("resp.Errors = %q, want %q", resp.Errors, t.errors) 67 | failed = true 68 | continue 69 | } 70 | if resp.VetErrors != t.wantVetErrors { 71 | stdlog.Printf("resp.VetErrs = %q, want %q", resp.VetErrors, t.wantVetErrors) 72 | failed = true 73 | continue 74 | } 75 | if t.withVet && (resp.VetErrors != "") == resp.VetOK { 76 | stdlog.Printf("resp.VetErrs & VetOK inconsistent; VetErrs = %q; VetOK = %v", resp.VetErrors, resp.VetOK) 77 | failed = true 78 | continue 79 | } 80 | if len(resp.Events) == 0 { 81 | stdlog.Printf("unexpected output: %q, want %q", "", t.want) 82 | failed = true 83 | continue 84 | } 85 | var b strings.Builder 86 | for _, e := range resp.Events { 87 | b.WriteString(e.Message) 88 | } 89 | if t.wantFunc != nil { 90 | if err := t.wantFunc(b.String()); err != nil { 91 | stdlog.Printf("%v\n", err) 92 | failed = true 93 | } 94 | } else { 95 | if !strings.Contains(b.String(), t.want) { 96 | stdlog.Printf("unexpected output: %q, want %q", b.String(), t.want) 97 | failed = true 98 | } 99 | } 100 | } 101 | if failed { 102 | stdlog.Fatalf("FAILED") 103 | } 104 | fmt.Println("OK") 105 | } 106 | 107 | var tests = []compileTest{ 108 | { 109 | name: "timezones_available", 110 | prog: ` 111 | package main 112 | 113 | import "time" 114 | 115 | func main() { 116 | loc, err := time.LoadLocation("America/New_York") 117 | if err != nil { 118 | panic(err.Error()) 119 | } 120 | println(loc.String()) 121 | } 122 | `, want: "America/New_York"}, 123 | 124 | { 125 | name: "faketime_works", 126 | prog: ` 127 | package main 128 | 129 | import ( 130 | "fmt" 131 | "time" 132 | ) 133 | 134 | func main() { 135 | fmt.Println(time.Now()) 136 | } 137 | `, want: "2009-11-10 23:00:00 +0000 UTC"}, 138 | 139 | { 140 | name: "faketime_tickers", 141 | prog: ` 142 | package main 143 | 144 | import ( 145 | "fmt" 146 | "time" 147 | ) 148 | 149 | func main() { 150 | t1 := time.Tick(time.Second * 3) 151 | t2 := time.Tick(time.Second * 7) 152 | t3 := time.Tick(time.Second * 11) 153 | end := time.After(time.Second * 19) 154 | want := "112131211" 155 | var got []byte 156 | for { 157 | var c byte 158 | select { 159 | case <-t1: 160 | c = '1' 161 | case <-t2: 162 | c = '2' 163 | case <-t3: 164 | c = '3' 165 | case <-end: 166 | if g := string(got); g != want { 167 | fmt.Printf("got %q, want %q\n", g, want) 168 | } else { 169 | fmt.Println("timers fired as expected") 170 | } 171 | return 172 | } 173 | got = append(got, c) 174 | } 175 | } 176 | `, want: "timers fired as expected"}, 177 | { 178 | name: "must_be_package_main", 179 | prog: ` 180 | package test 181 | 182 | func main() { 183 | println("test") 184 | } 185 | `, want: "", errors: "package name must be main"}, 186 | { 187 | name: "filesystem_contents", 188 | prog: ` 189 | package main 190 | 191 | import ( 192 | "fmt" 193 | "os" 194 | "path/filepath" 195 | ) 196 | 197 | func main() { 198 | filepath.Walk("/", func(path string, info os.FileInfo, err error) error { 199 | if path == "/proc" || path == "/sys" { 200 | return filepath.SkipDir 201 | } 202 | fmt.Println(path) 203 | return nil 204 | }) 205 | } 206 | `, wantFunc: func(got string) error { 207 | // The environment for the old nacl sandbox: 208 | if strings.TrimSpace(got) == `/ 209 | /dev 210 | /dev/null 211 | /dev/random 212 | /dev/urandom 213 | /dev/zero 214 | /etc 215 | /etc/group 216 | /etc/hosts 217 | /etc/passwd 218 | /etc/resolv.conf 219 | /tmp 220 | /usr 221 | /usr/local 222 | /usr/local/go 223 | /usr/local/go/lib 224 | /usr/local/go/lib/time 225 | /usr/local/go/lib/time/zoneinfo.zip` { 226 | return nil 227 | } 228 | have := map[string]bool{} 229 | for _, f := range strings.Split(got, "\n") { 230 | have[f] = true 231 | } 232 | for _, expect := range []string{ 233 | "/.dockerenv", 234 | "/etc/hostname", 235 | "/dev/zero", 236 | "/lib/ld-linux-x86-64.so.2", 237 | "/lib/libc.so.6", 238 | "/etc/nsswitch.conf", 239 | "/bin/env", 240 | "/tmpfs", 241 | } { 242 | if !have[expect] { 243 | return fmt.Errorf("missing expected sandbox file %q; got:\n%s", expect, got) 244 | } 245 | } 246 | return nil 247 | }, 248 | }, 249 | { 250 | name: "test_passes", 251 | prog: ` 252 | package main 253 | 254 | import "testing" 255 | 256 | func TestSanity(t *testing.T) { 257 | if 1+1 != 2 { 258 | t.Error("uhh...") 259 | } 260 | } 261 | `, want: `=== RUN TestSanity 262 | --- PASS: TestSanity (0.00s) 263 | PASS`}, 264 | 265 | { 266 | name: "test_without_import", 267 | prog: ` 268 | package main 269 | 270 | func TestSanity(t *testing.T) { 271 | t.Error("uhh...") 272 | } 273 | 274 | func ExampleNotExecuted() { 275 | // Output: it should not run 276 | } 277 | `, want: "", errors: "./prog.go:4:20: undefined: testing\n"}, 278 | 279 | { 280 | name: "test_with_import_ignored", 281 | prog: ` 282 | package main 283 | 284 | import ( 285 | "fmt" 286 | "testing" 287 | ) 288 | 289 | func TestSanity(t *testing.T) { 290 | t.Error("uhh...") 291 | } 292 | 293 | func main() { 294 | fmt.Println("test") 295 | } 296 | `, want: "test"}, 297 | 298 | { 299 | name: "example_runs", 300 | prog: ` 301 | package main//comment 302 | 303 | import "fmt" 304 | 305 | func ExampleOutput() { 306 | fmt.Println("The output") 307 | // Output: The output 308 | } 309 | `, want: `=== RUN ExampleOutput 310 | --- PASS: ExampleOutput (0.00s) 311 | PASS`}, 312 | 313 | { 314 | name: "example_unordered", 315 | prog: ` 316 | package main//comment 317 | 318 | import "fmt" 319 | 320 | func ExampleUnorderedOutput() { 321 | fmt.Println("2") 322 | fmt.Println("1") 323 | fmt.Println("3") 324 | // Unordered output: 3 325 | // 2 326 | // 1 327 | } 328 | `, want: `=== RUN ExampleUnorderedOutput 329 | --- PASS: ExampleUnorderedOutput (0.00s) 330 | PASS`}, 331 | 332 | { 333 | name: "example_fail", 334 | prog: ` 335 | package main 336 | 337 | import "fmt" 338 | 339 | func ExampleEmptyOutput() { 340 | // Output: 341 | } 342 | 343 | func ExampleEmptyOutputFail() { 344 | fmt.Println("1") 345 | // Output: 346 | } 347 | `, want: `=== RUN ExampleEmptyOutput 348 | --- PASS: ExampleEmptyOutput (0.00s) 349 | === RUN ExampleEmptyOutputFail 350 | --- FAIL: ExampleEmptyOutputFail (0.00s) 351 | got: 352 | 1 353 | want: 354 | 355 | FAIL`}, 356 | 357 | // Run program without executing this example function. 358 | { 359 | name: "example_no_output_skips_run", 360 | prog: ` 361 | package main 362 | 363 | func ExampleNoOutput() { 364 | panic(1) 365 | } 366 | `, want: `testing: warning: no tests to run 367 | PASS`}, 368 | 369 | { 370 | name: "example_output", 371 | prog: ` 372 | package main 373 | 374 | import "fmt" 375 | 376 | func ExampleShouldNotRun() { 377 | fmt.Println("The output") 378 | // Output: The output 379 | } 380 | 381 | func main() { 382 | fmt.Println("Main") 383 | } 384 | `, want: "Main"}, 385 | 386 | { 387 | name: "stdout_stderr_merge", 388 | prog: ` 389 | package main 390 | 391 | import ( 392 | "fmt" 393 | "os" 394 | ) 395 | 396 | func main() { 397 | fmt.Fprintln(os.Stdout, "A") 398 | fmt.Fprintln(os.Stderr, "B") 399 | fmt.Fprintln(os.Stdout, "A") 400 | fmt.Fprintln(os.Stdout, "A") 401 | } 402 | `, want: "A\nB\nA\nA\n"}, 403 | 404 | // Integration test for runtime.write fake timestamps. 405 | { 406 | name: "faketime_write_interaction", 407 | prog: ` 408 | package main 409 | 410 | import ( 411 | "fmt" 412 | "os" 413 | "time" 414 | ) 415 | 416 | func main() { 417 | fmt.Fprintln(os.Stdout, "A") 418 | fmt.Fprintln(os.Stderr, "B") 419 | fmt.Fprintln(os.Stdout, "A") 420 | fmt.Fprintln(os.Stdout, "A") 421 | time.Sleep(time.Second) 422 | fmt.Fprintln(os.Stderr, "B") 423 | time.Sleep(time.Second) 424 | fmt.Fprintln(os.Stdout, "A") 425 | } 426 | `, wantEvents: []Event{ 427 | {"A\n", "stdout", 0}, 428 | {"B\n", "stderr", time.Nanosecond}, 429 | {"A\nA\n", "stdout", time.Nanosecond}, 430 | {"B\n", "stderr", time.Second - 2*time.Nanosecond}, 431 | {"A\n", "stdout", time.Second}, 432 | }}, 433 | 434 | { 435 | name: "third_party_imports", 436 | prog: ` 437 | package main 438 | import ("fmt"; "github.com/bradfitz/iter") 439 | func main() { for i := range iter.N(5) { fmt.Println(i) } } 440 | `, 441 | want: "0\n1\n2\n3\n4\n", 442 | }, 443 | 444 | { 445 | name: "compile_with_vet", 446 | withVet: true, 447 | wantVetErrors: "./prog.go:5:2: fmt.Printf format %v reads arg #1, but call has 0 args\n", 448 | prog: ` 449 | package main 450 | import "fmt" 451 | func main() { 452 | fmt.Printf("hi %v") 453 | } 454 | `, 455 | }, 456 | 457 | { 458 | name: "compile_without_vet", 459 | withVet: false, 460 | prog: ` 461 | package main 462 | import "fmt" 463 | func main() { 464 | fmt.Printf("hi %v") 465 | } 466 | `, 467 | }, 468 | 469 | { 470 | name: "compile_modules_with_vet", 471 | withVet: true, 472 | wantVetErrors: "go: finding module for package github.com/bradfitz/iter\ngo: found github.com/bradfitz/iter in github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8\n# play\n./prog.go:6:2: fmt.Printf format %v reads arg #1, but call has 0 args\n", 473 | prog: ` 474 | package main 475 | import ("fmt"; "github.com/bradfitz/iter") 476 | func main() { 477 | for i := range iter.N(5) { fmt.Println(i) } 478 | fmt.Printf("hi %v") 479 | } 480 | `, 481 | }, 482 | 483 | { 484 | name: "multi_file_basic", 485 | prog: ` 486 | package main 487 | const foo = "bar" 488 | 489 | -- two.go -- 490 | package main 491 | func main() { 492 | println(foo) 493 | } 494 | `, 495 | wantEvents: []Event{ 496 | {"bar\n", "stderr", 0}, 497 | }, 498 | }, 499 | 500 | { 501 | name: "multi_file_use_package", 502 | withVet: true, 503 | prog: ` 504 | package main 505 | 506 | import "play.test/foo" 507 | 508 | func main() { 509 | foo.Hello() 510 | } 511 | 512 | -- go.mod -- 513 | module play.test 514 | 515 | -- foo/foo.go -- 516 | package foo 517 | 518 | import "fmt" 519 | 520 | func Hello() { fmt.Println("hello world") } 521 | `, 522 | }, 523 | { 524 | name: "timeouts_handled_gracefully", 525 | prog: ` 526 | package main 527 | 528 | import ( 529 | "time" 530 | ) 531 | 532 | func main() { 533 | c := make(chan struct{}) 534 | 535 | go func() { 536 | defer close(c) 537 | for { 538 | time.Sleep(10 * time.Millisecond) 539 | } 540 | }() 541 | 542 | <-c 543 | } 544 | `, want: "timeout running program"}, 545 | { 546 | name: "timezone_info_exists", 547 | prog: ` 548 | package main 549 | 550 | import ( 551 | "fmt" 552 | "time" 553 | ) 554 | 555 | func main() { 556 | loc, _ := time.LoadLocation("Europe/Berlin") 557 | 558 | // This will look for the name CEST in the Europe/Berlin time zone. 559 | const longForm = "Jan 2, 2006 at 3:04pm (MST)" 560 | t, _ := time.ParseInLocation(longForm, "Jul 9, 2012 at 5:02am (CEST)", loc) 561 | fmt.Println(t) 562 | 563 | // Note: without explicit zone, returns time in given location. 564 | const shortForm = "2006-Jan-02" 565 | t, _ = time.ParseInLocation(shortForm, "2012-Jul-09", loc) 566 | fmt.Println(t) 567 | 568 | } 569 | `, want: "2012-07-09 05:02:00 +0200 CEST\n2012-07-09 00:00:00 +0200 CEST\n"}, 570 | { 571 | name: "cgo_enabled_0", 572 | prog: ` 573 | package main 574 | 575 | import ( 576 | "fmt" 577 | "net" 578 | ) 579 | 580 | func main() { 581 | fmt.Println(net.ParseIP("1.2.3.4")) 582 | } 583 | `, want: "1.2.3.4\n"}, 584 | } 585 | -------------------------------------------------------------------------------- /src/server_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 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 | package main 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "encoding/json" 11 | "fmt" 12 | "io/ioutil" 13 | "net/http" 14 | "net/http/httptest" 15 | "os" 16 | "runtime" 17 | "sync" 18 | "testing" 19 | "time" 20 | 21 | "github.com/bradfitz/gomemcache/memcache" 22 | "github.com/google/go-cmp/cmp" 23 | ) 24 | 25 | type testLogger struct { 26 | t *testing.T 27 | } 28 | 29 | func (l testLogger) Printf(format string, args ...interface{}) { 30 | l.t.Logf(format, args...) 31 | } 32 | func (l testLogger) Errorf(format string, args ...interface{}) { 33 | l.t.Errorf(format, args...) 34 | } 35 | func (l testLogger) Fatalf(format string, args ...interface{}) { 36 | l.t.Fatalf(format, args...) 37 | } 38 | 39 | func testingOptions(t *testing.T) func(s *server) error { 40 | return func(s *server) error { 41 | s.db = &inMemStore{} 42 | s.log = testLogger{t} 43 | var err error 44 | s.examples, err = newExamplesHandler(false, time.Now()) 45 | if err != nil { 46 | return err 47 | } 48 | return nil 49 | } 50 | } 51 | 52 | func TestEdit(t *testing.T) { 53 | s, err := newServer(testingOptions(t)) 54 | if err != nil { 55 | t.Fatalf("newServer(testingOptions(t)): %v", err) 56 | } 57 | id := "bar" 58 | barBody := []byte("Snippy McSnipface") 59 | snip := &snippet{Body: barBody} 60 | if err := s.db.PutSnippet(context.Background(), id, snip); err != nil { 61 | t.Fatalf("s.dbPutSnippet(context.Background(), %+v, %+v): %v", id, snip, err) 62 | } 63 | 64 | testCases := []struct { 65 | desc string 66 | method string 67 | url string 68 | statusCode int 69 | headers map[string]string 70 | respBody []byte 71 | }{ 72 | {"OPTIONS no-op", http.MethodOptions, "https://play.golang.org/p/foo", http.StatusOK, nil, nil}, 73 | {"foo.play.golang.org to play.golang.org", http.MethodGet, "https://foo.play.golang.org", http.StatusFound, map[string]string{"Location": "https://play.golang.org"}, nil}, 74 | {"Non-existent page", http.MethodGet, "https://play.golang.org/foo", http.StatusNotFound, nil, nil}, 75 | {"Unknown snippet", http.MethodGet, "https://play.golang.org/p/foo", http.StatusNotFound, nil, nil}, 76 | {"Existing snippet", http.MethodGet, "https://play.golang.org/p/" + id, http.StatusFound, nil, nil}, 77 | {"Plaintext snippet", http.MethodGet, "https://play.golang.org/p/" + id + ".go", http.StatusOK, nil, barBody}, 78 | {"Download snippet", http.MethodGet, "https://play.golang.org/p/" + id + ".go?download=true", http.StatusOK, map[string]string{"Content-Disposition": fmt.Sprintf(`attachment; filename="%s.go"`, id)}, barBody}, 79 | } 80 | 81 | for _, tc := range testCases { 82 | req := httptest.NewRequest(tc.method, tc.url, nil) 83 | w := httptest.NewRecorder() 84 | s.handleEdit(w, req) 85 | resp := w.Result() 86 | corsHeader := "Access-Control-Allow-Origin" 87 | if got, want := resp.Header.Get(corsHeader), "*"; got != want { 88 | t.Errorf("%s: %q header: got %q; want %q", tc.desc, corsHeader, got, want) 89 | } 90 | if got, want := resp.StatusCode, tc.statusCode; got != want { 91 | t.Errorf("%s: got unexpected status code %d; want %d", tc.desc, got, want) 92 | } 93 | for k, v := range tc.headers { 94 | if got, want := resp.Header.Get(k), v; got != want { 95 | t.Errorf("Got header value %q of %q; want %q", k, got, want) 96 | } 97 | } 98 | if tc.respBody != nil { 99 | defer resp.Body.Close() 100 | b, err := ioutil.ReadAll(resp.Body) 101 | if err != nil { 102 | t.Errorf("%s: ioutil.ReadAll(resp.Body): %v", tc.desc, err) 103 | } 104 | if !bytes.Equal(b, tc.respBody) { 105 | t.Errorf("%s: got unexpected body %q; want %q", tc.desc, b, tc.respBody) 106 | } 107 | } 108 | } 109 | } 110 | 111 | func TestServer(t *testing.T) { 112 | s, err := newServer(testingOptions(t)) 113 | if err != nil { 114 | t.Fatalf("newServer(testingOptions(t)): %v", err) 115 | } 116 | 117 | const shareURL = "https://play.golang.org/share" 118 | testCases := []struct { 119 | desc string 120 | method string 121 | url string 122 | statusCode int 123 | reqBody []byte 124 | respBody []byte 125 | }{ 126 | // Share tests. 127 | {"OPTIONS no-op", http.MethodOptions, shareURL, http.StatusOK, nil, nil}, 128 | {"Non-POST request", http.MethodGet, shareURL, http.StatusMethodNotAllowed, nil, nil}, 129 | {"Standard flow", http.MethodPost, shareURL, http.StatusOK, []byte("Snippy McSnipface"), []byte("N_M_YelfGeR")}, 130 | {"Snippet too large", http.MethodPost, shareURL, http.StatusRequestEntityTooLarge, make([]byte, maxSnippetSize+1), nil}, 131 | 132 | // Examples tests. 133 | {"Hello example", http.MethodGet, "https://play.golang.org/doc/play/hello.txt", http.StatusOK, nil, []byte("Hello")}, 134 | {"HTTP example", http.MethodGet, "https://play.golang.org/doc/play/http.txt", http.StatusOK, nil, []byte("net/http")}, 135 | {"Versions json", http.MethodGet, "https://play.golang.org/version", http.StatusOK, nil, []byte(runtime.Version())}, 136 | } 137 | 138 | for _, tc := range testCases { 139 | req := httptest.NewRequest(tc.method, tc.url, bytes.NewReader(tc.reqBody)) 140 | w := httptest.NewRecorder() 141 | s.mux.ServeHTTP(w, req) 142 | resp := w.Result() 143 | corsHeader := "Access-Control-Allow-Origin" 144 | if got, want := resp.Header.Get(corsHeader), "*"; got != want { 145 | t.Errorf("%s: %q header: got %q; want %q", tc.desc, corsHeader, got, want) 146 | } 147 | if got, want := resp.StatusCode, tc.statusCode; got != want { 148 | t.Errorf("%s: got unexpected status code %d; want %d", tc.desc, got, want) 149 | } 150 | if tc.respBody != nil { 151 | defer resp.Body.Close() 152 | b, err := ioutil.ReadAll(resp.Body) 153 | if err != nil { 154 | t.Errorf("%s: ioutil.ReadAll(resp.Body): %v", tc.desc, err) 155 | } 156 | if !bytes.Contains(b, tc.respBody) { 157 | t.Errorf("%s: got unexpected body %q; want contains %q", tc.desc, b, tc.respBody) 158 | } 159 | } 160 | } 161 | } 162 | 163 | func TestCommandHandler(t *testing.T) { 164 | s, err := newServer(func(s *server) error { 165 | s.db = &inMemStore{} 166 | // testLogger makes tests fail. 167 | // Should we verify that s.log.Errorf was called 168 | // instead of just printing or failing the test? 169 | s.log = newStdLogger() 170 | s.cache = new(inMemCache) 171 | var err error 172 | s.examples, err = newExamplesHandler(false, time.Now()) 173 | if err != nil { 174 | return err 175 | } 176 | return nil 177 | }) 178 | if err != nil { 179 | t.Fatalf("newServer(testingOptions(t)): %v", err) 180 | } 181 | testHandler := s.commandHandler("test", func(_ context.Context, r *request) (*response, error) { 182 | if r.Body == "fail" { 183 | return nil, fmt.Errorf("non recoverable") 184 | } 185 | if r.Body == "error" { 186 | return &response{Errors: "errors"}, nil 187 | } 188 | if r.Body == "oom-error" { 189 | // To throw an oom in a local playground instance, increase the server timeout 190 | // to 20 seconds (within sandbox.go), spin up the Docker instance and run 191 | // this code: https://play.golang.org/p/aaCv86m0P14. 192 | return &response{Events: []Event{{"out of memory", "stderr", 0}}}, nil 193 | } 194 | if r.Body == "allocate-memory-error" { 195 | return &response{Events: []Event{{"cannot allocate memory", "stderr", 0}}}, nil 196 | } 197 | if r.Body == "oom-compile-error" { 198 | return &response{Errors: "out of memory"}, nil 199 | } 200 | if r.Body == "allocate-memory-compile-error" { 201 | return &response{Errors: "cannot allocate memory"}, nil 202 | } 203 | if r.Body == "build-timeout-error" { 204 | return &response{Errors: goBuildTimeoutError}, nil 205 | } 206 | if r.Body == "run-timeout-error" { 207 | return &response{Errors: runTimeoutError}, nil 208 | } 209 | resp := &response{Events: []Event{{r.Body, "stdout", 0}}} 210 | return resp, nil 211 | }) 212 | 213 | testCases := []struct { 214 | desc string 215 | method string 216 | statusCode int 217 | reqBody []byte 218 | respBody []byte 219 | shouldCache bool 220 | }{ 221 | {"OPTIONS request", http.MethodOptions, http.StatusOK, nil, nil, false}, 222 | {"GET request", http.MethodGet, http.StatusBadRequest, nil, nil, false}, 223 | {"Empty POST", http.MethodPost, http.StatusBadRequest, nil, nil, false}, 224 | {"Failed cmdFunc", http.MethodPost, http.StatusInternalServerError, []byte(`{"Body":"fail"}`), nil, false}, 225 | {"Standard flow", http.MethodPost, http.StatusOK, 226 | []byte(`{"Body":"ok"}`), 227 | []byte(`{"Errors":"","Events":[{"Message":"ok","Kind":"stdout","Delay":0}],"Status":0,"IsTest":false,"TestsFailed":0} 228 | `), 229 | true}, 230 | {"Cache-able Errors in response", http.MethodPost, http.StatusOK, 231 | []byte(`{"Body":"error"}`), 232 | []byte(`{"Errors":"errors","Events":null,"Status":0,"IsTest":false,"TestsFailed":0} 233 | `), 234 | true}, 235 | {"Out of memory error in response body event message", http.MethodPost, http.StatusInternalServerError, 236 | []byte(`{"Body":"oom-error"}`), nil, false}, 237 | {"Cannot allocate memory error in response body event message", http.MethodPost, http.StatusInternalServerError, 238 | []byte(`{"Body":"allocate-memory-error"}`), nil, false}, 239 | {"Out of memory error in response errors", http.MethodPost, http.StatusInternalServerError, 240 | []byte(`{"Body":"oom-compile-error"}`), nil, false}, 241 | {"Cannot allocate memory error in response errors", http.MethodPost, http.StatusInternalServerError, 242 | []byte(`{"Body":"allocate-memory-compile-error"}`), nil, false}, 243 | { 244 | desc: "Build timeout error", 245 | method: http.MethodPost, 246 | statusCode: http.StatusOK, 247 | reqBody: []byte(`{"Body":"build-timeout-error"}`), 248 | respBody: []byte(fmt.Sprintln(`{"Errors":"timeout running go build","Events":null,"Status":0,"IsTest":false,"TestsFailed":0}`)), 249 | }, 250 | { 251 | desc: "Run timeout error", 252 | method: http.MethodPost, 253 | statusCode: http.StatusOK, 254 | reqBody: []byte(`{"Body":"run-timeout-error"}`), 255 | respBody: []byte(fmt.Sprintln(`{"Errors":"timeout running program","Events":null,"Status":0,"IsTest":false,"TestsFailed":0}`)), 256 | }, 257 | } 258 | 259 | for _, tc := range testCases { 260 | t.Run(tc.desc, func(t *testing.T) { 261 | req := httptest.NewRequest(tc.method, "/compile", bytes.NewReader(tc.reqBody)) 262 | w := httptest.NewRecorder() 263 | testHandler(w, req) 264 | resp := w.Result() 265 | corsHeader := "Access-Control-Allow-Origin" 266 | if got, want := resp.Header.Get(corsHeader), "*"; got != want { 267 | t.Errorf("%s: %q header: got %q; want %q", tc.desc, corsHeader, got, want) 268 | } 269 | if got, want := resp.StatusCode, tc.statusCode; got != want { 270 | t.Errorf("%s: got unexpected status code %d; want %d", tc.desc, got, want) 271 | } 272 | if tc.respBody != nil { 273 | defer resp.Body.Close() 274 | b, err := ioutil.ReadAll(resp.Body) 275 | if err != nil { 276 | t.Errorf("%s: ioutil.ReadAll(resp.Body): %v", tc.desc, err) 277 | } 278 | if !bytes.Equal(b, tc.respBody) { 279 | t.Errorf("%s: got unexpected body %q; want %q", tc.desc, b, tc.respBody) 280 | } 281 | } 282 | 283 | // Test caching semantics. 284 | sbreq := new(request) // A sandbox request, used in the cache key. 285 | json.Unmarshal(tc.reqBody, sbreq) // Ignore errors, request may be empty. 286 | gotCache := new(response) 287 | if err := s.cache.Get(cacheKey("test", sbreq.Body), gotCache); (err == nil) != tc.shouldCache { 288 | t.Errorf("s.cache.Get(%q, %v) = %v, shouldCache: %v", cacheKey("test", sbreq.Body), gotCache, err, tc.shouldCache) 289 | } 290 | wantCache := new(response) 291 | if tc.shouldCache { 292 | if err := json.Unmarshal(tc.respBody, wantCache); err != nil { 293 | t.Errorf("json.Unmarshal(%q, %v) = %v, wanted no error", tc.respBody, wantCache, err) 294 | } 295 | } 296 | if diff := cmp.Diff(wantCache, gotCache); diff != "" { 297 | t.Errorf("s.Cache.Get(%q) mismatch (-want +got):\n%s", cacheKey("test", sbreq.Body), diff) 298 | } 299 | }) 300 | } 301 | } 302 | 303 | func TestPlaygroundGoproxy(t *testing.T) { 304 | const envKey = "PLAY_GOPROXY" 305 | defer os.Setenv(envKey, os.Getenv(envKey)) 306 | 307 | tests := []struct { 308 | name string 309 | env string 310 | want string 311 | }{ 312 | {name: "missing", env: "", want: "https://proxy.golang.org"}, 313 | {name: "set_to_default", env: "https://proxy.golang.org", want: "https://proxy.golang.org"}, 314 | {name: "changed", env: "https://company.intranet", want: "https://company.intranet"}, 315 | } 316 | for _, tt := range tests { 317 | t.Run(tt.name, func(t *testing.T) { 318 | if tt.env != "" { 319 | if err := os.Setenv(envKey, tt.env); err != nil { 320 | t.Errorf("unable to set environment variable for test: %s", err) 321 | } 322 | } else { 323 | if err := os.Unsetenv(envKey); err != nil { 324 | t.Errorf("unable to unset environment variable for test: %s", err) 325 | } 326 | } 327 | got := playgroundGoproxy() 328 | if got != tt.want { 329 | t.Errorf("playgroundGoproxy = %s; want %s; env: %s", got, tt.want, tt.env) 330 | } 331 | }) 332 | } 333 | } 334 | 335 | // inMemCache is a responseCache backed by a map. It is only suitable for testing. 336 | type inMemCache struct { 337 | l sync.Mutex 338 | m map[string]*response 339 | } 340 | 341 | // Set implements the responseCache interface. 342 | // Set stores a *response in the cache. It panics for other types to ensure test failure. 343 | func (i *inMemCache) Set(key string, v interface{}) error { 344 | i.l.Lock() 345 | defer i.l.Unlock() 346 | if i.m == nil { 347 | i.m = make(map[string]*response) 348 | } 349 | i.m[key] = v.(*response) 350 | return nil 351 | } 352 | 353 | // Get implements the responseCache interface. 354 | // Get fetches a *response from the cache, or returns a memcache.ErrcacheMiss. 355 | // It panics for other types to ensure test failure. 356 | func (i *inMemCache) Get(key string, v interface{}) error { 357 | i.l.Lock() 358 | defer i.l.Unlock() 359 | target := v.(*response) 360 | got, ok := i.m[key] 361 | if !ok { 362 | return memcache.ErrCacheMiss 363 | } 364 | *target = *got 365 | return nil 366 | } 367 | -------------------------------------------------------------------------------- /src/static/playground.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 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 | /* 6 | In the absence of any formal way to specify interfaces in JavaScript, 7 | here's a skeleton implementation of a playground transport. 8 | 9 | function Transport() { 10 | // Set up any transport state (eg, make a websocket connection). 11 | return { 12 | Run: function(body, output, options) { 13 | // Compile and run the program 'body' with 'options'. 14 | // Call the 'output' callback to display program output. 15 | return { 16 | Kill: function() { 17 | // Kill the running program. 18 | } 19 | }; 20 | } 21 | }; 22 | } 23 | 24 | // The output callback is called multiple times, and each time it is 25 | // passed an object of this form. 26 | var write = { 27 | Kind: 'string', // 'start', 'stdout', 'stderr', 'end', 'system' 28 | Body: 'string' // content of write or end status message 29 | } 30 | 31 | // The first call must be of Kind 'start' with no body. 32 | // Subsequent calls may be of Kind 'stdout' or 'stderr' 33 | // and must have a non-null Body string. 34 | // The final call should be of Kind 'end' with an optional 35 | // Body string, signifying a failure ("killed", for example), 36 | // or be of Kind 'system'. 37 | 38 | // The output callback must be of this form. 39 | // See PlaygroundOutput (below) for an implementation. 40 | function outputCallback(write) { 41 | } 42 | */ 43 | 44 | // HTTPTransport is the default transport. 45 | // enableVet enables running vet if a program was compiled and ran successfully. 46 | // If vet returned any errors, display them before the output of a program. 47 | function HTTPTransport(enableVet) { 48 | 'use strict'; 49 | 50 | function playback(output, data) { 51 | // Backwards compatibility: default values do not affect the output. 52 | var events = data.Events || []; 53 | var errors = data.Errors || ''; 54 | var vetErrors = data.VetErrors || ''; 55 | var status = data.Status || 0; 56 | var isTest = data.IsTest || false; 57 | var testsFailed = data.TestsFailed || 0; 58 | 59 | var timeout; 60 | output({ Kind: 'start' }); 61 | if (vetErrors !== '') { 62 | output({ Kind: 'stderr', Body: vetErrors }); 63 | output({ Kind: 'system', Body: '\nGo vet failed.\n\n' }); 64 | } 65 | function next() { 66 | if (!events || events.length === 0) { 67 | if (isTest) { 68 | if (testsFailed > 0) { 69 | output({ 70 | Kind: 'system', 71 | Body: 72 | '\n' + 73 | testsFailed + 74 | ' test' + 75 | (testsFailed > 1 ? 's' : '') + 76 | ' failed.', 77 | }); 78 | } else { 79 | output({ Kind: 'system', Body: '\nAll tests passed.' }); 80 | } 81 | } else { 82 | if (status > 0) { 83 | output({ Kind: 'end', Body: 'status ' + status + '.' }); 84 | } else { 85 | if (errors !== '') { 86 | // errors are displayed only in the case of timeout. 87 | output({ Kind: 'end', Body: errors + '.' }); 88 | } else { 89 | output({ Kind: 'end' }); 90 | } 91 | } 92 | } 93 | return; 94 | } 95 | var e = events.shift(); 96 | if (e.Delay === 0) { 97 | output({ Kind: e.Kind, Body: e.Message }); 98 | next(); 99 | return; 100 | } 101 | timeout = setTimeout(function() { 102 | output({ Kind: e.Kind, Body: e.Message }); 103 | next(); 104 | }, e.Delay / 1000000); 105 | } 106 | next(); 107 | return { 108 | Stop: function() { 109 | clearTimeout(timeout); 110 | }, 111 | }; 112 | } 113 | 114 | function error(output, msg) { 115 | output({ Kind: 'start' }); 116 | output({ Kind: 'stderr', Body: msg }); 117 | output({ Kind: 'end' }); 118 | } 119 | 120 | function buildFailed(output, msg) { 121 | output({ Kind: 'start' }); 122 | output({ Kind: 'stderr', Body: msg }); 123 | output({ Kind: 'system', Body: '\nGo build failed.' }); 124 | } 125 | 126 | var seq = 0; 127 | return { 128 | Run: function(body, output, options) { 129 | seq++; 130 | var cur = seq; 131 | var playing; 132 | $.ajax('/compile?backend=' + (options.backend || ''), { 133 | type: 'POST', 134 | data: { version: 2, body: body, withVet: enableVet }, 135 | dataType: 'json', 136 | success: function(data) { 137 | if (seq != cur) return; 138 | if (!data) return; 139 | if (playing != null) playing.Stop(); 140 | if (data.Errors) { 141 | if (data.Errors === 'process took too long') { 142 | // Playback the output that was captured before the timeout. 143 | playing = playback(output, data); 144 | } else { 145 | buildFailed(output, data.Errors); 146 | } 147 | return; 148 | } 149 | playing = playback(output, data); 150 | }, 151 | error: function() { 152 | error(output, 'Error communicating with remote server.'); 153 | }, 154 | }); 155 | return { 156 | Kill: function() { 157 | if (playing != null) playing.Stop(); 158 | output({ Kind: 'end', Body: 'killed' }); 159 | }, 160 | }; 161 | }, 162 | }; 163 | } 164 | 165 | function SocketTransport() { 166 | 'use strict'; 167 | 168 | var id = 0; 169 | var outputs = {}; 170 | var started = {}; 171 | var websocket; 172 | if (window.location.protocol == 'http:') { 173 | websocket = new WebSocket('ws://' + window.location.host + '/socket'); 174 | } else if (window.location.protocol == 'https:') { 175 | websocket = new WebSocket('wss://' + window.location.host + '/socket'); 176 | } 177 | 178 | websocket.onclose = function() { 179 | console.log('websocket connection closed'); 180 | }; 181 | 182 | websocket.onmessage = function(e) { 183 | var m = JSON.parse(e.data); 184 | var output = outputs[m.Id]; 185 | if (output === null) return; 186 | if (!started[m.Id]) { 187 | output({ Kind: 'start' }); 188 | started[m.Id] = true; 189 | } 190 | output({ Kind: m.Kind, Body: m.Body }); 191 | }; 192 | 193 | function send(m) { 194 | websocket.send(JSON.stringify(m)); 195 | } 196 | 197 | return { 198 | Run: function(body, output, options) { 199 | var thisID = id + ''; 200 | id++; 201 | outputs[thisID] = output; 202 | send({ Id: thisID, Kind: 'run', Body: body, Options: options }); 203 | return { 204 | Kill: function() { 205 | send({ Id: thisID, Kind: 'kill' }); 206 | }, 207 | }; 208 | }, 209 | }; 210 | } 211 | 212 | function PlaygroundOutput(el) { 213 | 'use strict'; 214 | 215 | return function(write) { 216 | if (write.Kind == 'start') { 217 | el.innerHTML = ''; 218 | return; 219 | } 220 | 221 | var cl = 'system'; 222 | if (write.Kind == 'stdout' || write.Kind == 'stderr') cl = write.Kind; 223 | 224 | var m = write.Body; 225 | if (write.Kind == 'end') { 226 | m = '\nProgram exited' + (m ? ': ' + m : '.'); 227 | } 228 | 229 | if (m.indexOf('IMAGE:') === 0) { 230 | // TODO(adg): buffer all writes before creating image 231 | var url = 'data:image/png;base64,' + m.substr(6); 232 | var img = document.createElement('img'); 233 | img.src = url; 234 | el.appendChild(img); 235 | return; 236 | } 237 | 238 | // ^L clears the screen. 239 | var s = m.split('\x0c'); 240 | if (s.length > 1) { 241 | el.innerHTML = ''; 242 | m = s.pop(); 243 | } 244 | 245 | m = m.replace(/&/g, '&'); 246 | m = m.replace(//g, '>'); 248 | 249 | var needScroll = el.scrollTop + el.offsetHeight == el.scrollHeight; 250 | 251 | var span = document.createElement('span'); 252 | span.className = cl; 253 | span.innerHTML = m; 254 | el.appendChild(span); 255 | 256 | if (needScroll) el.scrollTop = el.scrollHeight - el.offsetHeight; 257 | }; 258 | } 259 | 260 | (function() { 261 | function lineHighlight(error) { 262 | var regex = /prog.go:([0-9]+)/g; 263 | var r = regex.exec(error); 264 | while (r) { 265 | $('.lines div') 266 | .eq(r[1] - 1) 267 | .addClass('lineerror'); 268 | r = regex.exec(error); 269 | } 270 | } 271 | function highlightOutput(wrappedOutput) { 272 | return function(write) { 273 | if (write.Body) lineHighlight(write.Body); 274 | wrappedOutput(write); 275 | }; 276 | } 277 | function lineClear() { 278 | $('.lineerror').removeClass('lineerror'); 279 | } 280 | 281 | // opts is an object with these keys 282 | // codeEl - code editor element 283 | // outputEl - program output element 284 | // runEl - run button element 285 | // fmtEl - fmt button element (optional) 286 | // fmtImportEl - fmt "imports" checkbox element (optional) 287 | // shareEl - share button element (optional) 288 | // shareURLEl - share URL text input element (optional) 289 | // shareRedirect - base URL to redirect to on share (optional) 290 | // toysEl - toys select element (optional) 291 | // enableHistory - enable using HTML5 history API (optional) 292 | // transport - playground transport to use (default is HTTPTransport) 293 | // enableShortcuts - whether to enable shortcuts (Ctrl+S/Cmd+S to save) (default is false) 294 | // enableVet - enable running vet and displaying its errors 295 | function playground(opts) { 296 | var code = $(opts.codeEl); 297 | var transport = opts['transport'] || new HTTPTransport(opts['enableVet']); 298 | var running; 299 | 300 | // autoindent helpers. 301 | function insertTabs(n) { 302 | // Without the n > 0 check, Safari cannot type a blank line at the bottom of a playground snippet. 303 | // See go.dev/issue/49794. 304 | if (n > 0) { 305 | document.execCommand('insertText', false, '\t'.repeat(n)); 306 | } 307 | } 308 | function autoindent(el) { 309 | var curpos = el.selectionStart; 310 | var tabs = 0; 311 | while (curpos > 0) { 312 | curpos--; 313 | if (el.value[curpos] == '\t') { 314 | tabs++; 315 | } else if (tabs > 0 || el.value[curpos] == '\n') { 316 | break; 317 | } 318 | } 319 | setTimeout(function() { 320 | insertTabs(tabs); 321 | }, 1); 322 | } 323 | 324 | // NOTE(cbro): e is a jQuery event, not a DOM event. 325 | function handleSaveShortcut(e) { 326 | if (e.isDefaultPrevented()) return false; 327 | if (!e.metaKey && !e.ctrlKey) return false; 328 | if (e.key != 'S' && e.key != 's') return false; 329 | 330 | e.preventDefault(); 331 | 332 | // Share and save 333 | share(function(url) { 334 | window.location.href = url + '.go?download=true'; 335 | }); 336 | 337 | return true; 338 | } 339 | 340 | function keyHandler(e) { 341 | if (opts.enableShortcuts && handleSaveShortcut(e)) return; 342 | 343 | if (e.keyCode == 9 && !e.ctrlKey) { 344 | // tab (but not ctrl-tab) 345 | insertTabs(1); 346 | e.preventDefault(); 347 | return false; 348 | } 349 | if (e.keyCode == 13) { 350 | // enter 351 | if (e.shiftKey) { 352 | // +shift 353 | run(); 354 | e.preventDefault(); 355 | return false; 356 | } 357 | if (e.ctrlKey) { 358 | // +control 359 | fmt(); 360 | e.preventDefault(); 361 | } else { 362 | autoindent(e.target); 363 | } 364 | } 365 | return true; 366 | } 367 | code.unbind('keydown').bind('keydown', keyHandler); 368 | var outdiv = $(opts.outputEl).empty(); 369 | var output = $('
').appendTo(outdiv);
370 | 
371 |     function body() {
372 |       return $(opts.codeEl).val();
373 |     }
374 |     function setBody(text) {
375 |       $(opts.codeEl).val(text);
376 |     }
377 |     function origin(href) {
378 |       return ('' + href)
379 |         .split('/')
380 |         .slice(0, 3)
381 |         .join('/');
382 |     }
383 | 
384 |     var pushedPlay = window.location.pathname == '/play/';
385 |     function inputChanged() {
386 |       if (pushedPlay) {
387 |         return;
388 |       }
389 |       pushedPlay = true;
390 |       $(opts.shareURLEl).hide();
391 |       $(opts.toysEl).show();
392 |       var path = window.location.pathname;
393 |       var i = path.indexOf('/play/');
394 |       var p = path.substr(0, i+6);
395 |       // if (opts.versionEl !== null) {
396 |       //   var v = $(opts.versionEl).val();
397 |       //   if (v != '') {
398 |       //     p += '?v=' + v;
399 |       //   }
400 |       // }
401 |       window.history.pushState(null, '', p);
402 |     }
403 |     function popState(e) {
404 |       if (e === null) {
405 |         return;
406 |       }
407 |       if (e && e.state && e.state.code) {
408 |         setBody(e.state.code);
409 |       }
410 |     }
411 |     var rewriteHistory = false;
412 |     if (
413 |       window.history &&
414 |       window.history.pushState &&
415 |       window.addEventListener &&
416 |       opts.enableHistory
417 |     ) {
418 |       rewriteHistory = true;
419 |       code[0].addEventListener('input', inputChanged);
420 |       window.addEventListener('popstate', popState);
421 |     }
422 | 
423 |     function backend() {
424 |       if (!opts.versionEl) {
425 |         return '';
426 |       }
427 |       var vers = $(opts.versionEl);
428 |       if (!vers) {
429 |         return '';
430 |       }
431 |       return vers.val();
432 |     }
433 | 
434 |     function setError(error) {
435 |       if (running) running.Kill();
436 |       lineClear();
437 |       lineHighlight(error);
438 |       output
439 |         .empty()
440 |         .addClass('error')
441 |         .text(error);
442 |     }
443 |     function loading() {
444 |       lineClear();
445 |       if (running) running.Kill();
446 |       output.removeClass('error').text('Waiting for remote server...');
447 |     }
448 |     function runOnly() {
449 |       loading();
450 |       running = transport.Run(
451 |         body(),
452 |         highlightOutput(PlaygroundOutput(output[0])),
453 |         {backend: backend()},
454 |       );
455 |     }
456 | 
457 |     function fmtAnd(run) {
458 |       loading();
459 |       var data = { body: body() };
460 |       data['imports'] = 'true';
461 |       $.ajax('/fmt?backend='+backend(), {
462 |         data: data,
463 |         type: 'POST',
464 |         dataType: 'json',
465 |         success: function(data) {
466 |           if (data.Error) {
467 |             setError(data.Error);
468 |           } else {
469 |             setBody(data.Body);
470 |             setError('');
471 |           }
472 |           run();
473 |         },
474 |         error: function() {
475 |           setError('Error communicating with remote server.');
476 |         },
477 |       });
478 |     }
479 | 
480 |     function loadShare(id) {
481 |       $.ajax('/share?id='+id, {
482 |         processData: false,
483 |         type: 'GET',
484 |         complete: function(xhr) {
485 |           if(xhr.status != 200) {
486 |             setBody('Cannot load shared snippet; try again.');
487 |             return;
488 |           }
489 |           setBody(xhr.responseText);
490 |         },
491 |       })
492 |     }
493 | 
494 |     function fmt() {
495 |       fmtAnd(function(){});
496 |     }
497 | 
498 |     function run() {
499 |       fmtAnd(runOnly);
500 |     }
501 | 
502 |     var shareURL; // jQuery element to show the shared URL.
503 |     var sharing = false; // true if there is a pending request.
504 |     var shareCallbacks = [];
505 |     function share(opt_callback) {
506 |       if (opt_callback) shareCallbacks.push(opt_callback);
507 | 
508 |       if (sharing) return;
509 |       sharing = true;
510 | 
511 |       var errorMessages = {
512 |         413: 'Snippet is too large to share.'
513 |       };
514 | 
515 |       var sharingData = body();
516 |       $.ajax('/share', {
517 |         processData: false,
518 |         data: sharingData,
519 |         type: 'POST',
520 |         contentType: 'text/plain; charset=utf-8',
521 |         complete: function(xhr) {
522 |           sharing = false;
523 |           if (xhr.status != 200) {
524 |             var alertMsg = errorMessages[xhr.status] ? errorMessages[xhr.status] : 'Server error; try again.';
525 |             alert(alertMsg);
526 |             return;
527 |           }
528 |           if (opts.shareRedirect) {
529 |             window.location = opts.shareRedirect + xhr.responseText;
530 |           }
531 |           var path = '/p/' + xhr.responseText;
532 |           // if (opts.versionEl !== null && $(opts.versionEl).val() != "") {
533 |           //   path += "?v=" + $(opts.versionEl).val();
534 |           // }
535 |           var url = origin(window.location) + path;
536 |           for (var i = 0; i < shareCallbacks.length; i++) {
537 |             shareCallbacks[i](url);
538 |           }
539 |           shareCallbacks = [];
540 | 
541 |           if (shareURL) {
542 |             shareURL
543 |               .show()
544 |               .val(url)
545 |               .focus()
546 |               .select();
547 | 
548 |             $(opts.toysEl).hide();
549 |             if (rewriteHistory) {
550 |               var historyData = { code: sharingData };
551 |               window.history.pushState(historyData, '', path);
552 |               pushedPlay = false;
553 |             }
554 |           }
555 |         },
556 |       });
557 |     }
558 | 
559 |     $(opts.runEl).click(run);
560 |     $(opts.fmtEl).click(fmt);
561 | 
562 |     if (
563 |       opts.shareEl !== null &&
564 |       (opts.shareURLEl !== null || opts.shareRedirect !== null)
565 |     ) {
566 |       if (opts.shareURLEl) {
567 |         shareURL = $(opts.shareURLEl).hide();
568 |       }
569 |       $(opts.shareEl).click(function() {
570 |         share();
571 |       });
572 |     }
573 | 
574 |     var path = window.location.pathname;
575 |     var toyDisable = false;
576 |     if (path.startsWith('/go.dev/')) {
577 |       path = path.slice(7);
578 |     }
579 |     if (path.startsWith('/p/')) {
580 |       var id = path.slice(3);
581 |       id = id.replace(/\.go$/, "");
582 |       loadShare(id);
583 |       toyDisable = true;
584 |     }
585 | 
586 |     if (opts.toysEl !== null) {
587 |       $(opts.toysEl).bind('change', function() {
588 |         if (toyDisable) {
589 |           toyDisable = false;
590 |           return;
591 |         }
592 |         var toy = $(this).val();
593 |         $.ajax('/doc/play/' + toy, {
594 |           processData: false,
595 |           type: 'GET',
596 |           complete: function(xhr) {
597 |             if (xhr.status != 200) {
598 |               alert('Server error; try again.');
599 |               return;
600 |             }
601 |             setBody(xhr.responseText);
602 |             if (toy.includes('-dev') && opts.versionEl !== null) {
603 |               $(opts.versionEl).val('gotip');
604 |             }
605 |             run();
606 |           },
607 |         });
608 |       });
609 |     }
610 | 
611 |     if (opts.versionEl !== null) {
612 |      var select = $(opts.versionEl);
613 |       var v = (new URL(window.location)).searchParams.get('v');
614 |       if (v !== null && v != "") {
615 |       	select.val(v);
616 |         if (select.val() != v) {
617 |           select.append($('