OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 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 |
| 4 # found in the LICENSE file. |
| 5 |
| 6 from skypy.skyserver import SkyServer |
| 7 import argparse |
| 8 import json |
| 9 import logging |
| 10 import os |
| 11 import subprocess |
| 12 import sys |
| 13 import urllib |
| 14 import urlparse |
| 15 |
| 16 SKY_TOOLS_DIR = os.path.dirname(__file__) |
| 17 SKY_ROOT = os.path.dirname(SKY_TOOLS_DIR) |
| 18 SRC_ROOT = os.path.dirname(SKY_ROOT) |
| 19 |
| 20 SKY_SERVER_PORT = 9888 |
| 21 DEFAULT_URL = "sky://domokit.github.io/home" |
| 22 APK_NAME = 'SkyDemo.apk' |
| 23 ADB_PATH = os.path.join(SRC_ROOT, |
| 24 'third_party/android_tools/sdk/platform-tools/adb') |
| 25 |
| 26 |
| 27 PID_FILE_PATH = "/tmp/skydemo.pids" |
| 28 PID_FILE_KEYS = frozenset([ |
| 29 'remote_sky_server_port', |
| 30 'sky_server_pid', |
| 31 'sky_server_port', |
| 32 'sky_server_root', |
| 33 'build_dir', |
| 34 ]) |
| 35 |
| 36 # This 'strict dictionary' approach is useful for catching typos. |
| 37 class Pids(object): |
| 38 def __init__(self, known_keys, contents=None): |
| 39 self._known_keys = known_keys |
| 40 self._dict = contents if contents is not None else {} |
| 41 |
| 42 def __len__(self): |
| 43 return len(self._dict) |
| 44 |
| 45 def __getitem__(self, key): |
| 46 assert key in self._known_keys, '%s not in known_keys' % key |
| 47 return self._dict[key] |
| 48 |
| 49 def __setitem__(self, key, value): |
| 50 assert key in self._known_keys, '%s not in known_keys' % key |
| 51 self._dict[key] = value |
| 52 |
| 53 def __delitem__(self, key): |
| 54 assert key in self._known_keys, '%s not in known_keys' % key |
| 55 del self._dict[key] |
| 56 |
| 57 def __iter__(self): |
| 58 return iter(self._dict) |
| 59 |
| 60 def __contains__(self, key): |
| 61 assert key in self._known_keys, '%s not in allowed_keys' % key |
| 62 return key in self._dict |
| 63 |
| 64 def clear(): |
| 65 self._dict = {} |
| 66 |
| 67 @classmethod |
| 68 def read_from(cls, path, known_keys): |
| 69 contents = {} |
| 70 try: |
| 71 with open(path, 'r') as pid_file: |
| 72 contents = json.load(pid_file) |
| 73 except: |
| 74 if os.path.exists(path): |
| 75 logging.warn('Failed to read pid file: %s' % path) |
| 76 return cls(known_keys, contents) |
| 77 |
| 78 def write_to(self, path): |
| 79 try: |
| 80 with open(path, 'w') as pid_file: |
| 81 json.dump(self._dict, pid_file, indent=2, sort_keys=True) |
| 82 except: |
| 83 logging.warn('Failed to write pid file: %s' % path) |
| 84 |
| 85 |
| 86 def _convert_to_sky_url(url): |
| 87 result = urllib.parse.urlsplit(url) |
| 88 result.scheme = 'sky' |
| 89 return urllib.parse.urlunsplit(result) |
| 90 |
| 91 |
| 92 # A free function for possible future sharing with a 'load' command. |
| 93 def _url_from_args(args): |
| 94 if urlparse.urlparse(args.url_or_path).scheme: |
| 95 return args.url_or_path |
| 96 # The load happens on the remote device, use the remote port. |
| 97 remote_sky_server_port = self.pids.get('remote_sky_server_port', |
| 98 self.pids['sky_server_port']) |
| 99 url = SkyServer.url_for_path(remote_sky_server_port, |
| 100 self.pids['sky_server_root'], args.url_or_path) |
| 101 return _convert_to_sky_url(url) |
| 102 |
| 103 |
| 104 class StartSky(object): |
| 105 def add_subparser(self, subparsers): |
| 106 start_parser = subparsers.add_parser('start', |
| 107 help='launch SKyShell.apk on the device') |
| 108 start_parser.add_argument('build_dir', type=str) |
| 109 start_parser.add_argument('url_or_path', nargs='?', type=str, |
| 110 default=DEFAULT_URL) |
| 111 start_parser.set_defaults(func=self.run) |
| 112 |
| 113 def _server_root_for_url(self, url_or_path): |
| 114 path = os.path.abspath(url_or_path) |
| 115 if os.path.commonprefix([path, SRC_ROOT]) == SRC_ROOT: |
| 116 server_root = SRC_ROOT |
| 117 else: |
| 118 server_root = os.path.dirname(path) |
| 119 logging.warn( |
| 120 '%s is outside of mojo root, using %s as server root' % |
| 121 (path, server_root)) |
| 122 return server_root |
| 123 |
| 124 def _sky_server_for_args(self, args): |
| 125 # FIXME: This is a hack. sky_server should just take a build_dir |
| 126 # not a magical "configuration" name. |
| 127 configuration = os.path.basename(os.path.normpath(args.build_dir)) |
| 128 server_root = self._server_root_for_url(args.url_or_path) |
| 129 sky_server = SkyServer(SKY_SERVER_PORT, configuration, server_root) |
| 130 return sky_server |
| 131 |
| 132 def run(self, args, pids): |
| 133 apk_path = os.path.join(args.build_dir, 'apks', APK_NAME) |
| 134 if not os.path.exists(apk_path): |
| 135 print "'%s' does not exist?" % apk_path |
| 136 return 2 |
| 137 |
| 138 sky_server = self._sky_server_for_args(args) |
| 139 pids['sky_server_pid'] = sky_server.start() |
| 140 pids['sky_server_port'] = sky_server.port |
| 141 pids['sky_server_root'] = sky_server.root |
| 142 |
| 143 pids['build_dir'] = args.build_dir |
| 144 |
| 145 subprocess.check_call([ADB_PATH, 'install', '-r', apk_path]) |
| 146 |
| 147 port_string = 'tcp:%s' % sky_server.port |
| 148 subprocess.check_call([ |
| 149 ADB_PATH, 'reverse', port_string, port_string |
| 150 ]) |
| 151 pids['remote_sky_server_port'] = sky_server.port |
| 152 |
| 153 subprocess.check_call([ADB_PATH, 'shell', |
| 154 'am', 'start', |
| 155 '-a', 'android.intent.action.VIEW', |
| 156 '-d', _url_from_args(args)]) |
| 157 |
| 158 |
| 159 class StopSky(object): |
| 160 def add_subparser(self, subparsers): |
| 161 stop_parser = subparsers.add_parser('stop', |
| 162 help=('kill all running SkyShell.apk processes')) |
| 163 stop_parser.set_defaults(func=self.run) |
| 164 |
| 165 def _kill_if_exists(self, pids, key, name): |
| 166 pid = pids.pop(key, None) |
| 167 if not pid: |
| 168 logging.info('No pid for %s, nothing to do.' % name) |
| 169 return |
| 170 logging.info('Killing %s (%d).' % (name, pid)) |
| 171 try: |
| 172 os.kill(pid, signal.SIGTERM) |
| 173 except OSError: |
| 174 logging.info('%s (%d) already gone.' % (name, pid)) |
| 175 |
| 176 def run(self, args, pids): |
| 177 self._kill_if_exists(pids, 'sky_server_pid', 'sky_server') |
| 178 |
| 179 if 'remote_sky_server_port' in self.pids: |
| 180 port_string = 'tcp:%s' % self.pids['remote_sky_server_port'] |
| 181 subprocess.call([ADB_PATH, 'reverse', '--remove', port_string]) |
| 182 |
| 183 subprocess.call([ |
| 184 ADB_PATH, 'shell', 'am', 'force-stop', ANDROID_PACKAGE]) |
| 185 |
| 186 pids.clear() |
| 187 |
| 188 |
| 189 class SkyShellRunner(object): |
| 190 def main(self): |
| 191 logging.basicConfig(level=logging.WARNING) |
| 192 |
| 193 parser = argparse.ArgumentParser(description='Sky Shell Runner') |
| 194 subparsers = parser.add_subparsers(help='sub-command help') |
| 195 |
| 196 for command in [StartSky(), StopSky()]: |
| 197 command.add_subparser(subparsers) |
| 198 |
| 199 args = parser.parse_args() |
| 200 pids = Pids.read_from(PID_FILE_PATH, PID_FILE_KEYS) |
| 201 exit_code = args.func(args, pids) |
| 202 # We could do this with an at-exit handler instead? |
| 203 pids.write_to(PID_FILE_PATH) |
| 204 sys.exit(exit_code) |
| 205 |
| 206 |
| 207 if __name__ == '__main__': |
| 208 SkyShellRunner().main() |
OLD | NEW |