services/device/internal/impl: implement Status for device manager service

Use the InstanceState to describe the state for the device service. We may
decide down the road to have a special DeviceState enum instead.

Change-Id: Iba75370e48b32c23f758307dee789f5385773b09
diff --git a/services/device/internal/impl/device_service.go b/services/device/internal/impl/device_service.go
index c51a687..ce87157 100644
--- a/services/device/internal/impl/device_service.go
+++ b/services/device/internal/impl/device_service.go
@@ -654,6 +654,25 @@
 	return "Not implemented", nil
 }
 
-func (*deviceService) Status(*context.T, rpc.ServerCall) (device.Status, error) {
-	return nil, nil
+func (s *deviceService) Status(*context.T, rpc.ServerCall) (device.Status, error) {
+	state := device.InstanceStateRunning
+	if s.updating.updating {
+		state = device.InstanceStateUpdating
+	}
+	// Extract the version from the current link path.
+	//
+	// TODO(caprita): make the version available in the device's directory.
+	scriptPath, err := filepath.EvalSymlinks(s.config.CurrentLink)
+	if err != nil {
+		return nil, err
+	}
+	dir := filepath.Dir(scriptPath)
+	versionDir := filepath.Base(dir)
+	if versionDir == "." {
+		versionDir = "base"
+	}
+	return device.StatusInstance{Value: device.InstanceStatus{
+		State:   state,
+		Version: versionDir,
+	}}, nil
 }
diff --git a/services/device/internal/impl/impl_test.go b/services/device/internal/impl/impl_test.go
index 4a7743d..8e21f14 100644
--- a/services/device/internal/impl/impl_test.go
+++ b/services/device/internal/impl/impl_test.go
@@ -136,6 +136,11 @@
 	utiltest.Resolve(t, ctx, "claimable", 1)
 	// Brand new device manager must be claimed first.
 	utiltest.ClaimDevice(t, ctx, "claimable", "factoryDM", "mydevice", utiltest.NoPairingToken)
+
+	if v := utiltest.VerifyDeviceState(t, ctx, device.InstanceStateRunning, "factoryDM"); v != "factory" {
+		t.Errorf("Expected factory version, got %v instead", v)
+	}
+
 	// Simulate an invalid envelope in the application repository.
 	*envelope = utiltest.EnvelopeFromShell(sh, dmPauseBeforeStopEnv, utiltest.DeviceManagerCmd, "bogus", 0, 0, dmArgs...)
 
@@ -160,6 +165,7 @@
 	if scriptPathFactory == scriptPathV2 {
 		t.Fatalf("current link didn't change")
 	}
+	v2 := utiltest.VerifyDeviceState(t, ctx, device.InstanceStateUpdating, "factoryDM")
 
 	utiltest.UpdateDeviceExpectError(t, ctx, "factoryDM", impl.ErrOperationInProgress.ID)
 
@@ -219,6 +225,10 @@
 
 	servicetest.ReadPID(t, dmh)
 	utiltest.Resolve(t, ctx, "v3DM", 1) // Current link should have been launching v3.
+	v3 := utiltest.VerifyDeviceState(t, ctx, device.InstanceStateRunning, "v3DM")
+	if v2 == v3 {
+		t.Fatalf("version didn't change")
+	}
 
 	// Revert the device manager to its previous version (v2).
 	utiltest.RevertDevice(t, ctx, "v3DM")
diff --git a/services/device/internal/impl/reaping/instance_reaping_test.go b/services/device/internal/impl/reaping/instance_reaping_test.go
index d4c1aeb..81dd2ac 100644
--- a/services/device/internal/impl/reaping/instance_reaping_test.go
+++ b/services/device/internal/impl/reaping/instance_reaping_test.go
@@ -26,7 +26,7 @@
 	// Start a device manager.
 	// (Since it will be restarted, use the VeyronCredentials environment
 	// to maintain the same set of credentials across runs)
-	dmCreds, err := ioutil.TempDir("", "TestDeviceManagerUpdateAndRevert")
+	dmCreds, err := ioutil.TempDir("", "TestReapReconciliationViaAppCycle")
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/services/device/internal/impl/utiltest/helpers.go b/services/device/internal/impl/utiltest/helpers.go
index b92adf6..d461eb2 100644
--- a/services/device/internal/impl/utiltest/helpers.go
+++ b/services/device/internal/impl/utiltest/helpers.go
@@ -385,6 +385,21 @@
 	return dbg
 }
 
+func VerifyDeviceState(t *testing.T, ctx *context.T, want device.InstanceState, name string) string {
+	s, err := DeviceStub(name).Status(ctx)
+	if err != nil {
+		t.Fatalf(testutil.FormatLogLine(2, "Status(%v) failed: %v [%v]", name, verror.ErrorID(err), err))
+	}
+	status, ok := s.(device.StatusInstance)
+	if !ok {
+		t.Fatalf(testutil.FormatLogLine(2, "Status(%v) returned unknown type: %T", name, s))
+	}
+	if status.Value.State != want {
+		t.Fatalf(testutil.FormatLogLine(2, "Status(%v) state: wanted %v, got %v", name, want, status.Value.State))
+	}
+	return status.Value.Version
+}
+
 func Status(t *testing.T, ctx *context.T, nameComponents ...string) device.Status {
 	s, err := AppStub(nameComponents...).Status(ctx)
 	if err != nil {
diff --git a/services/device/internal/starter/starter.go b/services/device/internal/starter/starter.go
index e74c1e9..ac47761 100644
--- a/services/device/internal/starter/starter.go
+++ b/services/device/internal/starter/starter.go
@@ -343,6 +343,8 @@
 	}
 
 	shutdown = func() {
+		// TODO(caprita): Capture the Dying state by feeding it back to
+		// the dispatcher and exposing it in Status.
 		vlog.Infof("Stopping device server...")
 		server.Stop()
 		impl.Shutdown(dispatcher)