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

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: Add missing file 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
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 'org' in fields[-1]:
157 print fields[-1]
qsr 2015/01/16 00:04:19 Is that some debugging leftover?
eseidel 2015/01/16 00:14:06 Yes! Removed.
158 if fields and fields[-1] == package:
159 return fields[1]
160 return None
161
162 def _find_install_location_for_package(self, package):
163 pm_command = ['adb', 'shell', 'pm', 'path', package]
164 pm_output = subprocess.check_output(pm_command)
165 # e.g. package:/data/app/org.chromium.mojo.shell-1/base.apk
166 return pm_output.split(':')[-1]
167
142 def start_command(self, args): 168 def start_command(self, args):
143 # FIXME: Lame that we use self for a command-specific variable. 169 # FIXME: Lame that we use self for a command-specific variable.
144 self.paths = self._create_paths_for_build_dir(args.build_dir) 170 self.paths = self._create_paths_for_build_dir(args.build_dir)
145 self.stop_command(None) # Quit any existing process. 171 self.stop_command(None) # Quit any existing process.
146 self.pids = {} # Clear out our pid file. 172 self.pids = {} # Clear out our pid file.
147 173
148 if not os.path.exists(self.paths.mojo_shell_path): 174 if not os.path.exists(self.paths.mojo_shell_path):
149 print "mojo_shell not found in build_dir '%s'" % args.build_dir 175 print "mojo_shell not found in build_dir '%s'" % args.build_dir
150 print "Are you sure you sure that's a valid build_dir location?" 176 print "Are you sure you sure that's a valid build_dir location?"
151 print "See skydb start --help for more info" 177 print "See skydb start --help for more info"
(...skipping 23 matching lines...) Expand all
175 device_http_port = forwarder.Forwarder.DevicePortForHostPort( 201 device_http_port = forwarder.Forwarder.DevicePortForHostPort(
176 sky_server.port) 202 sky_server.port)
177 self.pids['remote_sky_server_port'] = device_http_port 203 self.pids['remote_sky_server_port'] = device_http_port
178 204
179 port_string = 'tcp:%s' % args.command_port 205 port_string = 'tcp:%s' % args.command_port
180 subprocess.check_call([ 206 subprocess.check_call([
181 'adb', 'forward', port_string, port_string 207 'adb', 'forward', port_string, port_string
182 ]) 208 ])
183 self.pids['remote_sky_command_port'] = args.command_port 209 self.pids['remote_sky_command_port'] = args.command_port
184 210
185 shell_command = self._build_mojo_shell_command(args) 211 shell_command = self._build_mojo_shell_command(args, is_android)
186 212
187 if not is_android: 213 # On android we can't launch inside gdb, but rather have to attach.
188 # Desktop-only work-around for mojo crashing under chromoting. 214 if not is_android and args.gdb:
189 if args.use_osmesa: 215 shell_command = ['gdbserver', ':%s' % GDB_PORT] + shell_command
qsr 2015/01/16 00:04:19 %d? Here and everywhere you format an integer
eseidel 2015/01/16 00:14:06 OK. What's the benefit?
qsr 2015/01/16 00:46:40 It will crash if this is not a number. Not much, b
190 shell_args.append(
191 '--args-for=mojo:native_viewport_service --use-osmesa')
192
193 # On android we can't launch inside gdb, but rather have to attach.
194 if args.gdb:
195 shell_command = ['gdbserver', ':%s' % GDB_PORT] + shell_command
196 216
197 print ' '.join(map(pipes.quote, shell_command)) 217 print ' '.join(map(pipes.quote, shell_command))
198 self.pids['mojo_shell_pid'] = subprocess.Popen(shell_command).pid 218 # This pid is meaningless on android (it's the adb shell pid)
219 start_command_pid = subprocess.Popen(shell_command).pid
220
221 if is_android:
222 # TODO(eseidel): am start -W does not seem to work?
223 pid_tries = 0
224 while True:
225 pid = self._find_remote_pid_for_package(ANDROID_PACKAGE)
226 if pid or pid_tries > 3:
227 break
228 logging.warn('No pid for %s yet, waiting' % ANDROID_PACKAGE)
229 time.sleep(5)
230 pid_tries += 1
231
232 self.pids['mojo_shell_pid'] = pid
233 if not self.pids['mojo_shell_pid']:
qsr 2015/01/16 00:04:18 Any reason not to write this: if not pid: ...
eseidel 2015/01/16 00:14:06 Done.
234 logging.error('Failed to find mojo_shell pid on device!')
235 return
236 else:
237 self.pids['mojo_shell_pid'] = start_command_pid
199 238
200 if args.gdb and is_android: 239 if args.gdb and is_android:
201 gdbserver_cmd = ['gdbserver', '--attach', ':%s' % GDB_PORT] 240 # We push our own copy of gdbserver with the package since
202 self.pids['remote_gdbserver_pid'] = subprocess.Popen(shell_command). pid 241 # the default gdbserver is a different version from our gdb.
242 package_path = \
243 self._find_install_location_for_package(ANDROID_PACKAGE)
244 gdb_server_path = os.path.join(
245 os.path.dirname(package_path), 'lib/arm/gdbserver')
246 gdbserver_cmd = [
247 'adb', 'shell',
248 gdb_server_path, '--attach',
249 ':%s' % GDB_PORT,
250 str(self.pids['mojo_shell_pid'])
251 ]
252 print ' '.join(map(pipes.quote, gdbserver_cmd))
253 self.pids['adb_shell_gdbserver_pid'] = \
254 subprocess.Popen(gdbserver_cmd).pid
qsr 2015/01/16 00:04:19 I am missing something here. Why are you transform
eseidel 2015/01/16 00:14:06 I'm only transforming it into a string for the pri
qsr 2015/01/16 00:46:40 That's what I was missing. Sorry.
203 255
204 port_string = 'tcp:%s' % GDB_PORT 256 port_string = 'tcp:%s' % GDB_PORT
205 subprocess.check_call([ 257 subprocess.check_call([
206 'adb', 'forward', port_string, port_string 258 'adb', 'forward', port_string, port_string
207 ]) 259 ])
208 self.pids['remote_gdbserver_port'] = GDB_PORT 260 self.pids['remote_gdbserver_port'] = GDB_PORT
209 261
210 if not args.gdb: 262 if not args.gdb:
211 if not self._wait_for_sky_command_port(): 263 if not self._wait_for_sky_command_port():
212 logging.error('Failed to start sky') 264 logging.error('Failed to start sky')
(...skipping 10 matching lines...) Expand all
223 return 275 return
224 logging.info('Killing %s (%s).' % (name, pid)) 276 logging.info('Killing %s (%s).' % (name, pid))
225 try: 277 try:
226 os.kill(pid, signal.SIGTERM) 278 os.kill(pid, signal.SIGTERM)
227 except OSError: 279 except OSError:
228 logging.info('%s (%s) already gone.' % (name, pid)) 280 logging.info('%s (%s) already gone.' % (name, pid))
229 281
230 def stop_command(self, args): 282 def stop_command(self, args):
231 # TODO(eseidel): mojo_shell crashes when attempting graceful shutdown. 283 # TODO(eseidel): mojo_shell crashes when attempting graceful shutdown.
232 # self._send_command_to_sky('/quit') 284 # self._send_command_to_sky('/quit')
233 self._kill_if_exists('mojo_shell_pid', 'mojo_shell')
234 285
235 self._kill_if_exists('sky_server_pid', 'sky_server') 286 self._kill_if_exists('sky_server_pid', 'sky_server')
287
236 # We could be much more surgical here: 288 # We could be much more surgical here:
237 if 'remote_sky_server_port' in self.pids: 289 if 'remote_sky_server_port' in self.pids:
238 device = android_commands.AndroidCommands( 290 device = android_commands.AndroidCommands(
239 self.pids['device_serial']) 291 self.pids['device_serial'])
240 forwarder.Forwarder.UnmapAllDevicePorts(device) 292 forwarder.Forwarder.UnmapAllDevicePorts(device)
241 293
242 if 'remote_sky_command_port' in self.pids: 294 if 'remote_sky_command_port' in self.pids:
243 # adb forward --remove takes the *host* port, not the remote port. 295 # adb forward --remove takes the *host* port, not the remote port.
244 port_string = 'tcp:%s' % self.pids['sky_command_port'] 296 port_string = 'tcp:%s' % self.pids['sky_command_port']
245 subprocess.call(['adb', 'forward', '--remove', port_string]) 297 subprocess.call(['adb', 'forward', '--remove', port_string])
246 298
247 subprocess.call([ 299 subprocess.call([
248 'adb', 'shell', 'am', 'force-stop', ANDROID_PACKAGE]) 300 'adb', 'shell', 'am', 'force-stop', ANDROID_PACKAGE])
301 else:
302 # Only try to kill mojo_shell if it's running locally.
303 self._kill_if_exists('mojo_shell_pid', 'mojo_shell')
249 304
250 if 'remote_gdbserver_port' in self.pids: 305 if 'remote_gdbserver_port' in self.pids:
306 self._kill_if_exists('adb_shell_gdbserver_pid', 'adb shell gdbserver ')
307
251 port_string = 'tcp:%s' % self.pids['remote_gdbserver_port'] 308 port_string = 'tcp:%s' % self.pids['remote_gdbserver_port']
252 subprocess.call(['adb', 'forward', '--remove', port_string]) 309 subprocess.call(['adb', 'forward', '--remove', port_string])
253 310
311 self._kill_if_exists('mojo_cache_linker_pid', 'mojo cache linker')
312
254 def load_command(self, args): 313 def load_command(self, args):
255 if not urlparse.urlparse(args.url_or_path).scheme: 314 if not urlparse.urlparse(args.url_or_path).scheme:
256 # The load happens on the remote device, use the remote port. 315 # The load happens on the remote device, use the remote port.
257 remote_sky_server_port = self.pids.get('remote_sky_server_port', 316 remote_sky_server_port = self.pids.get('remote_sky_server_port',
258 self.pids['sky_server_port']) 317 self.pids['sky_server_port'])
259 url = SkyServer.url_for_path(remote_sky_server_port, 318 url = SkyServer.url_for_path(remote_sky_server_port,
260 self.pids['sky_server_root'], args.url_or_path) 319 self.pids['sky_server_root'], args.url_or_path)
261 else: 320 else:
262 url = args.url_or_path 321 url = args.url_or_path
263 self._send_command_to_sky('/load', url) 322 self._send_command_to_sky('/load', url)
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after
316 'AndroidHandler', 375 'AndroidHandler',
317 'MojoMain', 376 'MojoMain',
318 'MojoShellActivity', 377 'MojoShellActivity',
319 'MojoShellApplication', 378 'MojoShellApplication',
320 'chromium', 379 'chromium',
321 ] 380 ]
322 subprocess.call(['adb', 'logcat', '-d', '-s'] + TAGS) 381 subprocess.call(['adb', 'logcat', '-d', '-s'] + TAGS)
323 382
324 def gdb_attach_command(self, args): 383 def gdb_attach_command(self, args):
325 self.paths = self._create_paths_for_build_dir(self.pids['build_dir']) 384 self.paths = self._create_paths_for_build_dir(self.pids['build_dir'])
385
386 self._kill_if_exists('mojo_cache_linker_pid', 'mojo cache linker')
387
388 links_path = '/tmp/mojo_cache_links'
389 if not os.path.exists(links_path):
390 os.makedirs(links_path)
qsr 2015/01/16 00:04:18 You do not seem to ever be cleaning this directory
eseidel 2015/01/16 00:14:06 Unclear. I could? Originally I had the new scrip
qsr 2015/01/16 00:46:40 I fear this will quickly be full on unused files.
391 shell_link_path = os.path.join(links_path, 'libmojo_shell.so')
392 if os.path.lexists(shell_link_path):
393 os.unlink(shell_link_path)
394 os.symlink(self.paths.mojo_shell_path, shell_link_path)
395
396 logcat_cmd = ['adb', 'logcat']
397 logcat = subprocess.Popen(logcat_cmd, stdout=subprocess.PIPE)
398
399 mojo_cache_linker_path = os.path.join(
400 self.paths.sky_tools_directory, 'mojo_cache_linker.py')
401 cache_linker_cmd = [
402 mojo_cache_linker_path,
403 links_path,
404 self.pids['build_dir'],
405 'http://localhost:%s' % self.pids['remote_sky_server_port']
406 ]
407 self.pids['mojo_cache_linker_pid'] = \
408 subprocess.Popen(cache_linker_cmd, stdin=logcat.stdout).pid
409
410 # TODO(eseidel): Need to sync down system libraries into a directory.
411 # For example, this is what adb_gdb uses:
412 # set print pretty 1
413 # python
414 # import sys
415 # sys.path.insert(0, '/src/mojo/src/tools/gdb/')
416 # try:
417 # import gdb_chrome
418 # finally:
419 # sys.path.pop(0)
420 # end
421 # file /tmp/eseidel-adb-gdb-tmp-19640/app_process
422 # directory /src/mojo/src
423 # set solib-absolute-prefix /tmp/eseidel-adb-gdb-libs
424 # set solib-search-path /tmp/eseidel-adb-gdb-libs/system:/tmp/eseidel-ad b-gdb-libs/system/vendor:/tmp/eseidel-adb-gdb-libs/system/vendor/lib:/tmp/eseide l-adb-gdb-libs/system/vendor/lib/egl:/tmp/eseidel-adb-gdb-libs/system/bin:/tmp/e seidel-adb-gdb-libs/system/lib:/tmp/eseidel-adb-gdb-libs/system/lib/hw::/tmp/ese idel-adb-gdb-libs:/src/mojo/src/foo/Debug/lib
425 # echo Attaching and reading symbols, this may take a while..
426 # target remote :5039
427
428 symbol_search_paths = [
429 links_path,
430 self.pids['build_dir'],
431 ]
432 gdb_path = os.path.join(SRC_ROOT, 'third_party/android_tools/ndk/toolcha ins/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gd b')
qsr 2015/01/16 00:04:19 This should be 80 chars long... You also might wan
eseidel 2015/01/16 00:14:06 Eek! I need to fix this lookup before landing. Th
326 gdb_command = [ 433 gdb_command = [
327 '/usr/bin/gdb', self.paths.mojo_shell_path, 434 gdb_path,
328 '--eval-command', 'target remote localhost:%s' % GDB_PORT 435 '--eval-command', 'file %s' % self.paths.mojo_shell_path,
436 '--eval-command', 'directory %s' % self.paths.src_root,
437 '--eval-command', 'target remote localhost:%s' % GDB_PORT,
438 '--eval-command', 'set solib-search-path %s' %
439 ':'.join(symbol_search_paths),
329 ] 440 ]
330 print " ".join(gdb_command) 441 print " ".join(gdb_command)
331 # We don't want python listenting for signals or anything, so exec 442 # We don't want python listenting for signals or anything, so exec
332 # gdb and let it take the entire process. 443 # gdb and let it take the entire process.
333 os.execv(gdb_command[0], gdb_command) 444 os.execv(gdb_command[0], gdb_command)
334 445
335 def print_crash_command(self, args): 446 def print_crash_command(self, args):
336 logcat_cmd = ['adb', 'logcat', '-d'] 447 logcat_cmd = ['adb', 'logcat', '-d']
337 logcat = subprocess.Popen(logcat_cmd, stdout=subprocess.PIPE) 448 logcat = subprocess.Popen(logcat_cmd, stdout=subprocess.PIPE)
338 449
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after
394 load_parser.set_defaults(func=self.load_command) 505 load_parser.set_defaults(func=self.load_command)
395 506
396 args = parser.parse_args() 507 args = parser.parse_args()
397 args.func(args) 508 args.func(args)
398 509
399 self._write_pid_file(PID_FILE_PATH, self.pids) 510 self._write_pid_file(PID_FILE_PATH, self.pids)
400 511
401 512
402 if __name__ == '__main__': 513 if __name__ == '__main__':
403 SkyDebugger().main() 514 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