mojo_run: support parallel Android runs of mojo_run.

This patch adds a `--free-host-ports` flag to devtools that allows to
run dev servers on system-allocated ports (so that we can have parallel
runs) while still using fixed ports on the device (so that the caching
still works).

Running with `--free-host-ports` makes `adb_remote_setup` unable to work
correctly, which is why this is being introduced behind a flag.

Fixes #470.

R=qsr@chromium.org

Review URL: https://codereview.chromium.org/1397193005 .

Cr-Mirrored-From: https://github.com/domokit/mojo
Cr-Mirrored-Commit: eec06de72752abcae761e709de28e9eebd3fa52a
diff --git a/devtoolslib/android_shell.py b/devtoolslib/android_shell.py
index 275a3dc..611732e 100644
--- a/devtoolslib/android_shell.py
+++ b/devtoolslib/android_shell.py
@@ -408,18 +408,20 @@
     logcat_watch_thread.start()
 
   @overrides(Shell)
-  def serve_local_directory(self, local_dir_path, port=0):
+  def serve_local_directory(self, local_dir_path, port=0, free_host_port=False):
     assert local_dir_path
     mappings = [('', [local_dir_path])]
-    server_address = start_http_server(mappings, host_port=port)
+    host_port = 0 if free_host_port else port
+    server_address = start_http_server(mappings, host_port=host_port)
 
     return 'http://127.0.0.1:%d/' % self._forward_device_port_to_host(
         port, server_address[1])
 
   @overrides(Shell)
-  def serve_local_directories(self, mappings, port=0):
+  def serve_local_directories(self, mappings, port=0, free_host_port=False):
     assert mappings
-    server_address = start_http_server(mappings, host_port=port)
+    host_port = 0 if free_host_port else port
+    server_address = start_http_server(mappings, host_port=host_port)
 
     return 'http://127.0.0.1:%d/' % self._forward_device_port_to_host(
         port, server_address[1])
diff --git a/devtoolslib/linux_shell.py b/devtoolslib/linux_shell.py
index a4a83a9..95161e7 100644
--- a/devtoolslib/linux_shell.py
+++ b/devtoolslib/linux_shell.py
@@ -25,12 +25,12 @@
     self.command_prefix = command_prefix if command_prefix else []
 
   @overrides(Shell)
-  def serve_local_directory(self, local_dir_path, port=0):
+  def serve_local_directory(self, local_dir_path, port=0, free_host_port=False):
     mappings = [('', [local_dir_path])]
     return 'http://%s:%d/' % http_server.start_http_server(mappings, port)
 
   @overrides(Shell)
-  def serve_local_directories(self, mappings, port=0):
+  def serve_local_directories(self, mappings, port=0, free_host_port=False):
     return 'http://%s:%d/' % http_server.start_http_server(mappings, port)
 
   @overrides(Shell)
diff --git a/devtoolslib/shell.py b/devtoolslib/shell.py
index c142611..b7f39d8 100644
--- a/devtoolslib/shell.py
+++ b/devtoolslib/shell.py
@@ -6,7 +6,7 @@
 class Shell(object):
   """Represents an abstract Mojo shell."""
 
-  def serve_local_directory(self, local_dir_path, port=0):
+  def serve_local_directory(self, local_dir_path, port=0, free_host_port=False):
     """Serves the content of the local (host) directory, making it available to
     the shell under the url returned by the function.
 
@@ -15,14 +15,19 @@
 
     Args:
       local_dir_path: path to the directory to be served
-      port: port at which the server will be available to the shell
+      port: port at which the server will be available to the shell. On Android
+          this can be different from the port on which the server runs on the
+          host.
+      free_host_port: spawn the server a system allocated port. This is ignored
+          on Linux, where |port| indicates the port on which the server will be
+          spawned.
 
     Returns:
       The url that the shell can use to access the content of |local_dir_path|.
     """
     raise NotImplementedError()
 
-  def serve_local_directories(self, mappings, port=0):
+  def serve_local_directories(self, mappings, port=0, free_host_port=False):
     """Serves the content of the local (host) directories, making it available
     to the shell under the url returned by the function.
 
@@ -35,7 +40,12 @@
           |local_base_path_list|. The prefixes should skip the leading slash.
           The first matching prefix and the first location that contains the
           requested file will be used each time.
-      port: port at which the server will be available to the shell
+      port: port at which the server will be available to the shell. On Android
+          this can be different from the port on which the server runs on the
+          host.
+      free_host_port: spawn the server a system allocated port. This is ignored
+          on Linux, where |port| indicates the port on which the server will be
+          spawned.
 
     Returns:
       The url that the shell can use to access the server.
diff --git a/devtoolslib/shell_arguments.py b/devtoolslib/shell_arguments.py
index 4a12fd5..88f3f0b 100644
--- a/devtoolslib/shell_arguments.py
+++ b/devtoolslib/shell_arguments.py
@@ -26,7 +26,7 @@
   return True if urlparse.urlparse(dest).scheme else False
 
 
-def _host_local_url_destination(shell, dest_file, port):
+def _host_local_url_destination(shell, dest_file, port, free_host_port):
   """Starts a local server to host |dest_file|.
 
   Returns:
@@ -36,20 +36,20 @@
   if not os.path.exists(directory):
     raise ValueError('local path passed as --map-url destination '
                      'does not exist')
-  server_url = shell.serve_local_directory(directory, port)
+  server_url = shell.serve_local_directory(directory, port, free_host_port)
   return server_url + os.path.relpath(dest_file, directory)
 
 
-def _host_local_origin_destination(shell, dest_dir, port):
+def _host_local_origin_destination(shell, dest_dir, port, free_host_port):
   """Starts a local server to host |dest_dir|.
 
   Returns:
     Url of the hosted directory.
   """
-  return shell.serve_local_directory(dest_dir, port)
+  return shell.serve_local_directory(dest_dir, port, free_host_port)
 
 
-def _rewrite(mapping, host_destination_functon, shell, port):
+def _rewrite(mapping, host_destination_functon, shell, port, free_host_port):
   """Takes a mapping given as <src>=<dest> and rewrites the <dest> part to be
   hosted locally using the given function if <dest> is not a web url.
   """
@@ -62,11 +62,12 @@
     return mapping
 
   src = parts[0]
-  dest = host_destination_functon(shell, parts[1], port)
+  dest = host_destination_functon(shell, parts[1], port, free_host_port)
   return src + '=' + dest
 
 
-def _apply_mappings(shell, original_arguments, map_urls, map_origins):
+def _apply_mappings(shell, original_arguments, map_urls, map_origins,
+                    free_host_ports):
   """Applies mappings for specified urls and origins. For each local path
   specified as destination a local server will be spawned and the mapping will
   be rewritten accordingly.
@@ -75,7 +76,7 @@
     shell: The shell that is being configured.
     original_arguments: Current list of shell arguments.
     map_urls: List of url mappings, each in the form of
-      <url>=<url-or-local-path>.
+        <url>=<url-or-local-path>.
     map_origins: List of origin mappings, each in the form of
         <origin>=<url-or-local-path>.
 
@@ -87,7 +88,8 @@
   if map_urls:
     # Sort the mappings to preserve caching regardless of argument order.
     for map_url in sorted(map_urls):
-      mapping = _rewrite(map_url, _host_local_url_destination, shell, next_port)
+      mapping = _rewrite(map_url, _host_local_url_destination, shell, next_port,
+                         free_host_ports)
       next_port += 1
       # All url mappings need to be coalesced into one shell argument.
       args = append_to_argument(args, '--url-mappings=', mapping)
@@ -95,14 +97,14 @@
   if map_origins:
     for map_origin in sorted(map_origins):
       mapping = _rewrite(map_origin, _host_local_origin_destination, shell,
-                         next_port)
+                         next_port, free_host_ports)
       next_port += 1
       # Origin mappings are specified as separate, repeated shell arguments.
       args.append('--map-origin=' + mapping)
   return args
 
 
-def configure_local_origin(shell, local_dir, fixed_port=True):
+def configure_local_origin(shell, local_dir, free_host_port):
   """Sets up a local http server to serve files in |local_dir| along with
   device port forwarding if needed.
 
@@ -111,7 +113,7 @@
   """
 
   origin_url = shell.serve_local_directory(
-      local_dir, _LOCAL_ORIGIN_PORT if fixed_port else 0)
+      local_dir, _LOCAL_ORIGIN_PORT, free_host_port)
   return ["--origin=" + origin_url]
 
 
@@ -145,7 +147,8 @@
   return arguments
 
 
-def _configure_dev_server(shell, shell_args, dev_server_config, verbose):
+def _configure_dev_server(shell, shell_args, dev_server_config, free_host_port,
+                          verbose):
   """Sets up a dev server on the host according to |dev_server_config|.
 
   Args:
@@ -159,7 +162,8 @@
   """
   port = dev_server_config.port if dev_server_config.port else 0
   server_url = shell.serve_local_directories(dev_server_config.mappings,
-                                             port=port)
+                                             port=port,
+                                             free_host_port=free_host_port)
   shell_args.append('--map-origin=%s=%s' % (dev_server_config.host, server_url))
 
   if verbose:
@@ -210,14 +214,15 @@
       shell_args.append('--args-for=mojo:native_viewport_service --use-osmesa')
 
   shell_args = _apply_mappings(shell, shell_args, shell_config.map_url_list,
-                               shell_config.map_origin_list)
+                               shell_config.map_origin_list,
+                               shell_config.free_host_ports)
 
   if shell_config.origin:
     if _is_web_url(shell_config.origin):
       shell_args.append('--origin=' + shell_config.origin)
     else:
       shell_args.extend(configure_local_origin(shell, shell_config.origin,
-                                               fixed_port=True))
+                                               shell_config.free_host_ports))
 
   if shell_config.content_handlers:
     for (mime_type,
@@ -228,6 +233,7 @@
 
   for dev_server_config in shell_config.dev_servers:
     shell_args = _configure_dev_server(shell, shell_args, dev_server_config,
+                                       shell_config.free_host_ports,
                                        shell_config.verbose)
 
   return shell, shell_args
diff --git a/devtoolslib/shell_config.py b/devtoolslib/shell_config.py
index b7f59d2..b3faa05 100644
--- a/devtoolslib/shell_config.py
+++ b/devtoolslib/shell_config.py
@@ -37,6 +37,7 @@
     self.target_device = None
     self.logcat_tags = None
     self.require_root = False
+    self.free_host_ports = False
 
     # Desktop-only.
     self.use_osmesa = None
@@ -77,6 +78,9 @@
   android_group.add_argument('--target-device', help='Device to run on.')
   android_group.add_argument('--logcat-tags', help='Comma-separated list of '
                              'additional logcat tags to display.')
+  android_group.add_argument('--free-host-ports', action='store_true',
+                             help='Use system-allocated ports on the host when '
+                             'spawning local servers.')
 
   desktop_group = parser.add_argument_group('Desktop-only',
       'These arguments apply only when running on desktop.')
@@ -156,6 +160,7 @@
                            'adb')
   shell_config.target_device = script_args.target_device
   shell_config.logcat_tags = script_args.logcat_tags
+  shell_config.free_host_ports = script_args.free_host_ports
 
   # Desktop-only.
   shell_config.use_osmesa = script_args.use_osmesa
diff --git a/docs/mojo_run.md b/docs/mojo_run.md
index 1b96f67..0675c9c 100644
--- a/docs/mojo_run.md
+++ b/docs/mojo_run.md
@@ -26,3 +26,23 @@
 
 By default, `mojo_run` uses mojo:kiosk_wm as the window manager. You can pass a
 different window manager url using the `--window-manager` flag to override this.
+
+## Running on multiple Android devices
+
+Two or more instances of `mojo_run` can simultaneously run on separate Android
+devices. For that, run in individual shells:
+
+```sh
+mojo_run APP_URL --android --target-device DEVICE_ID --free-host-ports
+```
+
+```sh
+mojo_run APP_URL --android --target-device ANOTHER_DEVICE_ID --free-host-ports
+```
+
+`--free-host-ports` makes `mojo_run` spawn the development servers on
+system-allocated ports on the server (so that multiple instances can run in
+parallel) while still forwarding them to fixed ports on the device (so that
+caching still works). This breaks the remote workflow over `adb_remote_setup`.
+
+DEVICE_ID can be obtained from `adb devices`.