exp/upbox: replace Run method with Start and Stop

Also export a Config method to get the path to a user's config file.

Change-Id: Ibaf10cfaaeaab7f6e59c2f3748936393a99a1e30
Reviewed-on: https://upspin-review.googlesource.com/10069
Reviewed-by: Rob Pike <r@golang.org>
diff --git a/cmd/upbox/main.go b/cmd/upbox/main.go
index 59c3313..a8783d9 100644
--- a/cmd/upbox/main.go
+++ b/cmd/upbox/main.go
@@ -15,6 +15,8 @@
 	"flag"
 	"fmt"
 	"os"
+	"os/exec"
+	"strings"
 
 	"upspin.io/exp/upbox"
 )
@@ -30,13 +32,36 @@
 
 	sc, err := upbox.SchemaFromFile(*schema, *basePort)
 	if err != nil {
-		fmt.Fprintln(os.Stderr, "upbox: error parsing schema:", err)
-		os.Exit(1)
+		fail(fmt.Errorf("parsing schema: %v", err))
 	}
 	sc.LogLevel = *logLevel
 
-	if err := sc.Run(); err != nil {
-		fmt.Fprintln(os.Stderr, "upbox:", err)
-		os.Exit(1)
+	if err := sc.Start(); err != nil {
+		fail(err)
 	}
+
+	// Start a shell as the first user.
+	args := []string{
+		"-config=" + sc.Config(sc.Users[0].Name),
+		"-log=" + *logLevel,
+		"shell",
+	}
+	fmt.Fprintf(os.Stderr, "upbox: upspin %s\n", strings.Join(args, " "))
+	shell := exec.Command("upspin", args...)
+	shell.Stdin = os.Stdin
+	shell.Stdout = os.Stdout
+	shell.Stderr = os.Stderr
+	err = shell.Run()
+	err2 := sc.Stop()
+	if err != nil {
+		fail(err)
+	}
+	if err2 != nil {
+		fail(err2)
+	}
+}
+
+func fail(err error) {
+	fmt.Fprintln(os.Stderr, "upbox:", err)
+	os.Exit(1)
 }
diff --git a/upbox/schema.go b/upbox/schema.go
index e482a0b..fd22581 100644
--- a/upbox/schema.go
+++ b/upbox/schema.go
@@ -127,8 +127,14 @@
 	// LogLevel specifies the logging level that each server should use.
 	LogLevel string
 
+	// user and server are mappings of user names into the Users and
+	// Servers slices. They are set by SchemaFromYAML.
 	user   map[string]*User
 	server map[string]*Server
+
+	// dir is the temporary directory in which the
+	// user config files and keys were written by Start.
+	dir string
 }
 
 // User defines an Upspin user to be created and used within a schema.
@@ -164,7 +170,9 @@
 	// Flags specifies command-line flags to supply to this server.
 	Flags map[string]string
 
-	addr string // the host:port of this server; set by SchemaFromFile
+	addr string // the host:port of this server; set by SchemaFromYAML
+
+	cmd *exec.Cmd // the running process; set by Start
 }
 
 // DefaultSchema is the schema that is used if none is provided.
@@ -327,9 +335,8 @@
 	return nil
 }
 
-// Run sets up the Users and Servers specified by the Schema
-// and runs an upspin shell as the first user in the Schema.
-func (sc *Schema) Run() error {
+// Start sets up the Users and Servers specified by the Schema.
+func (sc *Schema) Start() error {
 	// Build servers and commands.
 	args := []string{"install", "upspin.io/cmd/upspin"}
 	for _, s := range sc.Servers {
@@ -347,22 +354,30 @@
 	if err != nil {
 		return err
 	}
-	defer os.RemoveAll(tmpDir)
+	sc.dir = tmpDir
+
+	// If anything below fails, we should clean up.
+	cleanup := true
+	defer func() {
+		if cleanup {
+			sc.Stop()
+		}
+	}()
 
 	// Generate TLS certificates.
-	if err := generateCert(tmpDir); err != nil {
+	if err := generateCert(sc.dir); err != nil {
 		return err
 	}
 
 	// Generate keys.
 	// Write an empty  file for use by 'upspin keygen'.
-	configKeygen := filepath.Join(tmpDir, "config.keygen")
+	configKeygen := sc.Config("keygen")
 	if err := ioutil.WriteFile(configKeygen, []byte("secrets: none"), 0644); err != nil {
 		return err
 	}
 	for _, u := range sc.Users {
 		fmt.Fprintf(os.Stderr, "upbox: generating keys for user %q\n", u.Name)
-		dir := filepath.Join(tmpDir, u.Name)
+		dir := filepath.Join(sc.dir, u.Name)
 		if err := os.MkdirAll(dir, 0700); err != nil {
 			return err
 		}
@@ -381,15 +396,14 @@
 	if s, ok := sc.server["keyserver"]; ok {
 		keyUser = s.User
 		// Start keyserver.
-		_, err := sc.writeConfig(tmpDir, keyUser)
+		if err := sc.writeConfig(keyUser); err != nil {
+			return err
+		}
+		cmd, err := sc.startServer(s)
 		if err != nil {
 			return err
 		}
-		cmd, err := sc.startServer(tmpDir, s)
-		if err != nil {
-			return err
-		}
-		defer kill(cmd)
+		s.cmd = cmd
 	}
 	// Wait for the keyserver to start and add the users to it.
 	if err := waitReady(sc.KeyServer); err != nil {
@@ -400,14 +414,14 @@
 			continue
 		}
 
-		if _, err := sc.writeConfig(tmpDir, u.Name); err != nil {
+		if err := sc.writeConfig(u.Name); err != nil {
 			return fmt.Errorf("writing config for %v: %v", u.Name, err)
 		}
 
 		if keyUser == "" {
 			continue
 		}
-		pk, err := ioutil.ReadFile(filepath.Join(tmpDir, u.Name, "public.upspinkey"))
+		pk, err := ioutil.ReadFile(filepath.Join(sc.dir, u.Name, "public.upspinkey"))
 		if err != nil {
 			return err
 		}
@@ -430,7 +444,7 @@
 			return err
 		}
 		cmd := exec.Command("upspin",
-			"-config="+filepath.Join(tmpDir, "config."+keyUser),
+			"-config="+sc.Config(keyUser),
 			"-log="+sc.logLevel(),
 			"user", "-put",
 		)
@@ -449,11 +463,11 @@
 			continue
 		}
 
-		cmd, err := sc.startServer(tmpDir, s)
+		cmd, err := sc.startServer(s)
 		if err != nil {
 			return err
 		}
-		defer kill(cmd)
+		s.cmd = cmd
 	}
 	// Wait for the other servers to start.
 	for _, s := range sc.Servers {
@@ -465,31 +479,40 @@
 		}
 	}
 
-	// Start a shell as the first user.
-	args = []string{
-		"-config=" + filepath.Join(tmpDir, "config."+sc.Users[0].Name),
-		"-log=" + sc.logLevel(),
-		"shell",
+	cleanup = false // Exiting successfully, so don't clean up.
+	return nil
+}
+
+// Stop terminates any running server processes and
+// deletes all temporary config files and keys.
+func (sc *Schema) Stop() error {
+	if sc.dir == "" {
+		return errors.New("cannot stop; not started")
 	}
-	fmt.Fprintf(os.Stderr, "upbox: upspin %s\n", strings.Join(args, " "))
-	shell := exec.Command("upspin", args...)
-	shell.Stdin = os.Stdin
-	shell.Stdout = os.Stdout
-	shell.Stderr = os.Stderr
-	return shell.Run()
+	for _, s := range sc.Servers {
+		if s.cmd != nil && s.cmd.Process != nil {
+			s.cmd.Process.Kill()
+		}
+	}
+	return os.RemoveAll(sc.dir)
+}
+
+// Config returns the path to the config for the given user.
+func (sc *Schema) Config(user string) string {
+	return filepath.Join(sc.dir, "config."+user)
 }
 
 // writeConfig writes a config file for user named "config.name" inside dir.
-func (sc *Schema) writeConfig(dir, user string) (string, error) {
+func (sc *Schema) writeConfig(user string) error {
 	u, ok := sc.user[user]
 	if !ok {
-		return "", fmt.Errorf("unknown user %q", user)
+		return fmt.Errorf("unknown user %q", user)
 	}
 
 	configContent := []string{
 		"username: " + u.Name,
-		"secrets: " + filepath.Join(dir, user),
-		"tlscerts: " + dir,
+		"secrets: " + filepath.Join(sc.dir, u.Name),
+		"tlscerts: " + sc.dir,
 		"packing: " + u.Packing,
 		"storeserver: " + u.StoreServer,
 		"dirserver: " + u.DirServer,
@@ -504,20 +527,16 @@
 			"keyserver: remote,"+sc.KeyServer,
 		)
 	}
-	configFile := filepath.Join(dir, "config."+u.Name)
-	if err := ioutil.WriteFile(configFile, []byte(strings.Join(configContent, "\n")), 0644); err != nil {
-		return "", err
-	}
-	return configFile, nil
+	return ioutil.WriteFile(sc.Config(u.Name), []byte(strings.Join(configContent, "\n")), 0644)
 }
 
 // startServer starts the given server, returning the running exec.Cmd.
-func (sc *Schema) startServer(dir string, s *Server) (*exec.Cmd, error) {
+func (sc *Schema) startServer(s *Server) (*exec.Cmd, error) {
 	args := []string{
-		"-config=" + filepath.Join(dir, "config."+s.User),
+		"-config=" + sc.Config(s.User),
 		"-log=" + sc.logLevel(),
-		"-tls_cert=" + filepath.Join(dir, "cert.pem"),
-		"-tls_key=" + filepath.Join(dir, "key.pem"),
+		"-tls_cert=" + filepath.Join(sc.dir, "cert.pem"),
+		"-tls_key=" + filepath.Join(sc.dir, "key.pem"),
 		"-letscache=", // disable
 		"-https=" + s.addr,
 		"-addr=" + s.addr,
@@ -525,7 +544,7 @@
 	if s.Name == "keyserver" {
 		args = append(args,
 			"-test_user="+s.User,
-			"-test_secrets="+filepath.Join(dir, s.User),
+			"-test_secrets="+filepath.Join(sc.dir, s.User),
 		)
 	}
 	for k, v := range s.Flags {
@@ -547,12 +566,6 @@
 	return "info"
 }
 
-func kill(cmd *exec.Cmd) {
-	if cmd.Process != nil {
-		cmd.Process.Kill()
-	}
-}
-
 func prefix(p string, out io.Writer) io.Writer {
 	r, w := io.Pipe()
 	go func() {