Make Python tests run on Windows
diff --git a/src/python/grpcio_tests/tests/_runner.py b/src/python/grpcio_tests/tests/_runner.py
index 81c6100..926dcbe 100644
--- a/src/python/grpcio_tests/tests/_runner.py
+++ b/src/python/grpcio_tests/tests/_runner.py
@@ -177,15 +177,20 @@
         stderr_pipe.write_bypass(
             '\ninterrupted stderr:\n{}\n'.format(stderr_pipe.output().decode()))
         os._exit(1)
-    signal.signal(signal.SIGINT, sigint_handler)
-    signal.signal(signal.SIGSEGV, fault_handler)
-    signal.signal(signal.SIGBUS, fault_handler)
-    signal.signal(signal.SIGABRT, fault_handler)
-    signal.signal(signal.SIGFPE, fault_handler)
-    signal.signal(signal.SIGILL, fault_handler)
+    def try_set_handler(name, handler):
+      try:
+        signal.signal(getattr(signal, name), handler)
+      except AttributeError:
+        pass
+    try_set_handler('SIGINT', sigint_handler)
+    try_set_handler('SIGSEGV', fault_handler)
+    try_set_handler('SIGBUS', fault_handler)
+    try_set_handler('SIGABRT', fault_handler)
+    try_set_handler('SIGFPE', fault_handler)
+    try_set_handler('SIGILL', fault_handler)
     # Sometimes output will lag after a test has successfully finished; we
     # ignore such writes to our pipes.
-    signal.signal(signal.SIGPIPE, signal.SIG_IGN)
+    try_set_handler('SIGPIPE', signal.SIG_IGN)
 
     # Run the tests
     result.startTestRun()
diff --git a/tools/run_tests/build_python_msys2.sh b/tools/run_tests/build_python_msys2.sh
new file mode 100644
index 0000000..6e9d369
--- /dev/null
+++ b/tools/run_tests/build_python_msys2.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+# Copyright 2016, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+set -ex
+
+BUILD_PYTHON=`realpath "$(dirname $0)/build_python.sh"`
+export MSYSTEM=$1
+shift 1
+bash --login $BUILD_PYTHON "$@"
diff --git a/tools/run_tests/run_tests.py b/tools/run_tests/run_tests.py
index cbe17ee..3bc83c2 100755
--- a/tools/run_tests/run_tests.py
+++ b/tools/run_tests/run_tests.py
@@ -375,19 +375,15 @@
 
 
 class PythonConfig(collections.namedtuple('PythonConfig', [
-    'python', 'venv', 'venv_relative_python', 'toolchain',])):
-
-  @property
-  def venv_python(self):
-    return os.path.abspath('{}/{}'.format(self.venv, self.venv_relative_python))
-
+    'name', 'build', 'run'])):
+  """Tuple of commands (named s.t. 'what it says on the tin' applies)"""
 
 class PythonLanguage(object):
 
   def configure(self, config, args):
     self.config = config
     self.args = args
-    self.pythons = self._get_pythons(self.args.compiler)
+    self.pythons = self._get_pythons(self.args)
 
   def test_specs(self):
     # load list of known test suites
@@ -395,11 +391,11 @@
       tests_json = json.load(tests_json_file)
     environment = dict(_FORCE_ENVIRON_FOR_WRAPPERS)
     return [self.config.job_spec(
-        ['tools/run_tests/run_python.sh', config.venv_python],
+        config.run,
         timeout_seconds=5*60,
         environ=dict(environment.items() +
                      [('GRPC_PYTHON_TESTRUNNER_FILTER', suite_name)]),
-        shortname='%s.test.%s' % (config.venv, suite_name),)
+        shortname='%s.test.%s' % (config.name, suite_name),)
         for suite_name in tests_json
         for config in self.pythons]
 
@@ -413,14 +409,7 @@
     return []
 
   def build_steps(self):
-    return [
-        [
-            'tools/run_tests/build_python.sh',
-            config.python, config.venv,
-            config.venv_relative_python, config.toolchain
-        ]
-        for config in self.pythons
-    ]
+    return [config.build for config in self.pythons]
 
   def post_tests_steps(self):
     return []
@@ -431,23 +420,50 @@
   def dockerfile_dir(self):
     return 'tools/dockerfile/test/python_jessie_%s' % _docker_arch_suffix(self.args.arch)
 
-  def _get_pythons(self, compiler):
-    if os.name == 'nt':
-      venv_relative_python = 'Scripts/python.exe'
-      toolchain = 'mingw32'
+  def _get_pythons(self, args):
+    if args.arch == 'x86':
+      bits = '32'
     else:
-      venv_relative_python = 'bin/python'
-      toolchain = 'unix'
-    python27_config = PythonConfig('python2.7', 'py27', venv_relative_python, toolchain)
-    python34_config = PythonConfig('python3.4', 'py34', venv_relative_python, toolchain)
-    if compiler == 'default':
-      return (python27_config, python34_config,)
-    elif compiler == 'python2.7':
+      bits = '64'
+    if os.name == 'nt':
+      shell = ['bash']
+      builder = [os.path.abspath('tools/run_tests/build_python_msys2.sh')]
+      builder_prefix_arguments = ['MINGW{}'.format(bits)]
+      venv_relative_python = ['Scripts/python.exe']
+      toolchain = ['mingw32']
+      python_pattern_function = lambda major, minor, bits: (
+          '/c/Python{major}{minor}/python.exe'.format(major=major, minor=minor, bits=bits)
+	  if bits == '64' else
+	  '/c/Python{major}{minor}_{bits}bits/python.exe'.format(
+              major=major, minor=minor, bits=bits))
+    else:
+      shell = []
+      builder = [os.path.abspath('tools/run_tests/build_python.sh')]
+      builder_prefix_arguments = []
+      venv_relative_python = ['bin/python']
+      toolchain = ['unix']
+      # Bit-ness is handled by the test machine's environment
+      python_pattern_function = lambda major, minor, bits: 'python{major}.{minor}'.format(major=major, minor=minor)
+    runner = [os.path.abspath('tools/run_tests/run_python.sh')]
+    python_config_generator = lambda name, major, minor, bits: PythonConfig(
+        name,
+        shell + builder + builder_prefix_arguments
+	    + [python_pattern_function(major=major, minor=minor, bits=bits)]
+	    + [name] + venv_relative_python + toolchain,
+        shell + runner + [os.path.join(name, venv_relative_python[0])])
+    python27_config = python_config_generator(name='py27', major='2', minor='7', bits=bits)
+    python34_config = python_config_generator(name='py34', major='3', minor='4', bits=bits)
+    if args.compiler == 'default':
+      if os.name == 'nt':
+        return (python27_config,)
+      else:
+        return (python27_config, python34_config,)
+    elif args.compiler == 'python2.7':
       return (python27_config,)
-    elif compiler == 'python3.4':
+    elif args.compiler == 'python3.4':
       return (python34_config,)
     else:
-      raise Exception('Compiler %s not supported.' % compiler)
+      raise Exception('Compiler %s not supported.' % args.compiler)
 
   def __str__(self):
     return 'python'