Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(221)

Side by Side Diff: mojo/devtools/common/pylib/android_shell.py

Issue 1128153002: Rename the devtools library: pylib -> devtoolslib. (Closed) Base URL: git@github.com:domokit/mojo.git@master
Patch Set: Address offline comments/ devtools_lib -> devtoolslib. Created 5 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « mojo/devtools/common/pylib/__init__.py ('k') | mojo/devtools/common/pylib/apptest.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright 2014 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 import atexit
6 import itertools
7 import json
8 import logging
9 import os
10 import os.path
11 import random
12 import subprocess
13 import sys
14 import threading
15 import time
16 import urlparse
17
18 from pylib.http_server import StartHttpServer
19 from pylib.shell import Shell
20
21
22 # Tags used by the mojo shell application logs.
23 LOGCAT_TAGS = [
24 'AndroidHandler',
25 'MojoFileHelper',
26 'MojoMain',
27 'MojoShellActivity',
28 'MojoShellApplication',
29 'chromium',
30 ]
31
32 MOJO_SHELL_PACKAGE_NAME = 'org.chromium.mojo.shell'
33
34 MAPPING_PREFIX = '--map-origin='
35
36 DEFAULT_BASE_PORT = 31337
37
38 _logger = logging.getLogger()
39
40
41 def _IsMapOrigin(arg):
42 """Returns whether arg is a --map-origin argument."""
43 return arg.startswith(MAPPING_PREFIX)
44
45
46 def _Split(l, pred):
47 positive = []
48 negative = []
49 for v in l:
50 if pred(v):
51 positive.append(v)
52 else:
53 negative.append(v)
54 return (positive, negative)
55
56
57 def _ExitIfNeeded(process):
58 """Exits |process| if it is still alive."""
59 if process.poll() is None:
60 process.kill()
61
62
63 class AndroidShell(Shell):
64 """Wrapper around Mojo shell running on an Android device.
65
66 Args:
67 adb_path: Path to adb, optional if adb is in PATH.
68 target_device: Device to run on, if multiple devices are connected.
69 """
70
71 def __init__(self, adb_path="adb", target_device=None, verbose_pipe=None):
72 self.adb_path = adb_path
73 self.target_device = target_device
74 self.stop_shell_registered = False
75 self.adb_running_as_root = False
76 self.verbose_pipe = verbose_pipe if verbose_pipe else open(os.devnull, 'w')
77
78 def _CreateADBCommand(self, args):
79 adb_command = [self.adb_path]
80 if self.target_device:
81 adb_command.extend(['-s', self.target_device])
82 adb_command.extend(args)
83 return adb_command
84
85 def _ReadFifo(self, fifo_path, pipe, on_fifo_closed, max_attempts=5):
86 """Reads |fifo_path| on the device and write the contents to |pipe|. Calls
87 |on_fifo_closed| when the fifo is closed. This method will try to find the
88 path up to |max_attempts|, waiting 1 second between each attempt. If it
89 cannot find |fifo_path|, a exception will be raised.
90 """
91 fifo_command = self._CreateADBCommand(
92 ['shell', 'test -e "%s"; echo $?' % fifo_path])
93
94 def Run():
95 def _WaitForFifo():
96 for _ in xrange(max_attempts):
97 if subprocess.check_output(fifo_command)[0] == '0':
98 return
99 time.sleep(1)
100 if on_fifo_closed:
101 on_fifo_closed()
102 raise Exception("Unable to find fifo.")
103 _WaitForFifo()
104 stdout_cat = subprocess.Popen(self._CreateADBCommand([
105 'shell',
106 'cat',
107 fifo_path]),
108 stdout=pipe)
109 atexit.register(_ExitIfNeeded, stdout_cat)
110 stdout_cat.wait()
111 if on_fifo_closed:
112 on_fifo_closed()
113
114 thread = threading.Thread(target=Run, name="StdoutRedirector")
115 thread.start()
116
117 def _MapPort(self, device_port, host_port):
118 """Maps the device port to the host port. If |device_port| is 0, a random
119 available port is chosen. Returns the device port.
120 """
121 def _FindAvailablePortOnDevice():
122 opened = subprocess.check_output(
123 self._CreateADBCommand(['shell', 'netstat']))
124 opened = [int(x.strip().split()[3].split(':')[1])
125 for x in opened if x.startswith(' tcp')]
126 while True:
127 port = random.randint(4096, 16384)
128 if port not in opened:
129 return port
130 if device_port == 0:
131 device_port = _FindAvailablePortOnDevice()
132 subprocess.check_call(self._CreateADBCommand([
133 "reverse",
134 "tcp:%d" % device_port,
135 "tcp:%d" % host_port]))
136
137 unmap_command = self._CreateADBCommand(["reverse", "--remove",
138 "tcp:%d" % device_port])
139
140 def _UnmapPort():
141 subprocess.Popen(unmap_command)
142 atexit.register(_UnmapPort)
143 return device_port
144
145 def _StartHttpServerForDirectory(self, path, port=0):
146 """Starts an http server serving files from |path|. Returns the local
147 url.
148 """
149 assert path
150 print 'starting http for', path
151 server_address = StartHttpServer(path)
152
153 print 'local port=%d' % server_address[1]
154 return 'http://127.0.0.1:%d/' % self._MapPort(port, server_address[1])
155
156 def _StartHttpServerForOriginMapping(self, mapping, port):
157 """If |mapping| points at a local file starts an http server to serve files
158 from the directory and returns the new mapping.
159
160 This is intended to be called for every --map-origin value.
161 """
162 parts = mapping.split('=')
163 if len(parts) != 2:
164 return mapping
165 dest = parts[1]
166 # If the destination is a url, don't map it.
167 if urlparse.urlparse(dest)[0]:
168 return mapping
169 # Assume the destination is a local file. Start a local server that
170 # redirects to it.
171 localUrl = self._StartHttpServerForDirectory(dest, port)
172 print 'started server at %s for %s' % (dest, localUrl)
173 return parts[0] + '=' + localUrl
174
175 def _StartHttpServerForOriginMappings(self, map_parameters, fixed_port):
176 """Calls _StartHttpServerForOriginMapping for every --map-origin
177 argument.
178 """
179 if not map_parameters:
180 return []
181
182 original_values = list(itertools.chain(
183 *map(lambda x: x[len(MAPPING_PREFIX):].split(','), map_parameters)))
184 sorted(original_values)
185 result = []
186 for i, value in enumerate(original_values):
187 result.append(self._StartHttpServerForOriginMapping(
188 value, DEFAULT_BASE_PORT + 1 + i if fixed_port else 0))
189 return [MAPPING_PREFIX + ','.join(result)]
190
191 def _RunAdbAsRoot(self):
192 if self.adb_running_as_root:
193 return
194
195 if 'cannot run as root' in subprocess.check_output(
196 self._CreateADBCommand(['root'])):
197 raise Exception("Unable to run adb as root.")
198
199 # Wait for adbd to restart.
200 subprocess.check_call(
201 self._CreateADBCommand(['wait-for-device']),
202 stdout=self.verbose_pipe)
203 self.adb_running_as_root = True
204
205 def InstallApk(self, shell_apk_path):
206 """Installs the apk on the device.
207
208 Args:
209 shell_apk_path: Path to the shell Android binary.
210 """
211 subprocess.check_call(
212 self._CreateADBCommand(['install', '-r', shell_apk_path, '-i',
213 MOJO_SHELL_PACKAGE_NAME]),
214 stdout=self.verbose_pipe)
215
216 def SetUpLocalOrigin(self, local_dir, fixed_port=True):
217 """Sets up a local http server to serve files in |local_dir| along with
218 device port forwarding. Returns the origin flag to be set when running the
219 shell.
220 """
221
222 origin_url = self._StartHttpServerForDirectory(
223 local_dir, DEFAULT_BASE_PORT if fixed_port else 0)
224 return "--origin=" + origin_url
225
226 def StartShell(self,
227 arguments,
228 stdout=None,
229 on_application_stop=None,
230 fixed_port=True):
231 """Starts the mojo shell, passing it the given arguments.
232
233 The |arguments| list must contain the "--origin=" arg. SetUpLocalOrigin()
234 can be used to set up a local directory on the host machine as origin.
235 If |stdout| is not None, it should be a valid argument for subprocess.Popen.
236 """
237 if not self.stop_shell_registered:
238 atexit.register(self.StopShell)
239 self.stop_shell_registered = True
240
241 STDOUT_PIPE = "/data/data/%s/stdout.fifo" % MOJO_SHELL_PACKAGE_NAME
242
243 cmd = self._CreateADBCommand([
244 'shell',
245 'am',
246 'start',
247 '-S',
248 '-a', 'android.intent.action.VIEW',
249 '-n', '%s/.MojoShellActivity' % MOJO_SHELL_PACKAGE_NAME])
250
251 parameters = []
252 if stdout or on_application_stop:
253 # We need to run as root to access the fifo file we use for stdout
254 # redirection.
255 self._RunAdbAsRoot()
256
257 # Remove any leftover fifo file after the previous run.
258 subprocess.check_call(self._CreateADBCommand(
259 ['shell', 'rm', '-f', STDOUT_PIPE]))
260
261 parameters.append('--fifo-path=%s' % STDOUT_PIPE)
262 self._ReadFifo(STDOUT_PIPE, stdout, on_application_stop)
263 # The origin has to be specified whether it's local or external.
264 assert any("--origin=" in arg for arg in arguments)
265
266 # Extract map-origin arguments.
267 map_parameters, other_parameters = _Split(arguments, _IsMapOrigin)
268 parameters += other_parameters
269 parameters += self._StartHttpServerForOriginMappings(map_parameters,
270 fixed_port)
271
272 if parameters:
273 encodedParameters = json.dumps(parameters)
274 cmd += ['--es', 'encodedParameters', encodedParameters]
275
276 subprocess.check_call(cmd, stdout=self.verbose_pipe)
277
278 def Run(self, arguments):
279 """Runs the shell with given arguments until shell exits, passing the stdout
280 mingled with stderr produced by the shell onto the stdout.
281
282 Returns:
283 Exit code retured by the shell or None if the exit code cannot be
284 retrieved.
285 """
286 self.CleanLogs()
287 p = self.ShowLogs()
288 self.StartShell(arguments, sys.stdout, p.terminate)
289 p.wait()
290 return None
291
292 def RunAndGetOutput(self, arguments):
293 """Runs the shell with given arguments until shell exits.
294
295 Args:
296 arguments: list of arguments for the shell
297
298 Returns:
299 A tuple of (return_code, output). |return_code| is the exit code returned
300 by the shell or None if the exit code cannot be retrieved. |output| is the
301 stdout mingled with the stderr produced by the shell.
302 """
303 (r, w) = os.pipe()
304 with os.fdopen(r, "r") as rf:
305 with os.fdopen(w, "w") as wf:
306 self.StartShell(arguments, wf, wf.close, False)
307 output = rf.read()
308 return None, output
309
310 def StopShell(self):
311 """Stops the mojo shell."""
312 subprocess.check_call(self._CreateADBCommand(['shell',
313 'am',
314 'force-stop',
315 MOJO_SHELL_PACKAGE_NAME]))
316
317 def CleanLogs(self):
318 """Cleans the logs on the device."""
319 subprocess.check_call(self._CreateADBCommand(['logcat', '-c']))
320
321 def ShowLogs(self):
322 """Displays the log for the mojo shell.
323
324 Returns:
325 The process responsible for reading the logs.
326 """
327 logcat = subprocess.Popen(self._CreateADBCommand([
328 'logcat',
329 '-s',
330 ' '.join(LOGCAT_TAGS)]),
331 stdout=sys.stdout)
332 atexit.register(_ExitIfNeeded, logcat)
333 return logcat
OLDNEW
« no previous file with comments | « mojo/devtools/common/pylib/__init__.py ('k') | mojo/devtools/common/pylib/apptest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698