471 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			471 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
// Copyright 2012 The Go Authors.  All rights reserved.
 | 
						|
// Use of this source code is governed by a BSD-style
 | 
						|
// license that can be found in the LICENSE file.
 | 
						|
 | 
						|
// +build !appengine
 | 
						|
 | 
						|
// Package socket implements an WebSocket-based playground backend.
 | 
						|
// Clients connect to a websocket handler and send run/kill commands, and
 | 
						|
// the server sends the output and exit status of the running processes.
 | 
						|
// Multiple clients running multiple processes may be served concurrently.
 | 
						|
// The wire format is JSON and is described by the Message type.
 | 
						|
//
 | 
						|
// This will not run on App Engine as WebSockets are not supported there.
 | 
						|
package socket
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"encoding/json"
 | 
						|
	"errors"
 | 
						|
	"go/parser"
 | 
						|
	"go/token"
 | 
						|
	"io"
 | 
						|
	"io/ioutil"
 | 
						|
	"log"
 | 
						|
	"net"
 | 
						|
	"net/http"
 | 
						|
	"net/url"
 | 
						|
	"os"
 | 
						|
	"os/exec"
 | 
						|
	"path/filepath"
 | 
						|
	"runtime"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
	"sync"
 | 
						|
	"time"
 | 
						|
	"unicode/utf8"
 | 
						|
 | 
						|
	"code.google.com/p/go.net/websocket"
 | 
						|
)
 | 
						|
 | 
						|
// RunScripts specifies whether the socket handler should execute shell scripts
 | 
						|
// (snippets that start with a shebang).
 | 
						|
var RunScripts = true
 | 
						|
 | 
						|
// Environ provides an environment when a binary, such as the go tool, is
 | 
						|
// invoked.
 | 
						|
var Environ func() []string = os.Environ
 | 
						|
 | 
						|
const (
 | 
						|
	// The maximum number of messages to send per session (avoid flooding).
 | 
						|
	msgLimit = 1000
 | 
						|
 | 
						|
	// Batch messages sent in this interval and send as a single message.
 | 
						|
	msgDelay = 10 * time.Millisecond
 | 
						|
)
 | 
						|
 | 
						|
// 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
 | 
						|
	Options *Options `json:",omitempty"`
 | 
						|
}
 | 
						|
 | 
						|
// Options specify additional message options.
 | 
						|
type Options struct {
 | 
						|
	Race bool // use -race flag when building code (for "run" only)
 | 
						|
}
 | 
						|
 | 
						|
// NewHandler returns a websocket server which checks the origin of requests.
 | 
						|
func NewHandler(origin *url.URL) websocket.Server {
 | 
						|
	return websocket.Server{
 | 
						|
		Config:    websocket.Config{Origin: origin},
 | 
						|
		Handshake: handshake,
 | 
						|
		Handler:   websocket.Handler(socketHandler),
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// handshake checks the origin of a request during the websocket handshake.
 | 
						|
func handshake(c *websocket.Config, req *http.Request) error {
 | 
						|
	o, err := websocket.Origin(c, req)
 | 
						|
	if err != nil {
 | 
						|
		log.Println("bad websocket origin:", err)
 | 
						|
		return websocket.ErrBadWebSocketOrigin
 | 
						|
	}
 | 
						|
	_, port, err := net.SplitHostPort(c.Origin.Host)
 | 
						|
	if err != nil {
 | 
						|
		log.Println("bad websocket origin:", err)
 | 
						|
		return websocket.ErrBadWebSocketOrigin
 | 
						|
	}
 | 
						|
	ok := c.Origin.Scheme == o.Scheme && (c.Origin.Host == o.Host || c.Origin.Host == net.JoinHostPort(o.Host, port))
 | 
						|
	if !ok {
 | 
						|
		log.Println("bad websocket origin:", o)
 | 
						|
		return websocket.ErrBadWebSocketOrigin
 | 
						|
	}
 | 
						|
	log.Println("accepting connection from:", req.RemoteAddr)
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// socketHandler handles the websocket connection for a given present session.
 | 
						|
// It handles transcoding Messages to and from JSON format, and starting
 | 
						|
// and killing processes.
 | 
						|
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":
 | 
						|
				log.Println("running snippet from:", c.Request().RemoteAddr)
 | 
						|
				proc[m.Id].Kill()
 | 
						|
				lOut := limiter(in, out)
 | 
						|
				proc[m.Id] = startProcess(m.Id, m.Body, lOut, m.Options)
 | 
						|
			case "kill":
 | 
						|
				proc[m.Id].Kill()
 | 
						|
			}
 | 
						|
		case err := <-errc:
 | 
						|
			if err != io.EOF {
 | 
						|
				// 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
 | 
						|
	bin  string
 | 
						|
}
 | 
						|
 | 
						|
// 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, opt *Options) *process {
 | 
						|
	p := &process{
 | 
						|
		id:   id,
 | 
						|
		out:  out,
 | 
						|
		done: make(chan struct{}),
 | 
						|
	}
 | 
						|
	var err error
 | 
						|
	if path, args := shebang(body); RunScripts && path != "" {
 | 
						|
		err = p.startProcess(path, args, body)
 | 
						|
	} else {
 | 
						|
		err = p.start(body, opt)
 | 
						|
	}
 | 
						|
	if err != nil {
 | 
						|
		p.end(err)
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	go p.wait()
 | 
						|
	return p
 | 
						|
}
 | 
						|
 | 
						|
// 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 // block until process exits
 | 
						|
}
 | 
						|
 | 
						|
// shebang looks for a shebang ('#!') at the beginning of the passed string.
 | 
						|
// If found, it returns the path and args after the shebang.
 | 
						|
// args includes the command as args[0].
 | 
						|
func shebang(body string) (path string, args []string) {
 | 
						|
	body = strings.TrimSpace(body)
 | 
						|
	if !strings.HasPrefix(body, "#!") {
 | 
						|
		return "", nil
 | 
						|
	}
 | 
						|
	if i := strings.Index(body, "\n"); i >= 0 {
 | 
						|
		body = body[:i]
 | 
						|
	}
 | 
						|
	fs := strings.Fields(body[2:])
 | 
						|
	return fs[0], fs
 | 
						|
}
 | 
						|
 | 
						|
// startProcess starts a given program given its path and passing the given body
 | 
						|
// to the command standard input.
 | 
						|
func (p *process) startProcess(path string, args []string, body string) error {
 | 
						|
	cmd := &exec.Cmd{
 | 
						|
		Path:   path,
 | 
						|
		Args:   args,
 | 
						|
		Stdin:  strings.NewReader(body),
 | 
						|
		Stdout: &messageWriter{id: p.id, kind: "stdout", out: p.out},
 | 
						|
		Stderr: &messageWriter{id: p.id, kind: "stderr", out: p.out},
 | 
						|
	}
 | 
						|
	if err := cmd.Start(); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	p.run = cmd
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// start builds and starts the given program, sending its output to p.out,
 | 
						|
// and stores the running *exec.Cmd in the run field.
 | 
						|
func (p *process) start(body string, opt *Options) error {
 | 
						|
	// We "go build" and then exec the binary so that the
 | 
						|
	// resultant *exec.Cmd is a handle to the user's program
 | 
						|
	// (rather than the go tool process).
 | 
						|
	// This makes Kill work.
 | 
						|
 | 
						|
	bin := filepath.Join(tmpdir, "compile"+strconv.Itoa(<-uniq))
 | 
						|
	src := bin + ".go"
 | 
						|
	if runtime.GOOS == "windows" {
 | 
						|
		bin += ".exe"
 | 
						|
	}
 | 
						|
 | 
						|
	// write body to x.go
 | 
						|
	defer os.Remove(src)
 | 
						|
	err := ioutil.WriteFile(src, []byte(body), 0666)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// build x.go, creating x
 | 
						|
	p.bin = bin // to be removed by p.end
 | 
						|
	dir, file := filepath.Split(src)
 | 
						|
	args := []string{"go", "build", "-tags", "OMIT"}
 | 
						|
	if opt != nil && opt.Race {
 | 
						|
		p.out <- &Message{
 | 
						|
			Id: p.id, Kind: "stderr",
 | 
						|
			Body: "Running with race detector.\n",
 | 
						|
		}
 | 
						|
		args = append(args, "-race")
 | 
						|
	}
 | 
						|
	args = append(args, "-o", bin, file)
 | 
						|
	cmd := p.cmd(dir, args...)
 | 
						|
	cmd.Stdout = cmd.Stderr // send compiler output to stderr
 | 
						|
	if err := cmd.Run(); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// run x
 | 
						|
	if isNacl() {
 | 
						|
		cmd, err = p.naclCmd(bin)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		cmd = p.cmd("", bin)
 | 
						|
	}
 | 
						|
	if opt != nil && opt.Race {
 | 
						|
		cmd.Env = append(cmd.Env, "GOMAXPROCS=2")
 | 
						|
	}
 | 
						|
	if err := cmd.Start(); err != nil {
 | 
						|
		// If we failed to exec, that might be because they built
 | 
						|
		// a non-main package instead of an executable.
 | 
						|
		// Check and report that.
 | 
						|
		if name, err := packageName(body); err == nil && name != "main" {
 | 
						|
			return errors.New(`executable programs must use "package main"`)
 | 
						|
		}
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	p.run = cmd
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// wait waits for the running process to complete
 | 
						|
// and sends its error state to the client.
 | 
						|
func (p *process) wait() {
 | 
						|
	p.end(p.run.Wait())
 | 
						|
	close(p.done) // unblock waiting Kill calls
 | 
						|
}
 | 
						|
 | 
						|
// end sends an "end" message to the client, containing the process id and the
 | 
						|
// given error value. It also removes the binary.
 | 
						|
func (p *process) end(err error) {
 | 
						|
	if p.bin != "" {
 | 
						|
		defer os.Remove(p.bin)
 | 
						|
	}
 | 
						|
	m := &Message{Id: p.id, Kind: "end"}
 | 
						|
	if err != nil {
 | 
						|
		m.Body = err.Error()
 | 
						|
	}
 | 
						|
	// Wait for any outstanding reads to finish (potential race here).
 | 
						|
	time.AfterFunc(msgDelay, func() { p.out <- m })
 | 
						|
}
 | 
						|
 | 
						|
// 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.Env = Environ()
 | 
						|
	cmd.Stdout = &messageWriter{id: p.id, kind: "stdout", out: p.out}
 | 
						|
	cmd.Stderr = &messageWriter{id: p.id, kind: "stderr", out: p.out}
 | 
						|
	return cmd
 | 
						|
}
 | 
						|
 | 
						|
func isNacl() bool {
 | 
						|
	for _, v := range append(Environ(), os.Environ()...) {
 | 
						|
		if v == "GOOS=nacl" {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
// naclCmd returns an *exec.Cmd that executes bin under native client.
 | 
						|
func (p *process) naclCmd(bin string) (*exec.Cmd, error) {
 | 
						|
	pwd, err := os.Getwd()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	var args []string
 | 
						|
	env := []string{
 | 
						|
		"NACLENV_GOOS=" + runtime.GOOS,
 | 
						|
		"NACLENV_GOROOT=/go",
 | 
						|
		"NACLENV_NACLPWD=" + strings.Replace(pwd, runtime.GOROOT(), "/go", 1),
 | 
						|
	}
 | 
						|
	switch runtime.GOARCH {
 | 
						|
	case "amd64":
 | 
						|
		env = append(env, "NACLENV_GOARCH=amd64p32")
 | 
						|
		args = []string{"sel_ldr_x86_64"}
 | 
						|
	case "386":
 | 
						|
		env = append(env, "NACLENV_GOARCH=386")
 | 
						|
		args = []string{"sel_ldr_x86_32"}
 | 
						|
	case "arm":
 | 
						|
		env = append(env, "NACLENV_GOARCH=arm")
 | 
						|
		selLdr, err := exec.LookPath("sel_ldr_arm")
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		args = []string{"nacl_helper_bootstrap_arm", selLdr, "--reserved_at_zero=0xXXXXXXXXXXXXXXXX"}
 | 
						|
	default:
 | 
						|
		return nil, errors.New("native client does not support GOARCH=" + runtime.GOARCH)
 | 
						|
	}
 | 
						|
 | 
						|
	cmd := p.cmd("", append(args, "-l", "/dev/null", "-S", "-e", bin)...)
 | 
						|
	cmd.Env = append(cmd.Env, env...)
 | 
						|
 | 
						|
	return cmd, nil
 | 
						|
}
 | 
						|
 | 
						|
func packageName(body string) (string, error) {
 | 
						|
	f, err := parser.ParseFile(token.NewFileSet(), "prog.go",
 | 
						|
		strings.NewReader(body), parser.PackageClauseOnly)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
	return f.Name.String(), nil
 | 
						|
}
 | 
						|
 | 
						|
// 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
 | 
						|
 | 
						|
	mu   sync.Mutex
 | 
						|
	buf  []byte
 | 
						|
	send *time.Timer
 | 
						|
}
 | 
						|
 | 
						|
func (w *messageWriter) Write(b []byte) (n int, err error) {
 | 
						|
	// Buffer writes that occur in a short period to send as one Message.
 | 
						|
	w.mu.Lock()
 | 
						|
	w.buf = append(w.buf, b...)
 | 
						|
	if w.send == nil {
 | 
						|
		w.send = time.AfterFunc(msgDelay, w.sendNow)
 | 
						|
	}
 | 
						|
	w.mu.Unlock()
 | 
						|
	return len(b), nil
 | 
						|
}
 | 
						|
 | 
						|
func (w *messageWriter) sendNow() {
 | 
						|
	w.mu.Lock()
 | 
						|
	body := safeString(w.buf)
 | 
						|
	w.buf, w.send = nil, nil
 | 
						|
	w.mu.Unlock()
 | 
						|
	w.out <- &Message{Id: w.id, Kind: w.kind, Body: body}
 | 
						|
}
 | 
						|
 | 
						|
// safeString returns b as a valid UTF-8 string.
 | 
						|
func safeString(b []byte) string {
 | 
						|
	if utf8.Valid(b) {
 | 
						|
		return string(b)
 | 
						|
	}
 | 
						|
	var buf bytes.Buffer
 | 
						|
	for len(b) > 0 {
 | 
						|
		r, size := utf8.DecodeRune(b)
 | 
						|
		b = b[size:]
 | 
						|
		buf.WriteRune(r)
 | 
						|
	}
 | 
						|
	return buf.String()
 | 
						|
}
 | 
						|
 | 
						|
// 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
 | 
						|
}
 | 
						|
 | 
						|
var tmpdir string
 | 
						|
 | 
						|
func init() {
 | 
						|
	// find real path to temporary directory
 | 
						|
	var err error
 | 
						|
	tmpdir, err = filepath.EvalSymlinks(os.TempDir())
 | 
						|
	if err != nil {
 | 
						|
		log.Fatal(err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
var uniq = make(chan int) // a source of numbers for naming temporary files
 | 
						|
 | 
						|
func init() {
 | 
						|
	go func() {
 | 
						|
		for i := 0; ; i++ {
 | 
						|
			uniq <- i
 | 
						|
		}
 | 
						|
	}()
 | 
						|
}
 |