OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright 2015 The Chromium Authors. All rights reserved. | 2 # Copyright 2015 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 from skypy.skyserver import SkyServer | 6 from skypy.skyserver import SkyServer |
7 import argparse | 7 import argparse |
| 8 import hashlib |
8 import json | 9 import json |
9 import logging | 10 import logging |
10 import os | 11 import os |
| 12 import pipes |
| 13 import platform |
11 import re | 14 import re |
12 import signal | 15 import signal |
13 import subprocess | 16 import subprocess |
14 import sys | 17 import sys |
| 18 import tempfile |
15 import time | 19 import time |
16 import urlparse | 20 import urlparse |
17 import hashlib | |
18 import tempfile | |
19 | 21 |
20 SKY_TOOLS_DIR = os.path.dirname(os.path.abspath(__file__)) | 22 SKY_TOOLS_DIR = os.path.dirname(os.path.abspath(__file__)) |
21 SKY_ROOT = os.path.dirname(SKY_TOOLS_DIR) | 23 SKY_ROOT = os.path.dirname(SKY_TOOLS_DIR) |
22 SRC_ROOT = os.path.dirname(SKY_ROOT) | 24 SRC_ROOT = os.path.dirname(SKY_ROOT) |
23 | 25 |
| 26 GDB_PORT = 8888 |
24 SKY_SERVER_PORT = 9888 | 27 SKY_SERVER_PORT = 9888 |
25 OBSERVATORY_PORT = 8181 | 28 OBSERVATORY_PORT = 8181 |
26 DEFAULT_URL = "sky://domokit.github.io/sky_home" | 29 DEFAULT_URL = "sky://domokit.github.io/sky_home" |
27 APK_NAME = 'SkyDemo.apk' | 30 APK_NAME = 'SkyDemo.apk' |
28 ADB_PATH = os.path.join(SRC_ROOT, | 31 ADB_PATH = os.path.join(SRC_ROOT, |
29 'third_party/android_tools/sdk/platform-tools/adb') | 32 'third_party/android_tools/sdk/platform-tools/adb') |
30 ANDROID_PACKAGE = "org.domokit.sky.demo" | 33 ANDROID_PACKAGE = "org.domokit.sky.demo" |
31 SHA1_PATH = '/sdcard/%s/%s.sha1' % (ANDROID_PACKAGE, APK_NAME) | 34 SHA1_PATH = '/sdcard/%s/%s.sha1' % (ANDROID_PACKAGE, APK_NAME) |
32 | 35 |
33 PID_FILE_PATH = "/tmp/skydemo.pids" | 36 PID_FILE_PATH = "/tmp/skydemo.pids" |
34 PID_FILE_KEYS = frozenset([ | 37 PID_FILE_KEYS = frozenset([ |
35 'remote_sky_server_port', | 38 'remote_sky_server_port', |
36 'sky_server_pid', | 39 'sky_server_pid', |
37 'sky_server_port', | 40 'sky_server_port', |
38 'sky_server_root', | 41 'sky_server_root', |
39 'build_dir', | 42 'build_dir', |
| 43 'sky_shell_pid', |
| 44 'remote_gdbserver_port', |
40 ]) | 45 ]) |
41 | 46 |
| 47 # TODO(iansf): Fix undefined behavior when you have more than one device attache
d. |
| 48 SYSTEM_LIBS_ROOT_PATH = '/tmp/device_libs/%s' % (subprocess.check_output(['adb',
'get-serialno']).strip()) |
| 49 |
42 _IGNORED_PATTERNS = [ | 50 _IGNORED_PATTERNS = [ |
43 # Ignored because they're not indicative of specific errors. | 51 # Ignored because they're not indicative of specific errors. |
44 re.compile(r'^$'), | 52 re.compile(r'^$'), |
45 re.compile(r'^Analyzing \['), | 53 re.compile(r'^Analyzing \['), |
46 re.compile(r'^No issues found'), | 54 re.compile(r'^No issues found'), |
47 re.compile(r'^[0-9]+ errors? and [0-9]+ warnings? found.'), | 55 re.compile(r'^[0-9]+ errors? and [0-9]+ warnings? found.'), |
48 re.compile(r'^([0-9]+|No) (error|warning|issue)s? found.'), | 56 re.compile(r'^([0-9]+|No) (error|warning|issue)s? found.'), |
49 | 57 |
50 # TODO: Remove once sdk-extensions are in place | 58 # TODO: Remove once sdk-extensions are in place |
51 re.compile(r'^\[error\] Native functions can only be declared in'), | 59 re.compile(r'^\[error\] Native functions can only be declared in'), |
(...skipping 94 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
146 'Running `download_material_design_icons` for you.') | 154 'Running `download_material_design_icons` for you.') |
147 subprocess.check_call( | 155 subprocess.check_call( |
148 [os.path.join(sky_pkg_lib_dir, 'download_material_design_icons')]) | 156 [os.path.join(sky_pkg_lib_dir, 'download_material_design_icons')]) |
149 | 157 |
150 | 158 |
151 class StartSky(object): | 159 class StartSky(object): |
152 def add_subparser(self, subparsers): | 160 def add_subparser(self, subparsers): |
153 start_parser = subparsers.add_parser('start', | 161 start_parser = subparsers.add_parser('start', |
154 help='launch SkyShell.apk on the device') | 162 help='launch SkyShell.apk on the device') |
155 start_parser.add_argument('build_dir', type=str) | 163 start_parser.add_argument('build_dir', type=str) |
| 164 start_parser.add_argument('--gdb', action="store_true") |
156 start_parser.add_argument('url_or_path', nargs='?', type=str, | 165 start_parser.add_argument('url_or_path', nargs='?', type=str, |
157 default=DEFAULT_URL) | 166 default=DEFAULT_URL) |
158 start_parser.add_argument('--no_install', action="store_false", | 167 start_parser.add_argument('--no_install', action="store_false", |
159 default=True, dest="install", | 168 default=True, dest="install", |
160 help="Don't install SkyDemo.apk before starting") | 169 help="Don't install SkyDemo.apk before starting") |
161 start_parser.set_defaults(func=self.run) | 170 start_parser.set_defaults(func=self.run) |
162 | 171 |
163 def _server_root_for_url(self, url_or_path): | 172 def _server_root_for_url(self, url_or_path): |
164 path = os.path.abspath(url_or_path) | 173 path = os.path.abspath(url_or_path) |
165 if os.path.commonprefix([path, SRC_ROOT]) == SRC_ROOT: | 174 if os.path.commonprefix([path, SRC_ROOT]) == SRC_ROOT: |
166 server_root = SRC_ROOT | 175 server_root = SRC_ROOT |
167 else: | 176 else: |
168 server_root = os.path.dirname(path) | 177 server_root = os.path.dirname(path) |
169 logging.warn( | 178 logging.warn( |
170 '%s is outside of mojo root, using %s as server root' % | 179 '%s is outside of mojo root, using %s as server root' % |
171 (path, server_root)) | 180 (path, server_root)) |
172 return server_root | 181 return server_root |
173 | 182 |
174 def _sky_server_for_args(self, args, packages_root): | 183 def _sky_server_for_args(self, args, packages_root): |
175 # FIXME: This is a hack. sky_server should just take a build_dir | 184 # FIXME: This is a hack. sky_server should just take a build_dir |
176 # not a magical "configuration" name. | 185 # not a magical "configuration" name. |
177 configuration = os.path.basename(os.path.normpath(args.build_dir)) | 186 configuration = os.path.basename(os.path.normpath(args.build_dir)) |
178 server_root = self._server_root_for_url(args.url_or_path) | 187 server_root = self._server_root_for_url(args.url_or_path) |
179 sky_server = SkyServer(SKY_SERVER_PORT, configuration, server_root, pack
ages_root) | 188 sky_server = SkyServer(SKY_SERVER_PORT, configuration, server_root, pack
ages_root) |
180 return sky_server | 189 return sky_server |
181 | 190 |
| 191 def _find_remote_pid_for_package(self, package): |
| 192 ps_output = subprocess.check_output([ADB_PATH, 'shell', 'ps']) |
| 193 for line in ps_output.split('\n'): |
| 194 fields = line.split() |
| 195 if fields and fields[-1] == package: |
| 196 return fields[1] |
| 197 return None |
| 198 |
| 199 def _find_install_location_for_package(self, package): |
| 200 pm_command = [ADB_PATH, 'shell', 'pm', 'path', package] |
| 201 pm_output = subprocess.check_output(pm_command) |
| 202 # e.g. package:/data/app/org.chromium.mojo.shell-1/base.apk |
| 203 return pm_output.split(':')[-1] |
| 204 |
182 def run(self, args, pids): | 205 def run(self, args, pids): |
183 apk_path = os.path.join(args.build_dir, 'apks', APK_NAME) | 206 apk_path = os.path.join(args.build_dir, 'apks', APK_NAME) |
184 if not os.path.exists(apk_path): | 207 if not os.path.exists(apk_path): |
185 print "'%s' does not exist?" % apk_path | 208 print "'%s' does not exist?" % apk_path |
186 return 2 | 209 return 2 |
187 | 210 |
188 ensure_assets_are_downloaded(args.build_dir) | 211 ensure_assets_are_downloaded(args.build_dir) |
189 | 212 |
190 packages_root = dev_packages_root(args.build_dir) | 213 packages_root = dev_packages_root(args.build_dir) |
191 sky_server = self._sky_server_for_args(args, packages_root) | 214 sky_server = self._sky_server_for_args(args, packages_root) |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
229 subprocess.check_call([ | 252 subprocess.check_call([ |
230 ADB_PATH, 'forward', port_string, port_string | 253 ADB_PATH, 'forward', port_string, port_string |
231 ]) | 254 ]) |
232 | 255 |
233 port_string = 'tcp:%s' % sky_server.port | 256 port_string = 'tcp:%s' % sky_server.port |
234 subprocess.check_call([ | 257 subprocess.check_call([ |
235 ADB_PATH, 'reverse', port_string, port_string | 258 ADB_PATH, 'reverse', port_string, port_string |
236 ]) | 259 ]) |
237 pids['remote_sky_server_port'] = sky_server.port | 260 pids['remote_sky_server_port'] = sky_server.port |
238 | 261 |
| 262 |
239 subprocess.check_call([ADB_PATH, 'shell', | 263 subprocess.check_call([ADB_PATH, 'shell', |
240 'am', 'start', | 264 'am', 'start', |
241 '-a', 'android.intent.action.VIEW', | 265 '-a', 'android.intent.action.VIEW', |
242 '-d', _url_from_args(args, pids)]) | 266 '-d', _url_from_args(args, pids)]) |
243 | 267 |
| 268 if not args.gdb: |
| 269 return |
| 270 |
| 271 # TODO(eseidel): am start -W does not seem to work? |
| 272 pid_tries = 0 |
| 273 while True: |
| 274 pid = self._find_remote_pid_for_package(ANDROID_PACKAGE) |
| 275 if pid or pid_tries > 3: |
| 276 break |
| 277 logging.debug('No pid for %s yet, waiting' % ANDROID_PACKAGE) |
| 278 time.sleep(5) |
| 279 pid_tries += 1 |
| 280 |
| 281 if not pid: |
| 282 logging.error('Failed to find pid on device!') |
| 283 return |
| 284 |
| 285 pids['sky_shell_pid'] = pid |
| 286 |
| 287 # We push our own copy of gdbserver with the package since |
| 288 # the default gdbserver is a different version from our gdb. |
| 289 package_path = \ |
| 290 self._find_install_location_for_package(ANDROID_PACKAGE) |
| 291 gdb_server_path = os.path.join( |
| 292 os.path.dirname(package_path), 'lib/arm/gdbserver') |
| 293 gdbserver_cmd = [ |
| 294 ADB_PATH, 'shell', |
| 295 gdb_server_path, '--attach', |
| 296 ':%d' % GDB_PORT, |
| 297 str(pid) |
| 298 ] |
| 299 print ' '.join(map(pipes.quote, gdbserver_cmd)) |
| 300 subprocess.Popen(gdbserver_cmd) |
| 301 |
| 302 port_string = 'tcp:%d' % GDB_PORT |
| 303 subprocess.check_call([ |
| 304 ADB_PATH, 'forward', port_string, port_string |
| 305 ]) |
| 306 pids['remote_gdbserver_port'] = GDB_PORT |
| 307 |
| 308 |
| 309 class GDBAttach(object): |
| 310 def add_subparser(self, subparsers): |
| 311 start_parser = subparsers.add_parser('gdb_attach', |
| 312 help='attach to gdbserver running on device') |
| 313 start_parser.set_defaults(func=self.run) |
| 314 |
| 315 def _pull_system_libraries(self, pids, system_libs_root): |
| 316 # Pull down the system libraries this pid has already mapped in. |
| 317 # TODO(eseidel): This does not handle dynamic loads. |
| 318 library_cacher_path = os.path.join( |
| 319 SKY_TOOLS_DIR, 'android_library_cacher.py') |
| 320 subprocess.call([ |
| 321 library_cacher_path, system_libs_root, pids['sky_shell_pid'] |
| 322 ]) |
| 323 |
| 324 # TODO(eseidel): adb_gdb does, this, unclear why solib-absolute-prefix |
| 325 # doesn't make this explicit listing not necessary? |
| 326 return subprocess.check_output([ |
| 327 'find', system_libs_root, |
| 328 '-mindepth', '1', |
| 329 '-maxdepth', '4', |
| 330 '-type', 'd', |
| 331 ]).strip().split('\n') |
| 332 |
| 333 def run(self, args, pids): |
| 334 symbol_search_paths = [ |
| 335 pids['build_dir'], |
| 336 ] |
| 337 gdb_path = '/usr/bin/gdb' |
| 338 |
| 339 eval_commands = [ |
| 340 'directory %s' % SRC_ROOT, |
| 341 # TODO(eseidel): What file do I point it at? The apk? |
| 342 #'file %s' % self.paths.mojo_shell_path, |
| 343 'target remote localhost:%s' % GDB_PORT, |
| 344 ] |
| 345 |
| 346 system_lib_dirs = self._pull_system_libraries(pids, |
| 347 SYSTEM_LIBS_ROOT_PATH) |
| 348 eval_commands.append( |
| 349 'set solib-absolute-prefix %s' % SYSTEM_LIBS_ROOT_PATH) |
| 350 |
| 351 symbol_search_paths = system_lib_dirs + symbol_search_paths |
| 352 |
| 353 # TODO(eseidel): We need to look up the toolchain somehow? |
| 354 if platform.system() == 'Darwin': |
| 355 gdb_path = os.path.join(SRC_ROOT, 'third_party/android_tools/ndk/' |
| 356 'toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/' |
| 357 'bin/arm-linux-androideabi-gdb') |
| 358 else: |
| 359 gdb_path = os.path.join(SRC_ROOT, 'third_party/android_tools/ndk/' |
| 360 'toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/' |
| 361 'bin/arm-linux-androideabi-gdb') |
| 362 |
| 363 # Set solib-search-path after letting android modify symbol_search_paths |
| 364 eval_commands.append( |
| 365 'set solib-search-path %s' % ':'.join(symbol_search_paths)) |
| 366 |
| 367 exec_command = [gdb_path] |
| 368 for command in eval_commands: |
| 369 exec_command += ['--eval-command', command] |
| 370 |
| 371 print " ".join(exec_command) |
| 372 |
| 373 # Write out our pid file before we exec ourselves. |
| 374 pids.write_to(PID_FILE_PATH) |
| 375 |
| 376 # Exec gdb directly to avoid python intercepting symbols, etc. |
| 377 os.execv(exec_command[0], exec_command) |
| 378 |
| 379 |
244 | 380 |
245 class StopSky(object): | 381 class StopSky(object): |
246 def add_subparser(self, subparsers): | 382 def add_subparser(self, subparsers): |
247 stop_parser = subparsers.add_parser('stop', | 383 stop_parser = subparsers.add_parser('stop', |
248 help=('kill all running SkyShell.apk processes')) | 384 help=('kill all running SkyShell.apk processes')) |
249 stop_parser.set_defaults(func=self.run) | 385 stop_parser.set_defaults(func=self.run) |
250 | 386 |
251 def _kill_if_exists(self, pids, key, name): | 387 def _kill_if_exists(self, pids, key, name): |
252 pid = pids.pop(key, None) | 388 pid = pids.pop(key, None) |
253 if not pid: | 389 if not pid: |
254 logging.info('No pid for %s, nothing to do.' % name) | 390 logging.info('No pid for %s, nothing to do.' % name) |
255 return | 391 return |
256 logging.info('Killing %s (%d).' % (name, pid)) | 392 logging.info('Killing %s (%d).' % (name, pid)) |
257 try: | 393 try: |
258 os.kill(pid, signal.SIGTERM) | 394 os.kill(pid, signal.SIGTERM) |
259 except OSError: | 395 except OSError: |
260 logging.info('%s (%d) already gone.' % (name, pid)) | 396 logging.info('%s (%d) already gone.' % (name, pid)) |
261 | 397 |
| 398 def _adb_reverse_remove(self, port): |
| 399 port_string = 'tcp:%s' % port |
| 400 subprocess.call([ADB_PATH, 'reverse', '--remove', port_string]) |
| 401 |
| 402 def _adb_forward_remove(self, port): |
| 403 port_string = 'tcp:%s' % port |
| 404 subprocess.call([ADB_PATH, 'forward', '--remove', port_string]) |
| 405 |
262 def run(self, args, pids): | 406 def run(self, args, pids): |
263 self._kill_if_exists(pids, 'sky_server_pid', 'sky_server') | 407 self._kill_if_exists(pids, 'sky_server_pid', 'sky_server') |
264 | 408 |
265 if 'remote_sky_server_port' in pids: | 409 if 'remote_sky_server_port' in pids: |
266 port_string = 'tcp:%s' % pids['remote_sky_server_port'] | 410 self._adb_reverse_remove(pids['remote_sky_server_port']) |
267 subprocess.call([ADB_PATH, 'reverse', '--remove', port_string]) | 411 |
| 412 if 'remote_gdbserver_port' in pids: |
| 413 self._kill_if_exists('adb_shell_gdbserver_pid', |
| 414 'adb shell gdbserver') |
| 415 self._adb_forward_remove(pids['remote_gdbserver_port']) |
268 | 416 |
269 subprocess.call([ | 417 subprocess.call([ |
270 ADB_PATH, 'shell', 'am', 'force-stop', ANDROID_PACKAGE]) | 418 ADB_PATH, 'shell', 'am', 'force-stop', ANDROID_PACKAGE]) |
271 | 419 |
272 pids.clear() | 420 pids.clear() |
273 | 421 |
274 | 422 |
275 class Analyze(object): | 423 class Analyze(object): |
276 def add_subparser(self, subparsers): | 424 def add_subparser(self, subparsers): |
277 analyze_parser = subparsers.add_parser('analyze', | 425 analyze_parser = subparsers.add_parser('analyze', |
(...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
366 def main(self): | 514 def main(self): |
367 logging.basicConfig(level=logging.WARNING) | 515 logging.basicConfig(level=logging.WARNING) |
368 | 516 |
369 parser = argparse.ArgumentParser(description='Sky Shell Runner') | 517 parser = argparse.ArgumentParser(description='Sky Shell Runner') |
370 subparsers = parser.add_subparsers(help='sub-command help') | 518 subparsers = parser.add_subparsers(help='sub-command help') |
371 | 519 |
372 commands = [ | 520 commands = [ |
373 StartSky(), | 521 StartSky(), |
374 StopSky(), | 522 StopSky(), |
375 Analyze(), | 523 Analyze(), |
| 524 GDBAttach(), |
376 StartTracing(), | 525 StartTracing(), |
377 StopTracing(), | 526 StopTracing(), |
378 ] | 527 ] |
379 | 528 |
380 for command in commands: | 529 for command in commands: |
381 command.add_subparser(subparsers) | 530 command.add_subparser(subparsers) |
382 | 531 |
383 args = parser.parse_args() | 532 args = parser.parse_args() |
384 pids = Pids.read_from(PID_FILE_PATH, PID_FILE_KEYS) | 533 pids = Pids.read_from(PID_FILE_PATH, PID_FILE_KEYS) |
385 exit_code = args.func(args, pids) | 534 exit_code = args.func(args, pids) |
386 # We could do this with an at-exit handler instead? | 535 # We could do this with an at-exit handler instead? |
387 pids.write_to(PID_FILE_PATH) | 536 pids.write_to(PID_FILE_PATH) |
388 sys.exit(exit_code) | 537 sys.exit(exit_code) |
389 | 538 |
390 | 539 |
391 if __name__ == '__main__': | 540 if __name__ == '__main__': |
392 SkyShellRunner().main() | 541 SkyShellRunner().main() |
OLD | NEW |