OLD | NEW |
---|---|
1 # Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 import atexit | 5 import atexit |
6 import datetime | 6 import datetime |
7 import email.utils | 7 import email.utils |
8 import hashlib | 8 import hashlib |
9 import itertools | 9 import itertools |
10 import json | 10 import json |
11 import logging | 11 import logging |
12 import math | 12 import math |
13 import os | 13 import os |
14 import os.path | 14 import os.path |
15 import random | 15 import random |
16 import shutil | |
16 import subprocess | 17 import subprocess |
17 import sys | 18 import sys |
19 import tempfile | |
18 import threading | 20 import threading |
19 import time | 21 import time |
20 import urlparse | 22 import urlparse |
21 | 23 |
22 import SimpleHTTPServer | 24 import SimpleHTTPServer |
23 import SocketServer | 25 import SocketServer |
24 | 26 |
25 | 27 |
26 # Tags used by the mojo shell application logs. | 28 # Tags used by the mojo shell application logs. |
27 LOGCAT_TAGS = [ | 29 LOGCAT_TAGS = [ |
(...skipping 144 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
172 | 174 |
173 | 175 |
174 def _ExitIfNeeded(process): | 176 def _ExitIfNeeded(process): |
175 """ | 177 """ |
176 Exits |process| if it is still alive. | 178 Exits |process| if it is still alive. |
177 """ | 179 """ |
178 if process.poll() is None: | 180 if process.poll() is None: |
179 process.kill() | 181 process.kill() |
180 | 182 |
181 | 183 |
184 class Tmpdir(object): | |
msw
2015/05/22 02:41:43
nit: Maybe just inline this in _WaitForProcessIdAn
sky
2015/05/22 17:27:24
Done.
| |
185 """ | |
186 Creates a temp directory that is deleted when either Destroy() is called, or | |
187 normal exit. | |
188 """ | |
189 def __init__(self): | |
190 self.dir = tempfile.mkdtemp() | |
191 atexit.register(self.Destroy) | |
192 | |
193 def Destroy(self): | |
194 if self.dir: | |
195 shutil.rmtree(self.dir, True) | |
196 self.dir = None | |
197 | |
198 | |
182 class AndroidShell(object): | 199 class AndroidShell(object): |
183 """ Allows to set up and run a given mojo shell binary on an Android device. | 200 """ Allows to set up and run a given mojo shell binary on an Android device. |
184 | 201 |
185 Args: | 202 Args: |
186 shell_apk_path: path to the shell Android binary | 203 shell_apk_path: path to the shell Android binary |
187 local_dir: directory where locally build Mojo apps will be served, optional | 204 local_dir: directory where locally build Mojo apps will be served, optional |
188 adb_path: path to adb, optional if adb is in PATH | 205 adb_path: path to adb, optional if adb is in PATH |
189 target_device: device to run on, if multiple devices are connected | 206 target_device: device to run on, if multiple devices are connected |
207 src_root: root of the source tree | |
190 """ | 208 """ |
191 def __init__( | 209 def __init__( |
192 self, shell_apk_path, local_dir=None, adb_path="adb", target_device=None, | 210 self, shell_apk_path, local_dir=None, adb_path="adb", target_device=None, |
193 target_package=MOJO_SHELL_PACKAGE_NAME): | 211 target_package=MOJO_SHELL_PACKAGE_NAME, src_root=None): |
194 self.shell_apk_path = shell_apk_path | 212 self.shell_apk_path = shell_apk_path |
213 self.src_root = src_root | |
195 self.adb_path = adb_path | 214 self.adb_path = adb_path |
196 self.local_dir = local_dir | 215 self.local_dir = local_dir |
197 self.target_device = target_device | 216 self.target_device = target_device |
198 self.target_package = target_package | 217 self.target_package = target_package |
199 | 218 |
200 def _CreateADBCommand(self, args): | 219 def _CreateADBCommand(self, args): |
201 adb_command = [self.adb_path] | 220 adb_command = [self.adb_path] |
202 if self.target_device: | 221 if self.target_device: |
203 adb_command.extend(['-s', self.target_device]) | 222 adb_command.extend(['-s', self.target_device]) |
204 adb_command.extend(args) | 223 adb_command.extend(args) |
(...skipping 100 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
305 return [] | 324 return [] |
306 | 325 |
307 original_values = list(itertools.chain( | 326 original_values = list(itertools.chain( |
308 *map(lambda x: x[len(MAPPING_PREFIX):].split(','), map_parameters))) | 327 *map(lambda x: x[len(MAPPING_PREFIX):].split(','), map_parameters))) |
309 sorted(original_values) | 328 sorted(original_values) |
310 result = [] | 329 result = [] |
311 for value in original_values: | 330 for value in original_values: |
312 result.append(self._StartHttpServerForOriginMapping(value, 0)) | 331 result.append(self._StartHttpServerForOriginMapping(value, 0)) |
313 return [MAPPING_PREFIX + ','.join(result)] | 332 return [MAPPING_PREFIX + ','.join(result)] |
314 | 333 |
315 def PrepareShellRun(self, origin=None): | 334 def PrepareShellRun(self, origin=None, install=True): |
msw
2015/05/22 02:41:43
nit: The class member |shell_apk_path| isn't used
sky
2015/05/22 17:27:24
That would make the call sites more complex. I lef
| |
316 """ Prepares for StartShell: runs adb as root and installs the apk. If the | 335 """ Prepares for StartShell: runs adb as root and installs the apk. If the |
317 origin specified is 'localhost', a local http server will be set up to serve | 336 origin specified is 'localhost', a local http server will be set up to serve |
318 files from the build directory along with port forwarding. | 337 files from the build directory along with port forwarding. |
319 | 338 |
320 Returns arguments that should be appended to shell argument list.""" | 339 Returns arguments that should be appended to shell argument list.""" |
321 if 'cannot run as root' in subprocess.check_output( | 340 if 'cannot run as root' in subprocess.check_output( |
322 self._CreateADBCommand(['root'])): | 341 self._CreateADBCommand(['root'])): |
323 raise Exception("Unable to run adb as root.") | 342 raise Exception("Unable to run adb as root.") |
324 subprocess.check_call( | 343 if install: |
325 self._CreateADBCommand(['install', '-r', self.shell_apk_path, '-i', | 344 subprocess.check_call( |
326 self.target_package])) | 345 self._CreateADBCommand(['install', '-r', self.shell_apk_path, '-i', |
346 self.target_package])) | |
327 atexit.register(self.StopShell) | 347 atexit.register(self.StopShell) |
328 | 348 |
329 extra_shell_args = [] | 349 extra_shell_args = [] |
330 if origin is 'localhost': | 350 if origin is 'localhost': |
331 origin = self._StartHttpServerForDirectory(self.local_dir, 0) | 351 origin = self._StartHttpServerForDirectory(self.local_dir, 0) |
332 if origin: | 352 if origin: |
333 extra_shell_args.append("--origin=" + origin) | 353 extra_shell_args.append("--origin=" + origin) |
334 return extra_shell_args | 354 return extra_shell_args |
335 | 355 |
356 def PrepareGdb(self): | |
357 subprocess.check_call(self._CreateADBCommand(['forward', 'tcp:5039', | |
msw
2015/05/22 02:41:44
nit: Could this be inlined in (or called from) Sta
sky
2015/05/22 17:27:24
Done.
| |
358 'tcp:5039'])) | |
359 | |
360 def _GetProcessId(self, process): | |
361 """Returns the process id of the process on the remote device.""" | |
362 while True: | |
363 line = process.stdout.readline() | |
364 pid_command = 'launcher waiting for GDB. pid: ' | |
365 index = line.find(pid_command) | |
366 if index != -1: | |
367 return line[index + len(pid_command):].strip() | |
368 return 0 | |
369 | |
370 def _GetLocalGdbPath(self): | |
msw
2015/05/22 02:41:43
nit: inline in caller?
sky
2015/05/22 17:27:24
IMO the separate function makes this more readable
| |
371 """Returns the path to the android gdb.""" | |
372 return os.path.join(self.src_root, "third_party", "android_tools", "ndk", | |
373 "toolchains", "arm-linux-androideabi-4.9", "prebuilt", | |
374 "linux-x86_64", "bin", "arm-linux-androideabi-gdb") | |
375 | |
376 def _WaitForProcessIdAndStartGdb(self, process): | |
377 """Waits until we see the process id from the remote device, starts up | |
378 gdbserver on the remote device, and gdb on the local device.""" | |
379 # Wait until we see "PID" | |
380 pid = self._GetProcessId(process) | |
msw
2015/05/22 02:41:44
nit: maybe ensure this doesn't return 0?
sky
2015/05/22 17:27:24
Done.
| |
381 gdbserver_process = subprocess.Popen(self._CreateADBCommand(['shell', | |
382 'gdbserver', | |
383 '--attach', | |
384 ':5039', | |
385 pid])) | |
386 atexit.register(_ExitIfNeeded, gdbserver_process) | |
msw
2015/05/22 02:41:43
Interesting, will this do the right thing if the p
sky
2015/05/22 17:27:24
Earlier code makes sure the app isn't already runn
| |
387 | |
388 tmpdir = Tmpdir() | |
389 gdbinit_path = os.path.join(tmpdir.dir, 'gdbinit') | |
390 _CreateGdbInit(tmpdir.dir, gdbinit_path) | |
391 | |
392 _CreateSOLinks(tmpdir.dir, self.local_dir) | |
393 | |
394 # Wait a second for gdb to start up on the device. | |
msw
2015/05/22 02:41:43
Hmm, I wonder how reliable this is, and what the f
sky
2015/05/22 17:27:24
No doubt it's flakey. I added a TODO to try a coup
| |
395 time.sleep(1) | |
396 | |
397 local_gdb_process = subprocess.Popen([self._GetLocalGdbPath(), | |
398 "-x", | |
399 gdbinit_path], | |
400 cwd=tmpdir.dir) | |
401 atexit.register(_ExitIfNeeded, local_gdb_process) | |
402 local_gdb_process.wait() | |
403 | |
336 def StartShell(self, | 404 def StartShell(self, |
337 arguments, | 405 arguments, |
338 stdout=None, | 406 stdout=None, |
339 on_application_stop=None): | 407 on_application_stop=None, |
408 gdb=False): | |
340 """ | 409 """ |
341 Starts the mojo shell, passing it the given arguments. | 410 Starts the mojo shell, passing it the given arguments. |
342 | 411 |
343 The |arguments| list must contain the "--origin=" arg from PrepareShellRun. | 412 The |arguments| list must contain the "--origin=" arg from PrepareShellRun. |
344 If |stdout| is not None, it should be a valid argument for subprocess.Popen. | 413 If |stdout| is not None, it should be a valid argument for subprocess.Popen. |
345 """ | 414 """ |
346 STDOUT_PIPE = "/data/data/%s/stdout.fifo" % self.target_package | 415 STDOUT_PIPE = "/data/data/%s/stdout.fifo" % self.target_package |
347 | 416 |
348 cmd = self._CreateADBCommand([ | 417 cmd = self._CreateADBCommand([ |
349 'shell', | 418 'shell', |
350 'am', | 419 'am', |
351 'start', | 420 'start', |
352 '-S', | 421 '-S', |
353 '-a', 'android.intent.action.VIEW', | 422 '-a', 'android.intent.action.VIEW', |
354 '-n', '%s/%s.MojoShellActivity' % (self.target_package, | 423 '-n', '%s/%s.MojoShellActivity' % (self.target_package, |
355 MOJO_SHELL_PACKAGE_NAME)]) | 424 MOJO_SHELL_PACKAGE_NAME)]) |
356 | 425 |
426 logcat_process = None | |
427 | |
428 if gdb: | |
429 arguments += ['--wait-for-debugger'] | |
430 logcat_process = self.ShowLogs(stdout=subprocess.PIPE) | |
431 | |
357 parameters = [] | 432 parameters = [] |
358 if stdout or on_application_stop: | 433 if stdout or on_application_stop: |
359 subprocess.check_call(self._CreateADBCommand( | 434 subprocess.check_call(self._CreateADBCommand( |
360 ['shell', 'rm', '-f', STDOUT_PIPE])) | 435 ['shell', 'rm', '-f', STDOUT_PIPE])) |
361 parameters.append('--fifo-path=%s' % STDOUT_PIPE) | 436 parameters.append('--fifo-path=%s' % STDOUT_PIPE) |
362 max_attempts = 5 | 437 max_attempts = 5 |
363 if '--wait-for-debugger' in arguments: | 438 if '--wait-for-debugger' in arguments: |
364 max_attempts = 200 | 439 max_attempts = 200 |
365 self._ReadFifo(STDOUT_PIPE, stdout, on_application_stop, | 440 self._ReadFifo(STDOUT_PIPE, stdout, on_application_stop, |
366 max_attempts=max_attempts) | 441 max_attempts=max_attempts) |
367 | 442 |
368 # Extract map-origin arguments. | 443 # Extract map-origin arguments. |
369 map_parameters, other_parameters = _Split(arguments, _IsMapOrigin) | 444 map_parameters, other_parameters = _Split(arguments, _IsMapOrigin) |
370 parameters += other_parameters | 445 parameters += other_parameters |
371 parameters += self._StartHttpServerForOriginMappings(map_parameters) | 446 parameters += self._StartHttpServerForOriginMappings(map_parameters) |
372 | 447 |
373 if parameters: | 448 if parameters: |
374 encodedParameters = json.dumps(parameters) | 449 encodedParameters = json.dumps(parameters) |
375 cmd += ['--es', 'encodedParameters', encodedParameters] | 450 cmd += ['--es', 'encodedParameters', encodedParameters] |
376 | 451 |
377 with open(os.devnull, 'w') as devnull: | 452 with open(os.devnull, 'w') as devnull: |
378 subprocess.Popen(cmd, stdout=devnull).wait() | 453 cmd_process = subprocess.Popen(cmd, stdout=devnull) |
454 if logcat_process: | |
455 self._WaitForProcessIdAndStartGdb(logcat_process) | |
456 cmd_process.wait() | |
379 | 457 |
380 def StopShell(self): | 458 def StopShell(self): |
381 """ | 459 """ |
382 Stops the mojo shell. | 460 Stops the mojo shell. |
383 """ | 461 """ |
384 subprocess.check_call(self._CreateADBCommand(['shell', | 462 subprocess.check_call(self._CreateADBCommand(['shell', |
385 'am', | 463 'am', |
386 'force-stop', | 464 'force-stop', |
387 self.target_package])) | 465 self.target_package])) |
388 | 466 |
389 def CleanLogs(self): | 467 def CleanLogs(self): |
390 """ | 468 """ |
391 Cleans the logs on the device. | 469 Cleans the logs on the device. |
392 """ | 470 """ |
393 subprocess.check_call(self._CreateADBCommand(['logcat', '-c'])) | 471 subprocess.check_call(self._CreateADBCommand(['logcat', '-c'])) |
394 | 472 |
395 def ShowLogs(self): | 473 def ShowLogs(self, stdout=sys.stdout): |
396 """ | 474 """ |
397 Displays the log for the mojo shell. | 475 Displays the log for the mojo shell. |
398 | 476 |
399 Returns the process responsible for reading the logs. | 477 Returns the process responsible for reading the logs. |
400 """ | 478 """ |
401 logcat = subprocess.Popen(self._CreateADBCommand([ | 479 logcat = subprocess.Popen(self._CreateADBCommand([ |
402 'logcat', | 480 'logcat', |
403 '-s', | 481 '-s', |
404 ' '.join(LOGCAT_TAGS)]), | 482 ' '.join(LOGCAT_TAGS)]), |
405 stdout=sys.stdout) | 483 stdout=stdout) |
406 atexit.register(_ExitIfNeeded, logcat) | 484 atexit.register(_ExitIfNeeded, logcat) |
407 return logcat | 485 return logcat |
486 | |
msw
2015/05/22 02:41:43
nit: the rest of this file uses one blank line bet
sky
2015/05/22 17:27:24
That is true for class functions, but not top leve
| |
487 | |
488 def _CreateGdbInit(tmp_dir, gdb_init_path): | |
489 """ | |
490 Creates the gdbinit file. | |
491 Args: | |
492 tmp_dir: the directory where the gdbinit and other files lives. | |
493 gdb_path: path to gdbinit | |
msw
2015/05/22 02:41:43
nit: gdb_init_path
sky
2015/05/22 17:27:24
Done.
| |
494 """ | |
495 gdbinit = ('target remote localhost:5039\n' | |
msw
2015/05/22 02:41:43
How odd, I've never seen this before...
| |
496 'def reload-symbols\n' | |
497 ' set solib-search-path %s\n' | |
498 'end\n' | |
499 'def info-symbols\n' | |
500 ' info sharedlibrary\n' | |
501 'end\n' | |
502 'echo \\n\\n' | |
503 'You are now in gdb and need to type continue (or c) to continue ' | |
504 'execution.\\n' | |
505 'gdb is in the directory %s\\n' | |
506 'The following functions have been defined:\\n' | |
507 'reload-symbols: forces reloading symbols. If after a crash you\\n' | |
508 'still do not see symbols you likely need to create a link in\\n' | |
509 'the directory you are in.\\n' | |
510 'info-symbols: shows status of current shared libraries.\\n\\n' % | |
511 (tmp_dir, tmp_dir)) | |
512 with open(gdb_init_path, 'w') as f: | |
513 f.write(gdbinit) | |
514 | |
515 | |
516 def _CreateSOLinks(dest_dir, build_dir): | |
517 """ | |
518 Creates links from files (such as mojo files) to the real .so so that gdb can | |
519 find them. | |
520 """ | |
521 # The files to create links for. The key is the name as seen on the device, | |
522 # and the target an array of path elements as to where the .so lives (relative | |
523 # to the output directory). | |
524 files_to_link = { | |
525 'html_viewer.mojo': ['libhtml_viewer', 'html_viewer_library.so'] | |
msw
2015/05/22 02:41:44
nit: trailing comma
sky
2015/05/22 17:27:24
Done.
| |
526 } | |
527 for android_name, so_path in files_to_link.iteritems(): | |
528 src = os.path.join(build_dir, *so_path) | |
529 if not os.path.isfile(src): | |
530 print 'Expected file not found', src | |
531 sys.exit(-1) | |
532 os.symlink(src, os.path.join(dest_dir, android_name)) | |
msw
2015/05/22 02:41:43
Do we need to clean these up at all?
sky
2015/05/22 17:27:24
This is put in the directory that is removed by sh
| |
OLD | NEW |