TBR: veyron/services/device/impl,veyron/tools/mgmt: add device uninstall functionality

The uninstall feature of the device manager removes the soft link and the root
dir for the device manager (and pairs with --self_install); the corresponding
dmuninstall script wraps that feature (and pairs with dminstall).

To properly use in the integration test, exposing the pid of the device manager so we can shut it down before running uninstall.

Change-Id: I9c3d079341b35a62da313d82e5bececd1a869906
diff --git a/services/mgmt/device/deviced/main.go b/services/mgmt/device/deviced/main.go
index e9221ba..d3b3581 100644
--- a/services/mgmt/device/deviced/main.go
+++ b/services/mgmt/device/deviced/main.go
@@ -18,6 +18,7 @@
 	publishAs   = flag.String("name", "", "name to publish the device manager at")
 	installSelf = flag.Bool("install_self", false, "perform installation using environment and command-line flags")
 	installFrom = flag.String("install_from", "", "if not-empty, perform installation from the provided application envelope object name")
+	uninstall   = flag.Bool("uninstall", false, "uninstall the installation specified via the config")
 )
 
 func main() {
@@ -45,6 +46,14 @@
 		return
 	}
 
+	if *uninstall {
+		if err := impl.Uninstall(); err != nil {
+			vlog.Errorf("Uninstall failed: %v", err)
+			os.Exit(1)
+		}
+		return
+	}
+
 	server, err := runtime.NewServer()
 	if err != nil {
 		vlog.Fatalf("NewServer() failed: %v", err)
diff --git a/services/mgmt/device/impl/device_installer.go b/services/mgmt/device/impl/device_installer.go
index 393d0b6..ac291c0 100644
--- a/services/mgmt/device/impl/device_installer.go
+++ b/services/mgmt/device/impl/device_installer.go
@@ -94,3 +94,23 @@
 	return updateLink(filepath.Join(deviceDir, "deviced.sh"), configState.CurrentLink)
 	// TODO(caprita): Update system management daemon.
 }
+
+// Uninstall undoes SelfInstall, removing the device manager's installation
+// directory and soft link.
+func Uninstall() error {
+	configState, err := config.Load()
+	if err != nil {
+		return fmt.Errorf("failed to load config: %v", err)
+	}
+
+	vlog.VI(1).Infof("Config for device manager: %v", configState)
+	configState.Name = "dummy" // Just so that Validate passes.
+	if err := configState.Validate(); err != nil {
+		return fmt.Errorf("invalid config %v: %v", configState, err)
+	}
+	if err := os.RemoveAll(configState.Root); err != nil {
+		return fmt.Errorf("RemoveAll(%v) failed: %v", configState.Root, err)
+	}
+	return os.Remove(configState.CurrentLink)
+	// TODO(caprita): Update system management daemon.
+}
diff --git a/services/mgmt/device/impl/impl_test.go b/services/mgmt/device/impl/impl_test.go
index 4f2e138..0a06551 100644
--- a/services/mgmt/device/impl/impl_test.go
+++ b/services/mgmt/device/impl/impl_test.go
@@ -61,6 +61,7 @@
 	deviceManagerCmd = "deviceManager"
 	appCmd           = "app"
 	installerCmd     = "installer"
+	uninstallerCmd   = "uninstaller"
 )
 
 func init() {
@@ -71,6 +72,7 @@
 	modules.RegisterChild(deviceManagerCmd, "", deviceManager)
 	modules.RegisterChild(appCmd, "", app)
 	modules.RegisterChild(installerCmd, "", install)
+	modules.RegisterChild(uninstallerCmd, "", uninstall)
 	testutil.Init()
 
 	if modules.IsModulesProcess() {
@@ -213,6 +215,15 @@
 	return nil
 }
 
+// uninstall uninstalls the device manager.
+func uninstall(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+	if err := impl.Uninstall(); err != nil {
+		vlog.Fatalf("Uninstall failed: %v", err)
+		return err
+	}
+	return nil
+}
+
 // 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.
@@ -950,20 +961,20 @@
 	}
 }
 
-// TestDeviceManagerInstall verifies the 'self install' functionality of the
-// device manager: it runs SelfInstall in a child process, then runs the
-// executable from the soft link that the installation created.  This should
-// bring up a functioning device manager.
-func TestDeviceManagerInstall(t *testing.T) {
+// TestDeviceManagerInstallUninstall verifies the 'self install' and 'uninstall'
+// functionality of the device manager: it runs SelfInstall in a child process,
+// then runs the executable from the soft link that the installation created.
+// This should bring up a functioning device manager.  In the end it runs
+// Uninstall and verifies that the installation is gone.
+func TestDeviceManagerInstallUninstall(t *testing.T) {
 	sh, deferFn := createShellAndMountTable(t)
 	defer deferFn()
 
-	root, cleanup := setupRootDir(t)
+	testDir, cleanup := setupRootDir(t)
 	defer cleanup()
 
-	// Current link does not have to live in the root dir, but it's
-	// convenient to put it there so we have everything in one place.
-	currLink := filepath.Join(root, "current_link")
+	root := filepath.Join(testDir, "root")
+	currLink := filepath.Join(testDir, "current_link")
 
 	// Create an 'envelope' for the device manager that we can pass to the
 	// installer, to ensure that the device manager that the installer
@@ -1006,6 +1017,18 @@
 	dms.Expect("dm terminating")
 	dms.ExpectEOF()
 	dmh.Shutdown(os.Stderr, os.Stderr)
+
+	// Uninstall.
+	uninstallerEnv := []string{config.RootEnv + "=" + root, config.CurrentLinkEnv + "=" + currLink, config.HelperEnv + "=" + "unused"}
+	uninstallerh, uninstallers := runShellCommand(t, sh, uninstallerEnv, uninstallerCmd)
+	uninstallers.ExpectEOF()
+	uninstallerh.Shutdown(os.Stderr, os.Stderr)
+	if _, err := os.Stat(currLink); err == nil || !os.IsNotExist(err) {
+		t.Fatalf("Stat(%v) returned %v", currLink, err)
+	}
+	if _, err := os.Stat(root); err == nil || !os.IsNotExist(err) {
+		t.Fatalf("Stat(%v) returned %v", root, err)
+	}
 }
 
 func TestDeviceManagerGlobAndDebug(t *testing.T) {
diff --git a/tools/mgmt/device/dmuninstall b/tools/mgmt/device/dmuninstall
new file mode 100755
index 0000000..ce12747
--- /dev/null
+++ b/tools/mgmt/device/dmuninstall
@@ -0,0 +1,53 @@
+#!/bin/bash
+#
+# Uninstalls device manager from the local machine.
+#
+# Usage:
+#
+# ./dmuninstall <install parent dir>
+
+set -e
+
+usage() {
+  echo "usage:"
+  echo "./dmuninstall [--single_user] <install parent dir>"
+}
+
+main() {
+  if [[ "$1" == "--single_user" ]]; then
+    local -r SINGLE_USER=true
+    shift
+  else
+    local -r SINGLE_USER=false
+  fi
+  local -r INSTALL_PARENT_DIR="$1"
+  if [[ -z "${INSTALL_PARENT_DIR}" ]]; then
+    echo 'No local install directory specified!'
+    usage
+    exit 1
+  fi
+  shift
+  local -r INSTALL_DIR="${INSTALL_PARENT_DIR}/device_manager"
+  if [[ ! -d "${INSTALL_DIR}" ]]; then
+    echo "${INSTALL_DIR} does not exist or is not a directory!"
+    exit 1
+  fi
+
+  # Tell the device manager to uninstall itself.
+  local -r DM_ROOT="${INSTALL_DIR}/dmroot"
+  echo "Uninstalling device manager from ${DM_ROOT} ..."
+
+  local -r BIN_INSTALL="${INSTALL_DIR}/bin"
+
+  VEYRON_DM_CURRENT="${INSTALL_DIR}/deviced.curr" VEYRON_DM_ROOT="${DM_ROOT}" VEYRON_DM_HELPER="unused" "${BIN_INSTALL}/deviced" --uninstall
+  echo "Device manager uninstalled."
+
+  if [[ ${SINGLE_USER} == false ]]; then
+    sudo rm -rf "${INSTALL_DIR}"
+  else
+    rm -rf "${INSTALL_DIR}"
+  fi
+  echo "Removed ${INSTALL_DIR}"
+}
+
+main "$@"
diff --git a/tools/mgmt/test.sh b/tools/mgmt/test.sh
index 4a4b055..0374601 100755
--- a/tools/mgmt/test.sh
+++ b/tools/mgmt/test.sh
@@ -19,6 +19,7 @@
   PRINCIPAL_BIN="$(shell_test::build_go_binary 'veyron.io/veyron/veyron/tools/principal')"
   DEBUG_BIN="$(shell_test::build_go_binary 'veyron.io/veyron/veyron/tools/debug')"
   DMINSTALL_SCRIPT="$(go list -f {{.Dir}} veyron.io/veyron/veyron/tools/mgmt/device)/dminstall"
+  DMUNINSTALL_SCRIPT="$(go list -f {{.Dir}} veyron.io/veyron/veyron/tools/mgmt/device)/dmuninstall"
 }
 
 # TODO(caprita): Move to shell_tesh.sh
@@ -44,11 +45,33 @@
     fi
     sleep 1
   done
-  bash
   echo "Timed out waiting for ${NAME} to appear in the mounttable."
   return 1
 }
 
+###############################################################################
+# Waits until the given process is gone, within a set timeout.
+# Arguments:
+#   pid of process
+#   timeout in seconds
+# Returns:
+#   0 if the pid is gone, and 1 if the timeout expires before that happens.
+###############################################################################
+wait_for_process_exit() {
+  local -r PID="$1"
+  local -r TIMEOUT="$2"
+  for i in $(seq 1 "${TIMEOUT}"); do
+    local RESULT=$(shell::check_result kill -0 "${PID}")
+    if [[ "${RESULT}" != "0" ]]; then
+      # Process is gone, can return early.
+      return 0
+    fi
+    sleep 1
+  done
+  echo "Timed out waiting for PID ${PID} to disappear."
+  return 1
+}
+
 main() {
   cd "${WORKDIR}"
   build
@@ -63,9 +86,11 @@
   # test.sh by hand and exercise the code that requires root privileges.
 
   # Install and start device manager.
-  shell_test::start_server "${DMINSTALL_SCRIPT}" --single_user $(shell::tmp_dir) \
+  DM_INSTALL_DIR=$(shell::tmp_dir)
+  shell_test::start_server "${DMINSTALL_SCRIPT}" --single_user "${DM_INSTALL_DIR}" \
     "${BIN_STAGING_DIR}" -- --veyron.tcp.address=127.0.0.1:0 || shell_test::fail "line ${LINENO} failed to start device manager"
   # Dump dminstall's log, just to provide visibility into its steps.
+  local -r DM_PID="${START_SERVER_PID}"
   cat "${START_SERVER_LOG_FILE}"
 
   local -r DM_NAME=$(hostname)
@@ -142,6 +167,15 @@
   shell_test::assert_eq "$("${NAMESPACE_BIN}" glob "${INSTANCE_NAME}/stats/...")" "" "${LINENO}"
   shell_test::assert_ne "$("${NAMESPACE_BIN}" glob "${INSTANCE_NAME}/logs/...")" "" "${LINENO}"
 
+  kill "${DM_PID}"
+  wait_for_process_exit "${DM_PID}" 5
+
+  "${DMUNINSTALL_SCRIPT}" --single_user "${DM_INSTALL_DIR}" \
+    || shell_test::fail "line ${LINENO} failed to uninstall device manager"
+
+  if [[ -n "$(ls -A "${DM_INSTALL_DIR}")" ]]; then
+    shell_test::fail "${DM_INSTALL_DIR} is not empty"
+  fi
   shell_test::pass
 }