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

Side by Side Diff: sky/tools/skydb

Issue 848013004: Make --gdb work for android (Closed) Base URL: git@github.com:domokit/mojo.git@master
Patch Set: Fix nits Created 5 years, 11 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 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright 2014 The Chromium Authors. All rights reserved. 2 # Copyright 2014 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 json 8 import json
9 import logging 9 import logging
10 import os 10 import os
11 import pipes 11 import pipes
12 import requests 12 import requests
13 import signal 13 import signal
14 import skypy.paths 14 import skypy.paths
15 import StringIO 15 import StringIO
16 import subprocess 16 import subprocess
17 import sys 17 import sys
18 import time 18 import time
19 import urlparse 19 import urlparse
20 import re
qsr 2015/01/16 19:22:46 Alphabetize.
20 21
21 SRC_ROOT = skypy.paths.Paths('ignored').src_root 22 SRC_ROOT = skypy.paths.Paths('ignored').src_root
22 sys.path.insert(0, os.path.join(SRC_ROOT, 'build', 'android')) 23 sys.path.insert(0, os.path.join(SRC_ROOT, 'build', 'android'))
23 from pylib import android_commands 24 from pylib import android_commands
24 from pylib import constants 25 from pylib import constants
25 from pylib import forwarder 26 from pylib import forwarder
26 27
27 28
28 SUPPORTED_MIME_TYPES = [ 29 SUPPORTED_MIME_TYPES = [
29 'text/html', 30 'text/html',
30 'text/sky', 31 'text/sky',
31 'text/plain', 32 'text/plain',
32 ] 33 ]
33 34
34 DEFAULT_SKY_COMMAND_PORT = 7777 35 DEFAULT_SKY_COMMAND_PORT = 7777
35 GDB_PORT = 8888 36 GDB_PORT = 8888
36 SKY_SERVER_PORT = 9999 37 SKY_SERVER_PORT = 9999
37 PID_FILE_PATH = "/tmp/skydb.pids" 38 PID_FILE_PATH = "/tmp/skydb.pids"
38 DEFAULT_URL = "https://raw.githubusercontent.com/domokit/mojo/master/sky/example s/home.sky" 39 DEFAULT_URL = "https://raw.githubusercontent.com/domokit/mojo/master/sky/example s/home.sky"
39 40
40 ANDROID_PACKAGE = "org.chromium.mojo.shell" 41 ANDROID_PACKAGE = "org.chromium.mojo.shell"
41 ANDROID_ACTIVITY = "%s/.MojoShellActivity" % ANDROID_PACKAGE 42 ANDROID_ACTIVITY = "%s/.MojoShellActivity" % ANDROID_PACKAGE
42 43
44
43 # FIXME: Move this into mopy.config 45 # FIXME: Move this into mopy.config
44 def gn_args_from_build_dir(build_dir): 46 def gn_args_from_build_dir(build_dir):
45 gn_cmd = [ 47 gn_cmd = [
46 'gn', 'args', 48 'gn', 'args',
47 build_dir, 49 build_dir,
48 '--list', '--short' 50 '--list', '--short'
49 ] 51 ]
50 config = {} 52 config = {}
51 for line in subprocess.check_output(gn_cmd).strip().split('\n'): 53 for line in subprocess.check_output(gn_cmd).strip().split('\n'):
52 # FIXME: This doesn't handle = in values. 54 # FIXME: This doesn't handle = in values.
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
90 'am', 'start', 92 'am', 'start',
91 '-W', 93 '-W',
92 '-S', 94 '-S',
93 '-a', 'android.intent.action.VIEW', 95 '-a', 'android.intent.action.VIEW',
94 '-n', ANDROID_ACTIVITY, 96 '-n', ANDROID_ACTIVITY,
95 # FIXME: This quoting is very error-prone. Perhaps we should read 97 # FIXME: This quoting is very error-prone. Perhaps we should read
96 # our args from a file instead? 98 # our args from a file instead?
97 '--esa', 'parameters', ','.join(escaped_args), 99 '--esa', 'parameters', ','.join(escaped_args),
98 ] 100 ]
99 101
100 def _build_mojo_shell_command(self, args): 102 def _build_mojo_shell_command(self, args, is_android):
101 content_handlers = ['%s,%s' % (mime_type, 'mojo:sky_viewer') 103 content_handlers = ['%s,%s' % (mime_type, 'mojo:sky_viewer')
102 for mime_type in SUPPORTED_MIME_TYPES] 104 for mime_type in SUPPORTED_MIME_TYPES]
103 105
104 remote_command_port = self.pids.get('remote_sky_command_port', self.pids ['sky_command_port']) 106 remote_command_port = self.pids.get('remote_sky_command_port', self.pids ['sky_command_port'])
105 107
106 shell_args = [ 108 shell_args = [
107 '--v=1', 109 '--v=1',
108 '--content-handlers=%s' % ','.join(content_handlers), 110 '--content-handlers=%s' % ','.join(content_handlers),
109 '--url-mappings=mojo:window_manager=mojo:sky_debugger', 111 '--url-mappings=mojo:window_manager=mojo:sky_debugger',
110 '--args-for=mojo:sky_debugger_prompt %d' % remote_command_port, 112 '--args-for=mojo:sky_debugger_prompt %d' % remote_command_port,
111 'mojo:window_manager', 113 'mojo:window_manager',
112 ] 114 ]
113 115
116 # Desktop-only work-around for mojo crashing under chromoting.
117 if not is_android and args.use_osmesa:
118 shell_args.append(
119 '--args-for=mojo:native_viewport_service --use-osmesa')
120
121 if is_android and args.gdb:
122 shell_args.append('--wait_for_debugger')
123
114 if 'remote_sky_server_port' in self.pids: 124 if 'remote_sky_server_port' in self.pids:
115 shell_command = self._wrap_for_android(shell_args) 125 shell_command = self._wrap_for_android(shell_args)
116 else: 126 else:
117 shell_command = [self.paths.mojo_shell_path] + shell_args 127 shell_command = [self.paths.mojo_shell_path] + shell_args
118 128
119 return shell_command 129 return shell_command
120 130
121 def _connect_to_device(self): 131 def _connect_to_device(self):
122 device = android_commands.AndroidCommands( 132 device = android_commands.AndroidCommands(
123 android_commands.GetAttachedDevices()[0]) 133 android_commands.GetAttachedDevices()[0])
124 device.EnableAdbRoot() 134 device.EnableAdbRoot()
125 return device 135 return device
126 136
127 def sky_server_for_args(self, args): 137 def sky_server_for_args(self, args):
128 # FIXME: This is a hack. sky_server should just take a build_dir 138 # FIXME: This is a hack. sky_server should just take a build_dir
129 # not a magical "configuration" name. 139 # not a magical "configuration" name.
130 configuration = os.path.basename(os.path.normpath(args.build_dir)) 140 configuration = os.path.basename(os.path.normpath(args.build_dir))
131 server_root = self._server_root_for_url(args.url_or_path) 141 server_root = self._server_root_for_url(args.url_or_path)
132 sky_server = SkyServer(self.paths, SKY_SERVER_PORT, 142 sky_server = SkyServer(self.paths, SKY_SERVER_PORT,
133 configuration, server_root) 143 configuration, server_root)
134 return sky_server 144 return sky_server
135 145
136 def _create_paths_for_build_dir(self, build_dir): 146 def _create_paths_for_build_dir(self, build_dir):
137 # skypy.paths.Paths takes a root-relative build_dir argument. :( 147 # skypy.paths.Paths takes a root-relative build_dir argument. :(
138 abs_build_dir = os.path.abspath(build_dir) 148 abs_build_dir = os.path.abspath(build_dir)
139 root_relative_build_dir = os.path.relpath(abs_build_dir, SRC_ROOT) 149 root_relative_build_dir = os.path.relpath(abs_build_dir, SRC_ROOT)
140 return skypy.paths.Paths(root_relative_build_dir) 150 return skypy.paths.Paths(root_relative_build_dir)
141 151
152 def _find_remote_pid_for_package(self, package):
153 ps_output = subprocess.check_output(['adb', 'shell', 'ps'])
154 for line in ps_output.split('\n'):
155 fields = line.split()
156 if fields and fields[-1] == package:
157 return fields[1]
158 return None
159
160 def _find_install_location_for_package(self, package):
161 pm_command = ['adb', 'shell', 'pm', 'path', package]
162 pm_output = subprocess.check_output(pm_command)
163 # e.g. package:/data/app/org.chromium.mojo.shell-1/base.apk
164 return pm_output.split(':')[-1]
165
142 def start_command(self, args): 166 def start_command(self, args):
143 # FIXME: Lame that we use self for a command-specific variable. 167 # FIXME: Lame that we use self for a command-specific variable.
144 self.paths = self._create_paths_for_build_dir(args.build_dir) 168 self.paths = self._create_paths_for_build_dir(args.build_dir)
145 self.stop_command(None) # Quit any existing process. 169 self.stop_command(None) # Quit any existing process.
146 170
147 if not os.path.exists(self.paths.mojo_shell_path): 171 if not os.path.exists(self.paths.mojo_shell_path):
148 print "mojo_shell not found in build_dir '%s'" % args.build_dir 172 print "mojo_shell not found in build_dir '%s'" % args.build_dir
149 print "Are you sure you sure that's a valid build_dir location?" 173 print "Are you sure you sure that's a valid build_dir location?"
150 print "See skydb start --help for more info" 174 print "See skydb start --help for more info"
151 sys.exit(2) 175 sys.exit(2)
(...skipping 22 matching lines...) Expand all
174 device_http_port = forwarder.Forwarder.DevicePortForHostPort( 198 device_http_port = forwarder.Forwarder.DevicePortForHostPort(
175 sky_server.port) 199 sky_server.port)
176 self.pids['remote_sky_server_port'] = device_http_port 200 self.pids['remote_sky_server_port'] = device_http_port
177 201
178 port_string = 'tcp:%s' % args.command_port 202 port_string = 'tcp:%s' % args.command_port
179 subprocess.check_call([ 203 subprocess.check_call([
180 'adb', 'forward', port_string, port_string 204 'adb', 'forward', port_string, port_string
181 ]) 205 ])
182 self.pids['remote_sky_command_port'] = args.command_port 206 self.pids['remote_sky_command_port'] = args.command_port
183 207
184 shell_command = self._build_mojo_shell_command(args) 208 shell_command = self._build_mojo_shell_command(args, is_android)
185 209
186 if not is_android: 210 # On android we can't launch inside gdb, but rather have to attach.
187 # Desktop-only work-around for mojo crashing under chromoting. 211 if not is_android and args.gdb:
188 if args.use_osmesa: 212 shell_command = ['gdbserver', ':%d' % GDB_PORT] + shell_command
189 shell_command.append(
190 '--args-for=mojo:native_viewport_service --use-osmesa')
191
192 # On android we can't launch inside gdb, but rather have to attach.
193 if args.gdb:
194 shell_command = ['gdbserver', ':%s' % GDB_PORT] + shell_command
195 213
196 print ' '.join(map(pipes.quote, shell_command)) 214 print ' '.join(map(pipes.quote, shell_command))
197 self.pids['mojo_shell_pid'] = subprocess.Popen(shell_command).pid 215 # This pid is meaningless on android (it's the adb shell pid)
216 start_command_pid = subprocess.Popen(shell_command).pid
217
218 if is_android:
219 # TODO(eseidel): am start -W does not seem to work?
220 pid_tries = 0
221 while True:
222 pid = self._find_remote_pid_for_package(ANDROID_PACKAGE)
223 if pid or pid_tries > 3:
224 break
225 logging.debug('No pid for %s yet, waiting' % ANDROID_PACKAGE)
226 time.sleep(5)
227 pid_tries += 1
228
229 if not pid:
230 logging.error('Failed to find mojo_shell pid on device!')
231 return
232 self.pids['mojo_shell_pid'] = pid
233 else:
234 self.pids['mojo_shell_pid'] = start_command_pid
198 235
199 if args.gdb and is_android: 236 if args.gdb and is_android:
200 gdbserver_cmd = ['gdbserver', '--attach', ':%s' % GDB_PORT] 237 # We push our own copy of gdbserver with the package since
201 self.pids['remote_gdbserver_pid'] = subprocess.Popen(shell_command). pid 238 # the default gdbserver is a different version from our gdb.
239 package_path = \
240 self._find_install_location_for_package(ANDROID_PACKAGE)
241 gdb_server_path = os.path.join(
242 os.path.dirname(package_path), 'lib/arm/gdbserver')
243 gdbserver_cmd = [
244 'adb', 'shell',
245 gdb_server_path, '--attach',
246 ':%d' % GDB_PORT,
247 str(self.pids['mojo_shell_pid'])
248 ]
249 print ' '.join(map(pipes.quote, gdbserver_cmd))
250 self.pids['adb_shell_gdbserver_pid'] = \
251 subprocess.Popen(gdbserver_cmd).pid
202 252
203 port_string = 'tcp:%s' % GDB_PORT 253 port_string = 'tcp:%d' % GDB_PORT
204 subprocess.check_call([ 254 subprocess.check_call([
205 'adb', 'forward', port_string, port_string 255 'adb', 'forward', port_string, port_string
206 ]) 256 ])
207 self.pids['remote_gdbserver_port'] = GDB_PORT 257 self.pids['remote_gdbserver_port'] = GDB_PORT
208 258
209 if not args.gdb: 259 if not args.gdb:
210 if not self._wait_for_sky_command_port(): 260 if not self._wait_for_sky_command_port():
211 logging.error('Failed to start sky') 261 logging.error('Failed to start sky')
212 self.stop_command(None) 262 self.stop_command(None)
213 else: 263 else:
214 self.load_command(args) 264 self.load_command(args)
215 else: 265 else:
216 print 'No load issued, connect with gdb first and then run load.' 266 print 'No load issued, connect with gdb first and then run load.'
217 267
218 def _kill_if_exists(self, key, name): 268 def _kill_if_exists(self, key, name):
219 pid = self.pids.pop(key, None) 269 pid = self.pids.pop(key, None)
220 if not pid: 270 if not pid:
221 logging.info('No pid for %s, nothing to do.' % name) 271 logging.info('No pid for %s, nothing to do.' % name)
222 return 272 return
223 logging.info('Killing %s (%s).' % (name, pid)) 273 logging.info('Killing %s (%d).' % (name, pid))
224 try: 274 try:
225 os.kill(pid, signal.SIGTERM) 275 os.kill(pid, signal.SIGTERM)
226 except OSError: 276 except OSError:
227 logging.info('%s (%s) already gone.' % (name, pid)) 277 logging.info('%s (%d) already gone.' % (name, pid))
228 278
229 def stop_command(self, args): 279 def stop_command(self, args):
230 # TODO(eseidel): mojo_shell crashes when attempting graceful shutdown. 280 # TODO(eseidel): mojo_shell crashes when attempting graceful shutdown.
231 # self._send_command_to_sky('/quit') 281 # self._send_command_to_sky('/quit')
232 self._kill_if_exists('mojo_shell_pid', 'mojo_shell')
233 282
234 self._kill_if_exists('sky_server_pid', 'sky_server') 283 self._kill_if_exists('sky_server_pid', 'sky_server')
284
235 # We could be much more surgical here: 285 # We could be much more surgical here:
236 if 'remote_sky_server_port' in self.pids: 286 if 'remote_sky_server_port' in self.pids:
237 device = android_commands.AndroidCommands( 287 device = android_commands.AndroidCommands(
238 self.pids['device_serial']) 288 self.pids['device_serial'])
239 forwarder.Forwarder.UnmapAllDevicePorts(device) 289 forwarder.Forwarder.UnmapAllDevicePorts(device)
240 290
241 if 'remote_sky_command_port' in self.pids: 291 if 'remote_sky_command_port' in self.pids:
242 # adb forward --remove takes the *host* port, not the remote port. 292 # adb forward --remove takes the *host* port, not the remote port.
243 port_string = 'tcp:%s' % self.pids['sky_command_port'] 293 port_string = 'tcp:%s' % self.pids['sky_command_port']
244 subprocess.call(['adb', 'forward', '--remove', port_string]) 294 subprocess.call(['adb', 'forward', '--remove', port_string])
245 295
246 subprocess.call([ 296 subprocess.call([
247 'adb', 'shell', 'am', 'force-stop', ANDROID_PACKAGE]) 297 'adb', 'shell', 'am', 'force-stop', ANDROID_PACKAGE])
298 else:
299 # Only try to kill mojo_shell if it's running locally.
300 self._kill_if_exists('mojo_shell_pid', 'mojo_shell')
248 301
249 if 'remote_gdbserver_port' in self.pids: 302 if 'remote_gdbserver_port' in self.pids:
303 self._kill_if_exists('adb_shell_gdbserver_pid',
304 'adb shell gdbserver')
305
250 port_string = 'tcp:%s' % self.pids['remote_gdbserver_port'] 306 port_string = 'tcp:%s' % self.pids['remote_gdbserver_port']
251 subprocess.call(['adb', 'forward', '--remove', port_string]) 307 subprocess.call(['adb', 'forward', '--remove', port_string])
252 self.pids = {} # Clear out our pid file. 308 self.pids = {} # Clear out our pid file.
253 309
310 self._kill_if_exists('mojo_cache_linker_pid', 'mojo cache linker')
311
254 def load_command(self, args): 312 def load_command(self, args):
255 if not urlparse.urlparse(args.url_or_path).scheme: 313 if not urlparse.urlparse(args.url_or_path).scheme:
256 # The load happens on the remote device, use the remote port. 314 # The load happens on the remote device, use the remote port.
257 remote_sky_server_port = self.pids.get('remote_sky_server_port', 315 remote_sky_server_port = self.pids.get('remote_sky_server_port',
258 self.pids['sky_server_port']) 316 self.pids['sky_server_port'])
259 url = SkyServer.url_for_path(remote_sky_server_port, 317 url = SkyServer.url_for_path(remote_sky_server_port,
260 self.pids['sky_server_root'], args.url_or_path) 318 self.pids['sky_server_root'], args.url_or_path)
261 else: 319 else:
262 url = args.url_or_path 320 url = args.url_or_path
263 self._send_command_to_sky('/load', url) 321 self._send_command_to_sky('/load', url)
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after
316 'AndroidHandler', 374 'AndroidHandler',
317 'MojoMain', 375 'MojoMain',
318 'MojoShellActivity', 376 'MojoShellActivity',
319 'MojoShellApplication', 377 'MojoShellApplication',
320 'chromium', 378 'chromium',
321 ] 379 ]
322 subprocess.call(['adb', 'logcat', '-d', '-s'] + TAGS) 380 subprocess.call(['adb', 'logcat', '-d', '-s'] + TAGS)
323 381
324 def gdb_attach_command(self, args): 382 def gdb_attach_command(self, args):
325 self.paths = self._create_paths_for_build_dir(self.pids['build_dir']) 383 self.paths = self._create_paths_for_build_dir(self.pids['build_dir'])
384
385 self._kill_if_exists('mojo_cache_linker_pid', 'mojo cache linker')
386
387 links_path = '/tmp/mojo_cache_links'
388 if not os.path.exists(links_path):
389 os.makedirs(links_path)
390 shell_link_path = os.path.join(links_path, 'libmojo_shell.so')
391 if os.path.lexists(shell_link_path):
392 os.unlink(shell_link_path)
393 os.symlink(self.paths.mojo_shell_path, shell_link_path)
394
395 logcat_cmd = ['adb', 'logcat']
396 logcat = subprocess.Popen(logcat_cmd, stdout=subprocess.PIPE)
397
398 mojo_cache_linker_path = os.path.join(
399 self.paths.sky_tools_directory, 'mojo_cache_linker.py')
400 cache_linker_cmd = [
401 mojo_cache_linker_path,
402 links_path,
403 self.pids['build_dir'],
404 'http://localhost:%s' % self.pids['remote_sky_server_port']
405 ]
406 self.pids['mojo_cache_linker_pid'] = \
407 subprocess.Popen(cache_linker_cmd, stdin=logcat.stdout).pid
408
409 # Write out our pid file before we exec ourselves.
410 self._write_pid_file(PID_FILE_PATH, self.pids)
411
412 # TODO(eseidel): Need to sync down system libraries into a directory.
413 symbol_search_paths = [
414 links_path,
415 self.pids['build_dir'],
416 ]
417 # TODO(eseidel): We need to look up the toolchain somehow?
418 gdb_path = os.path.join(SRC_ROOT, 'third_party/android_tools/ndk/'
419 'toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/'
420 'bin/arm-linux-androideabi-gdb')
326 gdb_command = [ 421 gdb_command = [
327 '/usr/bin/gdb', self.paths.mojo_shell_path, 422 gdb_path,
328 '--eval-command', 'target remote localhost:%s' % GDB_PORT 423 '--eval-command', 'file %s' % self.paths.mojo_shell_path,
424 '--eval-command', 'directory %s' % self.paths.src_root,
425 '--eval-command', 'target remote localhost:%s' % GDB_PORT,
426 '--eval-command', 'set solib-search-path %s' %
427 ':'.join(symbol_search_paths),
329 ] 428 ]
330 print " ".join(gdb_command) 429 print " ".join(gdb_command)
331 # We don't want python listenting for signals or anything, so exec 430 # We don't want python listening for signals or anything, so exec
332 # gdb and let it take the entire process. 431 # gdb and let it take the entire process.
333 os.execv(gdb_command[0], gdb_command) 432 os.execv(gdb_command[0], gdb_command)
334 433
335 def print_crash_command(self, args): 434 def print_crash_command(self, args):
336 logcat_cmd = ['adb', 'logcat', '-d'] 435 logcat_cmd = ['adb', 'logcat', '-d']
337 logcat = subprocess.Popen(logcat_cmd, stdout=subprocess.PIPE) 436 logcat = subprocess.Popen(logcat_cmd, stdout=subprocess.PIPE)
338 437
339 stack_path = os.path.join(SRC_ROOT, 438 stack_path = os.path.join(SRC_ROOT,
340 'tools', 'android_stack_parser', 'stack') 439 'tools', 'android_stack_parser', 'stack')
341 stack = subprocess.Popen([stack_path, '-'], stdin=logcat.stdout) 440 stack = subprocess.Popen([stack_path, '-'], stdin=logcat.stdout)
342 logcat.wait() 441 logcat.wait()
343 stack.wait() 442 stack.wait()
344 443
345 def main(self): 444 def main(self):
346 logging.basicConfig(level=logging.INFO) 445 logging.basicConfig(level=logging.WARNING)
347 logging.getLogger("requests").setLevel(logging.WARNING) 446 logging.getLogger("requests").setLevel(logging.WARNING)
348 447
349 self.pids = self._load_pid_file(PID_FILE_PATH) 448 self.pids = self._load_pid_file(PID_FILE_PATH)
350 449
351 parser = argparse.ArgumentParser(description='Sky launcher/debugger') 450 parser = argparse.ArgumentParser(description='Sky launcher/debugger')
352 subparsers = parser.add_subparsers(help='sub-command help') 451 subparsers = parser.add_subparsers(help='sub-command help')
353 452
354 start_parser = subparsers.add_parser('start', 453 start_parser = subparsers.add_parser('start',
355 help='launch a new mojo_shell with sky') 454 help='launch a new mojo_shell with sky')
356 start_parser.add_argument('--gdb', action='store_true') 455 start_parser.add_argument('--gdb', action='store_true')
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after
398 load_parser.set_defaults(func=self.load_command) 497 load_parser.set_defaults(func=self.load_command)
399 498
400 args = parser.parse_args() 499 args = parser.parse_args()
401 args.func(args) 500 args.func(args)
402 501
403 self._write_pid_file(PID_FILE_PATH, self.pids) 502 self._write_pid_file(PID_FILE_PATH, self.pids)
404 503
405 504
406 if __name__ == '__main__': 505 if __name__ == '__main__':
407 SkyDebugger().main() 506 SkyDebugger().main()
OLDNEW
« sky/tools/mojo_cache_linker.py ('K') | « sky/tools/mojo_cache_linker.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698