Bveyron/services/mgmt/node/impl: add initial support for app installation, start,
and stop.

This is still work in progress, but wanted to checkpoint before going on
vacation; also, the CL was getting too large.

The main changes are:

- moved profile-related code to a separate file to start breaking up invoker.go; left a todo to split node manager and app management invokers into separate objects.

- implement initial versions of Install/Start/Stop (some features still missing)

- changed permissions to group/world 00; we can loosen up as we shift to using different UIDs for child apps

- bunch of other smaller changes

Left a copious number of TODOs scattered around.

Change-Id: I7fc204f907df2fabd36706922e3bf58d3487a008
diff --git a/services/mgmt/node/impl/impl_test.go b/services/mgmt/node/impl/impl_test.go
index 79d6c2b..16988ec 100644
--- a/services/mgmt/node/impl/impl_test.go
+++ b/services/mgmt/node/impl/impl_test.go
@@ -1,6 +1,8 @@
 package impl_test
 
 import (
+	"crypto/md5"
+	"encoding/base64"
 	"fmt"
 	"io/ioutil"
 	"os"
@@ -16,9 +18,11 @@
 	"veyron/services/mgmt/node/config"
 	"veyron/services/mgmt/node/impl"
 
+	"veyron2/ipc"
 	"veyron2/naming"
 	"veyron2/rt"
 	"veyron2/services/mgmt/application"
+	"veyron2/services/mgmt/node"
 	"veyron2/verror"
 	"veyron2/vlog"
 )
@@ -33,8 +37,9 @@
 	// create it here.
 	rt.Init()
 
-	blackbox.CommandTable["nodeManager"] = nodeManager
 	blackbox.CommandTable["execScript"] = execScript
+	blackbox.CommandTable["nodeManager"] = nodeManager
+	blackbox.CommandTable["app"] = app
 }
 
 // execScript launches the script passed as argument.
@@ -123,6 +128,38 @@
 	}
 }
 
+// appService defines a test service that the test app should be running.
+// TODO(caprita): Use this to make calls to the app and verify how Suspend/Stop
+// interact with an active service.
+type appService struct{}
+
+func (appService) Echo(_ ipc.ServerCall, message string) (string, error) {
+	return message, nil
+}
+
+func app(args []string) {
+	if expected, got := 1, len(args); expected != got {
+		vlog.Fatalf("Unexpected number of arguments: expected %d, got %d", expected, got)
+	}
+	publishName := args[0]
+
+	defer rt.R().Cleanup()
+	server, _ := newServer()
+	defer server.Stop()
+	if err := server.Serve(publishName, ipc.SoloDispatcher(new(appService), nil)); err != nil {
+		vlog.Fatalf("Serve(%v) failed: %v", publishName, err)
+	}
+	if call, err := rt.R().Client().StartCall(rt.R().NewContext(), "pingserver", "Ping", nil); err != nil {
+		vlog.Fatalf("StartCall failed: %v", err)
+	} else if err = call.Finish(); err != nil {
+		vlog.Fatalf("Finish failed: %v", err)
+	}
+	<-signals.ShutdownOnSignals()
+	if err := ioutil.WriteFile("testfile", []byte("goodbye world"), 0600); err != nil {
+		vlog.Fatalf("Failed to write testfile: %v", err)
+	}
+}
+
 // generateScript is very similar in behavior to its namesake in invoker.go.
 // However, we chose to re-implement it here for two reasons: (1) avoid making
 // generateScript public; and (2) how the test choses to invoke the node manager
@@ -145,22 +182,28 @@
 	return path
 }
 
+// nodeEnvelopeFromCmd returns a node manager application envelope that
+// describes the given command object.
+func nodeEnvelopeFromCmd(cmd *goexec.Cmd) *application.Envelope {
+	return envelopeFromCmd(application.NodeManagerTitle, cmd)
+}
+
 // envelopeFromCmd returns an envelope that describes the given command object.
-func envelopeFromCmd(cmd *goexec.Cmd) *application.Envelope {
+func envelopeFromCmd(title string, cmd *goexec.Cmd) *application.Envelope {
 	return &application.Envelope{
-		Title:  application.NodeManagerTitle,
+		Title:  title,
 		Args:   cmd.Args[1:],
 		Env:    cmd.Env,
 		Binary: "br",
 	}
 }
 
-// TestUpdateAndRevert makes the node manager go through the motions of updating
+// TestNodeManagerUpdateAndRevert makes the node manager go through the motions of updating
 // itself to newer versions (twice), and reverting itself back (twice).  It also
 // checks that update and revert fail when they're supposed to.  The initial
 // node manager is started 'by hand' via a blackbox command.  Further versions
 // are started through the soft link that the node manager itself updates.
-func TestUpdateAndRevert(t *testing.T) {
+func TestNodeManagerUpdateAndRevert(t *testing.T) {
 	// Set up mount table, application, and binary repositories.
 	defer setupLocalNamespace(t)()
 	envelope, cleanup := startApplicationRepository()
@@ -226,7 +269,7 @@
 	resolve(t, "factoryNM") // Verify the node manager has published itself.
 
 	// Simulate an invalid envelope in the application repository.
-	*envelope = *envelopeFromCmd(nm.Cmd)
+	*envelope = *nodeEnvelopeFromCmd(nm.Cmd)
 	envelope.Title = "bogus"
 	updateExpectError(t, "factoryNM", verror.BadArg)   // Incorrect title.
 	revertExpectError(t, "factoryNM", verror.NotFound) // No previous version available.
@@ -239,7 +282,7 @@
 	// node manager to stage the next version.
 	nmV2 := blackbox.HelperCommand(t, "nodeManager", "v2NM")
 	defer setupChildCommand(nmV2)()
-	*envelope = *envelopeFromCmd(nmV2.Cmd)
+	*envelope = *nodeEnvelopeFromCmd(nmV2.Cmd)
 	update(t, "factoryNM")
 
 	// Current link should have been updated to point to v2.
@@ -288,7 +331,7 @@
 	// Create a third version of the node manager and issue an update.
 	nmV3 := blackbox.HelperCommand(t, "nodeManager", "v3NM")
 	defer setupChildCommand(nmV3)()
-	*envelope = *envelopeFromCmd(nmV3.Cmd)
+	*envelope = *nodeEnvelopeFromCmd(nmV3.Cmd)
 	update(t, "v2NM")
 
 	scriptPathV3 := evalLink()
@@ -360,3 +403,104 @@
 	runNM.Expect("ready")
 	resolve(t, "factoryNM") // Current link should have been launching factory version.
 }
+
+type pingServerDisp chan struct{}
+
+func (p pingServerDisp) Ping(ipc.ServerCall) { close(p) }
+
+// TestAppStartStop installs an app, starts it, and then stops it.
+func TestAppStartStop(t *testing.T) {
+	// Set up mount table, application, and binary repositories.
+	defer setupLocalNamespace(t)()
+	envelope, cleanup := startApplicationRepository()
+	defer cleanup()
+	defer startBinaryRepository()()
+
+	// This is the local filesystem location that the node manager is told
+	// to use.
+	root := filepath.Join(os.TempDir(), "nodemanager")
+	defer os.RemoveAll(root)
+
+	// Set up the node manager.  Since we won't do node manager updates,
+	// don't worry about its application envelope and current link.
+	nm := blackbox.HelperCommand(t, "nodeManager", "nm", root, "unused app repo name", "unused curr link")
+	defer setupChildCommand(nm)()
+	if err := nm.Cmd.Start(); err != nil {
+		t.Fatalf("Start() failed: %v", err)
+	}
+	defer nm.Cleanup()
+	nm.Expect("ready")
+
+	// Create the local server that the app uses to let us know it's ready.
+	server, _ := newServer()
+	defer server.Stop()
+	pingCh := make(chan struct{})
+	if err := server.Serve("pingserver", ipc.SoloDispatcher(pingServerDisp(pingCh), nil)); err != nil {
+		t.Errorf("Failed to set up ping server")
+	}
+
+	// Create an envelope for an app.
+	app := blackbox.HelperCommand(t, "app", "app1")
+	defer setupChildCommand(app)()
+	appTitle := "google naps"
+	*envelope = *envelopeFromCmd(appTitle, app.Cmd)
+
+	appsName := "nm//apps"
+	stub, err := node.BindApplication(appsName)
+	if err != nil {
+		t.Fatalf("BindApplication(%v) failed: %v", appsName, err)
+	}
+	appID, err := stub.Install(rt.R().NewContext(), "ar")
+	if err != nil {
+		t.Fatalf("Install failed: %v", err)
+	}
+	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)
+	} else {
+		if want, got := 1, len(instanceIDs); want != got {
+			t.Fatalf("Expected %v instance ids, got %v instead", want, got)
+		}
+		instanceID = instanceIDs[0]
+	}
+	// Wait until the app pings us that it's ready.
+	<-pingCh
+
+	instanceName := naming.Join(appName, instanceID)
+	stub, err = node.BindApplication(instanceName)
+	if err != nil {
+		t.Fatalf("BindApplication(%v) failed: %v", instanceName, err)
+	}
+	if err := stub.Stop(rt.R().NewContext(), 5); err != nil {
+		t.Errorf("Stop failed: %v", err)
+	}
+
+	// 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
+	// written to its local root.
+	//
+	// TODO(caprita): add support to node manager to browse logs/app local
+	// root.
+	applicationDirName := func(title string) string {
+		h := md5.New()
+		h.Write([]byte(title))
+		hash := strings.TrimRight(base64.URLEncoding.EncodeToString(h.Sum(nil)), "=")
+		return "app-" + hash
+	}
+	components := strings.Split(appID, "/")
+	appTitle, installationID := components[0], components[1]
+	instanceDir := filepath.Join(root, applicationDirName(appTitle), "installation-"+installationID, "instances", "stopped-instance-"+instanceID)
+	rootDir := filepath.Join(instanceDir, "root")
+	testFile := filepath.Join(rootDir, "testfile")
+	if read, err := ioutil.ReadFile(testFile); err != nil {
+		t.Errorf("Failed to read %v: %v", testFile, err)
+	} else if want, got := "goodbye world", string(read); want != got {
+		t.Errorf("Expected to read %v, got %v instead", want, got)
+	}
+	// END HACK
+}