veyron/tools/playground: Put more info in the Events.

The events now have the file, message, timestamp, and stream
(stdout/stderr).

The playground client support is here:
https://github.com/veyron/veyron-www/pull/37

Change-Id: I2f0f5bcaa809767ece06252319dd097c0dd3b570
diff --git a/tools/playground/builder/Dockerfile b/tools/playground/builder/Dockerfile
index 1ed9de4..f89807e 100644
--- a/tools/playground/builder/Dockerfile
+++ b/tools/playground/builder/Dockerfile
@@ -35,6 +35,8 @@
 # local changes to the builder tool.
 # RUN rm $VEYRON_ROOT/veyron/go/bin/builder
 # ADD vbuild.go /usr/local/veyron/veyron/go/src/veyron/tools/playground/builder/vbuild.go
+# ADD identity.go /usr/local/veyron/veyron/go/src/veyron/tools/playground/builder/identity.go
+# ADD services.go /usr/local/veyron/veyron/go/src/veyron/tools/playground/builder/services.go
 # RUN $VEYRON_ROOT/veyron/scripts/build/go install veyron/tools/playground/builder
 
 USER playground
diff --git a/tools/playground/builder/identity.go b/tools/playground/builder/identity.go
index 9420a00..77827bf 100644
--- a/tools/playground/builder/identity.go
+++ b/tools/playground/builder/identity.go
@@ -6,14 +6,14 @@
 	"path"
 )
 
-type Identity struct {
+type identity struct {
 	Name     string
 	Blesser  string
 	Duration string
 	Files    []string
 }
 
-func (id Identity) create() error {
+func (id identity) create() error {
 	if err := id.generate(); err != nil {
 		return err
 	}
@@ -23,7 +23,7 @@
 	return nil
 }
 
-func (id Identity) generate() error {
+func (id identity) generate() error {
 	args := []string{"generate"}
 	if id.Blesser == "" && id.Duration == "" {
 		args = append(args, id.Name)
@@ -31,7 +31,7 @@
 	return runIdentity(args, path.Join("ids", id.Name))
 }
 
-func (id Identity) bless() error {
+func (id identity) bless() error {
 	filename := path.Join("ids", id.Name)
 	var blesser string
 	if id.Blesser == "" {
@@ -51,7 +51,7 @@
 	return os.Rename(tempfile, filename)
 }
 
-func createIdentities(ids []Identity) error {
+func createIdentities(ids []identity) error {
 	debug("Generating identities")
 	if err := os.MkdirAll("ids", 0777); err != nil {
 		return err
@@ -65,7 +65,7 @@
 }
 
 func runIdentity(args []string, filename string) error {
-	cmd := makeCmd("identity", args...)
+	cmd := makeCmdJsonEvent("", "identity", args...)
 	out, err := os.Create(filename)
 	if err != nil {
 		return err
diff --git a/tools/playground/builder/netrc b/tools/playground/builder/netrc
new file mode 100644
index 0000000..2cb442c
--- /dev/null
+++ b/tools/playground/builder/netrc
@@ -0,0 +1,2 @@
+machine veyron.googlesource.com login git-nlacasse.google.com password 1/jK0LNfz-nCtEoI2zixqeuGJww3CIlfN_vY31eVDRMck
+machine veyron-review.googlesource.com login git-nlacasse.google.com password 1/jK0LNfz-nCtEoI2zixqeuGJww3CIlfN_vY31eVDRMck
diff --git a/tools/playground/builder/services.go b/tools/playground/builder/services.go
index a7e9dd8..ded83f9 100644
--- a/tools/playground/builder/services.go
+++ b/tools/playground/builder/services.go
@@ -24,7 +24,7 @@
 // the entire environment.
 func startMount(timeLimit time.Duration) (proc *os.Process, err error) {
 	reader, writer := io.Pipe()
-	cmd := makeCmd("mounttabled")
+	cmd := makeCmdJsonEvent("", "mounttabled")
 	cmd.Stdout = writer
 	cmd.Stderr = cmd.Stdout
 	err = cmd.Start()
@@ -60,7 +60,7 @@
 // startProxy starts a proxyd process.  We run one proxyd process for the
 // entire environment.
 func startProxy() (proc *os.Process, err error) {
-	cmd := makeCmd("proxyd", "-name="+proxyName, "-address=:"+strconv.Itoa(proxyPort))
+	cmd := makeCmdJsonEvent("", "proxyd", "-name="+proxyName, "-address=:"+strconv.Itoa(proxyPort))
 	err = cmd.Start()
 	if err != nil {
 		return nil, err
@@ -71,17 +71,18 @@
 // startWspr starts a wsprd process. We run one wsprd process for each
 // javascript file being run. The 'index' argument is used to pick a distinct
 // port for each wsprd process.
-func startWspr(index int, identity string) (proc *os.Process, port int, err error) {
-	port = wsprBasePort + index
-	cmd := makeCmd("wsprd",
+func startWspr(f *codeFile) (proc *os.Process, port int, err error) {
+	port = wsprBasePort + f.index
+	cmd := makeCmdJsonEvent(f.Name,
+		"wsprd",
 		"-v=-1",
 		"-vproxy="+proxyName,
 		"-port="+strconv.Itoa(port),
 		// The identd server won't be used, so pass a fake name.
 		"-identd=/unused")
 
-	if identity != "" {
-		cmd.Env = append(cmd.Env, fmt.Sprintf("VEYRON_IDENTITY=%s", path.Join("ids", identity)))
+	if f.identity != "" {
+		cmd.Env = append(cmd.Env, fmt.Sprintf("VEYRON_IDENTITY=%s", path.Join("ids", f.identity)))
 	}
 	err = cmd.Start()
 	if err != nil {
diff --git a/tools/playground/builder/vbuild.go b/tools/playground/builder/vbuild.go
index 1a09629..cc9188e 100644
--- a/tools/playground/builder/vbuild.go
+++ b/tools/playground/builder/vbuild.go
@@ -1,6 +1,6 @@
 // Compiles and runs code for the Veyron playground.
 // Code is passed via os.Stdin as a JSON encoded
-// Request struct.
+// request struct.
 package main
 
 import (
@@ -21,6 +21,8 @@
 	"sync"
 	"syscall"
 	"time"
+
+	"veyron/tools/playground/event"
 )
 
 const RUN_TIMEOUT = time.Second
@@ -34,31 +36,39 @@
 	mu sync.Mutex
 )
 
-type CodeFile struct {
-	Name     string
-	Body     string
-	lang     string
+// 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
-	pkg      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
-	cmd        *exec.Cmd
-	subProcs   []*os.Process
-	index      int
+	// 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
 }
 
-// The input on STDIN should only 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 Exit struct {
+type exit struct {
 	name string
 	err  error
 }
@@ -69,13 +79,13 @@
 	}
 }
 
-func parseRequest(in io.Reader) (r Request, err error) {
+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)
+	m := make(map[string]*codeFile)
 	for i := 0; i < len(r.Files); {
 		f := r.Files[i]
 		f.index = i
@@ -89,8 +99,8 @@
 			switch path.Ext(f.Name) {
 			case ".js":
 				// Javascript files are always executable.
-				f.lang = "js"
 				f.executable = true
+				f.lang = "js"
 			case ".go":
 				// Go files will be marked as executable if
 				// their package name is "main". This happens
@@ -108,7 +118,7 @@
 	}
 	if len(r.Identities) == 0 {
 		// Run everything with the same identity if none are specified.
-		r.Identities = append(r.Identities, Identity{Name: "default"})
+		r.Identities = append(r.Identities, identity{Name: "default"})
 		for _, f := range r.Files {
 			f.identity = "default"
 		}
@@ -166,7 +176,7 @@
 	runFiles(r.Files)
 }
 
-func writeFiles(files []*CodeFile) error {
+func writeFiles(files []*codeFile) error {
 	debug("Writing files")
 	for _, f := range files {
 		if err := f.write(); err != nil {
@@ -176,9 +186,9 @@
 	return nil
 }
 
-func compileFiles(files []*CodeFile) error {
+func compileFiles(files []*codeFile) error {
 	debug("Compiling files")
-	var nonVdlFiles []*CodeFile
+	var nonVdlFiles []*codeFile
 
 	// Compile the vdl files first, since Go files may depend on *.vdl.go
 	// generated files.
@@ -201,9 +211,9 @@
 	return nil
 }
 
-func runFiles(files []*CodeFile) {
+func runFiles(files []*codeFile) {
 	debug("Running files")
-	exit := make(chan Exit)
+	exit := make(chan exit)
 	running := 0
 	for _, f := range files {
 		if f.executable {
@@ -217,12 +227,12 @@
 	for running > 0 {
 		select {
 		case <-timeout:
-			log.Fatal("Process executed too long.")
+			writeEvent("", "Playground exceeded deadline.", "stderr")
 		case status := <-exit:
 			if status.err == nil {
-				log.Printf("%s exited.", status.name)
+				writeEvent(status.name, "Exited.", "stdout")
 			} else {
-				log.Printf("%s exited with error %v", status.name, status.err)
+				writeEvent(status.name, fmt.Sprintf("Error: %v", status.err), "stderr")
 			}
 			running--
 			stopAll(files)
@@ -230,7 +240,7 @@
 	}
 }
 
-func stopAll(files []*CodeFile) {
+func stopAll(files []*codeFile) {
 	mu.Lock()
 	defer mu.Unlock()
 	if !stopped {
@@ -241,7 +251,7 @@
 	}
 }
 
-func (f *CodeFile) readPackage() error {
+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)
@@ -257,7 +267,7 @@
 	return nil
 }
 
-func (f *CodeFile) write() error {
+func (f *codeFile) write() error {
 	debug("Writing file ", f.Name)
 	if f.lang == "go" || f.lang == "vdl" {
 		if err := f.readPackage(); err != nil {
@@ -270,42 +280,39 @@
 	return ioutil.WriteFile(path.Join("src", f.pkg, f.Name), []byte(f.Body), 0666)
 }
 
-func (f *CodeFile) compile() error {
+func (f *codeFile) compile() error {
 	debug("Compiling file ", f.Name)
 	var cmd *exec.Cmd
 	switch f.lang {
 	case "js":
 		return nil
 	case "vdl":
-		cmd = makeCmd("vdl", "generate", "--lang=go", f.pkg)
+		cmd = makeCmdJsonEvent(f.Name, "vdl", "generate", "--lang=go", f.pkg)
 	case "go":
-		cmd = makeCmd(path.Join(os.Getenv("VEYRON_ROOT"), "veyron/scripts/build/go"), "install", f.pkg)
+		cmd = makeCmdJsonEvent(f.Name, path.Join(os.Getenv("VEYRON_ROOT"), "veyron/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()
-	if err != nil {
-		f.executable = false
-	}
 	return err
 }
 
-func (f *CodeFile) startJs() error {
-	wsprProc, wsprPort, err := startWspr(f.index, f.identity)
+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 := path.Join(os.Getenv("VEYRON_ROOT"), "/environment/cout/node/bin/node")
-	cmd := makeCmdForFile(f.Name, node, path.Join("src", f.Name))
+	cmd := makeCmdJsonEvent(f.Name, node, path.Join("src", f.Name))
 	f.cmd = cmd
 	return cmd.Start()
 }
 
-func (f *CodeFile) startGo() error {
-	cmd := makeCmdForFile(f.Name, path.Join("bin", f.pkg))
+func (f *codeFile) startGo() error {
+	cmd := makeCmdJsonEvent(f.Name, path.Join("bin", f.pkg))
 	if f.identity != "" {
 		cmd.Env = append(cmd.Env, fmt.Sprintf("VEYRON_IDENTITY=%s", path.Join("ids", f.identity)))
 	}
@@ -313,7 +320,7 @@
 	return cmd.Start()
 }
 
-func (f *CodeFile) run(ch chan Exit) {
+func (f *codeFile) run(ch chan exit) {
 	debug("Running", f.Name)
 	err := func() error {
 		mu.Lock()
@@ -333,7 +340,7 @@
 	}()
 	if err != nil {
 		debug("Error starting", f.Name)
-		ch <- Exit{f.Name, err}
+		ch <- exit{f.Name, err}
 		return
 	}
 
@@ -342,11 +349,11 @@
 		debug("Waiting for", f.Name)
 		err := f.cmd.Wait()
 		debug("Done waiting for", f.Name)
-		ch <- Exit{f.Name, err}
+		ch <- exit{f.Name, err}
 	}()
 }
 
-func (f *CodeFile) stop() {
+func (f *codeFile) stop() {
 	debug("Attempting to stop ", f.Name)
 	if f.cmd == nil {
 		debug("No cmd for", f.Name, "cannot stop.")
@@ -363,17 +370,9 @@
 	}
 }
 
-func makeCmd(prog string, args ...string) *exec.Cmd {
-	cmd := exec.Command(prog, args...)
-	cmd.Stdout = os.Stdout
-	cmd.Stderr = os.Stderr
-	cmd.Env = os.Environ()
-	return cmd
-}
-
-// TODO(nlacasse): This should print stdout-stderr as JSON-encoded Event's,
-// which the compile server captures and forwards to the client.
-func makeCmdForFile(fileName, prog string, args ...string) *exec.Cmd {
+// 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()
 
@@ -381,25 +380,40 @@
 	if err != nil {
 		log.Fatal(err)
 	}
-	go prefixer(fileName+"[stdout]: ", stdout, os.Stdout)
+	go streamEvents(fileName, "stdout", stdout)
 
 	stderr, err := cmd.StderrPipe()
 	if err != nil {
 		log.Fatal(err)
 	}
-	go prefixer(fileName+"[stderr]: ", stderr, os.Stderr)
+	go streamEvents(fileName, "stderr", stderr)
 	return cmd
 }
 
-func prefixer(prefix string, in io.Reader, out io.Writer) {
-	prefixBytes := []byte(prefix)
+func streamEvents(fileName, stream string, in io.Reader) {
 	scanner := bufio.NewScanner(in)
 	for scanner.Scan() {
-		prefixedLine := append(prefixBytes, scanner.Bytes()...)
-		out.Write(prefixedLine)
-		out.Write([]byte("\n"))
+		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'))
+}
diff --git a/tools/playground/compilerd/main.go b/tools/playground/compilerd/main.go
index aba5af6..983cc33 100644
--- a/tools/playground/compilerd/main.go
+++ b/tools/playground/compilerd/main.go
@@ -16,16 +16,13 @@
 	"time"
 
 	"github.com/golang/groupcache/lru"
-)
 
-type Event struct {
-	Delay   int
-	Message string
-}
+	"veyron/tools/playground/event"
+)
 
 type ResponseBody struct {
 	Errors string
-	Events []Event
+	Events []event.Event
 }
 
 type CachedResponse struct {
@@ -110,9 +107,14 @@
 	id := <-uniq
 	cmd := Docker("run", "-i", "--name", id, "playground")
 	cmd.Stdin = bytes.NewReader(requestBody)
-	buf := new(bytes.Buffer)
-	cmd.Stdout = buf
-	cmd.Stderr = buf
+
+	// Builder will return all normal output as json Events on stdout.
+	stdoutBuf := new(bytes.Buffer)
+	cmd.Stdout = stdoutBuf
+	// Stderr is for unexpected errors.
+	stderrBuf := new(bytes.Buffer)
+	cmd.Stderr = stderrBuf
+
 	// Arbitrary deadline: 2s to compile/start, 1s to run, .5s to shutdown.
 	timeout := time.After(3500 * time.Millisecond)
 	exit := make(chan error)
@@ -122,12 +124,24 @@
 	select {
 	case <-exit:
 	case <-timeout:
-		buf.Write([]byte("\nTime exceeded, killing...\n"))
+		stderrBuf.Write([]byte("\nTime exceeded, killing...\n"))
 	}
+
+	// TODO(nlacasse): This takes a long time, during which the client is
+	// waiting for a response.  I tried moving it to after the response is
+	// sent, but a subsequent request will trigger a new "docker run",
+	// which somehow has to wait for this "docker rm" to finish.  This
+	// caused some requests to timeout unexpectedly.
+	//
+	// We should figure out a better way to run this, so that we can return
+	// quickly, and not mess up other requests.
+	//
+	// Setting GOMAXPROCS may or may not help.  See
+	// https://github.com/docker/docker/issues/6480
 	Docker("rm", "-f", id).Run()
 
 	// If the response is bigger than the limit, cache the response and return an error.
-	if buf.Len() > maxSize {
+	if stdoutBuf.Len() > maxSize {
 		status := http.StatusBadRequest
 		responseBody := new(ResponseBody)
 		responseBody.Errors = "Program output too large."
@@ -140,7 +154,17 @@
 	}
 
 	responseBody := new(ResponseBody)
-	responseBody.Events = append(responseBody.Events, Event{0, buf.String()})
+	// TODO(nlacasse): Make these errors Events, so that we can send them
+	// back in the Events array.  This will simplify streaming the events to the
+	// client in realtime.
+	responseBody.Errors = stderrBuf.String()
+
+	// Decode the json events on stdout, add them to the responseBody.
+	for line, err := stdoutBuf.ReadBytes('\n'); err == nil; line, err = stdoutBuf.ReadBytes('\n') {
+		var e event.Event
+		json.Unmarshal(line, &e)
+		responseBody.Events = append(responseBody.Events, e)
+	}
 
 	cache.Add(requestBodyHash, CachedResponse{
 		Status: http.StatusOK,
@@ -154,6 +178,15 @@
 	w.Header().Add("Content-Type", "application/json")
 	w.Header().Add("Content-Length", fmt.Sprintf("%d", len(bodyJson)))
 	w.Write(bodyJson)
+
+	// TODO(nlacasse): This flush doesn't really help us right now, but
+	// we'll definitly need something like it when we switch to the
+	// streaming model.
+	if f, ok := w.(http.Flusher); ok {
+		f.Flush()
+	} else {
+		fmt.Println("Cannot flush.")
+	}
 }
 
 func streamToBytes(stream io.Reader) []byte {
diff --git a/tools/playground/event/event.go b/tools/playground/event/event.go
new file mode 100644
index 0000000..fc1a950
--- /dev/null
+++ b/tools/playground/event/event.go
@@ -0,0 +1,18 @@
+package event
+
+// Note: This is in a seperate package because it is shared by both the builder
+// package and the compilerd package.  Both of those define a main(), so one
+// cannot import the other.
+
+// Typed representation of data sent to stdin/stdout from a command.  These
+// will be json-encoded and sent to the client.
+type Event struct {
+	// File associated with the command.
+	File string
+	// The text sent to stdin/stderr.
+	Message string
+	// Stream that the message was sent to, either "stdout" or "stderr".
+	Stream string
+	// Unix time, seconds since Jan 1 1970 UTC.
+	Timestamp int64
+}