Add abhijeet.k@samsung.com to AUTHORS list.
[chromium-blink-merge.git] / mojo / tools / mopy / android.py
blob6584b9f198f3d8acc5b1ea26255a93b905556df9
1 # Copyright 2014 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 import atexit
6 import json
7 import logging
8 import os
9 import os.path
10 import random
11 import subprocess
12 import sys
13 import threading
14 import time
15 import urlparse
17 import SimpleHTTPServer
18 import SocketServer
20 from mopy.config import Config
21 from mopy.paths import Paths
24 # Tags used by the mojo shell application logs.
25 LOGCAT_TAGS = [
26 'AndroidHandler',
27 'MojoFileHelper',
28 'MojoMain',
29 'MojoShellActivity',
30 'MojoShellApplication',
31 'chromium',
34 ADB_PATH = os.path.join(Paths().src_root, 'third_party', 'android_tools', 'sdk',
35 'platform-tools', 'adb')
37 MOJO_SHELL_PACKAGE_NAME = 'org.chromium.mojo.shell'
40 class _SilentTCPServer(SocketServer.TCPServer):
41 """
42 A TCPServer that won't display any error, unless debugging is enabled. This is
43 useful because the client might stop while it is fetching an URL, which causes
44 spurious error messages.
45 """
46 def handle_error(self, request, client_address):
47 """
48 Override the base class method to have conditional logging.
49 """
50 if logging.getLogger().isEnabledFor(logging.DEBUG):
51 SocketServer.TCPServer.handle_error(self, request, client_address)
54 def _GetHandlerClassForPath(base_path):
55 class RequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
56 """
57 Handler for SocketServer.TCPServer that will serve the files from
58 |base_path| directory over http.
59 """
61 def translate_path(self, path):
62 path_from_current = (
63 SimpleHTTPServer.SimpleHTTPRequestHandler.translate_path(self, path))
64 return os.path.join(base_path, os.path.relpath(path_from_current))
66 def log_message(self, *_):
67 """
68 Override the base class method to disable logging.
69 """
70 pass
72 return RequestHandler
75 def _ExitIfNeeded(process):
76 """
77 Exits |process| if it is still alive.
78 """
79 if process.poll() is None:
80 process.kill()
83 def _ReadFifo(fifo_path, pipe, on_fifo_closed, max_attempts=5):
84 """
85 Reads |fifo_path| on the device and write the contents to |pipe|. Calls
86 |on_fifo_closed| when the fifo is closed. This method will try to find the
87 path up to |max_attempts|, waiting 1 second between each attempt. If it cannot
88 find |fifo_path|, a exception will be raised.
89 """
90 def Run():
91 def _WaitForFifo():
92 command = [ADB_PATH, 'shell', 'test -e "%s"; echo $?' % fifo_path]
93 for _ in xrange(max_attempts):
94 if subprocess.check_output(command)[0] == '0':
95 return
96 time.sleep(1)
97 if on_fifo_closed:
98 on_fifo_closed()
99 raise Exception("Unable to find fifo.")
100 _WaitForFifo()
101 stdout_cat = subprocess.Popen([ADB_PATH,
102 'shell',
103 'cat',
104 fifo_path],
105 stdout=pipe)
106 atexit.register(_ExitIfNeeded, stdout_cat)
107 stdout_cat.wait()
108 if on_fifo_closed:
109 on_fifo_closed()
111 thread = threading.Thread(target=Run, name="StdoutRedirector")
112 thread.start()
115 def _MapPort(device_port, host_port):
117 Maps the device port to the host port. If |device_port| is 0, a random
118 available port is chosen. Returns the device port.
120 def _FindAvailablePortOnDevice():
121 opened = subprocess.check_output([ADB_PATH, 'shell', 'netstat'])
122 opened = [int(x.strip().split()[3].split(':')[1])
123 for x in opened if x.startswith(' tcp')]
124 while True:
125 port = random.randint(4096, 16384)
126 if port not in opened:
127 return port
128 if device_port == 0:
129 device_port = _FindAvailablePortOnDevice()
130 subprocess.Popen([ADB_PATH,
131 "reverse",
132 "tcp:%d" % device_port,
133 "tcp:%d" % host_port]).wait()
134 def _UnmapPort():
135 subprocess.Popen([ADB_PATH, "reverse", "--remove", "tcp:%d" % device_port])
136 atexit.register(_UnmapPort)
137 return device_port
140 def StartHttpServerForDirectory(path):
141 """Starts an http server serving files from |path|. Returns the local url."""
142 print 'starting http for', path
143 httpd = _SilentTCPServer(('127.0.0.1', 0), _GetHandlerClassForPath(path))
144 atexit.register(httpd.shutdown)
146 http_thread = threading.Thread(target=httpd.serve_forever)
147 http_thread.daemon = True
148 http_thread.start()
150 print 'local port=', httpd.server_address[1]
151 return 'http://127.0.0.1:%d/' % _MapPort(0, httpd.server_address[1])
154 def PrepareShellRun(config, origin=None):
155 """ Prepares for StartShell: runs adb as root and installs the apk. If no
156 --origin is specified, local http server will be set up to serve files from
157 the build directory along with port forwarding.
159 Returns arguments that should be appended to shell argument list."""
160 build_dir = Paths(config).build_dir
162 subprocess.check_call([ADB_PATH, 'root'])
163 apk_path = os.path.join(build_dir, 'apks', 'MojoShell.apk')
164 subprocess.check_call(
165 [ADB_PATH, 'install', '-r', apk_path, '-i', MOJO_SHELL_PACKAGE_NAME])
166 atexit.register(StopShell)
168 extra_shell_args = []
169 origin_url = origin if origin else StartHttpServerForDirectory(build_dir)
170 extra_shell_args.append("--origin=" + origin_url)
172 return extra_shell_args
175 def _StartHttpServerForOriginMapping(mapping):
176 """If |mapping| points at a local file starts an http server to serve files
177 from the directory and returns the new mapping.
179 This is intended to be called for every --map-origin value."""
180 parts = mapping.split('=')
181 if len(parts) != 2:
182 return mapping
183 dest = parts[1]
184 # If the destination is a url, don't map it.
185 if urlparse.urlparse(dest)[0]:
186 return mapping
187 # Assume the destination is a local file. Start a local server that redirects
188 # to it.
189 localUrl = StartHttpServerForDirectory(dest)
190 print 'started server at %s for %s' % (dest, localUrl)
191 return parts[0] + '=' + localUrl
194 def _StartHttpServerForOriginMappings(arg):
195 """Calls _StartHttpServerForOriginMapping for every --map-origin argument."""
196 mapping_prefix = '--map-origin='
197 if not arg.startswith(mapping_prefix):
198 return arg
199 return mapping_prefix + ','.join([_StartHttpServerForOriginMapping(value)
200 for value in arg[len(mapping_prefix):].split(',')])
203 def StartShell(arguments, stdout=None, on_application_stop=None):
205 Starts the mojo shell, passing it the given arguments.
207 The |arguments| list must contain the "--origin=" arg from PrepareShellRun.
208 If |stdout| is not None, it should be a valid argument for subprocess.Popen.
210 STDOUT_PIPE = "/data/data/%s/stdout.fifo" % MOJO_SHELL_PACKAGE_NAME
212 cmd = [ADB_PATH,
213 'shell',
214 'am',
215 'start',
216 '-W',
217 '-S',
218 '-a', 'android.intent.action.VIEW',
219 '-n', '%s/.MojoShellActivity' % MOJO_SHELL_PACKAGE_NAME]
221 parameters = []
222 if stdout or on_application_stop:
223 subprocess.check_call([ADB_PATH, 'shell', 'rm', STDOUT_PIPE])
224 parameters.append('--fifo-path=%s' % STDOUT_PIPE)
225 _ReadFifo(STDOUT_PIPE, stdout, on_application_stop)
226 # The origin has to be specified whether it's local or external.
227 assert any("--origin=" in arg for arg in arguments)
228 parameters += [_StartHttpServerForOriginMappings(arg) for arg in arguments]
230 if parameters:
231 encodedParameters = json.dumps(parameters)
232 cmd += [ '--es', 'encodedParameters', encodedParameters]
234 with open(os.devnull, 'w') as devnull:
235 subprocess.Popen(cmd, stdout=devnull).wait()
238 def StopShell():
240 Stops the mojo shell.
242 subprocess.check_call(
243 [ADB_PATH, 'shell', 'am', 'force-stop', MOJO_SHELL_PACKAGE_NAME])
246 def CleanLogs():
248 Cleans the logs on the device.
250 subprocess.check_call([ADB_PATH, 'logcat', '-c'])
253 def ShowLogs():
255 Displays the log for the mojo shell.
257 Returns the process responsible for reading the logs.
259 logcat = subprocess.Popen([ADB_PATH,
260 'logcat',
261 '-s',
262 ' '.join(LOGCAT_TAGS)],
263 stdout=sys.stdout)
264 atexit.register(_ExitIfNeeded, logcat)
265 return logcat