Chromium Code Reviews| 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 |