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 |
109 |
110 |
111 | The playground can use most of the standard library, with some exceptions.
112 | The only communication a playground program has to the outside world
113 | is by writing to standard output and standard error.
114 |
115 |
116 |
117 | In the playground the time begins at 2009-11-10 23:00:00 UTC
118 | (determining the significance of this date is an exercise for the reader).
119 | This makes it easier to cache programs by giving them deterministic output.
120 |
121 |
122 |
123 | There are also limits on execution time and on CPU and memory usage.
124 |
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 |
--------------------------------------------------------------------------------