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 |
| 17 import signal |
16 import subprocess | 18 import subprocess |
17 import sys | 19 import sys |
| 20 import tempfile |
18 import threading | 21 import threading |
19 import time | 22 import time |
20 import urlparse | 23 import urlparse |
21 | 24 |
22 import SimpleHTTPServer | 25 import SimpleHTTPServer |
23 import SocketServer | 26 import SocketServer |
24 | 27 |
25 | 28 |
26 # Tags used by the mojo shell application logs. | 29 # Tags used by the mojo shell application logs. |
27 LOGCAT_TAGS = [ | 30 LOGCAT_TAGS = [ |
(...skipping 150 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
178 | 181 |
179 | 182 |
180 class AndroidShell(object): | 183 class AndroidShell(object): |
181 """ Allows to set up and run a given mojo shell binary on an Android device. | 184 """ Allows to set up and run a given mojo shell binary on an Android device. |
182 | 185 |
183 Args: | 186 Args: |
184 shell_apk_path: path to the shell Android binary | 187 shell_apk_path: path to the shell Android binary |
185 local_dir: directory where locally build Mojo apps will be served, optional | 188 local_dir: directory where locally build Mojo apps will be served, optional |
186 adb_path: path to adb, optional if adb is in PATH | 189 adb_path: path to adb, optional if adb is in PATH |
187 target_device: device to run on, if multiple devices are connected | 190 target_device: device to run on, if multiple devices are connected |
| 191 src_root: root of the source tree |
188 """ | 192 """ |
189 def __init__( | 193 def __init__( |
190 self, shell_apk_path, local_dir=None, adb_path="adb", target_device=None, | 194 self, shell_apk_path, local_dir=None, adb_path="adb", target_device=None, |
191 target_package=MOJO_SHELL_PACKAGE_NAME): | 195 target_package=MOJO_SHELL_PACKAGE_NAME, src_root=None): |
192 self.shell_apk_path = shell_apk_path | 196 self.shell_apk_path = shell_apk_path |
| 197 self.src_root = src_root |
193 self.adb_path = adb_path | 198 self.adb_path = adb_path |
194 self.local_dir = local_dir | 199 self.local_dir = local_dir |
195 self.target_device = target_device | 200 self.target_device = target_device |
196 self.target_package = target_package | 201 self.target_package = target_package |
197 | 202 |
198 def _CreateADBCommand(self, args): | 203 def _CreateADBCommand(self, args): |
199 adb_command = [self.adb_path] | 204 adb_command = [self.adb_path] |
200 if self.target_device: | 205 if self.target_device: |
201 adb_command.extend(['-s', self.target_device]) | 206 adb_command.extend(['-s', self.target_device]) |
202 adb_command.extend(args) | 207 adb_command.extend(args) |
(...skipping 100 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
303 return [] | 308 return [] |
304 | 309 |
305 original_values = list(itertools.chain( | 310 original_values = list(itertools.chain( |
306 *map(lambda x: x[len(MAPPING_PREFIX):].split(','), map_parameters))) | 311 *map(lambda x: x[len(MAPPING_PREFIX):].split(','), map_parameters))) |
307 sorted(original_values) | 312 sorted(original_values) |
308 result = [] | 313 result = [] |
309 for value in original_values: | 314 for value in original_values: |
310 result.append(self._StartHttpServerForOriginMapping(value, 0)) | 315 result.append(self._StartHttpServerForOriginMapping(value, 0)) |
311 return [MAPPING_PREFIX + ','.join(result)] | 316 return [MAPPING_PREFIX + ','.join(result)] |
312 | 317 |
313 def PrepareShellRun(self, origin=None): | 318 def PrepareShellRun(self, origin=None, install=True, gdb=False): |
314 """ Prepares for StartShell: runs adb as root and installs the apk. If the | 319 """ Prepares for StartShell: runs adb as root and installs the apk. If the |
315 origin specified is 'localhost', a local http server will be set up to serve | 320 origin specified is 'localhost', a local http server will be set up to serve |
316 files from the build directory along with port forwarding. | 321 files from the build directory along with port forwarding. |
317 | 322 |
318 Returns arguments that should be appended to shell argument list.""" | 323 Returns arguments that should be appended to shell argument list.""" |
319 # TODO(msw): Remove logging after devices are found; http://crbug.com/486220 | 324 # TODO(msw): Remove logging after devices are found; http://crbug.com/486220 |
320 logging.getLogger().debug("Path to adb: %s", self.adb_path) | 325 logging.getLogger().debug("Path to adb: %s", self.adb_path) |
321 logging.getLogger().debug("adb devices: %s", | 326 logging.getLogger().debug("adb devices: %s", |
322 subprocess.check_output(self._CreateADBCommand(['devices']))) | 327 subprocess.check_output(self._CreateADBCommand(['devices']))) |
323 | 328 |
324 subprocess.check_call(self._CreateADBCommand(['root'])) | 329 subprocess.check_call(self._CreateADBCommand(['root'])) |
325 subprocess.check_call( | 330 if install: |
326 self._CreateADBCommand(['install', '-r', self.shell_apk_path, '-i', | 331 subprocess.check_call( |
327 self.target_package])) | 332 self._CreateADBCommand(['install', '-r', self.shell_apk_path, '-i', |
| 333 self.target_package])) |
| 334 |
328 atexit.register(self.StopShell) | 335 atexit.register(self.StopShell) |
329 | 336 |
330 extra_args = [] | 337 extra_args = [] |
331 if origin is 'localhost': | 338 if origin is 'localhost': |
332 origin = self._StartHttpServerForDirectory(self.local_dir, 0) | 339 origin = self._StartHttpServerForDirectory(self.local_dir, 0) |
333 if origin: | 340 if origin: |
334 extra_args.append("--origin=" + origin) | 341 extra_args.append("--origin=" + origin) |
| 342 |
| 343 if gdb: |
| 344 # Remote debugging needs a port forwarded. |
| 345 subprocess.check_call(self._CreateADBCommand(['forward', 'tcp:5039', |
| 346 'tcp:5039'])) |
| 347 |
335 return extra_args | 348 return extra_args |
336 | 349 |
| 350 def _GetProcessId(self, process): |
| 351 """Returns the process id of the process on the remote device.""" |
| 352 while True: |
| 353 line = process.stdout.readline() |
| 354 pid_command = 'launcher waiting for GDB. pid: ' |
| 355 index = line.find(pid_command) |
| 356 if index != -1: |
| 357 return line[index + len(pid_command):].strip() |
| 358 return 0 |
| 359 |
| 360 def _GetLocalGdbPath(self): |
| 361 """Returns the path to the android gdb.""" |
| 362 return os.path.join(self.src_root, "third_party", "android_tools", "ndk", |
| 363 "toolchains", "arm-linux-androideabi-4.9", "prebuilt", |
| 364 "linux-x86_64", "bin", "arm-linux-androideabi-gdb") |
| 365 |
| 366 def _WaitForProcessIdAndStartGdb(self, process): |
| 367 """Waits until we see the process id from the remote device, starts up |
| 368 gdbserver on the remote device, and gdb on the local device.""" |
| 369 # Wait until we see "PID" |
| 370 pid = self._GetProcessId(process) |
| 371 assert pid != 0 |
| 372 # No longer need the logcat process. |
| 373 process.kill() |
| 374 # Disable python's processing of SIGINT while running gdb. Otherwise |
| 375 # control-c doesn't work well in gdb. |
| 376 signal.signal(signal.SIGINT, signal.SIG_IGN) |
| 377 gdbserver_process = subprocess.Popen(self._CreateADBCommand(['shell', |
| 378 'gdbserver', |
| 379 '--attach', |
| 380 ':5039', |
| 381 pid])) |
| 382 atexit.register(_ExitIfNeeded, gdbserver_process) |
| 383 |
| 384 temp_dir = tempfile.mkdtemp() |
| 385 atexit.register(shutil.rmtree, temp_dir, True) |
| 386 |
| 387 gdbinit_path = os.path.join(temp_dir, 'gdbinit') |
| 388 _CreateGdbInit(temp_dir, gdbinit_path, self.local_dir) |
| 389 |
| 390 _CreateSOLinks(temp_dir, self.local_dir) |
| 391 |
| 392 # Wait a second for gdb to start up on the device. Without this the local |
| 393 # gdb starts before the remote side has registered the port. |
| 394 # TODO(sky): maybe we should try a couple of times and then give up? |
| 395 time.sleep(1) |
| 396 |
| 397 local_gdb_process = subprocess.Popen([self._GetLocalGdbPath(), |
| 398 "-x", |
| 399 gdbinit_path], |
| 400 cwd=temp_dir) |
| 401 atexit.register(_ExitIfNeeded, local_gdb_process) |
| 402 local_gdb_process.wait() |
| 403 signal.signal(signal.SIGINT, signal.SIG_DFL) |
| 404 |
337 def StartShell(self, | 405 def StartShell(self, |
338 arguments, | 406 arguments, |
339 stdout=None, | 407 stdout=None, |
340 on_application_stop=None): | 408 on_application_stop=None, |
| 409 gdb=False): |
341 """ | 410 """ |
342 Starts the mojo shell, passing it the given arguments. | 411 Starts the mojo shell, passing it the given arguments. |
343 | 412 |
344 The |arguments| list must contain the "--origin=" arg from PrepareShellRun. | 413 The |arguments| list must contain the "--origin=" arg from PrepareShellRun. |
345 If |stdout| is not None, it should be a valid argument for subprocess.Popen. | 414 If |stdout| is not None, it should be a valid argument for subprocess.Popen. |
346 """ | 415 """ |
| 416 |
347 STDOUT_PIPE = "/data/data/%s/stdout.fifo" % self.target_package | 417 STDOUT_PIPE = "/data/data/%s/stdout.fifo" % self.target_package |
348 | 418 |
349 cmd = self._CreateADBCommand([ | 419 cmd = self._CreateADBCommand([ |
350 'shell', | 420 'shell', |
351 'am', | 421 'am', |
352 'start', | 422 'start', |
353 '-S', | 423 '-S', |
354 '-a', 'android.intent.action.VIEW', | 424 '-a', 'android.intent.action.VIEW', |
355 '-n', '%s/%s.MojoShellActivity' % (self.target_package, | 425 '-n', '%s/%s.MojoShellActivity' % (self.target_package, |
356 MOJO_SHELL_PACKAGE_NAME)]) | 426 MOJO_SHELL_PACKAGE_NAME)]) |
357 | 427 |
| 428 logcat_process = None |
| 429 |
| 430 if gdb: |
| 431 arguments += ['--wait-for-debugger'] |
| 432 logcat_process = self.ShowLogs(stdout=subprocess.PIPE) |
| 433 |
358 parameters = [] | 434 parameters = [] |
359 if stdout or on_application_stop: | 435 if stdout or on_application_stop: |
360 subprocess.check_call(self._CreateADBCommand( | 436 subprocess.check_call(self._CreateADBCommand( |
361 ['shell', 'rm', '-f', STDOUT_PIPE])) | 437 ['shell', 'rm', '-f', STDOUT_PIPE])) |
362 parameters.append('--fifo-path=%s' % STDOUT_PIPE) | 438 parameters.append('--fifo-path=%s' % STDOUT_PIPE) |
363 max_attempts = 5 | 439 max_attempts = 5 |
364 if '--wait-for-debugger' in arguments: | 440 if '--wait-for-debugger' in arguments: |
365 max_attempts = 200 | 441 max_attempts = 200 |
366 self._ReadFifo(STDOUT_PIPE, stdout, on_application_stop, | 442 self._ReadFifo(STDOUT_PIPE, stdout, on_application_stop, |
367 max_attempts=max_attempts) | 443 max_attempts=max_attempts) |
368 | 444 |
369 # Extract map-origin arguments. | 445 # Extract map-origin arguments. |
370 map_parameters, other_parameters = _Split(arguments, _IsMapOrigin) | 446 map_parameters, other_parameters = _Split(arguments, _IsMapOrigin) |
371 parameters += other_parameters | 447 parameters += other_parameters |
372 parameters += self._StartHttpServerForOriginMappings(map_parameters) | 448 parameters += self._StartHttpServerForOriginMappings(map_parameters) |
373 | 449 |
374 if parameters: | 450 if parameters: |
375 encodedParameters = json.dumps(parameters) | 451 encodedParameters = json.dumps(parameters) |
376 cmd += ['--es', 'encodedParameters', encodedParameters] | 452 cmd += ['--es', 'encodedParameters', encodedParameters] |
377 | 453 |
378 with open(os.devnull, 'w') as devnull: | 454 with open(os.devnull, 'w') as devnull: |
379 subprocess.Popen(cmd, stdout=devnull).wait() | 455 cmd_process = subprocess.Popen(cmd, stdout=devnull) |
| 456 if logcat_process: |
| 457 self._WaitForProcessIdAndStartGdb(logcat_process) |
| 458 cmd_process.wait() |
380 | 459 |
381 def StopShell(self): | 460 def StopShell(self): |
382 """ | 461 """ |
383 Stops the mojo shell. | 462 Stops the mojo shell. |
384 """ | 463 """ |
385 subprocess.check_call(self._CreateADBCommand(['shell', | 464 subprocess.check_call(self._CreateADBCommand(['shell', |
386 'am', | 465 'am', |
387 'force-stop', | 466 'force-stop', |
388 self.target_package])) | 467 self.target_package])) |
389 | 468 |
390 def CleanLogs(self): | 469 def CleanLogs(self): |
391 """ | 470 """ |
392 Cleans the logs on the device. | 471 Cleans the logs on the device. |
393 """ | 472 """ |
394 subprocess.check_call(self._CreateADBCommand(['logcat', '-c'])) | 473 subprocess.check_call(self._CreateADBCommand(['logcat', '-c'])) |
395 | 474 |
396 def ShowLogs(self): | 475 def ShowLogs(self, stdout=sys.stdout): |
397 """ | 476 """ |
398 Displays the log for the mojo shell. | 477 Displays the log for the mojo shell. |
399 | 478 |
400 Returns the process responsible for reading the logs. | 479 Returns the process responsible for reading the logs. |
401 """ | 480 """ |
402 logcat = subprocess.Popen(self._CreateADBCommand([ | 481 logcat = subprocess.Popen(self._CreateADBCommand([ |
403 'logcat', | 482 'logcat', |
404 '-s', | 483 '-s', |
405 ' '.join(LOGCAT_TAGS)]), | 484 ' '.join(LOGCAT_TAGS)]), |
406 stdout=sys.stdout) | 485 stdout=stdout) |
407 atexit.register(_ExitIfNeeded, logcat) | 486 atexit.register(_ExitIfNeeded, logcat) |
408 return logcat | 487 return logcat |
| 488 |
| 489 |
| 490 def _CreateGdbInit(tmp_dir, gdb_init_path, build_dir): |
| 491 """ |
| 492 Creates the gdbinit file. |
| 493 Args: |
| 494 tmp_dir: the directory where the gdbinit and other files lives. |
| 495 gdb_init_path: path to gdbinit |
| 496 build_dir: path where build files are located. |
| 497 """ |
| 498 gdbinit = ('target remote localhost:5039\n' |
| 499 'def reload-symbols\n' |
| 500 ' set solib-search-path %s:%s\n' |
| 501 'end\n' |
| 502 'def info-symbols\n' |
| 503 ' info sharedlibrary\n' |
| 504 'end\n' |
| 505 'reload-symbols\n' |
| 506 'echo \\n\\n' |
| 507 'You are now in gdb and need to type continue (or c) to continue ' |
| 508 'execution.\\n' |
| 509 'gdb is in the directory %s\\n' |
| 510 'The following functions have been defined:\\n' |
| 511 'reload-symbols: forces reloading symbols. If after a crash you\\n' |
| 512 'still do not see symbols you likely need to create a link in\\n' |
| 513 'the directory you are in.\\n' |
| 514 'info-symbols: shows status of current shared libraries.\\n' |
| 515 'NOTE: you may need to type reload-symbols again after a ' |
| 516 'crash.\\n\\n' % (tmp_dir, build_dir, tmp_dir)) |
| 517 with open(gdb_init_path, 'w') as f: |
| 518 f.write(gdbinit) |
| 519 |
| 520 |
| 521 def _CreateSOLinks(dest_dir, build_dir): |
| 522 """ |
| 523 Creates links from files (such as mojo files) to the real .so so that gdb can |
| 524 find them. |
| 525 """ |
| 526 # The files to create links for. The key is the name as seen on the device, |
| 527 # and the target an array of path elements as to where the .so lives (relative |
| 528 # to the output directory). |
| 529 # TODO(sky): come up with some way to automate this. |
| 530 files_to_link = { |
| 531 'html_viewer.mojo': ['libhtml_viewer', 'html_viewer_library.so'], |
| 532 'libmandoline_runner.so': ['mandoline_runner'], |
| 533 } |
| 534 for android_name, so_path in files_to_link.iteritems(): |
| 535 src = os.path.join(build_dir, *so_path) |
| 536 if not os.path.isfile(src): |
| 537 print 'Expected file not found', src |
| 538 sys.exit(-1) |
| 539 os.symlink(src, os.path.join(dest_dir, android_name)) |
OLD | NEW |