blob: 2ef28c46d62c342110d877f8b3a2c9df7589d1dd [file] [log] [blame]
// Compiles and runs code for the Veyron playground.
// Code is passed via os.Stdin as a JSON encoded
// request struct.
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"go/parser"
"go/token"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"time"
"veyron/tools/playground/event"
)
const runTimeout = 3 * time.Second
var (
verbose = flag.Bool("v", false, "Verbose mode")
// Whether we have stopped execution of running files.
stopped = false
mu sync.Mutex
)
// Type of data sent to the builder on stdin. Input should contain Files. We
// look for a file whose Name ends with .id, and parse that into Identities.
//
// TODO(ribrdb): Consider moving identity parsing into the http server.
type request struct {
Files []*codeFile
Identities []identity
}
// Type of file data. Only Name and Body should be initially set. The other
// fields are added as the file is parsed.
type codeFile struct {
Name string
Body string
// Language the file is written in. Inferred from the file extension.
lang string
// Identity to associate with the file's process.
identity string
// The executable flag denotes whether the file should be executed as
// part of the playground run. This is currently used only for
// javascript files, and go files with package "main".
executable bool
// Package name of the file (for go and vdl files).
pkg string
// Running cmd process for the file.
cmd *exec.Cmd
// Any subprocesses that are needed to support running the file (e.g. wspr).
subProcs []*os.Process
// The index of the file in the request.
index int
}
type exit struct {
name string
err error
}
func debug(args ...interface{}) {
if *verbose {
log.Println(args...)
}
}
func parseRequest(in io.Reader) (r request, err error) {
debug("Parsing input")
data, err := ioutil.ReadAll(in)
if err == nil {
err = json.Unmarshal(data, &r)
}
m := make(map[string]*codeFile)
for i := 0; i < len(r.Files); {
f := r.Files[i]
f.index = i
if path.Ext(f.Name) == ".id" {
err = json.Unmarshal([]byte(f.Body), &r.Identities)
if err != nil {
return
}
r.Files = append(r.Files[:i], r.Files[i+1:]...)
} else {
switch path.Ext(f.Name) {
case ".js":
// Javascript files are always executable.
f.executable = true
f.lang = "js"
case ".go":
// Go files will be marked as executable if
// their package name is "main". This happens
// in the "readPackage" function.
f.lang = "go"
case ".vdl":
f.lang = "vdl"
default:
return r, fmt.Errorf("Unknown file type %s", f.Name)
}
m[f.Name] = f
i++
}
}
if len(r.Identities) == 0 {
// Run everything with the same identity if none are specified.
r.Identities = append(r.Identities, identity{Name: "default"})
for _, f := range r.Files {
f.identity = "default"
}
} else {
for _, identity := range r.Identities {
for _, name := range identity.Files {
// Check that the file associated with the
// identity exists. We ignore cases where it
// doesn't because the test .id files get used
// for multiple different code files. See
// testdata/ids/authorized.id, for example.
if m[name] != nil {
m[name].identity = identity.Name
}
}
}
}
return
}
func main() {
flag.Parse()
r, err := parseRequest(os.Stdin)
if err != nil {
log.Fatal(err)
}
err = createIdentities(r.Identities)
if err != nil {
log.Fatal(err)
}
mt, err := startMount(runTimeout)
if err != nil {
log.Fatal(err)
}
defer mt.Kill()
proxy, err := startProxy()
if err != nil {
log.Fatal(err)
}
defer proxy.Kill()
if err := writeFiles(r.Files); err != nil {
log.Fatal(err)
}
if err := compileFiles(r.Files); err != nil {
log.Fatal(err)
}
runFiles(r.Files)
}
func writeFiles(files []*codeFile) error {
debug("Writing files")
for _, f := range files {
if err := f.write(); err != nil {
return fmt.Errorf("Error writing %s: %v", f.Name, err)
}
}
return nil
}
func compileFiles(files []*codeFile) error {
debug("Compiling files")
var nonVdlFiles []*codeFile
// Compile the vdl files first, since Go files may depend on *.vdl.go
// generated files.
for _, f := range files {
if f.lang != "vdl" {
nonVdlFiles = append(nonVdlFiles, f)
} else {
if err := f.compile(); err != nil {
return fmt.Errorf("Error compiling %s: %v", f.Name, err)
}
}
}
// Compile the non-vdl files
for _, f := range nonVdlFiles {
if err := f.compile(); err != nil {
return fmt.Errorf("Error compiling %s: %v", f.Name, err)
}
}
return nil
}
func runFiles(files []*codeFile) {
debug("Running files")
exit := make(chan exit)
running := 0
for _, f := range files {
if f.executable {
f.run(exit)
running++
}
}
timeout := time.After(runTimeout)
for running > 0 {
select {
case <-timeout:
writeEvent("", "Playground exceeded deadline.", "stderr")
stopAll(files)
case status := <-exit:
if status.err == nil {
writeEvent(status.name, "Exited.", "stdout")
} else {
writeEvent(status.name, fmt.Sprintf("Error: %v", status.err), "stderr")
}
running--
stopAll(files)
}
}
}
func stopAll(files []*codeFile) {
mu.Lock()
defer mu.Unlock()
if !stopped {
stopped = true
for _, f := range files {
f.stop()
}
}
}
func (f *codeFile) readPackage() error {
debug("Parsing package from ", f.Name)
file, err := parser.ParseFile(token.NewFileSet(), f.Name,
strings.NewReader(f.Body), parser.PackageClauseOnly)
if err != nil {
return err
}
f.pkg = file.Name.String()
if "main" == f.pkg {
f.executable = true
basename := path.Base(f.Name)
f.pkg = basename[:len(basename)-len(path.Ext(basename))]
}
return nil
}
func (f *codeFile) write() error {
debug("Writing file ", f.Name)
if f.lang == "go" || f.lang == "vdl" {
if err := f.readPackage(); err != nil {
return err
}
}
if err := os.MkdirAll(filepath.Join("src", f.pkg), 0777); err != nil {
return err
}
return ioutil.WriteFile(filepath.Join("src", f.pkg, f.Name), []byte(f.Body), 0666)
}
func (f *codeFile) compile() error {
debug("Compiling file ", f.Name)
var cmd *exec.Cmd
switch f.lang {
case "js":
return nil
case "vdl":
cmd = makeCmdJsonEvent(f.Name, "vdl", "generate", "--lang=go", f.pkg)
case "go":
cmd = makeCmdJsonEvent(f.Name, filepath.Join(os.Getenv("VEYRON_ROOT"), "scripts", "build", "go"), "install", f.pkg)
default:
return fmt.Errorf("Can't compile file %s with language %s.", f.Name, f.lang)
}
cmd.Stdout = cmd.Stderr
err := cmd.Run()
return err
}
func (f *codeFile) startJs() error {
wsprProc, wsprPort, err := startWspr(f)
if err != nil {
return fmt.Errorf("Error starting wspr: %v", err)
}
f.subProcs = append(f.subProcs, wsprProc)
os.Setenv("WSPR", "http://localhost:"+strconv.Itoa(wsprPort))
node := filepath.Join(os.Getenv("VEYRON_ROOT"), "environment", "cout", "node", "bin", "node")
cmd := makeCmdJsonEvent(f.Name, node, filepath.Join("src", f.Name))
f.cmd = cmd
return cmd.Start()
}
func (f *codeFile) startGo() error {
cmd := makeCmdJsonEvent(f.Name, filepath.Join("bin", f.pkg))
if f.identity != "" {
cmd.Env = append(cmd.Env, fmt.Sprintf("VEYRON_IDENTITY=%s", filepath.Join("ids", f.identity)))
}
f.cmd = cmd
return cmd.Start()
}
func (f *codeFile) run(ch chan exit) {
debug("Running", f.Name)
err := func() error {
mu.Lock()
defer mu.Unlock()
if stopped {
return fmt.Errorf("Execution already stopped, not running file %s", f.Name)
}
switch f.lang {
case "go":
return f.startGo()
case "js":
return f.startJs()
default:
return fmt.Errorf("Cannot run file: %v", f.Name)
}
}()
if err != nil {
debug("Error starting", f.Name)
ch <- exit{f.Name, err}
return
}
// Wait for the process to exit and send result to channel.
go func() {
debug("Waiting for", f.Name)
err := f.cmd.Wait()
debug("Done waiting for", f.Name)
ch <- exit{f.Name, err}
}()
}
func (f *codeFile) stop() {
debug("Attempting to stop ", f.Name)
if f.cmd == nil {
debug("No cmd for", f.Name, "cannot stop.")
} else if f.cmd.Process == nil {
debug("f.cmd exists for", f.Name, "but f.cmd.Process is nil! Cannot stop!.")
} else {
debug("Sending SIGTERM to", f.Name)
f.cmd.Process.Signal(syscall.SIGTERM)
}
for _, subProc := range f.subProcs {
debug("Killing sub process for", f.Name)
subProc.Kill()
}
}
// Creates a cmd who's output (stdout and stderr) are streamed to stdout as
// json-encoded Event objects.
func makeCmdJsonEvent(fileName, prog string, args ...string) *exec.Cmd {
cmd := exec.Command(prog, args...)
cmd.Env = os.Environ()
// TODO(nlacasse): There is a bug in this code which results in
// "read |0: bad file descriptor".
// The error seems to be caused by our use of cmd.StdoutPipe/StderrPipe
// and cmd.Wait. In particular, we seem to be calling Wait after the
// pipes have been closed.
// See: http://stackoverflow.com/questions/20134095/why-do-i-get-bad-file-descriptor-in-this-go-program-using-stderr-and-ioutil-re
// and https://code.google.com/p/go/issues/detail?id=2266
//
// One solution is to wrap cmd.Start/Run, so that wait is never called
// before the pipes are closed.
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Fatal(err)
}
go streamEvents(fileName, "stdout", stdout)
stderr, err := cmd.StderrPipe()
if err != nil {
log.Fatal(err)
}
go streamEvents(fileName, "stderr", stderr)
return cmd
}
func streamEvents(fileName, stream string, in io.Reader) {
scanner := bufio.NewScanner(in)
for scanner.Scan() {
writeEvent(fileName, scanner.Text(), stream)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
}
func writeEvent(fileName, message, stream string) {
e := event.Event{
File: fileName,
Message: message,
Stream: stream,
Timestamp: time.Now().Unix(),
}
jsonEvent, err := json.Marshal(e)
if err != nil {
log.Fatal(err)
}
// TODO(nlacasse): when we switch to streaming, we'll probably need to
// trigger a flush here.
os.Stdout.Write(append(jsonEvent, '\n'))
}