Repository: google/mobly

Issue:
Snippets are not compatible with UIAutomator
'am instrument' only sets up a UiAutomationConnection in -w mode (I don't know why). Snippets are not run in wait mode, so UiAutomationConnection is null. This crashes uiautomator, which needs that proxy object for privileged operations back to the shell.

We need to start the snippets in -w mode, using something like `start_standing_subprocess`.


Discussion:
Do we need to start all snippet this way?
I'd prefer not having to hold these processes unless necessary.
> I'd prefer not having to hold these processes unless necessary.

Why's that?
less management, less complexity, less code, less work :P

So do we need to start all snippet this way?

> less management

I think the only new management we have to do is calling stop_standing_subprocess() right?

> less complexity

Actually if we have codepaths for both, there's more complexity because we have to implement and test both ways of starting snippets.

Also (most importantly for me) the complexity will be exposed to the user, because now they'll have to know to load their snippet differently than before if/when it gets UIAutomator calls.

And there's no good way for us to produce a good error to help them here since the symptom will be that UIAutomator.getDevice() will just return `null` on the Java side. That's not something we can catch/wrap.

> less code, less work

More code and work to support both, see above
Can we ask the adb team about '-w' and understand why it's required for ui automator first?
Sure, can do. But I'm not sure what you mean by asking them 'first' -- adb versions are slow to roll out and have slow adoption. We still have to support adb 1.0.32 in Mobly. So even if they remove this requirement in a new version, I don't think it would matter much to us from a support perspective, would it?
I'm not saying it's a bug in adb. Just looking to understand more about the rationale behind it. May give us some insights on how to resolve it.
> Just looking to understand more about the rationale behind it.

Looks like UIAutomator works by keeping a shell connection open and sending privileged operations there by IPC for execution in shell context. So doesn't seem like a requirement we can drop for UIAutomator.

Resolving patch:
diff --git a/mobly/controllers/android_device_lib/snippet_client.py b/mobly/controllers/android_device_lib/snippet_client.py
--- a/mobly/controllers/android_device_lib/snippet_client.py
+++ b/mobly/controllers/android_device_lib/snippet_client.py
@@ -17,14 +17,18 @@
 import logging
 import re
 
-from mobly.controllers.android_device_lib import adb
+from mobly import utils
 from mobly.controllers.android_device_lib import jsonrpc_client_base
 
-_INSTRUMENTATION_RUNNER_PACKAGE = 'com.google.android.mobly.snippet.SnippetRunner'
+_INSTRUMENTATION_RUNNER_PACKAGE = (
+    'com.google.android.mobly.snippet.SnippetRunner')
 
-_LAUNCH_CMD = 'am instrument -e action start -e port %s %s/' + _INSTRUMENTATION_RUNNER_PACKAGE
+_LAUNCH_CMD = (
+    'am instrument -w -e action start -e port %s %s/' +
+    _INSTRUMENTATION_RUNNER_PACKAGE)
 
-_STOP_CMD = 'am instrument -w -e action stop %s/' + _INSTRUMENTATION_RUNNER_PACKAGE
+_STOP_CMD = (
+    'am instrument -w -e action stop %s/' + _INSTRUMENTATION_RUNNER_PACKAGE)
 
 
 class Error(Exception):
@@ -59,6 +63,7 @@ def __init__(self, package, host_port, adb_proxy, log=logging.getLogger()):
         self.package = package
         self.log = log
         self._serial = self._adb.serial
+        self._proc = None
 
     def _do_start_app(self):
         """Overrides superclass."""
@@ -67,11 +72,18 @@ def _do_start_app(self):
         # helpful since they need to create their own instrumentations and
         # manifest.
         self.log.info('Launching snippet apk %s', self.package)
-        self._adb.shell(cmd)
+        adb_cmd = ['adb', '-s', self._adb.serial, 'shell', cmd]
+        self._proc = utils.start_standing_subprocess(adb_cmd, shell=False)
 
     def stop_app(self):
         """Overrides superclass."""
-        cmd = _STOP_CMD % self.package
+        # Kill the pending 'adb shell am instrument -w' process if there is one.
+        # Although killing the snippet apk would abort this process anyway, we
+        # want to call stop_standing_subprocess() to perform a health check,
+        # print the failure stack trace if there was any, and reap it from the
+        # process table.
+        if self._proc:
+          utils.stop_standing_subprocess(self._proc)
         self.log.debug('Stopping snippet apk %s', self.package)
         out = self._adb.shell(_STOP_CMD % self.package).decode('utf-8')
         if 'OK (0 tests)' not in out:


Test patch: