summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/mozdevice/mozdevice/remote_process_monitor.py
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mozbase/mozdevice/mozdevice/remote_process_monitor.py')
-rw-r--r--testing/mozbase/mozdevice/mozdevice/remote_process_monitor.py285
1 files changed, 285 insertions, 0 deletions
diff --git a/testing/mozbase/mozdevice/mozdevice/remote_process_monitor.py b/testing/mozbase/mozdevice/mozdevice/remote_process_monitor.py
new file mode 100644
index 0000000000..29e0d4e008
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/remote_process_monitor.py
@@ -0,0 +1,285 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import re
+import time
+
+import six
+
+from .adb import ADBTimeoutError
+
+
+class RemoteProcessMonitor:
+ """
+ RemoteProcessMonitor provides a convenient way to run a remote process,
+ dump its log file, and wait for it to end.
+ """
+
+ def __init__(
+ self,
+ app_name,
+ device,
+ log,
+ message_logger,
+ remote_log_file,
+ remote_profile,
+ ):
+ self.app_name = app_name
+ self.device = device
+ self.log = log
+ self.remote_log_file = remote_log_file
+ self.remote_profile = remote_profile
+ self.counts = {}
+ self.counts["pass"] = 0
+ self.counts["fail"] = 0
+ self.counts["todo"] = 0
+ self.last_test_seen = "RemoteProcessMonitor"
+ self.message_logger = message_logger
+ if self.device.is_file(self.remote_log_file):
+ self.device.rm(self.remote_log_file)
+ self.log.info("deleted remote log %s" % self.remote_log_file)
+
+ def launch(self, app, debugger_info, test_url, extra_args, env, e10s):
+ """
+ Start the remote activity.
+ """
+ if self.app_name and self.device.process_exist(self.app_name):
+ self.log.info("%s is already running. Stopping..." % self.app_name)
+ self.device.stop_application(self.app_name)
+ args = []
+ if debugger_info:
+ args.extend(debugger_info.args)
+ args.append(app)
+ args.extend(extra_args)
+ activity = "TestRunnerActivity"
+ self.device.launch_activity(
+ self.app_name,
+ activity_name=activity,
+ e10s=e10s,
+ moz_env=env,
+ extra_args=args,
+ url=test_url,
+ )
+ return self.pid
+
+ @property
+ def pid(self):
+ """
+ Determine the pid of the remote process (or the first process with
+ the same name).
+ """
+ procs = self.device.get_process_list()
+ # limit the comparison to the first 75 characters due to a
+ # limitation in processname length in android.
+ pids = [proc[0] for proc in procs if proc[1] == self.app_name[:75]]
+ if pids is None or len(pids) < 1:
+ return 0
+ return pids[0]
+
+ def read_stdout(self):
+ """
+ Fetch the full remote log file, log any new content and return True if new
+ content is processed.
+ """
+ try:
+ new_log_content = self.device.get_file(
+ self.remote_log_file, offset=self.stdout_len
+ )
+ except ADBTimeoutError:
+ raise
+ except Exception as e:
+ self.log.error(
+ "%s | exception reading log: %s" % (self.last_test_seen, str(e))
+ )
+ return False
+ if not new_log_content:
+ return False
+
+ self.stdout_len += len(new_log_content)
+ new_log_content = six.ensure_str(new_log_content, errors="replace")
+
+ self.log_buffer += new_log_content
+ lines = self.log_buffer.split("\n")
+ lines = [l for l in lines if l]
+
+ if lines:
+ if self.log_buffer.endswith("\n"):
+ # all lines are complete; no need to buffer
+ self.log_buffer = ""
+ else:
+ # keep the last (unfinished) line in the buffer
+ self.log_buffer = lines[-1]
+ del lines[-1]
+ if not lines:
+ return False
+
+ for line in lines:
+ # This passes the line to the logger (to be logged or buffered)
+ if isinstance(line, six.text_type):
+ # if line is unicode - let's encode it to bytes
+ parsed_messages = self.message_logger.write(
+ line.encode("UTF-8", "replace")
+ )
+ else:
+ # if line is bytes type, write it as it is
+ parsed_messages = self.message_logger.write(line)
+
+ for message in parsed_messages:
+ if isinstance(message, dict):
+ if message.get("action") == "test_start":
+ self.last_test_seen = message["test"]
+ elif message.get("action") == "test_end":
+ self.last_test_seen = "{} (finished)".format(message["test"])
+ elif message.get("action") == "suite_end":
+ self.last_test_seen = "Last test finished"
+ elif message.get("action") == "log":
+ line = message["message"].strip()
+ m = re.match(".*:\s*(\d*)", line)
+ if m:
+ try:
+ val = int(m.group(1))
+ if "Passed:" in line:
+ self.counts["pass"] += val
+ self.last_test_seen = "Last test finished"
+ elif "Failed:" in line:
+ self.counts["fail"] += val
+ elif "Todo:" in line:
+ self.counts["todo"] += val
+ except ADBTimeoutError:
+ raise
+ except Exception:
+ pass
+
+ return True
+
+ def wait(self, timeout=None):
+ """
+ Wait for the remote process to end (or for its activity to go to background).
+ While waiting, periodically retrieve the process output and print it.
+ If the process is still running but no output is received in *timeout*
+ seconds, return False; else, once the process exits/goes to background,
+ return True.
+ """
+ self.log_buffer = ""
+ self.stdout_len = 0
+
+ timer = 0
+ output_timer = 0
+ interval = 10
+ status = True
+ top = self.app_name
+
+ # wait for log creation on startup
+ retries = 0
+ while retries < 20 and not self.device.is_file(self.remote_log_file):
+ retries += 1
+ time.sleep(1)
+ if self.device.is_file(self.remote_log_file):
+ # We must change the remote log's permissions so that the shell can read it.
+ self.device.chmod(self.remote_log_file, mask="666")
+ else:
+ self.log.warning(
+ "Failed wait for remote log: %s missing?" % self.remote_log_file
+ )
+
+ while top == self.app_name:
+ has_output = self.read_stdout()
+ if has_output:
+ output_timer = 0
+ if self.counts["pass"] > 0:
+ interval = 0.5
+ time.sleep(interval)
+ timer += interval
+ output_timer += interval
+ if timeout and output_timer > timeout:
+ status = False
+ break
+ if not has_output:
+ top = self.device.get_top_activity(timeout=60)
+ if top is None:
+ self.log.info("Failed to get top activity, retrying, once...")
+ top = self.device.get_top_activity(timeout=60)
+
+ # Flush anything added to stdout during the sleep
+ self.read_stdout()
+ self.log.info("wait for %s complete; top activity=%s" % (self.app_name, top))
+ if top == self.app_name:
+ self.log.info("%s unexpectedly found running. Killing..." % self.app_name)
+ self.kill()
+ if not status:
+ self.log.error(
+ "TEST-UNEXPECTED-FAIL | %s | "
+ "application timed out after %d seconds with no output"
+ % (self.last_test_seen, int(timeout))
+ )
+ return status
+
+ def kill(self):
+ """
+ End a troublesome remote process: Trigger ANR and breakpad dumps, then
+ force the application to end.
+ """
+
+ # Trigger an ANR report with "kill -3" (SIGQUIT)
+ try:
+ self.device.pkill(self.app_name, sig=3, attempts=1)
+ except ADBTimeoutError:
+ raise
+ except: # NOQA: E722
+ pass
+ time.sleep(3)
+
+ # Trigger a breakpad dump with "kill -6" (SIGABRT)
+ try:
+ self.device.pkill(self.app_name, sig=6, attempts=1)
+ except ADBTimeoutError:
+ raise
+ except: # NOQA: E722
+ pass
+
+ # Wait for process to end
+ retries = 0
+ while retries < 3:
+ if self.device.process_exist(self.app_name):
+ self.log.info(
+ "%s still alive after SIGABRT: waiting..." % self.app_name
+ )
+ time.sleep(5)
+ else:
+ break
+ retries += 1
+ if self.device.process_exist(self.app_name):
+ try:
+ self.device.pkill(self.app_name, sig=9, attempts=1)
+ except ADBTimeoutError:
+ raise
+ except: # NOQA: E722
+ self.log.error("%s still alive after SIGKILL!" % self.app_name)
+ if self.device.process_exist(self.app_name):
+ self.device.stop_application(self.app_name)
+
+ # Test harnesses use the MOZ_CRASHREPORTER environment variables to suppress
+ # the interactive crash reporter, but that may not always be effective;
+ # check for and cleanup errant crashreporters.
+ crashreporter = "%s.CrashReporter" % self.app_name
+ if self.device.process_exist(crashreporter):
+ self.log.warning(
+ "%s unexpectedly found running. Killing..." % crashreporter
+ )
+ try:
+ self.device.pkill(crashreporter)
+ except ADBTimeoutError:
+ raise
+ except: # NOQA: E722
+ pass
+ if self.device.process_exist(crashreporter):
+ self.log.error("%s still running!!" % crashreporter)
+
+ @staticmethod
+ def elf_arm(filename):
+ """
+ Determine if the specified file is an ARM binary.
+ """
+ data = open(filename, "rb").read(20)
+ return data[:4] == "\x7fELF" and ord(data[18]) == 40 # EM_ARM