├── .dockerignore ├── .gitignore ├── README.md ├── docker-compose.yml ├── docker ├── Dockerfile.actuator ├── Dockerfile.sandbox └── Dockerfile.web ├── make-images.sh ├── pull-images.sh ├── screenshot.png └── src ├── AUTHORS ├── CONTRIBUTORS ├── LICENSE ├── PATENTS ├── README.md ├── cache.go ├── client.go ├── edit.go ├── edit.html ├── enable-fake-time.patch ├── examples.go ├── examples ├── README.md ├── clear.txt ├── fib.txt ├── http.txt ├── image.txt ├── index-dev.txt ├── life.txt ├── multi.txt ├── peano.txt ├── pi.txt ├── sieve.txt ├── sleep.txt ├── solitaire.txt ├── test.txt └── tree.txt ├── fake_fs.lst ├── fmt.go ├── fmt_test.go ├── go.mod ├── go.sum ├── internal ├── internal.go └── internal_test.go ├── logger.go ├── main.go ├── metrics.go ├── play.go ├── play_test.go ├── sandbox.go ├── sandbox ├── metrics.go ├── sandbox.go ├── sandbox_test.go └── sandboxtypes │ └── types.go ├── sandbox_test.go ├── server.go ├── server_test.go ├── static ├── favicon.ico ├── godoc.css ├── gopher.png ├── jquery-linedtextarea.js ├── jquery.min.js ├── playground.js └── style.css ├── store.go ├── tests.go ├── txtar.go ├── txtar_test.go ├── version.go └── vet.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | docker 3 | .dockerignore 4 | .gitignore 5 | docker-compose.yml 6 | make-images.sh 7 | README.md 8 | screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang 游乐场 (Go Playground) 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,桌面操作系统可以访问官网[下载安装文件](https://www.docker.com/get-started/),服务器版本的 Docker 安装,可以[参考这里](https://soulteary.com/2022/06/21/building-a-cost-effective-linux-learning-environment-on-a-laptop-the-basics.html#%E6%9B%B4%E7%AE%80%E5%8D%95%E7%9A%84-docker-%E5%AE%89%E8%A3%85)。 19 | 20 | **然后**,执行下面的命令或者项目中的程序(`bash pull-images.sh`),获取必要的镜像文件: 21 | 22 | ```bash 23 | docker pull soulteary/golang-playground:web-1.19.0 24 | docker pull soulteary/golang-playground:sandbox-1.19.0 25 | docker pull soulteary/golang-playground:actuator-1.19.0 26 | docker pull memcached:1.6.15-alpine 27 | ``` 28 | 29 | **接着**,在镜像获取完毕之后,使用 `docker-compose up -d` 或 `docker compose up -d`,启动程序。 30 | 31 | **最后**,打开浏览器,访问 `http://localhost:8080`,就可以开始 Golang 之旅啦。 32 | 33 | ## 构建镜像 34 | 35 | 如果你希望进行二次开发,或者“自己动手”从零构建这套程序,可以执行项目目录中的程序(`bash make-images.sh`),程序会自动构建运行所需要的各种镜像。 36 | 37 | ```bash 38 | # bash make-images.sh 39 | Sending build context to Docker daemon 1.349MB 40 | Step 1/30 : ARG GO_VERSION=1.19.0 41 | Step 2/30 : FROM golang:${GO_VERSION}-alpine3.16 AS build-playground 42 | ---> 5e999c13ceac 43 | Step 3/30 : LABEL maintainer="soulteary@gmail.com" 44 | ---> Using cache 45 | ---> a253b22ef53a 46 | ... 47 | Successfully built 37e124ce9e7f 48 | Successfully tagged soulteary/golang-playground:web-1.19.0 49 | ... 50 | Successfully built 6017738b85ce 51 | Successfully tagged soulteary/golang-playground:sandbox-1.19.0 52 | Step 1/24 : ARG GO_VERSION=1.19.0 53 | Step 2/24 : FROM golang:${GO_VERSION}-alpine3.16 AS build-sandbox 54 | ... 55 | Successfully built c51b8a6647fb 56 | Successfully tagged soulteary/golang-playground:actuator-1.19.0 57 | ``` 58 | 59 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | sandbox: 5 | image: soulteary/golang-playground:sandbox-1.19.0 6 | restart: always 7 | command: -mode=server -listen=0.0.0.0:80 -workers=1 -untrusted-container=soulteary/golang-playground:actuator-1.19.0 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: soulteary/golang-playground:web-1.19.0 17 | restart: always 18 | environment: 19 | - SANDBOX_BACKEND_URL=http://sandbox:/run 20 | - MEMCACHED_ADDR=memcached:11211 21 | ports: 22 | - 8080:8080 23 | depends_on: 24 | - sandbox 25 | networks: 26 | - playground 27 | 28 | memcached: 29 | image: memcached:1.6.15-alpine 30 | command: memcached -m 64 31 | networks: 32 | - playground 33 | networks: 34 | playground: -------------------------------------------------------------------------------- /docker/Dockerfile.actuator: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION=1.19.0 2 | FROM golang:${GO_VERSION}-alpine3.16 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.16 AS build-actuator 20 | LABEL maintainer="soulteary@gmail.com" 21 | RUN echo '' > /etc/apk/repositories && \ 22 | echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.16/main" >> /etc/apk/repositories && \ 23 | echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.16/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 | 31 | 32 | FROM busybox:glibc 33 | LABEL maintainer="soulteary@gmail.com" 34 | COPY --from=build-actuator /bin/play-sandbox /bin/play-sandbox 35 | COPY --from=build-actuator /usr/share/zoneinfo /usr/share/zoneinfo 36 | ENTRYPOINT ["/bin/play-sandbox"] -------------------------------------------------------------------------------- /docker/Dockerfile.sandbox: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION=1.19.0 2 | FROM golang:${GO_VERSION}-alpine3.16 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.16 20 | LABEL maintainer="soulteary@gmail.com" 21 | RUN echo '' > /etc/apk/repositories && \ 22 | echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.16/main" >> /etc/apk/repositories && \ 23 | echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.16/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"] -------------------------------------------------------------------------------- /docker/Dockerfile.web: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION=1.19.0 2 | FROM golang:${GO_VERSION}-alpine3.16 AS build-playground 3 | LABEL maintainer="soulteary@gmail.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.16 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 | WORKDIR /usr/local/go-faketime 28 | RUN ./bin/go install --tags=faketime std 29 | RUN ./bin/go vet --tags=faketime std || true 30 | 31 | RUN mkdir /app 32 | COPY --from=build-playground /go/src/playground/playground /app 33 | COPY src/edit.html /app 34 | COPY src/static /app/static 35 | COPY src/examples /app/examples 36 | WORKDIR /app 37 | 38 | EXPOSE 8080 39 | ENTRYPOINT ["/app/playground"] -------------------------------------------------------------------------------- /make-images.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker build -t soulteary/golang-playground:web-1.19.0 -f docker/Dockerfile.web . 4 | docker build -t soulteary/golang-playground:sandbox-1.19.0 -f docker/Dockerfile.sandbox . 5 | docker build -t soulteary/golang-playground:actuator-1.19.0 -f docker/Dockerfile.actuator . 6 | -------------------------------------------------------------------------------- /pull-images.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker pull soulteary/golang-playground:web-1.19.0 4 | docker pull soulteary/golang-playground:sandbox-1.19.0 5 | docker pull soulteary/golang-playground:actuator-1.19.0 6 | docker pull memcached:1.6.15-alpine 7 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulteary/golang-playground/a7635d7604afe2720da533f0c08e8030b6d5c55a/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | "html/template" 9 | "net/http" 10 | "runtime" 11 | ) 12 | 13 | var editTemplate = template.Must(template.ParseFiles("edit.html")) 14 | 15 | type snippet struct { 16 | Body []byte `datastore:",noindex"` // golang.org/issues/23253 17 | } 18 | 19 | type editData struct { 20 | Snippet *snippet 21 | GoVersion string 22 | Examples []example 23 | } 24 | 25 | func (s *server) handleEdit(w http.ResponseWriter, r *http.Request) { 26 | w.Header().Set("Access-Control-Allow-Origin", "*") 27 | if r.Method == "OPTIONS" { 28 | // This is likely a pre-flight CORS request. 29 | return 30 | } 31 | 32 | snip := &snippet{Body: []byte(s.examples.hello())} 33 | 34 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 35 | data := &editData{ 36 | Snippet: snip, 37 | GoVersion: runtime.Version(), 38 | Examples: s.examples.examples, 39 | } 40 | if err := editTemplate.Execute(w, data); err != nil { 41 | s.log.Errorf("editTemplate.Execute(w, %+v): %v", data, err) 42 | return 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The Go Playground 5 | 6 | 7 | 8 | 9 | 63 | 64 | 65 | 82 |
83 | 84 |
85 |
86 | 87 |
88 |

About the Playground

89 | 90 |

91 | The Go Playground is a web service that runs on 92 | golang.org's servers. 93 | The service receives a Go program, vets, compiles, links, and 94 | runs the program inside a sandbox, then returns the output. 95 |

96 | 97 |

98 | If the program contains tests or examples 99 | and no main function, the service runs the tests. 100 | Benchmarks will likely not be supported since the program runs in a sandboxed 101 | environment with limited resources. 102 |

103 | 104 |

105 | There are limitations to the programs that can be run in the playground: 106 |

107 | 108 | 127 | 128 |

129 | The article "Inside 130 | the Go Playground" describes how the playground is implemented. 131 | The source code is available at 132 | https://go.googlesource.com/playground. 133 |

134 | 135 |

136 | The playground uses the latest stable release of Go.
137 | The current version is {{.GoVersion}}. 138 |

139 | 140 |

141 | The playground service is used by more than just the official Go project 142 | (Go by Example is one other instance) 143 | and we are happy for you to use it on your own site. 144 | All we ask is that you 145 | contact us first (note this is a public mailing list), 146 | use a unique user agent in your requests (so we can identify you), 147 | and that your service is of benefit to the Go community. 148 |

149 | 150 |

151 | Any requests for content removal should be directed to 152 | security@golang.org. 153 | Please include the URL and the reason for the request. 154 |

155 |
156 | 157 | 158 | -------------------------------------------------------------------------------- /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.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, 世界!", "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, 世界") 117 | } 118 | ` 119 | -------------------------------------------------------------------------------- /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/clear.txt: -------------------------------------------------------------------------------- 1 | // Title: 清屏 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/fib.txt: -------------------------------------------------------------------------------- 1 | // Title: 斐波纳契闭包 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/examples/http.txt: -------------------------------------------------------------------------------- 1 | // Title: HTTP 服务器 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/examples/image.txt: -------------------------------------------------------------------------------- 1 | // Title: 展示图像 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/examples/index-dev.txt: -------------------------------------------------------------------------------- 1 | // Title: 获得索引 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 | -------------------------------------------------------------------------------- /src/examples/life.txt: -------------------------------------------------------------------------------- 1 | // Title: 康威的生命游戏 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/multi.txt: -------------------------------------------------------------------------------- 1 | // Title: 多个文件 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/peano.txt: -------------------------------------------------------------------------------- 1 | // Title: 皮亚诺整数 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/examples/pi.txt: -------------------------------------------------------------------------------- 1 | // Title: 并发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/sieve.txt: -------------------------------------------------------------------------------- 1 | // Title: 并发质数筛 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/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/solitaire.txt: -------------------------------------------------------------------------------- 1 | // Title: 孔明棋求解器 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/test.txt: -------------------------------------------------------------------------------- 1 | // Title: 单元测试 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 | -------------------------------------------------------------------------------- /src/examples/tree.txt: -------------------------------------------------------------------------------- 1 | // Title: 二叉树比较 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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/sandbox.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 | // TODO(andybons): add logging 6 | // TODO(andybons): restrict memory use 7 | 8 | package main 9 | 10 | import ( 11 | "bytes" 12 | "context" 13 | "crypto/sha256" 14 | "encoding/json" 15 | "errors" 16 | "fmt" 17 | "go/ast" 18 | "go/doc" 19 | "go/parser" 20 | "go/token" 21 | "io" 22 | "io/ioutil" 23 | "net/http" 24 | "os" 25 | "os/exec" 26 | "path/filepath" 27 | "runtime" 28 | "strconv" 29 | "strings" 30 | "sync" 31 | "text/template" 32 | "time" 33 | "unicode" 34 | "unicode/utf8" 35 | 36 | "github.com/bradfitz/gomemcache/memcache" 37 | "go.opencensus.io/stats" 38 | "go.opencensus.io/tag" 39 | "golang.org/x/playground/internal" 40 | "golang.org/x/playground/sandbox/sandboxtypes" 41 | ) 42 | 43 | const ( 44 | // Time for 'go build' to download 3rd-party modules and compile. 45 | maxBuildTime = 10 * time.Second 46 | maxRunTime = 5 * time.Second 47 | 48 | // progName is the implicit program name written to the temp 49 | // dir and used in compiler and vet errors. 50 | progName = "prog.go" 51 | ) 52 | 53 | const ( 54 | goBuildTimeoutError = "timeout running go build" 55 | runTimeoutError = "timeout running program" 56 | ) 57 | 58 | // internalErrors are strings found in responses that will not be cached 59 | // due to their non-deterministic nature. 60 | var internalErrors = []string{ 61 | "out of memory", 62 | "cannot allocate memory", 63 | } 64 | 65 | type request struct { 66 | Body string 67 | WithVet bool // whether client supports vet response in a /compile request (Issue 31970) 68 | } 69 | 70 | type response struct { 71 | Errors string 72 | Events []Event 73 | Status int 74 | IsTest bool 75 | TestsFailed int 76 | 77 | // VetErrors, if non-empty, contains any vet errors. It is 78 | // only populated if request.WithVet was true. 79 | VetErrors string `json:",omitempty"` 80 | // VetOK reports whether vet ran & passsed. It is only 81 | // populated if request.WithVet was true. Only one of 82 | // VetErrors or VetOK can be non-zero. 83 | VetOK bool `json:",omitempty"` 84 | } 85 | 86 | // commandHandler returns an http.HandlerFunc. 87 | // This handler creates a *request, assigning the "Body" field a value 88 | // from the "body" form parameter or from the HTTP request body. 89 | // If there is no cached *response for the combination of cachePrefix and request.Body, 90 | // handler calls cmdFunc and in case of a nil error, stores the value of *response in the cache. 91 | // The handler returned supports Cross-Origin Resource Sharing (CORS) from any domain. 92 | func (s *server) commandHandler(cachePrefix string, cmdFunc func(context.Context, *request) (*response, error)) http.HandlerFunc { 93 | return func(w http.ResponseWriter, r *http.Request) { 94 | cachePrefix := cachePrefix // so we can modify it below 95 | w.Header().Set("Access-Control-Allow-Origin", "*") 96 | if r.Method == "OPTIONS" { 97 | // This is likely a pre-flight CORS request. 98 | return 99 | } 100 | 101 | var req request 102 | // Until programs that depend on golang.org/x/tools/godoc/static/playground.js 103 | // are updated to always send JSON, this check is in place. 104 | if b := r.FormValue("body"); b != "" { 105 | req.Body = b 106 | req.WithVet, _ = strconv.ParseBool(r.FormValue("withVet")) 107 | } else if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 108 | s.log.Errorf("error decoding request: %v", err) 109 | http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 110 | return 111 | } 112 | 113 | if req.WithVet { 114 | cachePrefix += "_vet" // "prog" -> "prog_vet" 115 | } 116 | 117 | resp := &response{} 118 | key := cacheKey(cachePrefix, req.Body) 119 | if err := s.cache.Get(key, resp); err != nil { 120 | if !errors.Is(err, memcache.ErrCacheMiss) { 121 | s.log.Errorf("s.cache.Get(%q, &response): %v", key, err) 122 | } 123 | resp, err = cmdFunc(r.Context(), &req) 124 | if err != nil { 125 | s.log.Errorf("cmdFunc error: %v", err) 126 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 127 | return 128 | } 129 | if strings.Contains(resp.Errors, goBuildTimeoutError) || strings.Contains(resp.Errors, runTimeoutError) { 130 | // TODO(golang.org/issue/38576) - This should be a http.StatusBadRequest, 131 | // but the UI requires a 200 to parse the response. It's difficult to know 132 | // if we've timed out because of an error in the code snippet, or instability 133 | // on the playground itself. Either way, we should try to show the user the 134 | // partial output of their program. 135 | s.writeJSONResponse(w, resp, http.StatusOK) 136 | return 137 | } 138 | for _, e := range internalErrors { 139 | if strings.Contains(resp.Errors, e) { 140 | s.log.Errorf("cmdFunc compilation error: %q", resp.Errors) 141 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 142 | return 143 | } 144 | } 145 | for _, el := range resp.Events { 146 | if el.Kind != "stderr" { 147 | continue 148 | } 149 | for _, e := range internalErrors { 150 | if strings.Contains(el.Message, e) { 151 | s.log.Errorf("cmdFunc runtime error: %q", el.Message) 152 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 153 | return 154 | } 155 | } 156 | } 157 | if err := s.cache.Set(key, resp); err != nil { 158 | s.log.Errorf("cache.Set(%q, resp): %v", key, err) 159 | } 160 | } 161 | 162 | s.writeJSONResponse(w, resp, http.StatusOK) 163 | } 164 | } 165 | 166 | func cacheKey(prefix, body string) string { 167 | h := sha256.New() 168 | io.WriteString(h, body) 169 | return fmt.Sprintf("%s-%s-%x", prefix, runtime.Version(), h.Sum(nil)) 170 | } 171 | 172 | // isTestFunc tells whether fn has the type of a testing function. 173 | func isTestFunc(fn *ast.FuncDecl) bool { 174 | if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 || 175 | fn.Type.Params.List == nil || 176 | len(fn.Type.Params.List) != 1 || 177 | len(fn.Type.Params.List[0].Names) > 1 { 178 | return false 179 | } 180 | ptr, ok := fn.Type.Params.List[0].Type.(*ast.StarExpr) 181 | if !ok { 182 | return false 183 | } 184 | // We can't easily check that the type is *testing.T 185 | // because we don't know how testing has been imported, 186 | // but at least check that it's *T or *something.T. 187 | if name, ok := ptr.X.(*ast.Ident); ok && name.Name == "T" { 188 | return true 189 | } 190 | if sel, ok := ptr.X.(*ast.SelectorExpr); ok && sel.Sel.Name == "T" { 191 | return true 192 | } 193 | return false 194 | } 195 | 196 | // isTest tells whether name looks like a test (or benchmark, according to prefix). 197 | // It is a Test (say) if there is a character after Test that is not a lower-case letter. 198 | // We don't want mistaken Testimony or erroneous Benchmarking. 199 | func isTest(name, prefix string) bool { 200 | if !strings.HasPrefix(name, prefix) { 201 | return false 202 | } 203 | if len(name) == len(prefix) { // "Test" is ok 204 | return true 205 | } 206 | r, _ := utf8.DecodeRuneInString(name[len(prefix):]) 207 | return !unicode.IsLower(r) 208 | } 209 | 210 | // getTestProg returns source code that executes all valid tests and examples in src. 211 | // If the main function is present or there are no tests or examples, it returns nil. 212 | // getTestProg emulates the "go test" command as closely as possible. 213 | // Benchmarks are not supported because of sandboxing. 214 | func getTestProg(src []byte) []byte { 215 | fset := token.NewFileSet() 216 | // Early bail for most cases. 217 | f, err := parser.ParseFile(fset, progName, src, parser.ImportsOnly) 218 | if err != nil || f.Name.Name != "main" { 219 | return nil 220 | } 221 | 222 | // importPos stores the position to inject the "testing" import declaration, if needed. 223 | importPos := fset.Position(f.Name.End()).Offset 224 | 225 | var testingImported bool 226 | for _, s := range f.Imports { 227 | if s.Path.Value == `"testing"` && s.Name == nil { 228 | testingImported = true 229 | break 230 | } 231 | } 232 | 233 | // Parse everything and extract test names. 234 | f, err = parser.ParseFile(fset, progName, src, parser.ParseComments) 235 | if err != nil { 236 | return nil 237 | } 238 | 239 | var tests []string 240 | for _, d := range f.Decls { 241 | n, ok := d.(*ast.FuncDecl) 242 | if !ok { 243 | continue 244 | } 245 | name := n.Name.Name 246 | switch { 247 | case name == "main": 248 | // main declared as a method will not obstruct creation of our main function. 249 | if n.Recv == nil { 250 | return nil 251 | } 252 | case isTest(name, "Test") && isTestFunc(n): 253 | tests = append(tests, name) 254 | } 255 | } 256 | 257 | // Tests imply imported "testing" package in the code. 258 | // If there is no import, bail to let the compiler produce an error. 259 | if !testingImported && len(tests) > 0 { 260 | return nil 261 | } 262 | 263 | // We emulate "go test". An example with no "Output" comment is compiled, 264 | // but not executed. An example with no text after "Output:" is compiled, 265 | // executed, and expected to produce no output. 266 | var ex []*doc.Example 267 | // exNoOutput indicates whether an example with no output is found. 268 | // We need to compile the program containing such an example even if there are no 269 | // other tests or examples. 270 | exNoOutput := false 271 | for _, e := range doc.Examples(f) { 272 | if e.Output != "" || e.EmptyOutput { 273 | ex = append(ex, e) 274 | } 275 | if e.Output == "" && !e.EmptyOutput { 276 | exNoOutput = true 277 | } 278 | } 279 | 280 | if len(tests) == 0 && len(ex) == 0 && !exNoOutput { 281 | return nil 282 | } 283 | 284 | if !testingImported && (len(ex) > 0 || exNoOutput) { 285 | // In case of the program with examples and no "testing" package imported, 286 | // add import after "package main" without modifying line numbers. 287 | importDecl := []byte(`;import "testing";`) 288 | src = bytes.Join([][]byte{src[:importPos], importDecl, src[importPos:]}, nil) 289 | } 290 | 291 | data := struct { 292 | Tests []string 293 | Examples []*doc.Example 294 | }{ 295 | tests, 296 | ex, 297 | } 298 | code := new(bytes.Buffer) 299 | if err := testTmpl.Execute(code, data); err != nil { 300 | panic(err) 301 | } 302 | src = append(src, code.Bytes()...) 303 | return src 304 | } 305 | 306 | var testTmpl = template.Must(template.New("main").Parse(` 307 | func main() { 308 | matchAll := func(t string, pat string) (bool, error) { return true, nil } 309 | tests := []testing.InternalTest{ 310 | {{range .Tests}} 311 | {"{{.}}", {{.}}}, 312 | {{end}} 313 | } 314 | examples := []testing.InternalExample{ 315 | {{range .Examples}} 316 | {"Example{{.Name}}", Example{{.Name}}, {{printf "%q" .Output}}, {{.Unordered}}}, 317 | {{end}} 318 | } 319 | testing.Main(matchAll, tests, nil, examples) 320 | } 321 | `)) 322 | 323 | var failedTestPattern = "--- FAIL" 324 | 325 | // compileAndRun tries to build and run a user program. 326 | // The output of successfully ran program is returned in *response.Events. 327 | // If a program cannot be built or has timed out, 328 | // *response.Errors contains an explanation for a user. 329 | func compileAndRun(ctx context.Context, req *request) (*response, error) { 330 | // TODO(andybons): Add semaphore to limit number of running programs at once. 331 | tmpDir, err := ioutil.TempDir("", "sandbox") 332 | if err != nil { 333 | return nil, fmt.Errorf("error creating temp directory: %v", err) 334 | } 335 | defer os.RemoveAll(tmpDir) 336 | 337 | br, err := sandboxBuild(ctx, tmpDir, []byte(req.Body), req.WithVet) 338 | if err != nil { 339 | return nil, err 340 | } 341 | if br.errorMessage != "" { 342 | return &response{Errors: br.errorMessage}, nil 343 | } 344 | 345 | execRes, err := sandboxRun(ctx, br.exePath, br.testParam) 346 | if err != nil { 347 | return nil, err 348 | } 349 | if execRes.Error != "" { 350 | return &response{Errors: execRes.Error}, nil 351 | } 352 | 353 | rec := new(Recorder) 354 | rec.Stdout().Write(execRes.Stdout) 355 | rec.Stderr().Write(execRes.Stderr) 356 | events, err := rec.Events() 357 | if err != nil { 358 | log.Printf("error decoding events: %v", err) 359 | return nil, fmt.Errorf("error decoding events: %v", err) 360 | } 361 | var fails int 362 | if br.testParam != "" { 363 | // In case of testing the TestsFailed field contains how many tests have failed. 364 | for _, e := range events { 365 | fails += strings.Count(e.Message, failedTestPattern) 366 | } 367 | } 368 | return &response{ 369 | Events: events, 370 | Status: execRes.ExitCode, 371 | IsTest: br.testParam != "", 372 | TestsFailed: fails, 373 | VetErrors: br.vetOut, 374 | VetOK: req.WithVet && br.vetOut == "", 375 | }, nil 376 | } 377 | 378 | // buildResult is the output of a sandbox build attempt. 379 | type buildResult struct { 380 | // goPath is a temporary directory if the binary was built with module support. 381 | // TODO(golang.org/issue/25224) - Why is the module mode built so differently? 382 | goPath string 383 | // exePath is the path to the built binary. 384 | exePath string 385 | // testParam is set if tests should be run when running the binary. 386 | testParam string 387 | // errorMessage is an error message string to be returned to the user. 388 | errorMessage string 389 | // vetOut is the output of go vet, if requested. 390 | vetOut string 391 | } 392 | 393 | // cleanup cleans up the temporary goPath created when building with module support. 394 | func (b *buildResult) cleanup() error { 395 | if b.goPath != "" { 396 | return os.RemoveAll(b.goPath) 397 | } 398 | return nil 399 | } 400 | 401 | // sandboxBuild builds a Go program and returns a build result that includes the build context. 402 | // 403 | // An error is returned if a non-user-correctable error has occurred. 404 | func sandboxBuild(ctx context.Context, tmpDir string, in []byte, vet bool) (br *buildResult, err error) { 405 | start := time.Now() 406 | defer func() { 407 | status := "success" 408 | if err != nil { 409 | status = "error" 410 | } 411 | // Ignore error. The only error can be invalid tag key or value 412 | // length, which we know are safe. 413 | stats.RecordWithTags(ctx, []tag.Mutator{tag.Upsert(kGoBuildSuccess, status)}, 414 | mGoBuildLatency.M(float64(time.Since(start))/float64(time.Millisecond))) 415 | }() 416 | 417 | files, err := splitFiles(in) 418 | if err != nil { 419 | return &buildResult{errorMessage: err.Error()}, nil 420 | } 421 | 422 | br = new(buildResult) 423 | defer br.cleanup() 424 | var buildPkgArg = "." 425 | if files.Num() == 1 && len(files.Data(progName)) > 0 { 426 | buildPkgArg = progName 427 | src := files.Data(progName) 428 | if code := getTestProg(src); code != nil { 429 | br.testParam = "-test.v" 430 | files.AddFile(progName, code) 431 | } 432 | } 433 | 434 | if !files.Contains("go.mod") { 435 | files.AddFile("go.mod", []byte("module play\n")) 436 | } 437 | 438 | for f, src := range files.m { 439 | // Before multi-file support we required that the 440 | // program be in package main, so continue to do that 441 | // for now. But permit anything in subdirectories to have other 442 | // packages. 443 | if !strings.Contains(f, "/") { 444 | fset := token.NewFileSet() 445 | f, err := parser.ParseFile(fset, f, src, parser.PackageClauseOnly) 446 | if err == nil && f.Name.Name != "main" { 447 | return &buildResult{errorMessage: "package name must be main"}, nil 448 | } 449 | } 450 | 451 | in := filepath.Join(tmpDir, f) 452 | if strings.Contains(f, "/") { 453 | if err := os.MkdirAll(filepath.Dir(in), 0755); err != nil { 454 | return nil, err 455 | } 456 | } 457 | if err := ioutil.WriteFile(in, src, 0644); err != nil { 458 | return nil, fmt.Errorf("error creating temp file %q: %v", in, err) 459 | } 460 | } 461 | 462 | br.exePath = filepath.Join(tmpDir, "a.out") 463 | goCache := filepath.Join(tmpDir, "gocache") 464 | 465 | cmd := exec.Command("/usr/local/go-faketime/bin/go", "build", "-o", br.exePath, "-tags=faketime") 466 | cmd.Dir = tmpDir 467 | cmd.Env = []string{"GOOS=linux", "GOARCH=amd64", "GOROOT=/usr/local/go-faketime"} 468 | cmd.Env = append(cmd.Env, "GOCACHE="+goCache) 469 | cmd.Env = append(cmd.Env, "CGO_ENABLED=0") 470 | // Create a GOPATH just for modules to be downloaded 471 | // into GOPATH/pkg/mod. 472 | cmd.Args = append(cmd.Args, "-modcacherw") 473 | cmd.Args = append(cmd.Args, "-mod=mod") 474 | br.goPath, err = ioutil.TempDir("", "gopath") 475 | if err != nil { 476 | log.Printf("error creating temp directory: %v", err) 477 | return nil, fmt.Errorf("error creating temp directory: %v", err) 478 | } 479 | cmd.Env = append(cmd.Env, "GO111MODULE=on", "GOPROXY="+playgroundGoproxy()) 480 | cmd.Args = append(cmd.Args, buildPkgArg) 481 | cmd.Env = append(cmd.Env, "GOPATH="+br.goPath) 482 | out := &bytes.Buffer{} 483 | cmd.Stderr, cmd.Stdout = out, out 484 | 485 | if err := cmd.Start(); err != nil { 486 | return nil, fmt.Errorf("error starting go build: %v", err) 487 | } 488 | ctx, cancel := context.WithTimeout(ctx, maxBuildTime) 489 | defer cancel() 490 | if err := internal.WaitOrStop(ctx, cmd, os.Interrupt, 250*time.Millisecond); err != nil { 491 | if errors.Is(err, context.DeadlineExceeded) { 492 | br.errorMessage = fmt.Sprintln(goBuildTimeoutError) 493 | } else if ee := (*exec.ExitError)(nil); !errors.As(err, &ee) { 494 | log.Printf("error building program: %v", err) 495 | return nil, fmt.Errorf("error building go source: %v", err) 496 | } 497 | // Return compile errors to the user. 498 | // Rewrite compiler errors to strip the tmpDir name. 499 | br.errorMessage = br.errorMessage + strings.Replace(string(out.Bytes()), tmpDir+"/", "", -1) 500 | 501 | // "go build", invoked with a file name, puts this odd 502 | // message before any compile errors; strip it. 503 | br.errorMessage = strings.Replace(br.errorMessage, "# command-line-arguments\n", "", 1) 504 | 505 | return br, nil 506 | } 507 | const maxBinarySize = 100 << 20 // copied from sandbox backend; TODO: unify? 508 | if fi, err := os.Stat(br.exePath); err != nil || fi.Size() == 0 || fi.Size() > maxBinarySize { 509 | if err != nil { 510 | return nil, fmt.Errorf("failed to stat binary: %v", err) 511 | } 512 | return nil, fmt.Errorf("invalid binary size %d", fi.Size()) 513 | } 514 | if vet { 515 | // TODO: do this concurrently with the execution to reduce latency. 516 | br.vetOut, err = vetCheckInDir(ctx, tmpDir, br.goPath) 517 | if err != nil { 518 | return nil, fmt.Errorf("running vet: %v", err) 519 | } 520 | } 521 | return br, nil 522 | } 523 | 524 | // sandboxRun runs a Go binary in a sandbox environment. 525 | func sandboxRun(ctx context.Context, exePath string, testParam string) (execRes sandboxtypes.Response, err error) { 526 | start := time.Now() 527 | defer func() { 528 | status := "success" 529 | if err != nil { 530 | status = "error" 531 | } 532 | // Ignore error. The only error can be invalid tag key or value 533 | // length, which we know are safe. 534 | stats.RecordWithTags(ctx, []tag.Mutator{tag.Upsert(kGoBuildSuccess, status)}, 535 | mGoRunLatency.M(float64(time.Since(start))/float64(time.Millisecond))) 536 | }() 537 | exeBytes, err := ioutil.ReadFile(exePath) 538 | if err != nil { 539 | return execRes, err 540 | } 541 | ctx, cancel := context.WithTimeout(ctx, maxRunTime) 542 | defer cancel() 543 | sreq, err := http.NewRequestWithContext(ctx, "POST", sandboxBackendURL(), bytes.NewReader(exeBytes)) 544 | if err != nil { 545 | return execRes, fmt.Errorf("NewRequestWithContext %q: %w", sandboxBackendURL(), err) 546 | } 547 | sreq.Header.Add("Idempotency-Key", "1") // lets Transport do retries with a POST 548 | if testParam != "" { 549 | sreq.Header.Add("X-Argument", testParam) 550 | } 551 | sreq.GetBody = func() (io.ReadCloser, error) { return ioutil.NopCloser(bytes.NewReader(exeBytes)), nil } 552 | res, err := sandboxBackendClient().Do(sreq) 553 | if err != nil { 554 | if errors.Is(ctx.Err(), context.DeadlineExceeded) { 555 | execRes.Error = runTimeoutError 556 | return execRes, nil 557 | } 558 | return execRes, fmt.Errorf("POST %q: %w", sandboxBackendURL(), err) 559 | } 560 | defer res.Body.Close() 561 | if res.StatusCode != http.StatusOK { 562 | log.Printf("unexpected response from backend: %v", res.Status) 563 | return execRes, fmt.Errorf("unexpected response from backend: %v", res.Status) 564 | } 565 | if err := json.NewDecoder(res.Body).Decode(&execRes); err != nil { 566 | log.Printf("JSON decode error from backend: %v", err) 567 | return execRes, errors.New("error parsing JSON from backend") 568 | } 569 | return execRes, nil 570 | } 571 | 572 | // playgroundGoproxy returns the GOPROXY environment config the playground should use. 573 | // It is fetched from the environment variable PLAY_GOPROXY. A missing or empty 574 | // value for PLAY_GOPROXY returns the default value of https://proxy.golang.org. 575 | func playgroundGoproxy() string { 576 | proxypath := os.Getenv("PLAY_GOPROXY") 577 | if proxypath != "" { 578 | return proxypath 579 | } 580 | return "https://proxy.golang.org" 581 | } 582 | 583 | // healthCheck attempts to build a binary from the source in healthProg. 584 | // It returns any error returned from sandboxBuild, or nil if none is returned. 585 | func (s *server) healthCheck(ctx context.Context) error { 586 | tmpDir, err := ioutil.TempDir("", "sandbox") 587 | if err != nil { 588 | return fmt.Errorf("error creating temp directory: %v", err) 589 | } 590 | defer os.RemoveAll(tmpDir) 591 | br, err := sandboxBuild(ctx, tmpDir, []byte(healthProg), false) 592 | if err != nil { 593 | return err 594 | } 595 | if br.errorMessage != "" { 596 | return errors.New(br.errorMessage) 597 | } 598 | return nil 599 | } 600 | 601 | // sandboxBackendURL returns the URL of the sandbox backend that 602 | // executes binaries. This backend is required for Go 1.14+ (where it 603 | // executes using gvisor, since Native Client support is removed). 604 | // 605 | // This function either returns a non-empty string or it panics. 606 | func sandboxBackendURL() string { 607 | if v := os.Getenv("SANDBOX_BACKEND_URL"); v != "" { 608 | return v 609 | } 610 | panic("need set SANDBOX_BACKEND_URL environment") 611 | } 612 | 613 | var sandboxBackendOnce struct { 614 | sync.Once 615 | c *http.Client 616 | } 617 | 618 | func sandboxBackendClient() *http.Client { 619 | sandboxBackendOnce.Do(initSandboxBackendClient) 620 | return sandboxBackendOnce.c 621 | } 622 | 623 | // initSandboxBackendClient runs from a sync.Once and initializes 624 | // sandboxBackendOnce.c with the *http.Client we'll use to contact the 625 | // sandbox execution backend. 626 | func initSandboxBackendClient() { 627 | sandboxBackendOnce.c = http.DefaultClient 628 | } 629 | 630 | const healthProg = ` 631 | package main 632 | 633 | import "fmt" 634 | 635 | func main() { fmt.Print("ok") } 636 | ` 637 | -------------------------------------------------------------------------------- /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/sandbox/sandbox.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 sandbox program is an HTTP server that receives untrusted 6 | // linux/amd64 binaries in a POST request and then executes them in 7 | // a gvisor sandbox using Docker, returning the output as a response 8 | // to the POST. 9 | // 10 | // It's part of the Go playground (https://play.golang.org/). 11 | package main 12 | 13 | import ( 14 | "bufio" 15 | "bytes" 16 | "context" 17 | "crypto/rand" 18 | "encoding/json" 19 | "errors" 20 | "flag" 21 | "fmt" 22 | "io" 23 | "io/ioutil" 24 | "log" 25 | "net/http" 26 | "os" 27 | "os/exec" 28 | "os/signal" 29 | "runtime" 30 | "sync" 31 | "syscall" 32 | "time" 33 | 34 | "go.opencensus.io/plugin/ochttp" 35 | "go.opencensus.io/stats" 36 | "go.opencensus.io/tag" 37 | "go.opencensus.io/trace" 38 | "golang.org/x/playground/internal" 39 | "golang.org/x/playground/sandbox/sandboxtypes" 40 | ) 41 | 42 | var ( 43 | listenAddr = flag.String("listen", ":80", "HTTP server listen address. Only applicable when --mode=server") 44 | mode = flag.String("mode", "server", "Whether to run in \"server\" mode or \"contained\" mode. The contained mode is used internally by the server mode.") 45 | dev = flag.Bool("dev", false, "run in dev mode (show help messages)") 46 | numWorkers = flag.Int("workers", runtime.NumCPU(), "number of parallel gvisor containers to pre-spin up & let run concurrently") 47 | container = flag.String("untrusted-container", "gcr.io/golang-org/playground-sandbox-gvisor:latest", "container image name that hosts the untrusted binary under gvisor") 48 | ) 49 | 50 | const ( 51 | maxBinarySize = 100 << 20 52 | startTimeout = 30 * time.Second 53 | runTimeout = 5 * time.Second 54 | maxOutputSize = 100 << 20 55 | memoryLimitBytes = 100 << 20 56 | ) 57 | 58 | var ( 59 | errTooMuchOutput = errors.New("Output too large") 60 | errRunTimeout = errors.New("timeout running program") 61 | ) 62 | 63 | // containedStartMessage is the first thing written to stdout by the 64 | // gvisor-contained process when it starts up. This lets the parent HTTP 65 | // server know that a particular container is ready to run a binary. 66 | const containedStartMessage = "golang-gvisor-process-started\n" 67 | 68 | // containedStderrHeader is written to stderr after the gvisor-contained process 69 | // successfully reads the processMeta JSON line + executable binary from stdin, 70 | // but before it's run. 71 | var containedStderrHeader = []byte("golang-gvisor-process-got-input\n") 72 | 73 | var ( 74 | readyContainer chan *Container 75 | runSem chan struct{} 76 | ) 77 | 78 | type Container struct { 79 | name string 80 | 81 | stdin io.WriteCloser 82 | stdout *limitedWriter 83 | stderr *limitedWriter 84 | 85 | cmd *exec.Cmd 86 | cancelCmd context.CancelFunc 87 | 88 | waitErr chan error // 1-buffered; receives error from WaitOrStop(..., cmd, ...) 89 | } 90 | 91 | func (c *Container) Close() { 92 | setContainerWanted(c.name, false) 93 | 94 | c.cancelCmd() 95 | if err := c.Wait(); err != nil { 96 | log.Printf("error in c.Wait() for %q: %v", c.name, err) 97 | } 98 | } 99 | 100 | func (c *Container) Wait() error { 101 | err := <-c.waitErr 102 | c.waitErr <- err 103 | return err 104 | } 105 | 106 | var httpServer *http.Server 107 | 108 | func main() { 109 | flag.Parse() 110 | if *mode == "contained" { 111 | runInGvisor() 112 | panic("runInGvisor didn't exit") 113 | } 114 | if flag.NArg() != 0 { 115 | flag.Usage() 116 | os.Exit(1) 117 | } 118 | log.Printf("Go playground sandbox starting.") 119 | 120 | readyContainer = make(chan *Container) 121 | runSem = make(chan struct{}, *numWorkers) 122 | go handleSignals() 123 | 124 | mux := http.NewServeMux() 125 | 126 | if out, err := exec.Command("docker", "version").CombinedOutput(); err != nil { 127 | log.Fatalf("failed to connect to docker: %v, %s", err, out) 128 | } 129 | if *dev { 130 | log.Printf("Running in dev mode; container published to host at: http://localhost:8080/") 131 | log.Printf("Run a binary with: curl -v --data-binary @/home/bradfitz/hello http://localhost:8080/run\n") 132 | } else { 133 | if _, err := exec.Command("docker", "images", "-q", *container).CombinedOutput(); err != nil { 134 | if out, err := exec.Command("docker", "pull", *container).CombinedOutput(); err != nil { 135 | log.Fatalf("error pulling %s: %v, %s", *container, err, out) 136 | } 137 | } 138 | log.Printf("Listening on %s", *listenAddr) 139 | } 140 | 141 | mux.Handle("/health", ochttp.WithRouteTag(http.HandlerFunc(healthHandler), "/health")) 142 | mux.Handle("/healthz", ochttp.WithRouteTag(http.HandlerFunc(healthHandler), "/healthz")) 143 | mux.Handle("/", ochttp.WithRouteTag(http.HandlerFunc(rootHandler), "/")) 144 | mux.Handle("/run", ochttp.WithRouteTag(http.HandlerFunc(runHandler), "/run")) 145 | 146 | makeWorkers() 147 | go internal.PeriodicallyDo(context.Background(), 10*time.Second, func(ctx context.Context, _ time.Time) { 148 | countDockerContainers(ctx) 149 | }) 150 | 151 | trace.ApplyConfig(trace.Config{DefaultSampler: trace.NeverSample()}) 152 | httpServer = &http.Server{ 153 | Addr: *listenAddr, 154 | Handler: &ochttp.Handler{Handler: mux}, 155 | } 156 | log.Fatal(httpServer.ListenAndServe()) 157 | } 158 | 159 | // dockerContainer is the structure of each line output from docker ps. 160 | type dockerContainer struct { 161 | // ID is the docker container ID. 162 | ID string `json:"ID"` 163 | // Image is the docker image name. 164 | Image string `json:"Image"` 165 | // Names is the docker container name. 166 | Names string `json:"Names"` 167 | } 168 | 169 | // countDockerContainers records the metric for the current number of docker containers. 170 | // It also records the count of any unwanted containers. 171 | func countDockerContainers(ctx context.Context) { 172 | cs, err := listDockerContainers(ctx) 173 | if err != nil { 174 | log.Printf("Error counting docker containers: %v", err) 175 | } 176 | stats.Record(ctx, mContainers.M(int64(len(cs)))) 177 | var unwantedCount int64 178 | for _, c := range cs { 179 | if c.Names != "" && !isContainerWanted(c.Names) { 180 | unwantedCount++ 181 | } 182 | } 183 | stats.Record(ctx, mUnwantedContainers.M(unwantedCount)) 184 | } 185 | 186 | // listDockerContainers returns the current running play_run containers reported by docker. 187 | func listDockerContainers(ctx context.Context) ([]dockerContainer, error) { 188 | out := new(bytes.Buffer) 189 | cmd := exec.Command("docker", "ps", "--quiet", "--filter", "name=play_run_", "--format", "{{json .}}") 190 | cmd.Stdout, cmd.Stderr = out, out 191 | if err := cmd.Start(); err != nil { 192 | return nil, fmt.Errorf("listDockerContainers: cmd.Start() failed: %w", err) 193 | } 194 | ctx, cancel := context.WithTimeout(ctx, time.Second) 195 | defer cancel() 196 | if err := internal.WaitOrStop(ctx, cmd, os.Interrupt, 250*time.Millisecond); err != nil { 197 | return nil, fmt.Errorf("listDockerContainers: internal.WaitOrStop() failed: %w", err) 198 | } 199 | return parseDockerContainers(out.Bytes()) 200 | } 201 | 202 | // parseDockerContainers parses the json formatted docker output from docker ps. 203 | // 204 | // If there is an error scanning the input, or non-JSON output is encountered, an error is returned. 205 | func parseDockerContainers(b []byte) ([]dockerContainer, error) { 206 | // Parse the output to ensure it is well-formatted in the structure we expect. 207 | var containers []dockerContainer 208 | // Each output line is it's own JSON object, so unmarshal one line at a time. 209 | scanner := bufio.NewScanner(bytes.NewReader(b)) 210 | for scanner.Scan() { 211 | var do dockerContainer 212 | if err := json.Unmarshal(scanner.Bytes(), &do); err != nil { 213 | return nil, fmt.Errorf("parseDockerContainers: error parsing docker ps output: %w", err) 214 | } 215 | containers = append(containers, do) 216 | } 217 | if err := scanner.Err(); err != nil { 218 | return nil, fmt.Errorf("parseDockerContainers: error reading docker ps output: %w", err) 219 | } 220 | return containers, nil 221 | } 222 | 223 | func handleSignals() { 224 | c := make(chan os.Signal, 1) 225 | signal.Notify(c, syscall.SIGINT) 226 | s := <-c 227 | log.Fatalf("closing on signal %d: %v", s, s) 228 | } 229 | 230 | var healthStatus struct { 231 | sync.Mutex 232 | lastCheck time.Time 233 | lastVal error 234 | } 235 | 236 | func getHealthCached() error { 237 | healthStatus.Lock() 238 | defer healthStatus.Unlock() 239 | const recentEnough = 5 * time.Second 240 | if healthStatus.lastCheck.After(time.Now().Add(-recentEnough)) { 241 | return healthStatus.lastVal 242 | } 243 | 244 | err := checkHealth() 245 | if healthStatus.lastVal == nil && err != nil { 246 | // On transition from healthy to unhealthy, close all 247 | // idle HTTP connections so clients with them open 248 | // don't reuse them. TODO: remove this if/when we 249 | // switch away from direct load balancing between 250 | // frontends and this sandbox backend. 251 | httpServer.SetKeepAlivesEnabled(false) // side effect of closing all idle ones 252 | httpServer.SetKeepAlivesEnabled(true) // and restore it back to normal 253 | } 254 | healthStatus.lastVal = err 255 | healthStatus.lastCheck = time.Now() 256 | return err 257 | } 258 | 259 | // checkHealth does a health check, without any caching. It's called via 260 | // getHealthCached. 261 | func checkHealth() error { 262 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 263 | defer cancel() 264 | c, err := getContainer(ctx) 265 | if err != nil { 266 | return fmt.Errorf("failed to get a sandbox container: %v", err) 267 | } 268 | // TODO: execute something too? for now we just check that sandboxed containers 269 | // are available. 270 | closed := make(chan struct{}) 271 | go func() { 272 | c.Close() 273 | close(closed) 274 | }() 275 | select { 276 | case <-closed: 277 | // success. 278 | return nil 279 | case <-ctx.Done(): 280 | return fmt.Errorf("timeout closing sandbox container") 281 | } 282 | } 283 | 284 | func healthHandler(w http.ResponseWriter, r *http.Request) { 285 | // TODO: split into liveness & readiness checks? 286 | if err := getHealthCached(); err != nil { 287 | w.WriteHeader(http.StatusInternalServerError) 288 | fmt.Fprintf(w, "health check failure: %v\n", err) 289 | return 290 | } 291 | io.WriteString(w, "OK\n") 292 | } 293 | 294 | func rootHandler(w http.ResponseWriter, r *http.Request) { 295 | if r.URL.Path != "/" { 296 | http.NotFound(w, r) 297 | return 298 | } 299 | io.WriteString(w, "Hi from sandbox\n") 300 | } 301 | 302 | // processMeta is the JSON sent to the gvisor container before the untrusted binary. 303 | // It currently contains only the arguments to pass to the binary. 304 | // It might contain environment or other things later. 305 | type processMeta struct { 306 | Args []string `json:"args"` 307 | } 308 | 309 | // runInGvisor is run when we're now inside gvisor. We have no network 310 | // at this point. We can read our binary in from stdin and then run 311 | // it. 312 | func runInGvisor() { 313 | const binPath = "/tmpfs/play" 314 | if _, err := io.WriteString(os.Stdout, containedStartMessage); err != nil { 315 | log.Fatalf("writing to stdout: %v", err) 316 | } 317 | slurp, err := ioutil.ReadAll(os.Stdin) 318 | if err != nil { 319 | log.Fatalf("reading stdin in contained mode: %v", err) 320 | } 321 | nl := bytes.IndexByte(slurp, '\n') 322 | if nl == -1 { 323 | log.Fatalf("no newline found in input") 324 | } 325 | metaJSON, bin := slurp[:nl], slurp[nl+1:] 326 | 327 | if err := ioutil.WriteFile(binPath, bin, 0755); err != nil { 328 | log.Fatalf("writing contained binary: %v", err) 329 | } 330 | defer os.Remove(binPath) // not that it matters much, this container will be nuked 331 | 332 | var meta processMeta 333 | if err := json.NewDecoder(bytes.NewReader(metaJSON)).Decode(&meta); err != nil { 334 | log.Fatalf("error decoding JSON meta: %v", err) 335 | } 336 | 337 | if _, err := os.Stderr.Write(containedStderrHeader); err != nil { 338 | log.Fatalf("writing header to stderr: %v", err) 339 | } 340 | 341 | cmd := exec.Command(binPath) 342 | cmd.Args = append(cmd.Args, meta.Args...) 343 | cmd.Stdout = os.Stdout 344 | cmd.Stderr = os.Stderr 345 | if err := cmd.Start(); err != nil { 346 | log.Fatalf("cmd.Start(): %v", err) 347 | } 348 | ctx, cancel := context.WithTimeout(context.Background(), runTimeout-500*time.Millisecond) 349 | defer cancel() 350 | if err = internal.WaitOrStop(ctx, cmd, os.Interrupt, 250*time.Millisecond); err != nil { 351 | if errors.Is(err, context.DeadlineExceeded) { 352 | fmt.Fprintln(os.Stderr, "timeout running program") 353 | } 354 | } 355 | os.Exit(errExitCode(err)) 356 | return 357 | } 358 | 359 | func makeWorkers() { 360 | ctx := context.Background() 361 | stats.Record(ctx, mMaxContainers.M(int64(*numWorkers))) 362 | for i := 0; i < *numWorkers; i++ { 363 | go workerLoop(ctx) 364 | } 365 | } 366 | 367 | func workerLoop(ctx context.Context) { 368 | for { 369 | c, err := startContainer(ctx) 370 | if err != nil { 371 | log.Printf("error starting container: %v", err) 372 | time.Sleep(5 * time.Second) 373 | continue 374 | } 375 | readyContainer <- c 376 | } 377 | } 378 | 379 | func randHex(n int) string { 380 | b := make([]byte, n/2) 381 | _, err := rand.Read(b) 382 | if err != nil { 383 | panic(err) 384 | } 385 | return fmt.Sprintf("%x", b) 386 | } 387 | 388 | var ( 389 | wantedMu sync.Mutex 390 | containerWanted = map[string]bool{} 391 | ) 392 | 393 | // setContainerWanted records whether a named container is wanted or 394 | // not. Any unwanted containers are cleaned up asynchronously as a 395 | // sanity check against leaks. 396 | // 397 | // TODO(bradfitz): add leak checker (background docker ps loop) 398 | func setContainerWanted(name string, wanted bool) { 399 | wantedMu.Lock() 400 | defer wantedMu.Unlock() 401 | if wanted { 402 | containerWanted[name] = true 403 | } else { 404 | delete(containerWanted, name) 405 | } 406 | } 407 | 408 | func isContainerWanted(name string) bool { 409 | wantedMu.Lock() 410 | defer wantedMu.Unlock() 411 | return containerWanted[name] 412 | } 413 | 414 | func getContainer(ctx context.Context) (*Container, error) { 415 | select { 416 | case c := <-readyContainer: 417 | return c, nil 418 | case <-ctx.Done(): 419 | return nil, ctx.Err() 420 | } 421 | } 422 | 423 | func startContainer(ctx context.Context) (c *Container, err error) { 424 | start := time.Now() 425 | defer func() { 426 | status := "success" 427 | if err != nil { 428 | status = "error" 429 | } 430 | // Ignore error. The only error can be invalid tag key or value length, which we know are safe. 431 | _ = stats.RecordWithTags(ctx, []tag.Mutator{tag.Upsert(kContainerCreateSuccess, status)}, 432 | mContainerCreateLatency.M(float64(time.Since(start))/float64(time.Millisecond))) 433 | }() 434 | 435 | name := "play_run_" + randHex(8) 436 | setContainerWanted(name, true) 437 | var cmd *exec.Cmd 438 | if _, err := os.Stat("/var/lib/docker/runsc"); err == nil { 439 | cmd = exec.Command("docker", "run", 440 | "--name="+name, 441 | "--rm", 442 | "--tmpfs=/tmpfs:exec", 443 | "-i", // read stdin 444 | 445 | "--runtime=runsc", 446 | "--network=none", 447 | "--memory="+fmt.Sprint(memoryLimitBytes), 448 | 449 | *container, 450 | "--mode=contained") 451 | } else { 452 | cmd = exec.Command("docker", "run", 453 | "--name="+name, 454 | "--rm", 455 | "--tmpfs=/tmpfs:exec", 456 | "-i", // read stdin 457 | 458 | "--network=none", 459 | "--memory="+fmt.Sprint(memoryLimitBytes), 460 | 461 | *container, 462 | "--mode=contained") 463 | } 464 | 465 | stdin, err := cmd.StdinPipe() 466 | if err != nil { 467 | return nil, err 468 | } 469 | pr, pw := io.Pipe() 470 | stdout := &limitedWriter{dst: &bytes.Buffer{}, n: maxOutputSize + int64(len(containedStartMessage))} 471 | stderr := &limitedWriter{dst: &bytes.Buffer{}, n: maxOutputSize} 472 | cmd.Stdout = &switchWriter{switchAfter: []byte(containedStartMessage), dst1: pw, dst2: stdout} 473 | cmd.Stderr = stderr 474 | if err := cmd.Start(); err != nil { 475 | return nil, err 476 | } 477 | 478 | ctx, cancel := context.WithCancel(ctx) 479 | c = &Container{ 480 | name: name, 481 | stdin: stdin, 482 | stdout: stdout, 483 | stderr: stderr, 484 | cmd: cmd, 485 | cancelCmd: cancel, 486 | waitErr: make(chan error, 1), 487 | } 488 | go func() { 489 | c.waitErr <- internal.WaitOrStop(ctx, cmd, os.Interrupt, 250*time.Millisecond) 490 | }() 491 | defer func() { 492 | if err != nil { 493 | c.Close() 494 | } 495 | }() 496 | 497 | startErr := make(chan error, 1) 498 | go func() { 499 | buf := make([]byte, len(containedStartMessage)) 500 | _, err := io.ReadFull(pr, buf) 501 | if err != nil { 502 | startErr <- fmt.Errorf("error reading header from sandbox container: %v", err) 503 | } else if string(buf) != containedStartMessage { 504 | startErr <- fmt.Errorf("sandbox container sent wrong header %q; want %q", buf, containedStartMessage) 505 | } else { 506 | startErr <- nil 507 | } 508 | }() 509 | 510 | timer := time.NewTimer(startTimeout) 511 | defer timer.Stop() 512 | select { 513 | case <-timer.C: 514 | err := fmt.Errorf("timeout starting container %q", name) 515 | cancel() 516 | <-startErr 517 | return nil, err 518 | 519 | case err := <-startErr: 520 | if err != nil { 521 | return nil, err 522 | } 523 | } 524 | 525 | log.Printf("started container %q", name) 526 | return c, nil 527 | } 528 | 529 | func runHandler(w http.ResponseWriter, r *http.Request) { 530 | t0 := time.Now() 531 | tlast := t0 532 | var logmu sync.Mutex 533 | logf := func(format string, args ...interface{}) { 534 | if !*dev { 535 | return 536 | } 537 | logmu.Lock() 538 | defer logmu.Unlock() 539 | t := time.Now() 540 | d := t.Sub(tlast) 541 | d0 := t.Sub(t0) 542 | tlast = t 543 | log.Print(fmt.Sprintf("+%10v +%10v ", d0, d) + fmt.Sprintf(format, args...)) 544 | } 545 | logf("/run") 546 | 547 | if r.Method != "POST" { 548 | http.Error(w, "expected a POST", http.StatusBadRequest) 549 | return 550 | } 551 | 552 | // Bound the number of requests being processed at once. 553 | // (Before we slurp the binary into memory) 554 | select { 555 | case runSem <- struct{}{}: 556 | case <-r.Context().Done(): 557 | return 558 | } 559 | defer func() { <-runSem }() 560 | 561 | bin, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, maxBinarySize)) 562 | if err != nil { 563 | log.Printf("failed to read request body: %v", err) 564 | http.Error(w, err.Error(), http.StatusInternalServerError) 565 | return 566 | } 567 | logf("read %d bytes", len(bin)) 568 | 569 | c, err := getContainer(r.Context()) 570 | if err != nil { 571 | if cerr := r.Context().Err(); cerr != nil { 572 | log.Printf("getContainer, client side cancellation: %v", cerr) 573 | return 574 | } 575 | http.Error(w, "failed to get container", http.StatusInternalServerError) 576 | log.Printf("failed to get container: %v", err) 577 | return 578 | } 579 | logf("got container %s", c.name) 580 | 581 | ctx, cancel := context.WithTimeout(context.Background(), runTimeout) 582 | closed := make(chan struct{}) 583 | defer func() { 584 | logf("leaving handler; about to close container") 585 | cancel() 586 | <-closed 587 | }() 588 | go func() { 589 | <-ctx.Done() 590 | if ctx.Err() == context.DeadlineExceeded { 591 | logf("timeout") 592 | } 593 | c.Close() 594 | close(closed) 595 | }() 596 | var meta processMeta 597 | meta.Args = r.Header["X-Argument"] 598 | metaJSON, _ := json.Marshal(&meta) 599 | metaJSON = append(metaJSON, '\n') 600 | if _, err := c.stdin.Write(metaJSON); err != nil { 601 | log.Printf("failed to write meta to child: %v", err) 602 | http.Error(w, "unknown error during docker run", http.StatusInternalServerError) 603 | return 604 | } 605 | if _, err := c.stdin.Write(bin); err != nil { 606 | log.Printf("failed to write binary to child: %v", err) 607 | http.Error(w, "unknown error during docker run", http.StatusInternalServerError) 608 | return 609 | } 610 | c.stdin.Close() 611 | logf("wrote+closed") 612 | err = c.Wait() 613 | select { 614 | case <-ctx.Done(): 615 | // Timed out or canceled before or exactly as Wait returned. 616 | // Either way, treat it as a timeout. 617 | sendError(w, "timeout running program") 618 | return 619 | default: 620 | logf("finished running; about to close container") 621 | cancel() 622 | } 623 | res := &sandboxtypes.Response{} 624 | if err != nil { 625 | if c.stderr.n < 0 || c.stdout.n < 0 { 626 | // Do not send truncated output, just send the error. 627 | sendError(w, errTooMuchOutput.Error()) 628 | return 629 | } 630 | var ee *exec.ExitError 631 | if !errors.As(err, &ee) { 632 | http.Error(w, "unknown error during docker run", http.StatusInternalServerError) 633 | return 634 | } 635 | res.ExitCode = ee.ExitCode() 636 | } 637 | res.Stdout = c.stdout.dst.Bytes() 638 | res.Stderr = cleanStderr(c.stderr.dst.Bytes()) 639 | sendResponse(w, res) 640 | } 641 | 642 | // limitedWriter is an io.Writer that returns an errTooMuchOutput when the cap (n) is hit. 643 | type limitedWriter struct { 644 | dst *bytes.Buffer 645 | n int64 // max bytes remaining 646 | } 647 | 648 | // Write is an io.Writer function that returns errTooMuchOutput when the cap (n) is hit. 649 | // 650 | // Partial data will be written to dst if p is larger than n, but errTooMuchOutput will be returned. 651 | func (l *limitedWriter) Write(p []byte) (int, error) { 652 | defer func() { l.n -= int64(len(p)) }() 653 | 654 | if l.n <= 0 { 655 | return 0, errTooMuchOutput 656 | } 657 | 658 | if int64(len(p)) > l.n { 659 | n, err := l.dst.Write(p[:l.n]) 660 | if err != nil { 661 | return n, err 662 | } 663 | return n, errTooMuchOutput 664 | } 665 | 666 | return l.dst.Write(p) 667 | } 668 | 669 | // switchWriter writes to dst1 until switchAfter is written, the it writes to dst2. 670 | type switchWriter struct { 671 | dst1 io.Writer 672 | dst2 io.Writer 673 | switchAfter []byte 674 | buf []byte 675 | found bool 676 | } 677 | 678 | func (s *switchWriter) Write(p []byte) (int, error) { 679 | if s.found { 680 | return s.dst2.Write(p) 681 | } 682 | 683 | s.buf = append(s.buf, p...) 684 | i := bytes.Index(s.buf, s.switchAfter) 685 | if i == -1 { 686 | if len(s.buf) >= len(s.switchAfter) { 687 | s.buf = s.buf[len(s.buf)-len(s.switchAfter)+1:] 688 | } 689 | return s.dst1.Write(p) 690 | } 691 | 692 | s.found = true 693 | nAfter := len(s.buf) - (i + len(s.switchAfter)) 694 | s.buf = nil 695 | 696 | n, err := s.dst1.Write(p[:len(p)-nAfter]) 697 | if err != nil { 698 | return n, err 699 | } 700 | n2, err := s.dst2.Write(p[len(p)-nAfter:]) 701 | return n + n2, err 702 | } 703 | 704 | func errExitCode(err error) int { 705 | if err == nil { 706 | return 0 707 | } 708 | var ee *exec.ExitError 709 | if errors.As(err, &ee) { 710 | return ee.ExitCode() 711 | } 712 | return 1 713 | } 714 | 715 | func sendError(w http.ResponseWriter, errMsg string) { 716 | sendResponse(w, &sandboxtypes.Response{Error: errMsg}) 717 | } 718 | 719 | func sendResponse(w http.ResponseWriter, r *sandboxtypes.Response) { 720 | jres, err := json.MarshalIndent(r, "", " ") 721 | if err != nil { 722 | http.Error(w, "error encoding JSON", http.StatusInternalServerError) 723 | log.Printf("json marshal: %v", err) 724 | return 725 | } 726 | w.Header().Set("Content-Type", "application/json") 727 | w.Header().Set("Content-Length", fmt.Sprint(len(jres))) 728 | w.Write(jres) 729 | } 730 | 731 | // cleanStderr removes spam stderr lines from the beginning of x 732 | // and returns a slice of x. 733 | func cleanStderr(x []byte) []byte { 734 | i := bytes.Index(x, containedStderrHeader) 735 | if i == -1 { 736 | return x 737 | } 738 | return x[i+len(containedStderrHeader):] 739 | } 740 | -------------------------------------------------------------------------------- /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/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/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/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("/favicon.ico", handleFavicon) 54 | s.mux.HandleFunc("/_ah/health", s.handleHealthCheck) 55 | 56 | staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))) 57 | s.mux.Handle("/static/", staticHandler) 58 | s.mux.Handle("/doc/play/", http.StripPrefix("/doc/play/", s.examples)) 59 | } 60 | 61 | func handleFavicon(w http.ResponseWriter, r *http.Request) { 62 | http.ServeFile(w, r, "./static/favicon.ico") 63 | } 64 | 65 | func (s *server) handleHealthCheck(w http.ResponseWriter, r *http.Request) { 66 | if err := s.healthCheck(r.Context()); err != nil { 67 | http.Error(w, "Health check failed: "+err.Error(), http.StatusInternalServerError) 68 | return 69 | } 70 | fmt.Fprint(w, "ok") 71 | } 72 | 73 | func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 74 | if r.Header.Get("X-Forwarded-Proto") == "http" { 75 | r.URL.Scheme = "https" 76 | r.URL.Host = r.Host 77 | http.Redirect(w, r, r.URL.String(), http.StatusFound) 78 | return 79 | } 80 | if r.Header.Get("X-Forwarded-Proto") == "https" { 81 | w.Header().Set("Strict-Transport-Security", "max-age=31536000; preload") 82 | } 83 | s.mux.ServeHTTP(w, r) 84 | } 85 | 86 | // writeJSONResponse JSON-encodes resp and writes to w with the given HTTP 87 | // status. 88 | func (s *server) writeJSONResponse(w http.ResponseWriter, resp interface{}, status int) { 89 | w.Header().Set("Content-Type", "application/json") 90 | var buf bytes.Buffer 91 | if err := json.NewEncoder(&buf).Encode(resp); err != nil { 92 | s.log.Errorf("error encoding response: %v", err) 93 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 94 | return 95 | } 96 | w.WriteHeader(status) 97 | if _, err := io.Copy(w, &buf); err != nil { 98 | s.log.Errorf("io.Copy(w, &buf): %v", err) 99 | return 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /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 | testCases := []struct { 118 | desc string 119 | method string 120 | url string 121 | statusCode int 122 | reqBody []byte 123 | respBody []byte 124 | }{ 125 | // Examples tests. 126 | {"Hello example", http.MethodGet, "https://play.golang.org/doc/play/hello.txt", http.StatusOK, nil, []byte("Hello")}, 127 | {"HTTP example", http.MethodGet, "https://play.golang.org/doc/play/http.txt", http.StatusOK, nil, []byte("net/http")}, 128 | {"Versions json", http.MethodGet, "https://play.golang.org/version", http.StatusOK, nil, []byte(runtime.Version())}, 129 | } 130 | 131 | for _, tc := range testCases { 132 | req := httptest.NewRequest(tc.method, tc.url, bytes.NewReader(tc.reqBody)) 133 | w := httptest.NewRecorder() 134 | s.mux.ServeHTTP(w, req) 135 | resp := w.Result() 136 | corsHeader := "Access-Control-Allow-Origin" 137 | if got, want := resp.Header.Get(corsHeader), "*"; got != want { 138 | t.Errorf("%s: %q header: got %q; want %q", tc.desc, corsHeader, got, want) 139 | } 140 | if got, want := resp.StatusCode, tc.statusCode; got != want { 141 | t.Errorf("%s: got unexpected status code %d; want %d", tc.desc, got, want) 142 | } 143 | if tc.respBody != nil { 144 | defer resp.Body.Close() 145 | b, err := ioutil.ReadAll(resp.Body) 146 | if err != nil { 147 | t.Errorf("%s: ioutil.ReadAll(resp.Body): %v", tc.desc, err) 148 | } 149 | if !bytes.Contains(b, tc.respBody) { 150 | t.Errorf("%s: got unexpected body %q; want contains %q", tc.desc, b, tc.respBody) 151 | } 152 | } 153 | } 154 | } 155 | 156 | func TestCommandHandler(t *testing.T) { 157 | s, err := newServer(func(s *server) error { 158 | s.db = &inMemStore{} 159 | // testLogger makes tests fail. 160 | // Should we verify that s.log.Errorf was called 161 | // instead of just printing or failing the test? 162 | s.log = newStdLogger() 163 | s.cache = new(inMemCache) 164 | var err error 165 | s.examples, err = newExamplesHandler(false, time.Now()) 166 | if err != nil { 167 | return err 168 | } 169 | return nil 170 | }) 171 | if err != nil { 172 | t.Fatalf("newServer(testingOptions(t)): %v", err) 173 | } 174 | testHandler := s.commandHandler("test", func(_ context.Context, r *request) (*response, error) { 175 | if r.Body == "fail" { 176 | return nil, fmt.Errorf("non recoverable") 177 | } 178 | if r.Body == "error" { 179 | return &response{Errors: "errors"}, nil 180 | } 181 | if r.Body == "oom-error" { 182 | // To throw an oom in a local playground instance, increase the server timeout 183 | // to 20 seconds (within sandbox.go), spin up the Docker instance and run 184 | // this code: https://play.golang.org/p/aaCv86m0P14. 185 | return &response{Events: []Event{{"out of memory", "stderr", 0}}}, nil 186 | } 187 | if r.Body == "allocate-memory-error" { 188 | return &response{Events: []Event{{"cannot allocate memory", "stderr", 0}}}, nil 189 | } 190 | if r.Body == "oom-compile-error" { 191 | return &response{Errors: "out of memory"}, nil 192 | } 193 | if r.Body == "allocate-memory-compile-error" { 194 | return &response{Errors: "cannot allocate memory"}, nil 195 | } 196 | if r.Body == "build-timeout-error" { 197 | return &response{Errors: goBuildTimeoutError}, nil 198 | } 199 | if r.Body == "run-timeout-error" { 200 | return &response{Errors: runTimeoutError}, nil 201 | } 202 | resp := &response{Events: []Event{{r.Body, "stdout", 0}}} 203 | return resp, nil 204 | }) 205 | 206 | testCases := []struct { 207 | desc string 208 | method string 209 | statusCode int 210 | reqBody []byte 211 | respBody []byte 212 | shouldCache bool 213 | }{ 214 | {"OPTIONS request", http.MethodOptions, http.StatusOK, nil, nil, false}, 215 | {"GET request", http.MethodGet, http.StatusBadRequest, nil, nil, false}, 216 | {"Empty POST", http.MethodPost, http.StatusBadRequest, nil, nil, false}, 217 | {"Failed cmdFunc", http.MethodPost, http.StatusInternalServerError, []byte(`{"Body":"fail"}`), nil, false}, 218 | {"Standard flow", http.MethodPost, http.StatusOK, 219 | []byte(`{"Body":"ok"}`), 220 | []byte(`{"Errors":"","Events":[{"Message":"ok","Kind":"stdout","Delay":0}],"Status":0,"IsTest":false,"TestsFailed":0} 221 | `), 222 | true}, 223 | {"Cache-able Errors in response", http.MethodPost, http.StatusOK, 224 | []byte(`{"Body":"error"}`), 225 | []byte(`{"Errors":"errors","Events":null,"Status":0,"IsTest":false,"TestsFailed":0} 226 | `), 227 | true}, 228 | {"Out of memory error in response body event message", http.MethodPost, http.StatusInternalServerError, 229 | []byte(`{"Body":"oom-error"}`), nil, false}, 230 | {"Cannot allocate memory error in response body event message", http.MethodPost, http.StatusInternalServerError, 231 | []byte(`{"Body":"allocate-memory-error"}`), nil, false}, 232 | {"Out of memory error in response errors", http.MethodPost, http.StatusInternalServerError, 233 | []byte(`{"Body":"oom-compile-error"}`), nil, false}, 234 | {"Cannot allocate memory error in response errors", http.MethodPost, http.StatusInternalServerError, 235 | []byte(`{"Body":"allocate-memory-compile-error"}`), nil, false}, 236 | { 237 | desc: "Build timeout error", 238 | method: http.MethodPost, 239 | statusCode: http.StatusOK, 240 | reqBody: []byte(`{"Body":"build-timeout-error"}`), 241 | respBody: []byte(fmt.Sprintln(`{"Errors":"timeout running go build","Events":null,"Status":0,"IsTest":false,"TestsFailed":0}`)), 242 | }, 243 | { 244 | desc: "Run timeout error", 245 | method: http.MethodPost, 246 | statusCode: http.StatusOK, 247 | reqBody: []byte(`{"Body":"run-timeout-error"}`), 248 | respBody: []byte(fmt.Sprintln(`{"Errors":"timeout running program","Events":null,"Status":0,"IsTest":false,"TestsFailed":0}`)), 249 | }, 250 | } 251 | 252 | for _, tc := range testCases { 253 | t.Run(tc.desc, func(t *testing.T) { 254 | req := httptest.NewRequest(tc.method, "/compile", bytes.NewReader(tc.reqBody)) 255 | w := httptest.NewRecorder() 256 | testHandler(w, req) 257 | resp := w.Result() 258 | corsHeader := "Access-Control-Allow-Origin" 259 | if got, want := resp.Header.Get(corsHeader), "*"; got != want { 260 | t.Errorf("%s: %q header: got %q; want %q", tc.desc, corsHeader, got, want) 261 | } 262 | if got, want := resp.StatusCode, tc.statusCode; got != want { 263 | t.Errorf("%s: got unexpected status code %d; want %d", tc.desc, got, want) 264 | } 265 | if tc.respBody != nil { 266 | defer resp.Body.Close() 267 | b, err := ioutil.ReadAll(resp.Body) 268 | if err != nil { 269 | t.Errorf("%s: ioutil.ReadAll(resp.Body): %v", tc.desc, err) 270 | } 271 | if !bytes.Equal(b, tc.respBody) { 272 | t.Errorf("%s: got unexpected body %q; want %q", tc.desc, b, tc.respBody) 273 | } 274 | } 275 | 276 | // Test caching semantics. 277 | sbreq := new(request) // A sandbox request, used in the cache key. 278 | json.Unmarshal(tc.reqBody, sbreq) // Ignore errors, request may be empty. 279 | gotCache := new(response) 280 | if err := s.cache.Get(cacheKey("test", sbreq.Body), gotCache); (err == nil) != tc.shouldCache { 281 | t.Errorf("s.cache.Get(%q, %v) = %v, shouldCache: %v", cacheKey("test", sbreq.Body), gotCache, err, tc.shouldCache) 282 | } 283 | wantCache := new(response) 284 | if tc.shouldCache { 285 | if err := json.Unmarshal(tc.respBody, wantCache); err != nil { 286 | t.Errorf("json.Unmarshal(%q, %v) = %v, wanted no error", tc.respBody, wantCache, err) 287 | } 288 | } 289 | if diff := cmp.Diff(wantCache, gotCache); diff != "" { 290 | t.Errorf("s.Cache.Get(%q) mismatch (-want +got):\n%s", cacheKey("test", sbreq.Body), diff) 291 | } 292 | }) 293 | } 294 | } 295 | 296 | func TestPlaygroundGoproxy(t *testing.T) { 297 | const envKey = "PLAY_GOPROXY" 298 | defer os.Setenv(envKey, os.Getenv(envKey)) 299 | 300 | tests := []struct { 301 | name string 302 | env string 303 | want string 304 | }{ 305 | {name: "missing", env: "", want: "https://proxy.golang.org"}, 306 | {name: "set_to_default", env: "https://proxy.golang.org", want: "https://proxy.golang.org"}, 307 | {name: "changed", env: "https://company.intranet", want: "https://company.intranet"}, 308 | } 309 | for _, tt := range tests { 310 | t.Run(tt.name, func(t *testing.T) { 311 | if tt.env != "" { 312 | if err := os.Setenv(envKey, tt.env); err != nil { 313 | t.Errorf("unable to set environment variable for test: %s", err) 314 | } 315 | } else { 316 | if err := os.Unsetenv(envKey); err != nil { 317 | t.Errorf("unable to unset environment variable for test: %s", err) 318 | } 319 | } 320 | got := playgroundGoproxy() 321 | if got != tt.want { 322 | t.Errorf("playgroundGoproxy = %s; want %s; env: %s", got, tt.want, tt.env) 323 | } 324 | }) 325 | } 326 | } 327 | 328 | // inMemCache is a responseCache backed by a map. It is only suitable for testing. 329 | type inMemCache struct { 330 | l sync.Mutex 331 | m map[string]*response 332 | } 333 | 334 | // Set implements the responseCache interface. 335 | // Set stores a *response in the cache. It panics for other types to ensure test failure. 336 | func (i *inMemCache) Set(key string, v interface{}) error { 337 | i.l.Lock() 338 | defer i.l.Unlock() 339 | if i.m == nil { 340 | i.m = make(map[string]*response) 341 | } 342 | i.m[key] = v.(*response) 343 | return nil 344 | } 345 | 346 | // Get implements the responseCache interface. 347 | // Get fetches a *response from the cache, or returns a memcache.ErrcacheMiss. 348 | // It panics for other types to ensure test failure. 349 | func (i *inMemCache) Get(key string, v interface{}) error { 350 | i.l.Lock() 351 | defer i.l.Unlock() 352 | target := v.(*response) 353 | got, ok := i.m[key] 354 | if !ok { 355 | return memcache.ErrCacheMiss 356 | } 357 | *target = *got 358 | return nil 359 | } 360 | -------------------------------------------------------------------------------- /src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulteary/golang-playground/a7635d7604afe2720da533f0c08e8030b6d5c55a/src/static/favicon.ico -------------------------------------------------------------------------------- /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/static/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulteary/golang-playground/a7635d7604afe2720da533f0c08e8030b6d5c55a/src/static/gopher.png -------------------------------------------------------------------------------- /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/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' 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 | 37 | // The output callback must be of this form. 38 | // See PlaygroundOutput (below) for an implementation. 39 | function outputCallback(write) { 40 | } 41 | */ 42 | 43 | // HTTPTransport is the default transport. 44 | // enableVet enables running vet if a program was compiled and ran successfully. 45 | // If vet returned any errors, display them before the output of a program. 46 | function HTTPTransport(enableVet) { 47 | 'use strict'; 48 | 49 | function playback(output, data) { 50 | // Backwards compatibility: default values do not affect the output. 51 | var events = data.Events || []; 52 | var errors = data.Errors || ''; 53 | var status = data.Status || 0; 54 | var isTest = data.IsTest || false; 55 | var testsFailed = data.TestsFailed || 0; 56 | 57 | var timeout; 58 | output({ Kind: 'start' }); 59 | function next() { 60 | if (!events || events.length === 0) { 61 | if (isTest) { 62 | if (testsFailed > 0) { 63 | output({ 64 | Kind: 'system', 65 | Body: 66 | '\n' + 67 | testsFailed + 68 | ' test' + 69 | (testsFailed > 1 ? 's' : '') + 70 | ' failed.', 71 | }); 72 | } else { 73 | output({ Kind: 'system', Body: '\nAll tests passed.' }); 74 | } 75 | } else { 76 | if (status > 0) { 77 | output({ Kind: 'end', Body: 'status ' + status + '.' }); 78 | } else { 79 | if (errors !== '') { 80 | // errors are displayed only in the case of timeout. 81 | output({ Kind: 'end', Body: errors + '.' }); 82 | } else { 83 | output({ Kind: 'end' }); 84 | } 85 | } 86 | } 87 | return; 88 | } 89 | var e = events.shift(); 90 | if (e.Delay === 0) { 91 | output({ Kind: e.Kind, Body: e.Message }); 92 | next(); 93 | return; 94 | } 95 | timeout = setTimeout(function() { 96 | output({ Kind: e.Kind, Body: e.Message }); 97 | next(); 98 | }, e.Delay / 1000000); 99 | } 100 | next(); 101 | return { 102 | Stop: function() { 103 | clearTimeout(timeout); 104 | }, 105 | }; 106 | } 107 | 108 | function error(output, msg) { 109 | output({ Kind: 'start' }); 110 | output({ Kind: 'stderr', Body: msg }); 111 | output({ Kind: 'end' }); 112 | } 113 | 114 | function buildFailed(output, msg) { 115 | output({ Kind: 'start' }); 116 | output({ Kind: 'stderr', Body: msg }); 117 | output({ Kind: 'system', Body: '\nGo build failed.' }); 118 | } 119 | 120 | var seq = 0; 121 | return { 122 | Run: function(body, output, options) { 123 | seq++; 124 | var cur = seq; 125 | var playing; 126 | $.ajax('/compile', { 127 | type: 'POST', 128 | data: { version: 2, body: body, withVet: enableVet }, 129 | dataType: 'json', 130 | success: function(data) { 131 | if (seq != cur) return; 132 | if (!data) return; 133 | if (playing != null) playing.Stop(); 134 | if (data.Errors) { 135 | if (data.Errors === 'process took too long') { 136 | // Playback the output that was captured before the timeout. 137 | playing = playback(output, data); 138 | } else { 139 | buildFailed(output, data.Errors); 140 | } 141 | return; 142 | } 143 | if (!data.Events) { 144 | data.Events = []; 145 | } 146 | if (data.VetErrors) { 147 | // Inject errors from the vet as the first events in the output. 148 | data.Events.unshift({ 149 | Message: 'Go vet exited.\n\n', 150 | Kind: 'system', 151 | Delay: 0, 152 | }); 153 | data.Events.unshift({ 154 | Message: data.VetErrors, 155 | Kind: 'stderr', 156 | Delay: 0, 157 | }); 158 | } 159 | 160 | if (!enableVet || data.VetOK || data.VetErrors) { 161 | playing = playback(output, data); 162 | return; 163 | } 164 | 165 | // In case the server support doesn't support 166 | // compile+vet in same request signaled by the 167 | // 'withVet' parameter above, also try the old way. 168 | // TODO: remove this when it falls out of use. 169 | // It is 2019-05-13 now. 170 | $.ajax('/vet', { 171 | data: { body: body }, 172 | type: 'POST', 173 | dataType: 'json', 174 | success: function(dataVet) { 175 | if (dataVet.Errors) { 176 | // inject errors from the vet as the first events in the output 177 | data.Events.unshift({ 178 | Message: 'Go vet exited.\n\n', 179 | Kind: 'system', 180 | Delay: 0, 181 | }); 182 | data.Events.unshift({ 183 | Message: dataVet.Errors, 184 | Kind: 'stderr', 185 | Delay: 0, 186 | }); 187 | } 188 | playing = playback(output, data); 189 | }, 190 | error: function() { 191 | playing = playback(output, data); 192 | }, 193 | }); 194 | }, 195 | error: function() { 196 | error(output, 'Error communicating with remote server.'); 197 | }, 198 | }); 199 | return { 200 | Kill: function() { 201 | if (playing != null) playing.Stop(); 202 | output({ Kind: 'end', Body: 'killed' }); 203 | }, 204 | }; 205 | }, 206 | }; 207 | } 208 | 209 | function SocketTransport() { 210 | 'use strict'; 211 | 212 | var id = 0; 213 | var outputs = {}; 214 | var started = {}; 215 | var websocket; 216 | if (window.location.protocol == 'http:') { 217 | websocket = new WebSocket('ws://' + window.location.host + '/socket'); 218 | } else if (window.location.protocol == 'https:') { 219 | websocket = new WebSocket('wss://' + window.location.host + '/socket'); 220 | } 221 | 222 | websocket.onclose = function() { 223 | console.log('websocket connection closed'); 224 | }; 225 | 226 | websocket.onmessage = function(e) { 227 | var m = JSON.parse(e.data); 228 | var output = outputs[m.Id]; 229 | if (output === null) return; 230 | if (!started[m.Id]) { 231 | output({ Kind: 'start' }); 232 | started[m.Id] = true; 233 | } 234 | output({ Kind: m.Kind, Body: m.Body }); 235 | }; 236 | 237 | function send(m) { 238 | websocket.send(JSON.stringify(m)); 239 | } 240 | 241 | return { 242 | Run: function(body, output, options) { 243 | var thisID = id + ''; 244 | id++; 245 | outputs[thisID] = output; 246 | send({ Id: thisID, Kind: 'run', Body: body, Options: options }); 247 | return { 248 | Kill: function() { 249 | send({ Id: thisID, Kind: 'kill' }); 250 | }, 251 | }; 252 | }, 253 | }; 254 | } 255 | 256 | function PlaygroundOutput(el) { 257 | 'use strict'; 258 | 259 | return function(write) { 260 | if (write.Kind == 'start') { 261 | el.innerHTML = ''; 262 | return; 263 | } 264 | 265 | var cl = 'system'; 266 | if (write.Kind == 'stdout' || write.Kind == 'stderr') cl = write.Kind; 267 | 268 | var m = write.Body; 269 | if (write.Kind == 'end') { 270 | m = '\n程序退出' + (m ? ': ' + m : '.'); 271 | } 272 | 273 | if (m.indexOf('IMAGE:') === 0) { 274 | // TODO(adg): buffer all writes before creating image 275 | var url = 'data:image/png;base64,' + m.substr(6); 276 | var img = document.createElement('img'); 277 | img.src = url; 278 | el.appendChild(img); 279 | return; 280 | } 281 | 282 | // ^L clears the screen. 283 | var s = m.split('\x0c'); 284 | if (s.length > 1) { 285 | el.innerHTML = ''; 286 | m = s.pop(); 287 | } 288 | 289 | m = m.replace(/&/g, '&'); 290 | m = m.replace(//g, '>'); 292 | 293 | var needScroll = el.scrollTop + el.offsetHeight == el.scrollHeight; 294 | 295 | var span = document.createElement('span'); 296 | span.className = cl; 297 | span.innerHTML = m; 298 | el.appendChild(span); 299 | 300 | if (needScroll) el.scrollTop = el.scrollHeight - el.offsetHeight; 301 | }; 302 | } 303 | 304 | (function() { 305 | function lineHighlight(error) { 306 | var regex = /prog.go:([0-9]+)/g; 307 | var r = regex.exec(error); 308 | while (r) { 309 | $('.lines div') 310 | .eq(r[1] - 1) 311 | .addClass('lineerror'); 312 | r = regex.exec(error); 313 | } 314 | } 315 | function highlightOutput(wrappedOutput) { 316 | return function(write) { 317 | if (write.Body) lineHighlight(write.Body); 318 | wrappedOutput(write); 319 | }; 320 | } 321 | function lineClear() { 322 | $('.lineerror').removeClass('lineerror'); 323 | } 324 | 325 | // opts is an object with these keys 326 | // codeEl - code editor element 327 | // outputEl - program output element 328 | // runEl - run button element 329 | // fmtEl - fmt button element (optional) 330 | // fmtImportEl - fmt "imports" checkbox element (optional) 331 | // toysEl - toys select element (optional) 332 | // enableHistory - enable using HTML5 history API (optional) 333 | // transport - playground transport to use (default is HTTPTransport) 334 | // enableShortcuts - whether to enable shortcuts (Ctrl+S/Cmd+S to save) (default is false) 335 | // enableVet - enable running vet and displaying its errors 336 | function playground(opts) { 337 | var code = $(opts.codeEl); 338 | var transport = opts['transport'] || new HTTPTransport(opts['enableVet']); 339 | var running; 340 | 341 | // autoindent helpers. 342 | function insertTabs(n) { 343 | // find the selection start and end 344 | var start = code[0].selectionStart; 345 | var end = code[0].selectionEnd; 346 | // split the textarea content into two, and insert n tabs 347 | var v = code[0].value; 348 | var u = v.substr(0, start); 349 | for (var i = 0; i < n; i++) { 350 | u += '\t'; 351 | } 352 | u += v.substr(end); 353 | // set revised content 354 | code[0].value = u; 355 | // reset caret position after inserted tabs 356 | code[0].selectionStart = start + n; 357 | code[0].selectionEnd = start + n; 358 | } 359 | function autoindent(el) { 360 | var curpos = el.selectionStart; 361 | var tabs = 0; 362 | while (curpos > 0) { 363 | curpos--; 364 | if (el.value[curpos] == '\t') { 365 | tabs++; 366 | } else if (tabs > 0 || el.value[curpos] == '\n') { 367 | break; 368 | } 369 | } 370 | setTimeout(function() { 371 | insertTabs(tabs); 372 | }, 1); 373 | } 374 | 375 | function keyHandler(e) { 376 | if (e.keyCode == 9 && !e.ctrlKey) { 377 | // tab (but not ctrl-tab) 378 | insertTabs(1); 379 | e.preventDefault(); 380 | return false; 381 | } 382 | if (e.keyCode == 13) { 383 | // enter 384 | if (e.shiftKey) { 385 | // +shift 386 | run(); 387 | e.preventDefault(); 388 | return false; 389 | } 390 | if (e.ctrlKey) { 391 | // +control 392 | fmt(); 393 | e.preventDefault(); 394 | } else { 395 | autoindent(e.target); 396 | } 397 | } 398 | return true; 399 | } 400 | code.unbind('keydown').bind('keydown', keyHandler); 401 | var outdiv = $(opts.outputEl).empty(); 402 | var output = $('
').appendTo(outdiv);
403 | 
404 |     function body() {
405 |       return $(opts.codeEl).val();
406 |     }
407 |     function setBody(text) {
408 |       $(opts.codeEl).val(text);
409 |     }
410 |     function origin(href) {
411 |       return ('' + href)
412 |         .split('/')
413 |         .slice(0, 3)
414 |         .join('/');
415 |     }
416 | 
417 |     var pushedEmpty = window.location.pathname == '/';
418 |     function inputChanged() {
419 |       if (pushedEmpty) {
420 |         return;
421 |       }
422 |       pushedEmpty = true;
423 |       window.history.pushState(null, '', '/');
424 |     }
425 |     function popState(e) {
426 |       if (e === null) {
427 |         return;
428 |       }
429 |       if (e && e.state && e.state.code) {
430 |         setBody(e.state.code);
431 |       }
432 |     }
433 |     var rewriteHistory = false;
434 |     if (
435 |       window.history &&
436 |       window.history.pushState &&
437 |       window.addEventListener &&
438 |       opts.enableHistory
439 |     ) {
440 |       rewriteHistory = true;
441 |       code[0].addEventListener('input', inputChanged);
442 |       window.addEventListener('popstate', popState);
443 |     }
444 | 
445 |     function setError(error) {
446 |       if (running) running.Kill();
447 |       lineClear();
448 |       lineHighlight(error);
449 |       output
450 |         .empty()
451 |         .addClass('error')
452 |         .text(error);
453 |     }
454 |     function loading() {
455 |       lineClear();
456 |       if (running) running.Kill();
457 |       output.removeClass('error').text('等待远程服务器响应...');
458 |     }
459 |     function run() {
460 |       loading();
461 |       running = transport.Run(
462 |         body(),
463 |         highlightOutput(PlaygroundOutput(output[0]))
464 |       );
465 |     }
466 | 
467 |     function fmt() {
468 |       loading();
469 |       var data = { body: body() };
470 |       if ($(opts.fmtImportEl).is(':checked')) {
471 |         data['imports'] = 'true';
472 |       }
473 |       $.ajax('/fmt', {
474 |         data: data,
475 |         type: 'POST',
476 |         dataType: 'json',
477 |         success: function(data) {
478 |           if (data.Error) {
479 |             setError(data.Error);
480 |           } else {
481 |             setBody(data.Body);
482 |             setError('');
483 |           }
484 |         },
485 |       });
486 |     }
487 | 
488 |     $(opts.runEl).click(run);
489 |     $(opts.fmtEl).click(fmt);
490 | 
491 |     if (opts.toysEl !== null) {
492 |       $(opts.toysEl).bind('change', function() {
493 |         var toy = $(this).val();
494 |         $.ajax('/doc/play/' + toy, {
495 |           processData: false,
496 |           type: 'GET',
497 |           complete: function(xhr) {
498 |             if (xhr.status != 200) {
499 |               alert('Server error; try again.');
500 |               return;
501 |             }
502 |             setBody(xhr.responseText);
503 |           },
504 |         });
505 |       });
506 |     }
507 |   }
508 | 
509 |   window.playground = playground;
510 | })();
511 | 


--------------------------------------------------------------------------------
/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 | 
139 | #embedLabel {
140 | 	font-family: sans-serif;
141 | 	padding-top: 5px;
142 | }
143 | #banner > select {
144 | 	font-size: 0.875rem;
145 | 	border: 0.0625rem solid #375EAB;
146 | }
147 | .lines {
148 | 	float: left;
149 | 	overflow: hidden;
150 | 	text-align: right;
151 | }
152 | .lines div {
153 | 	padding-right: 5px;
154 | 	color: lightgray;
155 | }
156 | .lineerror {
157 | 	color: red;
158 | 	background: #FDD;
159 | }
160 | .exit {
161 | 	color: lightgray;
162 | }
163 | 
164 | .embedded #banner {
165 | 	display: none;
166 | }
167 | .embedded #wrap {
168 | 	top: 0;
169 | }
170 | 


--------------------------------------------------------------------------------
/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 | func (s *inMemStore) PutSnippet(_ context.Context, id string, snip *snippet) error {
25 | 	s.Lock()
26 | 	if s.m == nil {
27 | 		s.m = map[string]*snippet{}
28 | 	}
29 | 	b := make([]byte, len(snip.Body))
30 | 	copy(b, snip.Body)
31 | 	s.m[id] = &snippet{Body: b}
32 | 	s.Unlock()
33 | 	return nil
34 | }
35 | 
36 | func (s *inMemStore) GetSnippet(_ context.Context, id string, snip *snippet) error {
37 | 	var ErrNoSuchEntity = errors.New("datastore: no such entity")
38 | 
39 | 	s.RLock()
40 | 	defer s.RUnlock()
41 | 	v, ok := s.m[id]
42 | 	if !ok {
43 | 		return ErrNoSuchEntity
44 | 	}
45 | 	*snip = *v
46 | 	return nil
47 | }
48 | 


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


--------------------------------------------------------------------------------