Add support for launching webcam software for use in recipes

* Copy ensure_webcam_is_running.py from recipes repo
* Turn it into a wrapper that can launch another script
  (fix_python_path is copied from test_env.py as _ForcePythonInterpreter)
* Support it in mb.py
* Add it to video_capture_unittests

No-Try: True
Bug: chromium:755660
Change-Id: I376724a77e443620724add7818592e9368d02079
Reviewed-on: https://webrtc-review.googlesource.com/77320
Commit-Queue: Oleh Prypin <oprypin@webrtc.org>
Reviewed-by: Patrik Höglund <phoglund@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#23275}
diff --git a/tools_webrtc/ensure_webcam_is_running.py b/tools_webrtc/ensure_webcam_is_running.py
new file mode 100755
index 0000000..952ebd6
--- /dev/null
+++ b/tools_webrtc/ensure_webcam_is_running.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python
+# Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
+#
+# Use of this source code is governed by a BSD-style license
+# that can be found in the LICENSE file in the root of the source
+# tree. An additional intellectual property rights grant can be found
+# in the file PATENTS.  All contributing project authors may
+# be found in the AUTHORS file in the root of the source tree.
+
+"""Checks if a virtual webcam is running and starts it if not.
+
+Returns a non-zero return code if the webcam could not be started.
+
+Prerequisites:
+* The Python interpreter must have the psutil package installed.
+* Windows: a scheduled task named 'ManyCam' must exist and be configured to
+  launch ManyCam preconfigured to auto-play the test clip.
+* Mac: ManyCam must be installed in the default location and be preconfigured
+  to auto-play the test clip.
+* Linux: The v4l2loopback kernel module must be compiled and loaded to the
+  kernel already and the v4l2_file_player application must be compiled and put
+  in the location specified below.
+
+NOTICE: When running this script as a buildbot step, make sure to set
+usePTY=False for the build step when adding it, or the subprocess will die as
+soon the step has executed.
+
+If any command line arguments are passed to the script, it is executed as a
+command in a subprocess.
+"""
+
+import os
+# psutil is not installed on non-Linux machines by default.
+import psutil  # pylint: disable=F0401
+import subprocess
+import sys
+import time
+
+
+WEBCAM_WIN = ('schtasks', '/run', '/tn', 'ManyCam')
+WEBCAM_MAC = ('open', '/Applications/ManyCam/ManyCam.app')
+E = os.path.expandvars
+WEBCAM_LINUX = (
+    E('$HOME/fake-webcam-driver/linux/v4l2_file_player/v4l2_file_player'),
+    E('$HOME/webrtc_video_quality/reference_video.yuv'),
+    '640', '480', '/dev/video0',
+)
+
+
+def IsWebCamRunning():
+  if sys.platform == 'win32':
+    process_name = 'ManyCam.exe'
+  elif sys.platform.startswith('darwin'):
+    process_name = 'ManyCam'
+  elif sys.platform.startswith('linux'):
+    process_name = 'v4l2_file_player'
+  else:
+    raise Exception('Unsupported platform: %s' % sys.platform)
+  for p in psutil.process_iter():
+    try:
+      if process_name == p.name:
+        print 'Found a running virtual webcam (%s with PID %s)' % (p.name,
+                                                                   p.pid)
+        return True
+    except psutil.AccessDenied:
+      pass  # This is normal if we query sys processes, etc.
+  return False
+
+
+def StartWebCam():
+  try:
+    if sys.platform == 'win32':
+      subprocess.check_call(WEBCAM_WIN)
+      print 'Successfully launched virtual webcam.'
+    elif sys.platform.startswith('darwin'):
+      subprocess.check_call(WEBCAM_MAC)
+      print 'Successfully launched virtual webcam.'
+    elif sys.platform.startswith('linux'):
+
+      # Must redirect stdout/stderr/stdin to avoid having the subprocess
+      # being killed when the parent shell dies (happens on the bots).
+      process = subprocess.Popen(WEBCAM_LINUX, stdout=subprocess.PIPE,
+                                 stderr=subprocess.PIPE,
+                                 stdin=subprocess.PIPE)
+      # If the v4l2loopback module is not loaded or incorrectly configured,
+      # the process will still launch but will die immediately.
+      # Wait for a second and then check for aliveness to catch such errors.
+      time.sleep(1)
+      if process.poll() is None:
+        print 'Successfully launched virtual webcam with PID %s' % process.pid
+      else:
+        print 'Failed to launch virtual webcam.'
+        return False
+
+  except Exception as e:
+    print 'Failed to launch virtual webcam: %s' % e
+    return False
+
+  return True
+
+
+def _ForcePythonInterpreter(cmd):
+  """Returns the fixed command line to call the right python executable."""
+  out = cmd[:]
+  if out[0] == 'python':
+    out[0] = sys.executable
+  elif out[0].endswith('.py'):
+    out.insert(0, sys.executable)
+  return out
+
+
+def Main(argv):
+  if IsWebCamRunning():
+    return 0
+  if not StartWebCam():
+    return 1
+
+  if argv:
+    return subprocess.call(_ForcePythonInterpreter(argv))
+
+
+if __name__ == '__main__':
+  sys.exit(Main(sys.argv[1:]))
diff --git a/tools_webrtc/mb/gn_isolate_map.pyl b/tools_webrtc/mb/gn_isolate_map.pyl
index ef18910..848a9b3 100644
--- a/tools_webrtc/mb/gn_isolate_map.pyl
+++ b/tools_webrtc/mb/gn_isolate_map.pyl
@@ -121,6 +121,7 @@
   "video_capture_tests": {
     "label": "//modules/video_capture:video_capture_tests",
     "type": "console_test_launcher",
+    "use_webcam": True,
   },
   "video_engine_tests": {
     "label": "//:video_engine_tests",
diff --git a/tools_webrtc/mb/mb.py b/tools_webrtc/mb/mb.py
index 0291066..e888367 100755
--- a/tools_webrtc/mb/mb.py
+++ b/tools_webrtc/mb/mb.py
@@ -844,18 +844,20 @@
     else:
       extra_files = ['../../testing/test_env.py']
 
+      if isolate_map[target].get('use_webcam', False):
+        cmdline.append('../../tools_webrtc/ensure_webcam_is_running.py')
+        extra_files.append('../../tools_webrtc/ensure_webcam_is_running.py')
+
       # This needs to mirror the settings in //build/config/ui.gni:
       # use_x11 = is_linux && !use_ozone.
       use_x11 = is_linux and not 'use_ozone=true' in vals['gn_args']
 
       xvfb = use_x11 and test_type == 'windowed_test_launcher'
       if xvfb:
-        extra_files += [
-            '../../testing/xvfb.py',
-        ]
-
-      cmdline = (['../../testing/xvfb.py'] if xvfb else
-                 ['../../testing/test_env.py'])
+        cmdline.append('../../testing/xvfb.py')
+        extra_files.append('../../testing/xvfb.py')
+      else:
+        cmdline.append('../../testing/test_env.py')
 
       # Memcheck is only supported for linux. Ignore in other platforms.
       if is_linux and 'rtc_use_memcheck=true' in vals['gn_args']:
diff --git a/tools_webrtc/mb/mb_unittest.py b/tools_webrtc/mb/mb_unittest.py
index 6dea9b8..379a56d 100755
--- a/tools_webrtc/mb/mb_unittest.py
+++ b/tools_webrtc/mb/mb_unittest.py
@@ -711,6 +711,57 @@
         '--tsan=0',
     ])
 
+  def test_isolate_test_launcher_with_webcam(self):
+    test_files = {
+      '/tmp/swarming_targets': 'base_unittests\n',
+      '/fake_src/testing/buildbot/gn_isolate_map.pyl': (
+          "{'base_unittests': {"
+          "  'label': '//base:base_unittests',"
+          "  'type': 'console_test_launcher',"
+          "  'use_webcam': True,"
+          "}}\n"
+      ),
+      '/fake_src/out/Default/base_unittests.runtime_deps': (
+          "base_unittests\n"
+          "some_resource_file\n"
+      ),
+    }
+    mbw = self.check(['gen', '-c', 'debug_goma', '//out/Default',
+                      '--swarming-targets-file', '/tmp/swarming_targets',
+                      '--isolate-map-file',
+                      '/fake_src/testing/buildbot/gn_isolate_map.pyl'],
+                     files=test_files, ret=0)
+
+    isolate_file = mbw.files['/fake_src/out/Default/base_unittests.isolate']
+    isolate_file_contents = ast.literal_eval(isolate_file)
+    files = isolate_file_contents['variables']['files']
+    command = isolate_file_contents['variables']['command']
+
+    self.assertEqual(files, [
+        '../../testing/test_env.py',
+        '../../third_party/gtest-parallel/gtest-parallel',
+        '../../third_party/gtest-parallel/gtest_parallel.py',
+        '../../tools_webrtc/ensure_webcam_is_running.py',
+        '../../tools_webrtc/gtest-parallel-wrapper.py',
+        'base_unittests',
+        'some_resource_file',
+    ])
+    self.assertEqual(command, [
+        '../../tools_webrtc/ensure_webcam_is_running.py',
+        '../../testing/test_env.py',
+        '../../tools_webrtc/gtest-parallel-wrapper.py',
+        '--output_dir=${ISOLATED_OUTDIR}/test_logs',
+        '--gtest_color=no',
+        '--timeout=900',
+        '--retry_failed=3',
+        './base_unittests',
+        '--',
+        '--asan=0',
+        '--lsan=0',
+        '--msan=0',
+        '--tsan=0',
+    ])
+
   def test_isolate(self):
     files = {
       '/fake_src/out/Default/toolchain.ninja': "",