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.
17 import SimpleHTTPServer
20 from mopy
.config
import Config
21 from mopy
.paths
import Paths
24 # Tags used by the mojo shell application logs.
30 'MojoShellApplication',
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
):
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.
46 def handle_error(self
, request
, client_address
):
48 Override the base class method to have conditional logging.
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
):
57 Handler for SocketServer.TCPServer that will serve the files from
58 |base_path| directory over http.
61 def translate_path(self
, path
):
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
, *_
):
68 Override the base class method to disable logging.
75 def _ExitIfNeeded(process
):
77 Exits |process| if it is still alive.
79 if process
.poll() is None:
83 def _ReadFifo(fifo_path
, pipe
, on_fifo_closed
, max_attempts
=5):
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.
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':
99 raise Exception("Unable to find fifo.")
101 stdout_cat
= subprocess
.Popen([ADB_PATH
,
106 atexit
.register(_ExitIfNeeded
, stdout_cat
)
111 thread
= threading
.Thread(target
=Run
, name
="StdoutRedirector")
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')]
125 port
= random
.randint(4096, 16384)
126 if port
not in opened
:
129 device_port
= _FindAvailablePortOnDevice()
130 subprocess
.Popen([ADB_PATH
,
132 "tcp:%d" % device_port
,
133 "tcp:%d" % host_port
]).wait()
135 subprocess
.Popen([ADB_PATH
, "reverse", "--remove", "tcp:%d" % device_port
])
136 atexit
.register(_UnmapPort
)
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
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('=')
184 # If the destination is a url, don't map it.
185 if urlparse
.urlparse(dest
)[0]:
187 # Assume the destination is a local file. Start a local server that redirects
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
):
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
218 '-a', 'android.intent.action.VIEW',
219 '-n', '%s/.MojoShellActivity' % MOJO_SHELL_PACKAGE_NAME
]
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
]
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()
240 Stops the mojo shell.
242 subprocess
.check_call(
243 [ADB_PATH
, 'shell', 'am', 'force-stop', MOJO_SHELL_PACKAGE_NAME
])
248 Cleans the logs on the device.
250 subprocess
.check_call([ADB_PATH
, 'logcat', '-c'])
255 Displays the log for the mojo shell.
257 Returns the process responsible for reading the logs.
259 logcat
= subprocess
.Popen([ADB_PATH
,
262 ' '.join(LOGCAT_TAGS
)],
264 atexit
.register(_ExitIfNeeded
, logcat
)