Index: mojo/tools/mopy/android.py |
diff --git a/mojo/tools/mopy/android.py b/mojo/tools/mopy/android.py |
index 74499b20447a6dcd72f011ca3f182bfaf2670bcb..1bf66df429bdcc0a45e7e67d2553806da17bf902 100644 |
--- a/mojo/tools/mopy/android.py |
+++ b/mojo/tools/mopy/android.py |
@@ -13,8 +13,11 @@ import math |
import os |
import os.path |
import random |
+import shutil |
+import signal |
import subprocess |
import sys |
+import tempfile |
import threading |
import time |
import urlparse |
@@ -185,11 +188,13 @@ class AndroidShell(object): |
local_dir: directory where locally build Mojo apps will be served, optional |
adb_path: path to adb, optional if adb is in PATH |
target_device: device to run on, if multiple devices are connected |
+ src_root: root of the source tree |
""" |
def __init__( |
self, shell_apk_path, local_dir=None, adb_path="adb", target_device=None, |
- target_package=MOJO_SHELL_PACKAGE_NAME): |
+ target_package=MOJO_SHELL_PACKAGE_NAME, src_root=None): |
self.shell_apk_path = shell_apk_path |
+ self.src_root = src_root |
self.adb_path = adb_path |
self.local_dir = local_dir |
self.target_device = target_device |
@@ -310,7 +315,7 @@ class AndroidShell(object): |
result.append(self._StartHttpServerForOriginMapping(value, 0)) |
return [MAPPING_PREFIX + ','.join(result)] |
- def PrepareShellRun(self, origin=None): |
+ def PrepareShellRun(self, origin=None, install=True, gdb=False): |
""" Prepares for StartShell: runs adb as root and installs the apk. If the |
origin specified is 'localhost', a local http server will be set up to serve |
files from the build directory along with port forwarding. |
@@ -322,9 +327,11 @@ class AndroidShell(object): |
subprocess.check_output(self._CreateADBCommand(['devices']))) |
subprocess.check_call(self._CreateADBCommand(['root'])) |
- subprocess.check_call( |
- self._CreateADBCommand(['install', '-r', self.shell_apk_path, '-i', |
- self.target_package])) |
+ if install: |
+ subprocess.check_call( |
+ self._CreateADBCommand(['install', '-r', self.shell_apk_path, '-i', |
+ self.target_package])) |
+ |
atexit.register(self.StopShell) |
extra_args = [] |
@@ -332,18 +339,81 @@ class AndroidShell(object): |
origin = self._StartHttpServerForDirectory(self.local_dir, 0) |
if origin: |
extra_args.append("--origin=" + origin) |
+ |
+ if gdb: |
+ # Remote debugging needs a port forwarded. |
+ subprocess.check_call(self._CreateADBCommand(['forward', 'tcp:5039', |
+ 'tcp:5039'])) |
+ |
return extra_args |
+ def _GetProcessId(self, process): |
+ """Returns the process id of the process on the remote device.""" |
+ while True: |
+ line = process.stdout.readline() |
+ pid_command = 'launcher waiting for GDB. pid: ' |
+ index = line.find(pid_command) |
+ if index != -1: |
+ return line[index + len(pid_command):].strip() |
+ return 0 |
+ |
+ def _GetLocalGdbPath(self): |
+ """Returns the path to the android gdb.""" |
+ return os.path.join(self.src_root, "third_party", "android_tools", "ndk", |
+ "toolchains", "arm-linux-androideabi-4.9", "prebuilt", |
+ "linux-x86_64", "bin", "arm-linux-androideabi-gdb") |
+ |
+ def _WaitForProcessIdAndStartGdb(self, process): |
+ """Waits until we see the process id from the remote device, starts up |
+ gdbserver on the remote device, and gdb on the local device.""" |
+ # Wait until we see "PID" |
+ pid = self._GetProcessId(process) |
+ assert pid != 0 |
+ # No longer need the logcat process. |
+ process.kill() |
+ # Disable python's processing of SIGINT while running gdb. Otherwise |
+ # control-c doesn't work well in gdb. |
+ signal.signal(signal.SIGINT, signal.SIG_IGN) |
+ gdbserver_process = subprocess.Popen(self._CreateADBCommand(['shell', |
+ 'gdbserver', |
+ '--attach', |
+ ':5039', |
+ pid])) |
+ atexit.register(_ExitIfNeeded, gdbserver_process) |
+ |
+ temp_dir = tempfile.mkdtemp() |
+ atexit.register(shutil.rmtree, temp_dir, True) |
+ |
+ gdbinit_path = os.path.join(temp_dir, 'gdbinit') |
+ _CreateGdbInit(temp_dir, gdbinit_path, self.local_dir) |
+ |
+ _CreateSOLinks(temp_dir, self.local_dir) |
+ |
+ # Wait a second for gdb to start up on the device. Without this the local |
+ # gdb starts before the remote side has registered the port. |
+ # TODO(sky): maybe we should try a couple of times and then give up? |
+ time.sleep(1) |
+ |
+ local_gdb_process = subprocess.Popen([self._GetLocalGdbPath(), |
+ "-x", |
+ gdbinit_path], |
+ cwd=temp_dir) |
+ atexit.register(_ExitIfNeeded, local_gdb_process) |
+ local_gdb_process.wait() |
+ signal.signal(signal.SIGINT, signal.SIG_DFL) |
+ |
def StartShell(self, |
arguments, |
stdout=None, |
- on_application_stop=None): |
+ on_application_stop=None, |
+ gdb=False): |
""" |
Starts the mojo shell, passing it the given arguments. |
The |arguments| list must contain the "--origin=" arg from PrepareShellRun. |
If |stdout| is not None, it should be a valid argument for subprocess.Popen. |
""" |
+ |
STDOUT_PIPE = "/data/data/%s/stdout.fifo" % self.target_package |
cmd = self._CreateADBCommand([ |
@@ -355,6 +425,12 @@ class AndroidShell(object): |
'-n', '%s/%s.MojoShellActivity' % (self.target_package, |
MOJO_SHELL_PACKAGE_NAME)]) |
+ logcat_process = None |
+ |
+ if gdb: |
+ arguments += ['--wait-for-debugger'] |
+ logcat_process = self.ShowLogs(stdout=subprocess.PIPE) |
+ |
parameters = [] |
if stdout or on_application_stop: |
subprocess.check_call(self._CreateADBCommand( |
@@ -376,7 +452,10 @@ class AndroidShell(object): |
cmd += ['--es', 'encodedParameters', encodedParameters] |
with open(os.devnull, 'w') as devnull: |
- subprocess.Popen(cmd, stdout=devnull).wait() |
+ cmd_process = subprocess.Popen(cmd, stdout=devnull) |
+ if logcat_process: |
+ self._WaitForProcessIdAndStartGdb(logcat_process) |
+ cmd_process.wait() |
def StopShell(self): |
""" |
@@ -393,7 +472,7 @@ class AndroidShell(object): |
""" |
subprocess.check_call(self._CreateADBCommand(['logcat', '-c'])) |
- def ShowLogs(self): |
+ def ShowLogs(self, stdout=sys.stdout): |
""" |
Displays the log for the mojo shell. |
@@ -403,6 +482,58 @@ class AndroidShell(object): |
'logcat', |
'-s', |
' '.join(LOGCAT_TAGS)]), |
- stdout=sys.stdout) |
+ stdout=stdout) |
atexit.register(_ExitIfNeeded, logcat) |
return logcat |
+ |
+ |
+def _CreateGdbInit(tmp_dir, gdb_init_path, build_dir): |
+ """ |
+ Creates the gdbinit file. |
+ Args: |
+ tmp_dir: the directory where the gdbinit and other files lives. |
+ gdb_init_path: path to gdbinit |
+ build_dir: path where build files are located. |
+ """ |
+ gdbinit = ('target remote localhost:5039\n' |
+ 'def reload-symbols\n' |
+ ' set solib-search-path %s:%s\n' |
+ 'end\n' |
+ 'def info-symbols\n' |
+ ' info sharedlibrary\n' |
+ 'end\n' |
+ 'reload-symbols\n' |
+ 'echo \\n\\n' |
+ 'You are now in gdb and need to type continue (or c) to continue ' |
+ 'execution.\\n' |
+ 'gdb is in the directory %s\\n' |
+ 'The following functions have been defined:\\n' |
+ 'reload-symbols: forces reloading symbols. If after a crash you\\n' |
+ 'still do not see symbols you likely need to create a link in\\n' |
+ 'the directory you are in.\\n' |
+ 'info-symbols: shows status of current shared libraries.\\n' |
+ 'NOTE: you may need to type reload-symbols again after a ' |
+ 'crash.\\n\\n' % (tmp_dir, build_dir, tmp_dir)) |
+ with open(gdb_init_path, 'w') as f: |
+ f.write(gdbinit) |
+ |
+ |
+def _CreateSOLinks(dest_dir, build_dir): |
+ """ |
+ Creates links from files (such as mojo files) to the real .so so that gdb can |
+ find them. |
+ """ |
+ # The files to create links for. The key is the name as seen on the device, |
+ # and the target an array of path elements as to where the .so lives (relative |
+ # to the output directory). |
+ # TODO(sky): come up with some way to automate this. |
+ files_to_link = { |
+ 'html_viewer.mojo': ['libhtml_viewer', 'html_viewer_library.so'], |
+ 'libmandoline_runner.so': ['mandoline_runner'], |
+ } |
+ for android_name, so_path in files_to_link.iteritems(): |
+ src = os.path.join(build_dir, *so_path) |
+ if not os.path.isfile(src): |
+ print 'Expected file not found', src |
+ sys.exit(-1) |
+ os.symlink(src, os.path.join(dest_dir, android_name)) |