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 |