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