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

Side by Side Diff: mojo/tools/mopy/android.py

Issue 1100903003: Move android.py to devtools. (Closed) Base URL: git@github.com:domokit/mojo.git@master
Patch Set: Created 5 years, 8 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/tools/devtools.py ('k') | no next file » | 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 datetime
7 import email.utils
8 import hashlib
9 import itertools
10 import json
11 import logging
12 import math
13 import os
14 import os.path
15 import random
16 import subprocess
17 import sys
18 import threading
19 import time
20 import urlparse
21
22 import SimpleHTTPServer
23 import SocketServer
24
25
26 # Tags used by the mojo shell application logs.
27 LOGCAT_TAGS = [
28 'AndroidHandler',
29 'MojoFileHelper',
30 'MojoMain',
31 'MojoShellActivity',
32 'MojoShellApplication',
33 'chromium',
34 ]
35
36 MOJO_SHELL_PACKAGE_NAME = 'org.chromium.mojo.shell'
37
38 MAPPING_PREFIX = '--map-origin='
39
40 DEFAULT_BASE_PORT = 31337
41
42 ZERO = datetime.timedelta(0)
43
44 class UTC_TZINFO(datetime.tzinfo):
45 """UTC time zone representation."""
46
47 def utcoffset(self, _):
48 return ZERO
49
50 def tzname(self, _):
51 return "UTC"
52
53 def dst(self, _):
54 return ZERO
55
56 UTC = UTC_TZINFO()
57
58 _logger = logging.getLogger()
59
60
61 class _SilentTCPServer(SocketServer.TCPServer):
62 """
63 A TCPServer that won't display any error, unless debugging is enabled. This is
64 useful because the client might stop while it is fetching an URL, which causes
65 spurious error messages.
66 """
67 def handle_error(self, request, client_address):
68 """
69 Override the base class method to have conditional logging.
70 """
71 if logging.getLogger().isEnabledFor(logging.DEBUG):
72 SocketServer.TCPServer.handle_error(self, request, client_address)
73
74
75 def _GetHandlerClassForPath(base_path):
76 class RequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
77 """
78 Handler for SocketServer.TCPServer that will serve the files from
79 |base_path| directory over http.
80 """
81
82 def __init__(self, *args, **kwargs):
83 self.etag = None
84 SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self, *args, **kwargs)
85
86 def get_etag(self):
87 if self.etag:
88 return self.etag
89
90 path = self.translate_path(self.path)
91 if not os.path.isfile(path):
92 return None
93
94 sha256 = hashlib.sha256()
95 BLOCKSIZE = 65536
96 with open(path, 'rb') as hashed:
97 buf = hashed.read(BLOCKSIZE)
98 while len(buf) > 0:
99 sha256.update(buf)
100 buf = hashed.read(BLOCKSIZE)
101 self.etag = '"%s"' % sha256.hexdigest()
102 return self.etag
103
104 def send_head(self):
105 # Always close the connection after each request, as the keep alive
106 # support from SimpleHTTPServer doesn't like when the client requests to
107 # close the connection before downloading the full response content.
108 # pylint: disable=W0201
109 self.close_connection = 1
110
111 path = self.translate_path(self.path)
112 if os.path.isfile(path):
113 # Handle If-None-Match
114 etag = self.get_etag()
115 if ('If-None-Match' in self.headers and
116 etag == self.headers['If-None-Match']):
117 self.send_response(304)
118 return None
119
120 # Handle If-Modified-Since
121 if ('If-None-Match' not in self.headers and
122 'If-Modified-Since' in self.headers):
123 last_modified = datetime.datetime.fromtimestamp(
124 math.floor(os.stat(path).st_mtime), tz=UTC)
125 ims = datetime.datetime(
126 *email.utils.parsedate(self.headers['If-Modified-Since'])[:6],
127 tzinfo=UTC)
128 if last_modified <= ims:
129 self.send_response(304)
130 return None
131
132 return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self)
133
134 def end_headers(self):
135 path = self.translate_path(self.path)
136
137 if os.path.isfile(path):
138 etag = self.get_etag()
139 if etag:
140 self.send_header('ETag', etag)
141 self.send_header('Cache-Control', 'must-revalidate')
142
143 return SimpleHTTPServer.SimpleHTTPRequestHandler.end_headers(self)
144
145 def translate_path(self, path):
146 path_from_current = (
147 SimpleHTTPServer.SimpleHTTPRequestHandler.translate_path(self, path))
148 return os.path.join(base_path, os.path.relpath(path_from_current))
149
150 def log_message(self, *_):
151 """
152 Override the base class method to disable logging.
153 """
154 pass
155
156 RequestHandler.protocol_version = 'HTTP/1.1'
157 return RequestHandler
158
159
160 def _IsMapOrigin(arg):
161 """Returns whether arg is a --map-origin argument."""
162 return arg.startswith(MAPPING_PREFIX)
163
164
165 def _Split(l, pred):
166 positive = []
167 negative = []
168 for v in l:
169 if pred(v):
170 positive.append(v)
171 else:
172 negative.append(v)
173 return (positive, negative)
174
175
176 def _ExitIfNeeded(process):
177 """
178 Exits |process| if it is still alive.
179 """
180 if process.poll() is None:
181 process.kill()
182
183
184 class AndroidShell(object):
185 """ Allows to set up and run a given mojo shell binary on an Android device.
186
187 Args:
188 shell_apk_path: path to the shell Android binary
189 local_dir: directory where locally build Mojo apps will be served, optional
190 adb_path: path to adb, optional if adb is in PATH
191 """
192 def __init__(self, shell_apk_path, local_dir=None, adb_path="adb"):
193 self.shell_apk_path = shell_apk_path
194 self.adb_path = adb_path
195 self.local_dir = local_dir
196
197 def _ReadFifo(self, fifo_path, pipe, on_fifo_closed, max_attempts=5):
198 """
199 Reads |fifo_path| on the device and write the contents to |pipe|. Calls
200 |on_fifo_closed| when the fifo is closed. This method will try to find the
201 path up to |max_attempts|, waiting 1 second between each attempt. If it
202 cannot find |fifo_path|, a exception will be raised.
203 """
204 fifo_command = [self.adb_path, 'shell', 'test -e "%s"; echo $?' % fifo_path]
205
206 def Run():
207 def _WaitForFifo():
208 for _ in xrange(max_attempts):
209 if subprocess.check_output(fifo_command)[0] == '0':
210 return
211 time.sleep(1)
212 if on_fifo_closed:
213 on_fifo_closed()
214 raise Exception("Unable to find fifo.")
215 _WaitForFifo()
216 stdout_cat = subprocess.Popen([self.adb_path,
217 'shell',
218 'cat',
219 fifo_path],
220 stdout=pipe)
221 atexit.register(_ExitIfNeeded, stdout_cat)
222 stdout_cat.wait()
223 if on_fifo_closed:
224 on_fifo_closed()
225
226 thread = threading.Thread(target=Run, name="StdoutRedirector")
227 thread.start()
228
229 def _MapPort(self, device_port, host_port):
230 """
231 Maps the device port to the host port. If |device_port| is 0, a random
232 available port is chosen. Returns the device port.
233 """
234 def _FindAvailablePortOnDevice():
235 opened = subprocess.check_output([self.adb_path, 'shell', 'netstat'])
236 opened = [int(x.strip().split()[3].split(':')[1])
237 for x in opened if x.startswith(' tcp')]
238 while True:
239 port = random.randint(4096, 16384)
240 if port not in opened:
241 return port
242 if device_port == 0:
243 device_port = _FindAvailablePortOnDevice()
244 subprocess.Popen([self.adb_path,
245 "reverse",
246 "tcp:%d" % device_port,
247 "tcp:%d" % host_port]).wait()
248
249 unmap_command = [self.adb_path, "reverse", "--remove",
250 "tcp:%d" % device_port]
251
252 def _UnmapPort():
253 subprocess.Popen(unmap_command)
254 atexit.register(_UnmapPort)
255 return device_port
256
257 def _StartHttpServerForDirectory(self, path, port=0):
258 """Starts an http server serving files from |path|. Returns the local
259 url."""
260 assert path
261 print 'starting http for', path
262 httpd = _SilentTCPServer(('127.0.0.1', 0), _GetHandlerClassForPath(path))
263 atexit.register(httpd.shutdown)
264
265 http_thread = threading.Thread(target=httpd.serve_forever)
266 http_thread.daemon = True
267 http_thread.start()
268
269 print 'local port=%d' % httpd.server_address[1]
270 return 'http://127.0.0.1:%d/' % self._MapPort(port, httpd.server_address[1])
271
272 def _StartHttpServerForOriginMapping(self, mapping, port):
273 """If |mapping| points at a local file starts an http server to serve files
274 from the directory and returns the new mapping.
275
276 This is intended to be called for every --map-origin value."""
277 parts = mapping.split('=')
278 if len(parts) != 2:
279 return mapping
280 dest = parts[1]
281 # If the destination is a url, don't map it.
282 if urlparse.urlparse(dest)[0]:
283 return mapping
284 # Assume the destination is a local file. Start a local server that
285 # redirects to it.
286 localUrl = self._StartHttpServerForDirectory(dest, port)
287 print 'started server at %s for %s' % (dest, localUrl)
288 return parts[0] + '=' + localUrl
289
290 def _StartHttpServerForOriginMappings(self, map_parameters, fixed_port):
291 """Calls _StartHttpServerForOriginMapping for every --map-origin
292 argument."""
293 if not map_parameters:
294 return []
295
296 original_values = list(itertools.chain(
297 *map(lambda x: x[len(MAPPING_PREFIX):].split(','), map_parameters)))
298 sorted(original_values)
299 result = []
300 for i, value in enumerate(original_values):
301 result.append(self._StartHttpServerForOriginMapping(
302 value, DEFAULT_BASE_PORT + 1 + i if fixed_port else 0))
303 return [MAPPING_PREFIX + ','.join(result)]
304
305 def PrepareShellRun(self, origin=None, fixed_port=True):
306 """ Prepares for StartShell: runs adb as root and installs the apk. If no
307 --origin is specified, local http server will be set up to serve files from
308 the build directory along with port forwarding.
309
310 Returns arguments that should be appended to shell argument list."""
311 if 'cannot run as root' in subprocess.check_output([self.adb_path, 'root']):
312 raise Exception("Unable to run adb as root.")
313 subprocess.check_call(
314 [self.adb_path, 'install', '-r', self.shell_apk_path, '-i',
315 MOJO_SHELL_PACKAGE_NAME])
316 atexit.register(self.StopShell)
317
318 extra_shell_args = []
319 origin_url = origin if origin else self._StartHttpServerForDirectory(
320 self.local_dir, DEFAULT_BASE_PORT if fixed_port else 0)
321 extra_shell_args.append("--origin=" + origin_url)
322
323 return extra_shell_args
324
325 def StartShell(self,
326 arguments,
327 stdout=None,
328 on_application_stop=None,
329 fixed_port=True):
330 """
331 Starts the mojo shell, passing it the given arguments.
332
333 The |arguments| list must contain the "--origin=" arg from PrepareShellRun.
334 If |stdout| is not None, it should be a valid argument for subprocess.Popen.
335 """
336 STDOUT_PIPE = "/data/data/%s/stdout.fifo" % MOJO_SHELL_PACKAGE_NAME
337
338 cmd = [self.adb_path,
339 'shell',
340 'am',
341 'start',
342 '-S',
343 '-a', 'android.intent.action.VIEW',
344 '-n', '%s/.MojoShellActivity' % MOJO_SHELL_PACKAGE_NAME]
345
346 parameters = []
347 if stdout or on_application_stop:
348 subprocess.check_call([self.adb_path, 'shell', 'rm', STDOUT_PIPE])
349 parameters.append('--fifo-path=%s' % STDOUT_PIPE)
350 self._ReadFifo(STDOUT_PIPE, stdout, on_application_stop)
351 # The origin has to be specified whether it's local or external.
352 assert any("--origin=" in arg for arg in arguments)
353
354 # Extract map-origin arguments.
355 map_parameters, other_parameters = _Split(arguments, _IsMapOrigin)
356 parameters += other_parameters
357 parameters += self._StartHttpServerForOriginMappings(map_parameters,
358 fixed_port)
359
360 if parameters:
361 encodedParameters = json.dumps(parameters)
362 cmd += ['--es', 'encodedParameters', encodedParameters]
363
364 with open(os.devnull, 'w') as devnull:
365 subprocess.Popen(cmd, stdout=devnull).wait()
366
367 def StopShell(self):
368 """
369 Stops the mojo shell.
370 """
371 subprocess.check_call(
372 [self.adb_path, 'shell', 'am', 'force-stop', MOJO_SHELL_PACKAGE_NAME])
373
374 def CleanLogs(self):
375 """
376 Cleans the logs on the device.
377 """
378 subprocess.check_call([self.adb_path, 'logcat', '-c'])
379
380 def ShowLogs(self):
381 """
382 Displays the log for the mojo shell.
383
384 Returns the process responsible for reading the logs.
385 """
386 logcat = subprocess.Popen([self.adb_path,
387 'logcat',
388 '-s',
389 ' '.join(LOGCAT_TAGS)],
390 stdout=sys.stdout)
391 atexit.register(_ExitIfNeeded, logcat)
392 return logcat
OLDNEW
« no previous file with comments | « mojo/tools/devtools.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698