Bug 1452239 - adb.py - support output callbacks; r=bc
authorGeoff Brown <gbrown@mozilla.com>
Mon, 16 Apr 2018 14:05:29 -0600
changeset 467520 8eaced3b0ff4e49cbb31bce9133aec928ee9a0d8
parent 467407 0ceabd10aac2272e83850e278c7876f32dbae42e
child 467521 a064cecfa429da6dd820793a8ebf5aa9df511b13
push id9165
push userasasaki@mozilla.com
push dateThu, 26 Apr 2018 21:04:54 +0000
treeherdermozilla-beta@064c3804de2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbc
bugs1452239, 1445716
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1452239 - adb.py - support output callbacks; r=bc The output callback will be used by geckoview-junit tests, bug 1445716.
testing/mozbase/mozdevice/mozdevice/adb.py
--- a/testing/mozbase/mozdevice/mozdevice/adb.py
+++ b/testing/mozbase/mozdevice/mozdevice/adb.py
@@ -3,33 +3,34 @@
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import
 
 import os
 import posixpath
 import re
 import shutil
+import signal
 import subprocess
 import tempfile
 import time
 import traceback
 
 from abc import ABCMeta, abstractmethod
 from distutils import dir_util
 
 
 class ADBProcess(object):
     """ADBProcess encapsulates the data related to executing the adb process."""
 
     def __init__(self, args):
         #: command argument argument list.
         self.args = args
         #: Temporary file handle to be used for stdout.
-        self.stdout_file = tempfile.TemporaryFile()
+        self.stdout_file = tempfile.TemporaryFile(mode='w+b')
         #: boolean indicating if the command timed out.
         self.timedout = None
         #: exitcode of the process.
         self.exitcode = None
         #: subprocess Process object used to execute the command.
         self.proc = subprocess.Popen(args,
                                      stdout=self.stdout_file,
                                      stderr=subprocess.STDOUT)
@@ -972,33 +973,35 @@ class ADBDevice(ADBCommand):
         else:
             self._validate_port(local, is_local=True)
             cmd.extend(["--remove", local])
 
         self.command_output(cmd, timeout=timeout)
 
     # Device Shell methods
 
-    def shell(self, cmd, env=None, cwd=None, timeout=None, root=False):
+    def shell(self, cmd, env=None, cwd=None, timeout=None, root=False,
+              stdout_callback=None):
         """Executes a shell command on the device.
 
         :param str cmd: The command to be executed.
         :param env: Contains the environment variables and
             their values.
         :type env: dict or None
         :param cwd: The directory from which to execute.
         :type cwd: str or None
         :param timeout: The maximum time in
             seconds for any spawned adb process to complete before
             throwing an ADBTimeoutError.  This timeout is per adb call. The
             total time spent may exceed this value. If it is not
             specified, the value set in the ADBDevice constructor is used.
         :type timeout: integer or None
         :param bool root: Flag specifying if the command should
             be executed as root.
+        :param stdout_callback: Function called for each line of output.
         :returns: :class:`mozdevice.ADBProcess`
         :raises: ADBRootError
 
         shell() provides a low level interface for executing commands
         on the device via adb shell.
 
         shell() executes on the host in such as fashion that stdout
         contains the stdout and stderr of the host abd process
@@ -1027,16 +1030,41 @@ class ADBDevice(ADBCommand):
         process takes longer than the specified timeout, the process
         is terminated. The return code is extracted from the stdout
         and is then removed from the file.
 
         It is the caller's responsibilty to clean up by closing
         the stdout temporary files.
 
         """
+        def _timed_read_line_handler(signum, frame):
+            raise IOError('ReadLineTimeout')
+
+        def _timed_read_line(filehandle, timeout=None):
+            """
+            Attempt to readline from filehandle. If readline does not return
+            within timeout seconds, raise IOError('ReadLineTimeout').
+            On Windows, required signal facilities are usually not available;
+            as a result, the timeout is not respected and some reads may
+            block on Windows.
+            """
+            if not hasattr(signal, 'SIGALRM'):
+                return filehandle.readline().rstrip()
+            if timeout is None:
+                timeout = 5
+            default_alarm_handler = signal.getsignal(signal.SIGALRM)
+            signal.signal(signal.SIGALRM, _timed_read_line_handler)
+            signal.alarm(timeout)
+            try:
+                line = filehandle.readline().rstrip()
+            finally:
+                signal.alarm(0)
+                signal.signal(signal.SIGALRM, default_alarm_handler)
+            return line
+
         if root and not self._have_root_shell:
             # If root was requested and we do not already have a root
             # shell, then use the appropriate version of su to invoke
             # the shell cmd. Prefer Android's su version since it may
             # falsely report support for su -c.
             if self._have_android_su:
                 cmd = "su 0 %s" % cmd
             elif self._have_su:
@@ -1063,17 +1091,33 @@ class ADBDevice(ADBCommand):
         args.extend(["wait-for-device", "shell", cmd])
         adb_process = ADBProcess(args)
 
         if timeout is None:
             timeout = self._timeout
 
         start_time = time.time()
         exitcode = adb_process.proc.poll()
+        if stdout_callback:
+            stdout_dup = os.fdopen(os.dup(adb_process.stdout_file.fileno()))
+            offset = 0
         while ((time.time() - start_time) <= timeout) and exitcode is None:
+            if stdout_callback:
+                while True:
+                    try:
+                        stdout_dup.seek(offset, os.SEEK_SET)
+                        line = _timed_read_line(stdout_dup)
+                        offset = stdout_dup.tell()
+                        if line and len(line) > 0:
+                            stdout_callback(line)
+                        else:
+                            # no new output, so sleep and poll
+                            break
+                    except IOError:
+                        pass
             time.sleep(self._polling_interval)
             exitcode = adb_process.proc.poll()
         if exitcode is None:
             adb_process.proc.kill()
             adb_process.timedout = True
             adb_process.exitcode = adb_process.proc.poll()
         elif exitcode == 0:
             adb_process.exitcode = self._get_exitcode(adb_process.stdout_file)