Inside the "present" tool

Andrew Gerrand

Google

The Playground API

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"}

Playground drawbacks

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.

Enter WebSockets

WebSockets 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.

Hello, WebSocket

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?")
}

Messages

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\"); }"}

On the wire

package main

import ( "fmt"; "time" )

func main() {
    for {
        fmt.Println("Hello, Gophers!")
        time.Sleep(time.Second)
    }
}

Implementation

socketHandler (1/3)

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)

socketHandler (2/3)

    // 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
            }
        }
    }()

socketHandler (3/3)

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

// Process represents a running process.
type Process struct {
    id   string
    out  chan<- *Message
    done chan struct{} // closed when wait completes
    run  *exec.Cmd
}

StartProcess

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

Process.start (1/2)

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

Process.start (2/2)

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

Process.cmd

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

Process.wait and Process.end

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

Process.Kill

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

One more thing

Limiting output (1/2)

            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()
            }

Limiting output (2/2)

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

Thank you

Use the left and right arrow keys or click the left and right edges of the page to navigate between slides.
(Press 'H' or navigate to hide this message.)