installer: add argument "--apps".

If we are using station mode, we might want to install startup scripts
on DUT, but don't want it to start goofy but maybe some DUT initializing
script.

The new argument '--apps' takes a comma separated string which defines
enabling each main apps or not.
For example:

  --apps="-goofy,+whale_servo"

will disable goofy and enable whale_servo after installation.
If an app is not listed, then the enabling status will be left as is.

BUG=chrome-os-partner:30239
TEST=manual

Change-Id: I078de66507b363f109ad1e296edb1fa445640173
Reviewed-on: https://chromium-review.googlesource.com/357751
Commit-Ready: Wei-Han Chen <stimim@chromium.org>
Tested-by: Wei-Han Chen <stimim@chromium.org>
Reviewed-by: Hung-Te Lin <hungte@chromium.org>
diff --git a/py/toolkit/installer.py b/py/toolkit/installer.py
index 890a91a..0798ba4 100755
--- a/py/toolkit/installer.py
+++ b/py/toolkit/installer.py
@@ -110,7 +110,8 @@
   _sudo = True
 
   def __init__(self, src, dest, no_enable, enable_presenter,
-               enable_device, non_cros=False, device_id=None, system_root='/'):
+               enable_device, non_cros=False, device_id=None, system_root='/',
+               apps=None):
     self._src = src
     self._system_root = system_root
     if dest == self._system_root:
@@ -161,6 +162,7 @@
     self._device_tag_file = os.path.join(self._usr_local_dest, 'factory',
                                          'init', 'run_goofy_device')
     self._device_id = device_id
+    self._apps = apps
 
     if (not os.path.exists(self._usr_local_src) or
         not os.path.exists(self._var_src)):
@@ -211,6 +213,47 @@
       with open(os.path.join(event_log.DEVICE_ID_PATH), 'w') as f:
         f.write(self._device_id)
 
+  def _EnableApp(self, app, enabled):
+    """Enable / disable @app.
+
+    In factory/init/startup, a main app is considered disabled if and only:
+      1. file "factory/init/main.d/disable-@app" exists OR
+      2. file "factory/init/main.d/enable-@app" doesn't exist AND
+        file "factory/init/main.d/@app.sh" is not executable.
+
+    Therefore, we enable an app by removing file "disable-@app" and creating
+    file "enable-@app", and vise versa.
+    """
+    app_enable = os.path.join(self._usr_local_dest,
+                              'factory', 'init', 'main.d', 'enable-' + app)
+    app_disable = os.path.join(self._usr_local_dest,
+                               'factory', 'init', 'main.d', 'disable-' + app)
+    if enabled:
+      print '*** Enabling {app} ***'.format(app=app)
+      Spawn(['rm', '-f', app_disable], sudo=True, log=True, check_call=True)
+      Spawn(['touch', app_enable], sudo=True, log=True, check_call=True)
+    else:
+      print '*** Disabling {app} ***'.format(app=app)
+      Spawn(['touch', app_disable], sudo=True, log=True, check_call=True)
+      Spawn(['rm', '-f', app_enable], sudo=True, log=True, check_call=True)
+
+  def _EnableApps(self):
+    if not self._apps:
+      return
+
+    app_list = []
+    for app in self._apps:
+      if app[0] == '+':
+        app_list.append((app[1:], True))
+      elif app[0] == '-':
+        app_list.append((app[1:], False))
+      else:
+        raise ValueError(
+            'Use +{app} to enable and -{app} to disable'.format(app=app))
+
+    for app, enabled in app_list:
+      self._EnableApp(app, enabled)
+
   def Install(self):
     print '*** Installing factory toolkit...'
     for src, dest in ((self._usr_local_src, self._usr_local_dest),
@@ -252,6 +295,7 @@
     self._SetTagFile('device', self._device_tag_file, self._enable_device)
 
     self._SetDeviceID()
+    self._EnableApps()
 
     print '*** Installation completed.'
 
@@ -415,6 +459,11 @@
   parser.add_argument('--extract-overlord', dest='extract_overlord',
                       metavar='OUTPUT_DIR', type=str, default=None,
                       help='Extract overlord from the toolkit')
+  parser.add_argument('--apps', type=lambda s: s.split(','), default=None,
+                      help=('Enable or disable some apps under '
+                            'factory/init/main.d/. Use prefix "-" to disable, '
+                            'prefix "+" to enable, and use "," to seperate. '
+                            'For example: --apps="-goofy,+whale_servo"'))
 
   args = parser.parse_args()
 
@@ -472,7 +521,8 @@
         src=src_root, dest=dest, no_enable=args.no_enable,
         enable_presenter=args.enable_presenter,
         enable_device=args.enable_device, non_cros=args.non_cros,
-        device_id=args.device_id)
+        device_id=args.device_id,
+        apps=args.apps)
 
     print installer.WarningMessage(args.dest if patch_test_image else None)
 
diff --git a/py/toolkit/installer_unittest.py b/py/toolkit/installer_unittest.py
index 399c68a..435f39f 100755
--- a/py/toolkit/installer_unittest.py
+++ b/py/toolkit/installer_unittest.py
@@ -29,11 +29,15 @@
        'This goes to DUT, too!'),
       ('usr/local/factory/py/umpire/archiver.py',
        'I only run on Umpire server!'),
+      ('usr/local/factory/init/main.d/a.sh',
+       'This is a.sh'),
+      ('usr/local/factory/init/main.d/b.sh',
+       'This is b.sh'),
   ]
 
   def setUp(self):
     self.src = tempfile.mkdtemp(prefix='ToolkitInstallerTest.')
-    os.makedirs(os.path.join(self.src, 'usr/local/factory/init'))
+    os.makedirs(os.path.join(self.src, 'usr/local/factory/init/main.d'))
     os.makedirs(os.path.join(self.src, 'var/factory/state'))
     os.makedirs(os.path.join(self.src, 'usr/local/factory/py/umpire/client'))
 
@@ -63,11 +67,11 @@
 
   def createInstaller(self, enabled_tag=True, system_root='/',
                       enable_presenter=True, enable_device=False,
-                      non_cros=False):
+                      non_cros=False, apps=None):
     self._installer = installer.FactoryToolkitInstaller(
         self.src, self.dest, not enabled_tag, enable_presenter,
         enable_device, non_cros=non_cros,
-        system_root=system_root)
+        system_root=system_root, apps=apps)
     self._installer._sudo = False # pylint: disable=W0212
 
   def testNonRoot(self):
@@ -159,6 +163,33 @@
     self.assertFalse(os.path.exists(
         os.path.join(self.dest, 'usr/local/factory/enabled')))
 
+  def testEnableApp(self):
+    self.makeLiveDevice()
+    os.makedirs(os.path.join(self.dest, 'usr/local/factory/init/main.d'))
+    os.getuid = lambda: 0  # root
+    self._override_in_cros_device = True
+    self.createInstaller(system_root=self.dest, apps=['+a', '-b'])
+    self._installer.Install()
+
+    self.assertTrue(os.path.exists(os.path.join(
+        self.dest, 'usr/local/factory/init/main.d/enable-a')))
+    self.assertFalse(os.path.exists(os.path.join(
+        self.dest, 'usr/local/factory/init/main.d/disable-a')))
+    self.assertFalse(os.path.exists(os.path.join(
+        self.dest, 'usr/local/factory/init/main.d/enable-b')))
+    self.assertTrue(os.path.exists(os.path.join(
+        self.dest, 'usr/local/factory/init/main.d/disable-b')))
+
+  def testEnableAppWrongFormat(self):
+    self.makeLiveDevice()
+    os.makedirs(os.path.join(self.dest, 'usr/local/factory/init/main.d'))
+    os.getuid = lambda: 0  # root
+    self._override_in_cros_device = True
+    self.createInstaller(system_root=self.dest, apps=['a', '-b'])
+
+    with self.assertRaises(ValueError):
+      self._installer.Install()
+
 
 if __name__ == '__main__':
   logging.basicConfig(level=logging.INFO)