Inside the "present" tool
Andrew Gerrand
Andrew Gerrand
The API used by the Playground (and the Tour) is a simple HTTP POST request
that returns a JSON-encoded response.
Request:
POST /compile HTTP/1.1 Host:play.golang.org Content-Length:113 Content-Type:application/x-www-form-urlencoded; charset=UTF-8 body=package+main%0A%0Aimport+%22fmt%22%0A%0Afunc+main()+%7B%0A%09fmt.Println(%22Hello%2C+playground%22)%0A%7D%0A
Response body:
{"compile_errors":"","output":"Hello, playground\n"}
The compile service has no concept of time. (Necessary to limit resource use.)
The API reflects this; output is sent in one blob, not streamed.
Even when running locally, the API is bad for demonstrating code that uses time.
Rob needed to use time in his Go Concurrency Patterns talk.
3WebSockets are a bi-directional communication channel between a JavaScript program running in a web browser and a web server. They are part of HTML 5.
The websocket
package in Go's go.net
sub-repository provides a WebSocket client and server.
I thought I could use WebSockets to stream program output to a running
presentation.
And thus the present
tool was born.
var sock = new WebSocket("ws://localhost:4000/"); sock.onmessage = function(m) { console.log("Received:", m.data); } sock.send("Hello!\n")
package main import ( "fmt" "golang.org/x/net/websocket" "net/http" ) func main() { http.Handle("/", websocket.Handler(handler)) http.ListenAndServe("localhost:4000", nil) } func handler(c *websocket.Conn) { var s string fmt.Fscan(c, &s) fmt.Println("Received:", s) fmt.Fprint(c, "How do you do?") }
The client (browser) and server (present) communicate with JSON-encoded messages.
// Message is the wire format for the websocket connection to the browser. // It is used for both sending output messages and receiving commands, as // distinguished by the Kind field. type Message struct { Id string // client-provided unique id for the process Kind string // in: "run", "kill" out: "stdout", "stderr", "end" Body string }
Go's encoding/json
format can convert these Message
values to and from JSON.
Go:
Message{Id: "0", Kind: "run", Body: `package main; func main() { print("hello"); }`}
JSON:
{"Id":"0","Kind":"run","Body":"package main; func main() { print(\"hello\"); }"}
package main import ( "fmt"; "time" ) func main() { for { fmt.Println("Hello, Gophers!") time.Sleep(time.Second) } }
First, register the handler with the net/http
package:
http.Handle("/socket", websocket.Handler(socketHandler))
Implementation:
func socketHandler(c *websocket.Conn) { in, out := make(chan *Message), make(chan *Message) errc := make(chan error, 1)
// Decode messages from client and send to the in channel. go func() { dec := json.NewDecoder(c) for { var m Message if err := dec.Decode(&m); err != nil { errc <- err return } in <- &m } }() // Receive messages from the out channel and encode to the client. go func() { enc := json.NewEncoder(c) for m := range out { if err := enc.Encode(m); err != nil { errc <- err return } } }()
// Start and kill Processes and handle errors. proc := make(map[string]*Process) for { select { case m := <-in: switch m.Kind { case "run": proc[m.Id].Kill() proc[m.Id] = StartProcess(m.Id, m.Body, out) case "kill": proc[m.Id].Kill() } case err := <-errc: // A encode or decode has failed; bail. log.Println(err) // Shut down any running processes. for _, p := range proc { p.Kill() } return } } }
// Process represents a running process. type Process struct { id string out chan<- *Message done chan struct{} // closed when wait completes run *exec.Cmd }
// StartProcess builds and runs the given program, sending its output // and end event as Messages on the provided channel. func StartProcess(id, body string, out chan<- *Message) *Process { p := &Process{ id: id, out: out, done: make(chan struct{}), } if err := p.start(body); err != nil { p.end(err) return nil } go p.wait() return p }
// start builds and starts the given program, sends its output to p.out, // and stores the running *exec.Cmd in the run field. func (p *Process) start(body string) error { // x is the base name for .go and executable files x := filepath.Join(tmpdir, "compile"+strconv.Itoa(<-uniq)) src := x + ".go" bin := x if runtime.GOOS == "windows" { bin += ".exe" } // write body to x.go defer os.Remove(src) if err := ioutil.WriteFile(src, []byte(body), 0666); err != nil { return err }
// build x.go, creating x dir, file := filepath.Split(src) err := p.cmd(dir, "go", "build", "-o", bin, file).Run() defer os.Remove(bin) if err != nil { return err } // run x cmd := p.cmd("", bin) if err = cmd.Start(); err != nil { return err } p.run = cmd return nil }
// cmd builds an *exec.Cmd that writes its standard output and error to the // Process' output channel. func (p *Process) cmd(dir string, args ...string) *exec.Cmd { cmd := exec.Command(args[0], args[1:]...) cmd.Dir = dir cmd.Stdout = &messageWriter{p.id, "stdout", p.out} cmd.Stderr = &messageWriter{p.id, "stderr", p.out} return cmd }
// messageWriter is an io.Writer that converts all writes to Message sends on // the out channel with the specified id and kind. type messageWriter struct { id, kind string out chan<- *Message } func (w *messageWriter) Write(b []byte) (n int, err error) { w.out <- &Message{Id: w.id, Kind: w.kind, Body: string(b)} return len(b), nil }
// wait waits for the running process to complete // and sends its error state to the client. func (p *Process) wait() { defer close(p.done) p.end(p.run.Wait()) }
// end sends an "end" message to the client, containing the process id and the // given error value. func (p *Process) end(err error) { m := &Message{Id: p.id, Kind: "end"} if err != nil { m.Body = err.Error() } p.out <- m }
// Kill stops the process if it is running and waits for it to exit. func (p *Process) Kill() { if p == nil { return } p.run.Process.Kill() <-p.done }
switch m.Kind { case "run": proc[m.Id].Kill() lOut := limiter(in, out) proc[m.Id] = StartProcess(m.Id, m.Body, lOut) case "kill": proc[m.Id].Kill() }
// limiter returns a channel that wraps dest. Messages sent to the channel are // sent to dest. After msgLimit Messages have been passed on, a "kill" Message // is sent to the kill channel, and only "end" messages are passed. func limiter(kill chan<- *Message, dest chan<- *Message) chan<- *Message { ch := make(chan *Message) go func() { n := 0 for m := range ch { switch { case n < msgLimit || m.Kind == "end": dest <- m if m.Kind == "end" { return } case n == msgLimit: // Process produced too much output. Kill it. kill <- &Message{Id: m.Id, Kind: "kill"} } n++ } }() return ch }