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

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

Issue 1152663002: Makes android.py support gdb (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: 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
OLDNEW
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
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
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
OLDNEW
« mojo/tools/android_mojo_shell.py ('K') | « mojo/tools/android_mojo_shell.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698