Use exceptions instead of prints

Change-Id: Ib60369ebd8f7346b7edffcd21fd9c097da84b1ef
diff --git a/deploy.py b/deploy.py
index 9e7976a..437548d 100755
--- a/deploy.py
+++ b/deploy.py
@@ -21,7 +21,27 @@
 ]
 
 
-def adb(*args, serial = None):
+class HostCommandError(BaseException):
+    """An error happened while issuing a command on the host."""
+    def __init__(self, command, error_message):
+        self.command = command
+        self.error_message = error_message
+        message = 'Command `{}` failed: {}'.format(command, error_message)
+        super(HostCommandError, self).__init__(message)
+
+
+class DeviceCommandError(BaseException):
+    """An error happened while sending a command to a device."""
+    def __init__(self, serial, command, error_message):
+        self.serial = serial
+        self.command = command
+        self.error_message = error_message
+        message = 'Command `{}` failed on {}: {}'.format(
+            command, serial, error_message)
+        super(DeviceCommandError, self).__init__(message)
+
+
+def adb(*args, serial = None, raise_on_error = True):
     """Run ADB command attached to serial.
 
     Example:
@@ -35,50 +55,85 @@
         List of options to ADB (including command).
     :param str serial:
         Identifier for ADB connection to device.
+    :param raise_on_error bool:
+        Whether to raise a DeviceCommandError exception if the return code is
+        less than 0.
     :returns subprocess.CompletedProcess:
         Completed process.
+    :raises DeviceCommandError:
+        If the command failed.
     """
 
     # Make sure the adb server is started to avoid the infamous "out of date"
     # message that pollutes stdout.
-    subprocess.run(
+    ret = subprocess.run(
         ['adb', 'start-server'], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
         universal_newlines=True)
+    if ret.returncode < 0:
+        if raise_on_error:
+            raise DeviceCommandError(
+                serial if serial else '??', str(args), ret.stderr)
+        else:
+            return None
 
     command = ['adb']
     if serial:
         command += ['-s', serial]
     if args:
         command += list(args)
-    return subprocess.run(
+    ret = subprocess.run(
         command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
         universal_newlines=True)
 
+    if raise_on_error and ret.returncode < 0:
+        raise DeviceCommandError(
+            serial if serial else '??', str(args), ret.stderr)
 
-def aapt(*args):
+    return ret
+
+
+def aapt(*args, raise_on_error = True):
     """Run an AAPT command.
 
     :param *args:
         The AAPT command with its options.
+    :param raise_on_error bool:
+        Whether to raise a DeviceCommandError exception if the return code is
+        less than 0.
     :returns subprocess.CompletedProcess:
         Completed process.
+    :raises HostCommandError:
+        If the command failed.
     """
     command = ['aapt']
     if args:
         command += list(args)
-    return subprocess.run(
+    ret = subprocess.run(
         command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
         universal_newlines=True)
 
+    if raise_on_error and ret.returncode < 0:
+        raise HostCommandError(str(args), ret.stderr)
+
+    return ret
+
 
 def force_awake(serial, always=True):
-    """Force the device to stay awake."""
-    return adb('shell', 'svc power stayon {}'.format(
+    """Force the device to stay awake.
+
+    Raises:
+        DeviceCommandError: If the underlying adb command failed.
+    """
+    adb('shell', 'svc power stayon {}'.format(
         'true' if always else 'false'), serial=serial)
 
 
 def unlock(serial):
-    """Wake-up the device and unlock it."""
+    """Wake-up the device and unlock it.
+
+    Raises:
+        DeviceCommandError: If the underlying adb commands failed.
+    """
     adb('shell', 'input keyevent KEYCODE_POWER', serial=serial)
     time.sleep(1)
     adb('shell', 'input keyevent KEYCODE_MENU', serial=serial)
@@ -99,6 +154,7 @@
         Key of property to get.
     :returns str:
         Value of system property.
+    :raise DeviceCommandError: If the underlying adb command failed.
     """
     process = adb('shell', 'getprop', key, serial=serial)
     return process.stdout.strip()
@@ -115,49 +171,43 @@
         Identifier for ADB connection to device.
     :returns bool:
         True if device runs GMS, false otherwise.
+    :raise DeviceCommandError: If the underlying adb command failed.
     """
     return getprop(serial, 'ro.build.id').startswith('FP2-gms-')
 
 
 def uninstall_apk(serial, filename, prebuilts_dir):
-    """Uninstall apk from prebuilts_dir on device."""
+    """Uninstall apk from prebuilts_dir on device.
+
+    Raises:
+        ValueError: If the package name could not be read from the apk.
+        DeviceCommandError: If the uninstall command failed.
+    """
     ret = aapt('dump', 'badging', '{}/{}'.format(prebuilts_dir, filename))
-    if 0 < ret.returncode:
-        print('Could retrieve app `{}` info, error was: {}'.format(
-            filename, ret.stderr), file=sys.stderr)
-    else:
-        package = None
-        for line in ret.stdout.splitlines():
-            if line.startswith('package'):
-                for token in line.split(' '):
-                    if token.startswith('name='):
-                        # Extract the package name out of the token
-                        # (name='some.package.name')
-                        package = token[6:-1]
-                        break
-        if not package:
-            print('Could not find package of app `{}`'.format(filename),
-                file=sys.stderr)
-        else:
-            print('Uninstalling `{}`'.format(package))
-            ret = adb('uninstall', package, serial=serial)
-            if 0 < ret.returncode:
-                print('Could not uninstall app `{}`, error was: {}'.format(
-                    filename, ret.stderr), file=sys.stderr)
-            else:
-                print('App `{}` uninstalled.'.format(filename))
+    package = None
+    for line in ret.stdout.splitlines():
+        if line.startswith('package'):
+            for token in line.split(' '):
+                if token.startswith('name='):
+                    # Extract the package name out of the token
+                    # (name='some.package.name')
+                    package = token[6:-1]
+                    break
+    if not package:
+        raise ValueError('Could not find package of app `{}`'.format(
+            filename))
+
+    adb('uninstall', package, serial=serial)
 
 
 def install_apk(serial, filename, prebuilts_dir):
-    """Install apk from prebuilts_dir on device."""
-    print('Installing {}.'.format(filename))
+    """Install apk from prebuilts_dir on device.
+
+    Raises:
+        DeviceCommandError: If the install command failed.
+    """
     path = os.path.join(prebuilts_dir, filename)
-    ret = adb('install', '-r', path, serial=serial)
-    if 0 < ret.returncode:
-        print('Could not install app `{}`, error was: {}'.format(
-            filename, ret.stderr), file=sys.stderr)
-    else:
-        print('App `{}` installed.'.format(filename))
+    adb('install', '-r', path, serial=serial)
 
 
 # Prepare the DUT
@@ -166,26 +216,20 @@
 
     # Uninstall the smartviser apps
     for app in PREBUILT_APKS + [PREBUILT_PROXY_APKS[flavour]]:
+        print('Uninstalling `{}`…'.format(app))
         uninstall_apk(serial, app, prebuilts_dir)
 
     # Copy the scenarios
-    ret = adb('push', scenarios_dir, '/sdcard/viser', serial=serial)
-    if 0 < ret.returncode:
-        print('Could not push the scenarios, error was: {}'.format(ret.stderr),
-                file=sys.stderr)
-    else:
-        print('Scenarios pushed.')
+    print('Pushing scenarios from `{}`…'.format(scenarios_dir))
+    adb('push', scenarios_dir, '/sdcard/viser', serial=serial)
 
     # Copy the scenarios data
-    ret = adb('push', data_dir, '/sdcard/viser/data', serial=serial)
-    if 0 < ret.returncode:
-        print('Could not push the scenarios data, error was: {}'.format(ret.stderr),
-                file=sys.stderr)
-    else:
-        print('Scenarios data pushed.')
+    print('Pushing scenarios data from `{}`…'.format(data_dir))
+    adb('push', data_dir, '/sdcard/viser/data', serial=serial)
 
     # Install the smartviser apps (starting with the proxy app)
     for app in [PREBUILT_PROXY_APKS[flavour]] + PREBUILT_APKS:
+        print('Installing `{}`…'.format(app))
         install_apk(serial, app, prebuilts_dir)
 
 
@@ -288,36 +332,39 @@
                     serials.append(serial)
 
     for serial in serials:
-        print('Configuring device {}'.format(serial))
+        print('Configuring device {}…'.format(serial))
 
         dut = Device(serial)
 
-        # Make sure the screen stays on - we're going to use UI automation
-        force_awake(serial)
-        unlock(serial)
+        try:
+            # Make sure the screen stays on - we're going to use UI automation
+            force_awake(serial)
+            unlock(serial)
 
-        # Push the scenarios, their data, and install the apps
-        prepare_dut(serial, '../scenarios', '../scenarios-data', PREBUILTS_PATH)
+            # Push the scenarios, their data, and install the apps
+            prepare_dut(
+                serial, '../scenarios', '../scenarios-data', PREBUILTS_PATH)
 
-        # Start the viser app
-        ret = adb(
+            # Start the viser app
+            adb(
                 'shell', 'monkey', '-p', 'com.lunarlabs.panda', '-c',
                 'android.intent.category.LAUNCHER', '1', serial=serial)
-        if 0 < ret.returncode:
-            print('Could not start the viser app, error was: {}'.format(app,
-                    ret.stderr), file=sys.stderr)
-            exit(-1)
 
-        configure_perms(dut)
+            configure_perms(dut)
 
-        # TODO DO NOT DO THE FOLLOWING IF NO SIM CARD IS IN THE DUT
-        # time.sleep(10)
-        # configure_sms(dut)
+            # TODO DO NOT DO THE FOLLOWING IF NO SIM CARD IS IN THE DUT
+            # time.sleep(10)
+            # configure_sms(dut)
 
-        configure_settings(dut)
-
-        # Leave the device alone now
-        force_awake(serial, always=False)
+            configure_settings(dut)
+        except (HostCommandError, DeviceCommandError) as e:
+            print('ERROR {}'.format(e), file=sys.stderr)
+        finally:
+            try:
+                # Leave the device alone now
+                force_awake(serial, always=False)
+            except DeviceCommandError as e:
+                print('WARNING {}'.format(e), file=sys.stderr)
 
 
 if __name__ == '__main__':