| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 # Copyright 2014 The Chromium Authors. All rights reserved. | |
| 3 # Use of this source code is governed by a BSD-style license that can be | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 import logging | |
| 7 import os | |
| 8 import subprocess | |
| 9 import time | |
| 10 import sys | |
| 11 | |
| 12 import mopy.paths | |
| 13 | |
| 14 from shutil import copyfileobj, rmtree | |
| 15 from signal import SIGTERM | |
| 16 from tempfile import mkdtemp, TemporaryFile | |
| 17 | |
| 18 | |
| 19 class TimeoutError(Exception): | |
| 20 """Allows distinction between timeout failures and generic OSErrors.""" | |
| 21 pass | |
| 22 | |
| 23 | |
| 24 def _poll_for_condition( | |
| 25 condition, max_seconds=10, sleep_interval=0.1, desc='[unnamed condition]'): | |
| 26 """Poll until a condition becomes true. | |
| 27 | |
| 28 Arguments: | |
| 29 condition: callable taking no args and returning bool. | |
| 30 max_seconds: maximum number of seconds to wait. | |
| 31 Might bail up to sleep_interval seconds early. | |
| 32 sleep_interval: number of seconds to sleep between polls. | |
| 33 desc: description put in TimeoutError. | |
| 34 | |
| 35 Returns: | |
| 36 The true value that caused the poll loop to terminate. | |
| 37 | |
| 38 Raises: | |
| 39 TimeoutError if condition doesn't become true before max_seconds is reached. | |
| 40 """ | |
| 41 start_time = time.time() | |
| 42 while time.time() + sleep_interval - start_time <= max_seconds: | |
| 43 value = condition() | |
| 44 if value: | |
| 45 return value | |
| 46 time.sleep(sleep_interval) | |
| 47 | |
| 48 raise TimeoutError('Timed out waiting for condition: %s' % desc) | |
| 49 | |
| 50 | |
| 51 class _BackgroundShell(object): | |
| 52 """Manages a mojo_shell instance that listens for external applications.""" | |
| 53 | |
| 54 def __init__(self, mojo_shell_path, shell_args=None): | |
| 55 """In a background process, run a shell at mojo_shell_path listening | |
| 56 for external apps on an instance-specific socket. | |
| 57 | |
| 58 Arguments: | |
| 59 mojo_shell_path: path to the mojo_shell binary to run. | |
| 60 shell_args: a list of arguments to pass to mojo_shell. | |
| 61 | |
| 62 Raises: | |
| 63 a TimeoutError if the shell takes too long to create the socket. | |
| 64 """ | |
| 65 self._tempdir = mkdtemp(prefix='background_shell_') | |
| 66 self._socket_path = os.path.join(self._tempdir, 'socket') | |
| 67 self._output_file = TemporaryFile() | |
| 68 | |
| 69 shell_command = [mojo_shell_path, | |
| 70 '--enable-external-applications=' + self._socket_path] | |
| 71 if shell_args: | |
| 72 shell_command += shell_args | |
| 73 logging.getLogger().debug(shell_command) | |
| 74 | |
| 75 self._shell = subprocess.Popen(shell_command, stdout=self._output_file, | |
| 76 stderr=subprocess.STDOUT) | |
| 77 _poll_for_condition(lambda: os.path.exists(self._socket_path), | |
| 78 desc="External app socket creation.") | |
| 79 | |
| 80 | |
| 81 def __del__(self): | |
| 82 if self._shell: | |
| 83 self._shell.terminate() | |
| 84 self._shell.wait() | |
| 85 if self._shell.returncode != -SIGTERM: | |
| 86 copyfileobj(self._output_file, sys.stdout) | |
| 87 rmtree(self._tempdir) | |
| 88 | |
| 89 | |
| 90 @property | |
| 91 def socket_path(self): | |
| 92 """The path of the socket where the shell is listening for external apps.""" | |
| 93 return self._socket_path | |
| 94 | |
| 95 | |
| 96 class BackgroundAppGroup(object): | |
| 97 """Manages a group of mojo apps running in the background.""" | |
| 98 | |
| 99 def __init__(self, paths, app_urls, shell_args=None): | |
| 100 """In a background process, spins up mojo_shell with external | |
| 101 applications enabled, passing an optional list of extra arguments. | |
| 102 Then, launches apps indicated by app_urls in the background. | |
| 103 The apps and shell are automatically torn down upon destruction. | |
| 104 | |
| 105 Arguments: | |
| 106 paths: a mopy.paths.Paths object. | |
| 107 app_urls: a list of URLs for apps to run via mojo_launcher. | |
| 108 shell_args: a list of arguments to pass to mojo_shell. | |
| 109 | |
| 110 Raises: | |
| 111 a TimeoutError if the shell takes too long to begin running. | |
| 112 """ | |
| 113 self._shell = _BackgroundShell(paths.mojo_shell_path, shell_args) | |
| 114 | |
| 115 # Run apps defined by app_urls in the background. | |
| 116 self._apps = [] | |
| 117 for app_url in app_urls: | |
| 118 launch_command = [ | |
| 119 paths.mojo_launcher_path, | |
| 120 '--shell-path=' + self._shell.socket_path, | |
| 121 '--app-url=' + app_url, | |
| 122 '--app-path=' + paths.FileFromUrl(app_url), | |
| 123 '--vmodule=*/mojo/shell/*=2'] | |
| 124 logging.getLogger().debug(launch_command) | |
| 125 app_output_file = TemporaryFile() | |
| 126 self._apps.append((app_output_file, | |
| 127 subprocess.Popen(launch_command, | |
| 128 stdout=app_output_file, | |
| 129 stderr=subprocess.STDOUT))) | |
| 130 | |
| 131 | |
| 132 def __del__(self): | |
| 133 self._StopApps() | |
| 134 | |
| 135 | |
| 136 def __enter__(self): | |
| 137 return self | |
| 138 | |
| 139 | |
| 140 def __exit__(self, ex_type, ex_value, traceback): | |
| 141 self._StopApps() | |
| 142 | |
| 143 | |
| 144 def _StopApps(self): | |
| 145 """Terminate all background apps.""" | |
| 146 for output_file, app in self._apps: | |
| 147 app.terminate() | |
| 148 app.wait() | |
| 149 if app.returncode != -SIGTERM: | |
| 150 copyfileobj(output_file, sys.stdout) | |
| 151 self._apps = [] | |
| 152 | |
| 153 | |
| 154 @property | |
| 155 def socket_path(self): | |
| 156 """The path of the socket where the shell is listening for external apps.""" | |
| 157 return self._shell._socket_path | |
| OLD | NEW |