| 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 |