veyron/services/mgmt/node: Hook up the logreader

This change hooks up the logreader implementation to the node manager
apps objects.

Change-Id: I8ccca706a34b02fea32e31ca5a3ca1696fbca15f
diff --git a/services/mgmt/node/impl/app_invoker.go b/services/mgmt/node/impl/app_invoker.go
index e26b2bb..c359a2a 100644
--- a/services/mgmt/node/impl/app_invoker.go
+++ b/services/mgmt/node/impl/app_invoker.go
@@ -581,17 +581,22 @@
 }
 
 // instanceDir returns the path to the directory containing the app instance
+// referred to by the given suffix relative to the given root directory.
+func instanceDir(root string, suffix []string) (string, error) {
+	if nComponents := len(suffix); nComponents != 3 {
+		return "", errInvalidSuffix
+	}
+	app, installation, instance := suffix[0], suffix[1], suffix[2]
+	instancesDir := filepath.Join(root, applicationDirName(app), installationDirName(installation), "instances")
+	instanceDir := filepath.Join(instancesDir, instanceDirName(instance))
+	return instanceDir, nil
+}
+
+// instanceDir returns the path to the directory containing the app instance
 // referred to by the invoker's suffix, as well as the corresponding stopped
 // instance dir.  Returns an error if the suffix does not name an instance.
 func (i *appInvoker) instanceDir() (string, error) {
-	components := i.suffix
-	if nComponents := len(components); nComponents != 3 {
-		return "", errInvalidSuffix
-	}
-	app, installation, instance := components[0], components[1], components[2]
-	instancesDir := filepath.Join(i.config.Root, applicationDirName(app), installationDirName(installation), "instances")
-	instanceDir := filepath.Join(instancesDir, instanceDirName(instance))
-	return instanceDir, nil
+	return instanceDir(i.config.Root, i.suffix)
 }
 
 func (i *appInvoker) Resume(call ipc.ServerContext) error {
@@ -856,7 +861,8 @@
 		return nil
 	}
 	for _, path := range instances {
-		if _, err := loadInstanceInfo(filepath.Dir(path)); err != nil {
+		instanceDir := filepath.Dir(path)
+		if _, err := loadInstanceInfo(instanceDir); err != nil {
 			continue
 		}
 		relpath, _ := filepath.Rel(i.config.Root, path)
@@ -869,12 +875,25 @@
 		installID := strings.TrimPrefix(elems[1], "installation-")
 		instanceID := strings.TrimPrefix(elems[3], "instance-")
 		if title, ok := appIDMap[appID]; ok {
-			tree.find([]string{title, installID, instanceID}, true)
+			n := tree.find([]string{title, installID, instanceID, "logs"}, true)
+			i.addLogFiles(n, filepath.Join(instanceDir, "logs"))
 		}
 	}
 	return tree
 }
 
+func (i *appInvoker) addLogFiles(n *treeNode, dir string) {
+	filepath.Walk(dir, func(path string, _ os.FileInfo, _ error) error {
+		if path == dir {
+			// Skip the logs directory itself.
+			return nil
+		}
+		relpath, _ := filepath.Rel(dir, path)
+		n.find(strings.Split(relpath, string(filepath.Separator)), true)
+		return nil
+	})
+}
+
 func (i *appInvoker) Glob(ctx ipc.ServerContext, pattern string, stream mounttable.GlobbableServiceGlobStream) error {
 	g, err := glob.Parse(pattern)
 	if err != nil {
diff --git a/services/mgmt/node/impl/dispatcher.go b/services/mgmt/node/impl/dispatcher.go
index ffc2cf6..655e758 100644
--- a/services/mgmt/node/impl/dispatcher.go
+++ b/services/mgmt/node/impl/dispatcher.go
@@ -15,10 +15,12 @@
 	vflag "veyron.io/veyron/veyron/security/flag"
 	"veyron.io/veyron/veyron/security/serialization"
 	"veyron.io/veyron/veyron/services/mgmt/lib/toplevelglob"
+	logsimpl "veyron.io/veyron/veyron/services/mgmt/logreader/impl"
 	inode "veyron.io/veyron/veyron/services/mgmt/node"
 	"veyron.io/veyron/veyron/services/mgmt/node/config"
 
 	"veyron.io/veyron/veyron2/ipc"
+	"veyron.io/veyron/veyron2/naming"
 	"veyron.io/veyron/veyron2/rt"
 	"veyron.io/veyron/veyron2/security"
 	"veyron.io/veyron/veyron2/services/mgmt/node"
@@ -245,6 +247,15 @@
 		})
 		return ipc.ReflectInvoker(receiver), d.auth, nil
 	case appsSuffix:
+		if method != "Glob" && len(components) >= 5 && components[4] == "logs" {
+			appInstanceDir, err := instanceDir(d.config.Root, components[1:4])
+			if err != nil {
+				return nil, nil, err
+			}
+			logsDir := filepath.Join(appInstanceDir, "logs")
+			suffix := naming.Join(components[5:]...)
+			return logsimpl.NewLogFileInvoker(logsDir, suffix), d.auth, nil
+		}
 		receiver := node.NewServerApplication(&appInvoker{
 			callback: d.internal.callback,
 			config:   d.config,
diff --git a/services/mgmt/node/impl/impl_test.go b/services/mgmt/node/impl/impl_test.go
index fdd2c32..579e5c2 100644
--- a/services/mgmt/node/impl/impl_test.go
+++ b/services/mgmt/node/impl/impl_test.go
@@ -17,6 +17,7 @@
 	"path"
 	"path/filepath"
 	"reflect"
+	"regexp"
 	"sort"
 	"strconv"
 	"strings"
@@ -39,6 +40,7 @@
 	"veyron.io/veyron/veyron2/rt"
 	"veyron.io/veyron/veyron2/security"
 	"veyron.io/veyron/veyron2/services/mgmt/application"
+	"veyron.io/veyron/veyron2/services/mgmt/logreader"
 	"veyron.io/veyron/veyron2/services/mgmt/node"
 	"veyron.io/veyron/veyron2/services/mounttable"
 	"veyron.io/veyron/veyron2/verror"
@@ -962,7 +964,7 @@
 	runNM.Cleanup()
 }
 
-func TestNodeManagerGlob(t *testing.T) {
+func TestNodeManagerGlobAndLogs(t *testing.T) {
 	// Set up mount table, application, and binary repositories.
 	defer setupLocalNamespace(t)()
 	envelope, cleanup := startApplicationRepository()
@@ -1013,37 +1015,64 @@
 			"apps/google naps",
 			"apps/google naps/" + installID,
 			"apps/google naps/" + installID + "/" + instance1ID,
+			"apps/google naps/" + installID + "/" + instance1ID + "/logs",
+			"apps/google naps/" + installID + "/" + instance1ID + "/logs/STDERR-<timestamp>",
+			"apps/google naps/" + installID + "/" + instance1ID + "/logs/STDOUT-<timestamp>",
 			"nm",
 		}},
 		{"nm/apps", "*", []string{"google naps"}},
 		{"nm/apps/google naps", "*", []string{installID}},
+		{"nm/apps/google naps/" + installID, "*", []string{instance1ID}},
+		{"nm/apps/google naps/" + installID + "/" + instance1ID, "*", []string{"logs"}},
+		{"nm/apps/google naps/" + installID + "/" + instance1ID + "/logs", "*", []string{"STDERR-<timestamp>", "STDOUT-<timestamp>"}},
 	}
+	re := regexp.MustCompile("(STDOUT|STDERR)-[0-9]+$")
 	for _, tc := range testcases {
-		c, err := mounttable.BindGlobbable(tc.name)
-		if err != nil {
-			t.Fatalf("BindGlobbable failed: %v", err)
+		results := doGlob(t, tc.name, tc.pattern)
+		for i, name := range results {
+			results[i] = re.ReplaceAllString(name, "$1-<timestamp>")
 		}
-
-		stream, err := c.Glob(rt.R().NewContext(), tc.pattern)
-		if err != nil {
-			t.Errorf("Glob failed: %v", err)
-		}
-		results := []string{}
-		iterator := stream.RecvStream()
-		for iterator.Advance() {
-			results = append(results, iterator.Value().Name)
-		}
-		sort.Strings(results)
 		if !reflect.DeepEqual(results, tc.expected) {
-			t.Errorf("unexpected result. Got %q, want %q", results, tc.expected)
-		}
-		if err := iterator.Err(); err != nil {
-			t.Errorf("unexpected stream error: %v", err)
-		}
-		if err := stream.Finish(); err != nil {
-			t.Errorf("Finish failed: %v", err)
+			t.Errorf("unexpected result for (%q, %q). Got %q, want %q", tc.name, tc.pattern, results, tc.expected)
 		}
 	}
+
+	// Call Size() on the log file objects
+	files := doGlob(t, "nm", "apps/google naps/"+installID+"/"+instance1ID+"/logs/*")
+	for _, file := range files {
+		name := naming.Join("nm", file)
+		c, err := logreader.BindLogFile(name)
+		if err != nil {
+			t.Fatalf("BindLogFile failed: %v", err)
+		}
+		if _, err := c.Size(rt.R().NewContext()); err != nil {
+			t.Errorf("Size(%q) failed: %v", name, err)
+		}
+	}
+}
+
+func doGlob(t *testing.T, name, pattern string) []string {
+	c, err := mounttable.BindGlobbable(name)
+	if err != nil {
+		t.Fatalf("BindGlobbable failed: %v", err)
+	}
+	stream, err := c.Glob(rt.R().NewContext(), pattern)
+	if err != nil {
+		t.Errorf("Glob failed: %v", err)
+	}
+	results := []string{}
+	iterator := stream.RecvStream()
+	for iterator.Advance() {
+		results = append(results, iterator.Value().Name)
+	}
+	if err := iterator.Err(); err != nil {
+		t.Errorf("unexpected stream error: %v", err)
+	}
+	if err := stream.Finish(); err != nil {
+		t.Errorf("Finish failed: %v", err)
+	}
+	sort.Strings(results)
+	return results
 }
 
 func listAndVerifyAssociations(t *testing.T, stub node.Node, run veyron2.Runtime, expected []node.Association) {
diff --git a/services/mgmt/node/impl/node_invoker.go b/services/mgmt/node/impl/node_invoker.go
index a751d11..aae24f4 100644
--- a/services/mgmt/node/impl/node_invoker.go
+++ b/services/mgmt/node/impl/node_invoker.go
@@ -393,7 +393,6 @@
 }
 
 func (i *nodeInvoker) Glob(ctx ipc.ServerContext, pattern string, stream mounttable.GlobbableServiceGlobStream) error {
-	// TODO(rthellend): Finish implementing Glob
 	g, err := glob.Parse(pattern)
 	if err != nil {
 		return err