Merge "veyron2/services/store: Add a Dir interface."
diff --git a/examples/rockpaperscissors/rpsbot/main.go b/examples/rockpaperscissors/rpsbot/main.go
index 2e0d662..67af3f2 100644
--- a/examples/rockpaperscissors/rpsbot/main.go
+++ b/examples/rockpaperscissors/rpsbot/main.go
@@ -16,9 +16,13 @@
 	"veyron/examples/rockpaperscissors/impl"
 	"veyron/lib/signals"
 	sflag "veyron/security/flag"
+	"veyron/services/mgmt/debug"
+
+	"veyron2"
 	"veyron2/context"
 	"veyron2/ipc"
 	"veyron2/rt"
+	"veyron2/security"
 	"veyron2/vlog"
 )
 
@@ -29,6 +33,29 @@
 	address  = flag.String("address", ":0", "address to listen on")
 )
 
+// The dispatcher returns the RPS invoker unless a suffix is used, or the method
+// called is Glob. This is intended to exercise the DebugServer code.
+type dispatcher struct {
+	rpsInvoker ipc.Invoker
+	auth       security.Authorizer
+	debug      ipc.Dispatcher
+}
+
+func (d *dispatcher) Lookup(suffix, method string) (ipc.Invoker, security.Authorizer, error) {
+	if len(suffix) == 0 && method != "Glob" {
+		return d.rpsInvoker, d.auth, nil
+	}
+	return d.debug.Lookup(suffix, method)
+}
+
+func newDispatcher(runtime veyron2.Runtime, service *impl.RPS, auth security.Authorizer) *dispatcher {
+	return &dispatcher{
+		rpsInvoker: ipc.ReflectInvoker(rps.NewServerRockPaperScissors(service)),
+		auth:       auth,
+		debug:      debug.NewDispatcher(runtime, vlog.Log.LogDir(), auth),
+	}
+}
+
 func main() {
 	r := rt.Init()
 	defer r.Cleanup()
@@ -38,10 +65,10 @@
 	}
 	defer server.Stop()
 
-	rand.Seed(time.Now().UTC().UnixNano())
+	rand.Seed(time.Now().UnixNano())
 	rpsService := impl.NewRPS()
 
-	dispatcher := ipc.LeafDispatcher(rps.NewServerRockPaperScissors(rpsService), sflag.NewAuthorizerOrDie())
+	dispatcher := newDispatcher(r, rpsService, sflag.NewAuthorizerOrDie())
 
 	ep, err := server.Listen(*protocol, *address)
 	if err != nil {
diff --git a/services/mgmt/node/impl/app_invoker.go b/services/mgmt/node/impl/app_invoker.go
index efbfe8e..c81e079 100644
--- a/services/mgmt/node/impl/app_invoker.go
+++ b/services/mgmt/node/impl/app_invoker.go
@@ -9,6 +9,7 @@
 // <config.Root>/
 //   app-<hash 1>/                  - the application dir is named using a hash of the application title
 //     installation-<id 1>/         - installations are labelled with ids
+//       <status>                   - one of the values for installationState enum
 //       <version 1 timestamp>/     - timestamp of when the version was downloaded
 //         bin                      - application binary
 //         previous                 - symbolic link to previous version directory (TODO)
@@ -105,6 +106,43 @@
 	"veyron2/vlog"
 )
 
+// installationState describes the states that an installation can be in at any
+// time.
+type installationState int
+
+const (
+	active installationState = iota
+	uninstalled
+)
+
+// String returns the name that will be used to encode the state as a file name
+// in the installation's dir.
+func (s installationState) String() string {
+	switch s {
+	case active:
+		return "active"
+	case uninstalled:
+		return "uninstalled"
+	default:
+		return "unknown"
+	}
+}
+
+func installationStateIs(installationDir string, state installationState) bool {
+	if _, err := os.Stat(filepath.Join(installationDir, state.String())); err != nil {
+		return false
+	}
+	return true
+}
+
+func transitionInstallation(installationDir string, initial, target installationState) error {
+	return transitionState(installationDir, initial, target)
+}
+
+func initializeInstallation(installationDir string, initial installationState) error {
+	return initializeState(installationDir, initial)
+}
+
 // instanceState describes the states that an instance can be in at any time.
 type instanceState int
 
@@ -138,9 +176,17 @@
 	}
 }
 
-func transition(instanceDir string, initial, target instanceState) error {
-	initialState := filepath.Join(instanceDir, initial.String())
-	targetState := filepath.Join(instanceDir, target.String())
+func transitionInstance(instanceDir string, initial, target instanceState) error {
+	return transitionState(instanceDir, initial, target)
+}
+
+func initializeInstance(instanceDir string, initial instanceState) error {
+	return initializeState(instanceDir, initial)
+}
+
+func transitionState(dir string, initial, target fmt.Stringer) error {
+	initialState := filepath.Join(dir, initial.String())
+	targetState := filepath.Join(dir, target.String())
 	if err := os.Rename(initialState, targetState); err != nil {
 		if os.IsNotExist(err) {
 			return errInvalidOperation
@@ -151,8 +197,8 @@
 	return nil
 }
 
-func initializeState(instanceDir string, initial instanceState) error {
-	initialStatus := filepath.Join(instanceDir, initial.String())
+func initializeState(dir string, initial fmt.Stringer) error {
+	initialStatus := filepath.Join(dir, initial.String())
 	if err := ioutil.WriteFile(initialStatus, []byte("status"), 0600); err != nil {
 		vlog.Errorf("WriteFile(%v) failed: %v", initialStatus, err)
 		return errOperationFailed
@@ -303,7 +349,7 @@
 		return "", errOperationFailed
 	}
 	deferrer := func() {
-		if err := os.RemoveAll(versionDir); err != nil {
+		if err := os.RemoveAll(installationDir); err != nil {
 			vlog.Errorf("RemoveAll(%v) failed: %v", versionDir, err)
 		}
 	}
@@ -327,6 +373,9 @@
 		vlog.Errorf("Symlink(%v, %v) failed: %v", versionDir, link, err)
 		return "", errOperationFailed
 	}
+	if err := initializeInstallation(installationDir, active); err != nil {
+		return "", err
+	}
 	deferrer = nil
 	return naming.Join(envelope.Title, installationID), nil
 }
@@ -383,6 +432,9 @@
 	if err != nil {
 		return "", "", err
 	}
+	if !installationStateIs(installationDir, active) {
+		return "", "", errInvalidOperation
+	}
 	instanceID := generateID()
 	instanceDir := filepath.Join(installationDir, "instances", instanceDirName(instanceID))
 	if mkdir(instanceDir) != nil {
@@ -399,7 +451,7 @@
 		vlog.Errorf("Symlink(%v, %v) failed: %v", versionDir, versionLink, err)
 		return instanceDir, instanceID, errOperationFailed
 	}
-	if err := initializeState(instanceDir, suspended); err != nil {
+	if err := initializeInstance(instanceDir, suspended); err != nil {
 		return instanceDir, instanceID, err
 	}
 	return instanceDir, instanceID, nil
@@ -494,7 +546,7 @@
 }
 
 func (i *appInvoker) run(instanceDir string) error {
-	if err := transition(instanceDir, suspended, starting); err != nil {
+	if err := transitionInstance(instanceDir, suspended, starting); err != nil {
 		return err
 	}
 	cmd, err := genCmd(instanceDir)
@@ -502,10 +554,10 @@
 		err = i.startCmd(instanceDir, cmd)
 	}
 	if err != nil {
-		transition(instanceDir, starting, suspended)
+		transitionInstance(instanceDir, starting, suspended)
 		return err
 	}
-	return transition(instanceDir, starting, started)
+	return transitionInstance(instanceDir, starting, started)
 }
 
 func (i *appInvoker) Start(ipc.ServerContext) ([]string, error) {
@@ -589,17 +641,17 @@
 	if err != nil {
 		return err
 	}
-	if err := transition(instanceDir, suspended, stopped); err == errOperationFailed || err == nil {
+	if err := transitionInstance(instanceDir, suspended, stopped); err == errOperationFailed || err == nil {
 		return err
 	}
-	if err := transition(instanceDir, started, stopping); err != nil {
+	if err := transitionInstance(instanceDir, started, stopping); err != nil {
 		return err
 	}
 	if err := stop(instanceDir); err != nil {
-		transition(instanceDir, stopping, started)
+		transitionInstance(instanceDir, stopping, started)
 		return err
 	}
-	return transition(instanceDir, stopping, stopped)
+	return transitionInstance(instanceDir, stopping, stopped)
 }
 
 func (i *appInvoker) Suspend(ipc.ServerContext) error {
@@ -607,27 +659,30 @@
 	if err != nil {
 		return err
 	}
-	if err := transition(instanceDir, started, suspending); err != nil {
+	if err := transitionInstance(instanceDir, started, suspending); err != nil {
 		return err
 	}
 	if err := stop(instanceDir); err != nil {
-		transition(instanceDir, suspending, started)
+		transitionInstance(instanceDir, suspending, started)
 		return err
 	}
-	return transition(instanceDir, suspending, suspended)
+	return transitionInstance(instanceDir, suspending, suspended)
 }
 
-func (*appInvoker) Uninstall(ipc.ServerContext) error {
+func (i *appInvoker) Uninstall(ipc.ServerContext) error {
+	installationDir, err := i.installationDir()
+	if err != nil {
+		return err
+	}
+	return transitionInstallation(installationDir, active, uninstalled)
+}
+
+func (*appInvoker) Update(ipc.ServerContext) error {
 	// TODO(jsimsa): Implement.
 	return nil
 }
 
-func (i *appInvoker) Update(ipc.ServerContext) error {
-	// TODO(jsimsa): Implement.
-	return nil
-}
-
-func (i *appInvoker) UpdateTo(_ ipc.ServerContext, von string) error {
+func (*appInvoker) UpdateTo(_ ipc.ServerContext, von string) error {
 	// TODO(jsimsa): Implement.
 	return nil
 }
diff --git a/services/mgmt/node/impl/impl_test.go b/services/mgmt/node/impl/impl_test.go
index 2dd443f..d6856f2 100644
--- a/services/mgmt/node/impl/impl_test.go
+++ b/services/mgmt/node/impl/impl_test.go
@@ -452,25 +452,37 @@
 	return appID
 }
 
-func startApp(t *testing.T, appID string) string {
+func startAppImpl(t *testing.T, appID string) (string, error) {
 	appsName := "nm//apps"
 	appName := naming.Join(appsName, appID)
 	stub, err := node.BindApplication(appName)
 	if err != nil {
 		t.Fatalf("BindApplication(%v) failed: %v", appName, err)
 	}
-	var instanceID string
 	if instanceIDs, err := stub.Start(rt.R().NewContext()); err != nil {
-		t.Fatalf("Start failed: %v", err)
+		return "", err
 	} else {
 		if want, got := 1, len(instanceIDs); want != got {
 			t.Fatalf("Expected %v instance ids, got %v instead", want, got)
 		}
-		instanceID = instanceIDs[0]
+		return instanceIDs[0], nil
+	}
+}
+
+func startApp(t *testing.T, appID string) string {
+	instanceID, err := startAppImpl(t, appID)
+	if err != nil {
+		t.Fatalf("Start failed: %v", err)
 	}
 	return instanceID
 }
 
+func startAppShouldFail(t *testing.T, appID string, expectedError verror.ID) {
+	if _, err := startAppImpl(t, appID); err == nil || !verror.Is(err, expectedError) {
+		t.Fatalf("Start(%v) expected to fail with %v, got %v instead", appID, expectedError, err)
+	}
+}
+
 func stopApp(t *testing.T, appID, instanceID string) {
 	appsName := "nm//apps"
 	appName := naming.Join(appsName, appID)
@@ -510,6 +522,18 @@
 	}
 }
 
+func uninstallApp(t *testing.T, appID string) {
+	appsName := "nm//apps"
+	appName := naming.Join(appsName, appID)
+	stub, err := node.BindApplication(appName)
+	if err != nil {
+		t.Fatalf("BindApplication(%v) failed: %v", appName, err)
+	}
+	if err := stub.Uninstall(rt.R().NewContext()); err != nil {
+		t.Fatalf("Uninstall failed: %v", err)
+	}
+}
+
 func verifyAppWorkspace(t *testing.T, root, appID, instanceID string) {
 	// HACK ALERT: for now, we peek inside the node manager's directory
 	// structure (which ought to be opaque) to check for what the app has
@@ -596,6 +620,12 @@
 
 	verifyAppWorkspace(t, root, appID, instanceID)
 
+	// Uninstall the app.
+	uninstallApp(t, appID)
+
+	// Starting new instances should no longer be allowed.
+	startAppShouldFail(t, appID, verror.BadArg)
+
 	// Cleanly shut down the node manager.
 	syscall.Kill(nm.Cmd.Process.Pid, syscall.SIGINT)
 	nm.Expect("nm terminating")
diff --git a/tools/playground/README.md b/tools/playground/README.md
index 30d1d50..aa0c6af 100644
--- a/tools/playground/README.md
+++ b/tools/playground/README.md
@@ -1,18 +1,12 @@
 # Running the playground compile server locally
 
-Install Docker for Linux:
+Install Docker on Goobuntu:
 
-TODO(sadovsky): These instructions don't seem to work, and furthermore, adding
-docker.list to sources.list.d may pose a security risk.
+  * http://go/installdocker
 
-  $ sudo sh -c "echo deb https://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list"
-  $ sudo apt-get update
-  $ sudo apt-get install lxc-docker
-  $ aptitude versions lxc-docker
+Install Docker on OS X:
 
-Or for OS X, from this site:
-
-  https://github.com/boot2docker/osx-installer/releases
+  * https://github.com/boot2docker/osx-installer/releases
 
 Build the playground Docker image (this will take a while...):
 
@@ -20,6 +14,9 @@
 
   $ sudo docker build -t playground $VEYRON_ROOT/veyron/go/src/veyron/tools/playground/builder/.
 
+Note: If you want to ensure an up-to-date version of veyron is installed in the
+Docker image, run the above command with the "--no-cache" flag.
+
 Install the playground binaries:
 
   $ go install veyron/tools/playground/...
diff --git a/tools/playground/builder/Dockerfile b/tools/playground/builder/Dockerfile
index f846ab1..b34ad18 100644
--- a/tools/playground/builder/Dockerfile
+++ b/tools/playground/builder/Dockerfile
@@ -8,10 +8,14 @@
 RUN /usr/local/veyron/bin/veyron profile setup core
 RUN rm /root/.netrc
 
+# Uncomment the following line to use the local copy of vbuild.go.  This is
+# useful when developing and testing local changes to vbuild.go.
+# ADD vbuild.go $VEYRON_ROOT/veyron/go/src/tools/playground/builder
+
 WORKDIR /usr/local/veyron/veyron
-ENV PATH /usr/local/veyron/veyron/go/bin:/usr/local/veyron/veyron/scripts/build:/usr/local/bin:/usr/bin:/bin
-RUN go install veyron/tools/identity veyron/services/mounttable/mounttabled veyron2/vdl/vdl
-RUN go install veyron/tools/playground/builder veyron/tools/playground/compilerd
+ENV PATH /usr/local/veyron/veyron/go/bin:/usr/local/bin:/usr/bin:/bin
+RUN $VEYRON_ROOT/veyron/scripts/build/go install veyron/tools/identity veyron/services/mounttable/mounttabled veyron2/vdl/vdl
+RUN $VEYRON_ROOT/veyron/scripts/build/go install veyron/tools/playground/builder veyron/tools/playground/compilerd
 
 RUN /usr/sbin/useradd -d /home/playground -m playground