Add native process management for Windows and its Java bindings (without a sane Java API for now)

--
MOS_MIGRATED_REVID=126306559
diff --git a/src/main/cpp/blaze_util_mingw.cc b/src/main/cpp/blaze_util_mingw.cc
index d4403fb..6faa2d9 100644
--- a/src/main/cpp/blaze_util_mingw.cc
+++ b/src/main/cpp/blaze_util_mingw.cc
@@ -384,7 +384,7 @@
       NULL,           // _In_opt_    LPSECURITY_ATTRIBUTES lpThreadAttributes,
       TRUE,           // _In_        BOOL                  bInheritHandles,
       //                 _In_        DWORD                 dwCreationFlags,
-      DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP,
+      DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_BREAKAWAY_FROM_JOB,
       NULL,           // _In_opt_    LPVOID                lpEnvironment,
       NULL,           // _In_opt_    LPCTSTR               lpCurrentDirectory,
       &startupInfo,   // _In_        LPSTARTUPINFO         lpStartupInfo,
diff --git a/src/main/java/com/google/devtools/build/lib/windows/WindowsJniLoader.java b/src/main/java/com/google/devtools/build/lib/windows/WindowsJniLoader.java
index ca62fe9..b95aa61 100644
--- a/src/main/java/com/google/devtools/build/lib/windows/WindowsJniLoader.java
+++ b/src/main/java/com/google/devtools/build/lib/windows/WindowsJniLoader.java
@@ -21,4 +21,8 @@
   public static void loadJni() {
     System.loadLibrary("windows_jni");
   }
+
+  public static void loadJniForTesting(String jniDll) {
+    System.load(jniDll);
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/windows/WindowsProcesses.java b/src/main/java/com/google/devtools/build/lib/windows/WindowsProcesses.java
index f6d6260..1f0c34e 100644
--- a/src/main/java/com/google/devtools/build/lib/windows/WindowsProcesses.java
+++ b/src/main/java/com/google/devtools/build/lib/windows/WindowsProcesses.java
@@ -14,6 +14,8 @@
 
 package com.google.devtools.build.lib.windows;
 
+import java.util.List;
+
 /**
  * Process management on Windows.
  */
@@ -23,8 +25,106 @@
     // Prevent construction
   }
 
-  private static native String helloWorld(int arg, String fruit);
-  private static native int nativeGetpid();
+  /**
+   * returns the PID of the current process.
+   */
+  static native int nativeGetpid();
+
+  /**
+   * Creates a process with the specified Windows command line.
+   *
+   * <p>Appropriately quoting arguments is the responsibility of the caller.
+   *
+   * @param commandLine the command line (needs to be quoted Windows style)
+   * @param env the environment of the new process. null means inherit that of the Bazel server
+   * @param stdoutFile the file the stdout should be redirected to. if null, nativeReadStdout will
+   *                   work.
+   * @param stderrFile the file the stdout should be redirected to. if null, nativeReadStderr will
+   *                   work.
+   * @return the opaque identifier of the created process
+   */
+  static native long nativeCreateProcess(String commandLine, byte[] env, String stdoutFile,
+      String stderrFile);
+
+  /**
+   * Writes data from the given array to the stdin of the specified process.
+   *
+   * <p>Blocks until either some data was written or the process is terminated.
+   *
+   * @return the number of bytes written
+   */
+  static native int nativeWriteStdin(long process, byte[] bytes, int offset, int length);
+
+  /**
+   * Reads data from the stdout of the specified process into the given array.
+   *
+   * <p>Blocks until either some data was read or the process is terminated.
+   *
+   * @return the number of bytes read or -1 if there was an error.
+   */
+  static native int nativeReadStdout(long process, byte[] bytes, int offset, int length);
+
+  /**
+   * Reads data from the stderr of the specified process into the given array.
+   *
+   * <p>Blocks until either some data was read or the process is terminated.
+   *
+   * @return the number of bytes read or -1 if there was an error.
+   */
+  static native int nativeReadStderr(long process, byte[] bytes, int offset, int length);
+
+  /**
+   * Interrupts a {@link #nativeWaitFor(long) call on the specified process}.
+   *
+   * <p>Should only be called once on per process and only when a {@link #nativeWaitFor(long)}
+   * call is in progress. Otherwise its behavior is undefined.
+   *
+   * <p>The {@link #nativeWaitFor(long)} call will then return an error.
+   *
+   * <p>Does not modify the error state of the process.
+   */
+  static native void nativeInterrupt(long process);
+
+  /**
+   * Returns if the given process was interrupted by a {@link #nativeInterrupt(long)} call.
+   *
+   * <p>Does not modify the error state of the process.
+   */
+  static native boolean nativeIsInterrupted(long process);
+  /**
+   * Waits until the given process terminates and returns with its exit code or -1 if there was an
+   * error.
+   */
+  static native int nativeWaitFor(long process);
+
+  /**
+   * Returns the process ID of the given process or -1 if there was an error.
+   */
+  static native int nativeGetProcessPid(long process);
+
+  /**
+   * Terminates the given process. Returns true if the termination was successful.
+   */
+  static native boolean nativeTerminate(long process);
+
+  /**
+   * Releases the native data structures associated with the process.
+   *
+   * <p>Calling any other method on the same process after this call will result in the JVM
+   * crashing or worse.
+   */
+  static native void nativeDelete(long process);
+
+  /**
+   * Returns a string representation of the last error caused by any call on the given process
+   * or the empty string if the last operation was successful.
+   *
+   * <p>Does <b>NOT</b> terminate the process if it is still running.
+   *
+   * <p>After this call returns, subsequent calls will return the empty string if there was no
+   * failed operation in between.
+   */
+  static native String nativeGetLastError(long process);
 
   public static int getpid() {
     ensureJni();
@@ -39,4 +139,53 @@
     System.loadLibrary("windows_jni");
     jniLoaded = true;
   }
+
+  static String quoteCommandLine(List<String> argv) {
+    StringBuilder result = new StringBuilder();
+    for (int iArg = 0; iArg < argv.size(); iArg++) {
+      if (iArg != 0) {
+        result.append(" ");
+      }
+      String arg = argv.get(iArg);
+      boolean hasSpace = arg.contains(" ");
+      if (!arg.contains("\"") && !arg.contains("\\") && !hasSpace) {
+        // fast path. Just append the input string.
+        result.append(arg);
+      } else {
+        // We need to quote things if the argument contains a space.
+        if (hasSpace) {
+          result.append("\"");
+        }
+
+        for (int iChar = 0; iChar < arg.length(); iChar++) {
+          boolean lastChar = iChar == arg.length() - 1;
+          switch (arg.charAt(iChar)) {
+            case '"':
+              // Escape double quotes
+              result.append("\\\"");
+              break;
+            case '\\':
+              // Backslashes at the end of the string are quoted if we add quotes
+              if (lastChar) {
+                result.append(hasSpace ? "\\\\" : "\\");
+              } else {
+                // Backslashes everywhere else are quoted if they are followed by a
+                // quote or a backslash
+                result.append(arg.charAt(iChar + 1) == '"' || arg.charAt(iChar + 1) == '\\'
+                    ? "\\\\" : "\\");
+              }
+              break;
+            default:
+              result.append(arg.charAt(iChar));
+          }
+        }
+        // Add ending quotes if we added a quote at the beginning.
+        if (hasSpace) {
+          result.append("\"");
+        }
+      }
+    }
+
+    return result.toString();
+  }
 }
diff --git a/src/main/native/BUILD b/src/main/native/BUILD
index 5f6bdbf..8780377 100644
--- a/src/main/native/BUILD
+++ b/src/main/native/BUILD
@@ -68,6 +68,7 @@
     srcs = ["windows_processes.cc"],
     outs = ["windows_jni.dll"],
     cmd = "$(location build_windows_jni.sh) $@ $(SRCS)",
+    output_to_bindir = 1,
     tools = ["build_windows_jni.sh"],
     visibility = ["//src:__subpackages__"],
 )
diff --git a/src/main/native/windows_processes.cc b/src/main/native/windows_processes.cc
index 0de6cec..5a70aba8 100644
--- a/src/main/native/windows_processes.cc
+++ b/src/main/native/windows_processes.cc
@@ -17,19 +17,437 @@
 #include <string.h>
 #include <windows.h>
 
-extern "C" JNIEXPORT jstring JNICALL
-Java_com_google_devtools_build_lib_windows_WindowsProcesses_helloWorld(
-    JNIEnv* env, jclass clazz, jint arg, jstring fruit) {
-  char buf[512];
-  const char* utf_fruit = env->GetStringUTFChars(fruit, NULL);
-  snprintf(buf, sizeof(buf), "I have %d delicious %s fruits", arg, utf_fruit);
-  jstring result = env->NewStringUTF(buf);
-  env->ReleaseStringUTFChars(fruit, utf_fruit);
+#include <string>
+
+std::string GetLastErrorString(const std::string& cause) {
+  DWORD last_error = GetLastError();
+  if (last_error == 0) {
+    return "";
+  }
+
+  LPSTR message;
+  DWORD size = FormatMessageA(
+      FORMAT_MESSAGE_ALLOCATE_BUFFER
+          | FORMAT_MESSAGE_FROM_SYSTEM
+          | FORMAT_MESSAGE_IGNORE_INSERTS,
+      NULL,
+      last_error,
+      MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
+      (LPSTR) &message,
+      0,
+      NULL);
+
+  if (size == 0) {
+    char buf[256];
+    snprintf(buf, sizeof(buf),
+        "%s: Error %d (cannot format message due to error %d)",
+        cause.c_str(), last_error, GetLastError());
+    buf[sizeof(buf) - 1] = 0;
+    return cause + ": " + std::string(buf);
+  }
+
+  std::string result = std::string(message);
+  LocalFree(message);
   return result;
 }
-
 extern "C" JNIEXPORT jint JNICALL
 Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeGetpid(
     JNIEnv* env, jclass clazz) {
   return GetCurrentProcessId();
 }
+
+struct NativeProcess {
+  HANDLE stdin_;
+  HANDLE stdout_;
+  HANDLE stderr_;
+  HANDLE process_;
+  HANDLE job_;
+  HANDLE event_;
+  std::string error_;
+
+  NativeProcess();
+};
+
+NativeProcess::NativeProcess() {
+  stdin_ = INVALID_HANDLE_VALUE;
+  stdout_ = INVALID_HANDLE_VALUE;
+  stderr_ = INVALID_HANDLE_VALUE;
+  process_ = INVALID_HANDLE_VALUE;
+  job_ = INVALID_HANDLE_VALUE;
+  error_ = "";
+}
+
+extern "C" JNIEXPORT jlong JNICALL
+Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeCreateProcess(
+  JNIEnv *env, jclass clazz, jstring java_commandline, jbyteArray java_env,
+  jstring java_stdout_redirect, jstring java_stderr_redirect) {
+  const char* commandline = env->GetStringUTFChars(java_commandline, NULL);
+  const char* stdout_redirect = NULL;
+  const char* stderr_redirect = NULL;
+
+  if (java_stdout_redirect != NULL) {
+    stdout_redirect = env->GetStringUTFChars(java_stdout_redirect, NULL);
+  }
+
+  if (java_stderr_redirect != NULL) {
+    stderr_redirect = env->GetStringUTFChars(java_stderr_redirect, NULL);
+  }
+
+  jsize env_size = -1;
+  jbyte* env_bytes = NULL;
+
+
+  char* mutable_commandline = new char[strlen(commandline) + 1];
+  strncpy(mutable_commandline, commandline, strlen(commandline) + 1);
+
+  NativeProcess* result = new NativeProcess();
+
+  SECURITY_ATTRIBUTES sa = {0};
+  sa.nLength = sizeof(SECURITY_ATTRIBUTES);
+  sa.bInheritHandle = TRUE;
+
+  HANDLE stdin_process = INVALID_HANDLE_VALUE;
+  HANDLE stdout_process = INVALID_HANDLE_VALUE;
+  HANDLE stderr_process = INVALID_HANDLE_VALUE;
+  HANDLE thread = INVALID_HANDLE_VALUE;
+  HANDLE event = INVALID_HANDLE_VALUE;
+  PROCESS_INFORMATION process_info = {0};
+  STARTUPINFO startup_info = {0};
+
+  if (java_env != NULL) {
+    env_size = env->GetArrayLength(java_env);
+    env_bytes = env->GetByteArrayElements(java_env, NULL);
+
+    if (env_size < 2) {
+      result->error_ = "Environment array must contain at least two bytes";
+      goto cleanup;
+    }
+
+    if (env_bytes[env_size - 1] != 0 || env_bytes[env_size - 2] != 0) {
+      result->error_ = "Environment array must end with two null bytes";
+      goto cleanup;
+    }
+  }
+
+  event = CreateEvent(NULL, TRUE, FALSE, NULL);
+  if (event == NULL) {
+    event = INVALID_HANDLE_VALUE;
+    result->error_ = GetLastErrorString("CreateEvent()");
+    goto cleanup;
+  }
+
+  result->event_ = event;
+
+  if (!CreatePipe(&stdin_process, &result->stdin_, &sa, 0)) {
+    result->error_ = GetLastErrorString("CreatePipe(stdin)");
+    goto cleanup;
+  }
+
+  if (stdout_redirect != NULL) {
+    stdout_process = CreateFile(
+        stdout_redirect,
+        FILE_APPEND_DATA,
+        0,
+        &sa,
+        OPEN_ALWAYS,
+        FILE_ATTRIBUTE_NORMAL,
+        NULL);
+
+    if (stdout_process == INVALID_HANDLE_VALUE) {
+      result->error_ = GetLastErrorString("CreateFile(stdout)");
+      goto cleanup;
+    }
+  } else {
+    if (!CreatePipe(&result->stdout_, &stdout_process, &sa, 0)) {
+      result->error_ = GetLastErrorString("CreatePipe(stdout)");
+      goto cleanup;
+    }
+  }
+
+  if (stderr_redirect != NULL) {
+    if (!strcmp(stdout_redirect, stderr_redirect)) {
+      stderr_process = stdout_process;
+    } else {
+      stderr_process = CreateFile(
+          stderr_redirect,
+          FILE_APPEND_DATA,
+          0,
+          &sa,
+          OPEN_ALWAYS,
+          FILE_ATTRIBUTE_NORMAL,
+          NULL);
+
+      if (stderr_process == INVALID_HANDLE_VALUE) {
+        result->error_ = GetLastErrorString("CreateFile(stderr)");
+        goto cleanup;
+      }
+    }
+  } else {
+    if (!CreatePipe(&result->stderr_, &stderr_process, &sa, 0)) {
+      result->error_ = GetLastErrorString("CreatePipe(stderr)");
+      goto cleanup;
+    }
+  }
+
+
+  // MDSN says that the default for job objects is that breakaway is not
+  // allowed. Thus, we don't need to do any more setup here.
+  HANDLE job = CreateJobObject(NULL, NULL);
+  if (job == NULL) {
+    result->error_ = GetLastErrorString("CreateJobObject()");
+    goto cleanup;
+  }
+
+  result->job_ = job;
+
+  startup_info.hStdInput = stdin_process;
+  startup_info.hStdOutput = stdout_process;
+  startup_info.hStdError = stderr_process;
+  startup_info.dwFlags |= STARTF_USESTDHANDLES;
+
+  BOOL ok = CreateProcess(
+      NULL,
+      mutable_commandline,
+      NULL,
+      NULL,
+      TRUE,
+      DETACHED_PROCESS
+          | CREATE_NEW_PROCESS_GROUP   // So that Ctrl-Break does not affect it
+          | CREATE_BREAKAWAY_FROM_JOB  // We'll put it in a new job
+          | CREATE_SUSPENDED,  // So that it doesn't start a new job itself
+      env_bytes,
+      NULL,
+      &startup_info,
+      &process_info);
+
+  if (!ok) {
+    result->error_ = GetLastErrorString("CreateProcess()");
+    goto cleanup;
+  }
+
+  result->process_ = process_info.hProcess;
+  thread = process_info.hThread;
+
+  if (!AssignProcessToJobObject(result->job_, result->process_)) {
+    result->error_ = GetLastErrorString("AssignProcessToJobObject()");
+    goto cleanup;
+  }
+
+  // Now that we put the process in a new job object, we can start executing it
+  if (ResumeThread(thread) == -1) {
+    result->error_ = GetLastErrorString("ResumeThread()");
+    goto cleanup;
+  }
+
+  result->error_ = "";
+
+cleanup:
+  // Standard file handles are closed even if the process was successfully
+  // created. If this was not so, operations on these file handles would not
+  // return immediately if the process is terminated.
+  if (stdin_process != INVALID_HANDLE_VALUE) {
+    CloseHandle(stdin_process);
+  }
+
+  if (stdout_process != INVALID_HANDLE_VALUE) {
+    CloseHandle(stdout_process);
+  }
+
+  if (stderr_process != INVALID_HANDLE_VALUE
+      && stderr_process != stdout_process) {
+    CloseHandle(stderr_process);
+  }
+
+  if (thread != INVALID_HANDLE_VALUE) {
+    CloseHandle(thread);
+  }
+
+  delete[] mutable_commandline;
+  if (env_bytes != NULL) {
+    env->ReleaseByteArrayElements(java_env, env_bytes, 0);
+  }
+  env->ReleaseStringUTFChars(java_commandline, commandline);
+
+  if (stdout_redirect != NULL) {
+    env->ReleaseStringUTFChars(java_stdout_redirect, stdout_redirect);
+  }
+
+  if (stderr_redirect != NULL) {
+    env->ReleaseStringUTFChars(java_stderr_redirect, stderr_redirect);
+  }
+
+  return reinterpret_cast<jlong>(result);
+}
+
+extern "C" JNIEXPORT jint JNICALL
+Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeWriteStdin(
+    JNIEnv *env, jclass clazz, jlong process_long, jbyteArray java_bytes,
+    jint offset, jint length) {
+  NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long);
+  jsize array_size = env->GetArrayLength(java_bytes);
+  if (offset < 0 || length <= 0 || offset > array_size - length) {
+    process->error_ = "Array index out of bounds";
+    return -1;
+  }
+
+  jbyte* bytes = env->GetByteArrayElements(java_bytes, NULL);
+  DWORD bytes_written;
+
+  if (!WriteFile(process->stdin_, bytes + offset, length, &bytes_written,
+      NULL)) {
+    process->error_ = GetLastErrorString("WriteFile()");
+    bytes_written = -1;
+  }
+
+  env->ReleaseByteArrayElements(java_bytes, bytes, 0);
+  process->error_ = "";
+  return bytes_written;
+}
+
+jint ReadFromHandle(HANDLE handle, NativeProcess* process, JNIEnv* env,
+    jbyteArray java_bytes, jint offset, jint length) {
+  if (handle == INVALID_HANDLE_VALUE) {
+    process->error_ = "File handle closed";
+    return -1;
+  }
+
+  jsize array_size = env->GetArrayLength(java_bytes);
+  if (offset < 0 || length <= 0 || offset > array_size - length) {
+    process->error_ = "Array index out of bounds";
+    return -1;
+  }
+
+  jbyte* bytes = env->GetByteArrayElements(java_bytes, NULL);
+  DWORD bytes_read;
+  if (!ReadFile(handle, bytes + offset, length, &bytes_read, NULL)) {
+    process->error_ = GetLastErrorString("ReadFile()");
+    bytes_read = -1;
+  }
+
+  env->ReleaseByteArrayElements(java_bytes, bytes, 0);
+  process->error_ = "";
+  return bytes_read;
+}
+
+extern "C" JNIEXPORT jint JNICALL
+Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeReadStdout(
+    JNIEnv *env, jclass clazz, jlong process_long, jbyteArray java_bytes,
+    jint offset, jint length) {
+  NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long);
+  return ReadFromHandle(process->stdout_, process, env, java_bytes, offset,
+      length);
+}
+
+extern "C" JNIEXPORT jint JNICALL
+Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeReadStderr(
+    JNIEnv *env, jclass clazz, jlong process_long, jbyteArray java_bytes,
+    jint offset, jint length) {
+  NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long);
+  return ReadFromHandle(process->stderr_, process, env, java_bytes, offset,
+      length);
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeInterrupt(
+    JNIEnv *env, jclass clazz, jlong process_long) {
+  NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long);
+  SetEvent(process->event_);
+}
+
+extern "C" JNIEXPORT jboolean JNICALL
+Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeIsInterrupted(
+    JNIEnv *env, jclass clazz, jlong process_long) {
+  NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long);
+  return WaitForSingleObject(process->event_, 0) != WAIT_TIMEOUT;
+}
+
+extern "C" JNIEXPORT jint JNICALL
+Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeWaitFor(
+    JNIEnv *env, jclass clazz, jlong process_long) {
+  NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long);
+  HANDLE handles[2] = { process->process_, process->event_ };
+  switch (WaitForMultipleObjects(2, handles, FALSE, INFINITE)) {
+    case 0: {
+      // Process terminated
+      DWORD exit_code;
+      if (!GetExitCodeProcess(process->process_, &exit_code)) {
+        process->error_ = GetLastErrorString("GetExitCodeProcess()");
+        return -1;
+      }
+
+      process->error_ = "";
+      return exit_code;
+    }
+
+    case 1:
+      // Interrupted
+      process->error_ = "Interrupted";
+      return -1;
+
+    case WAIT_FAILED:
+      process->error_ = GetLastErrorString("WaitForMultipleObjects()");
+      return -1;
+
+    default:
+      process->error_ = "WaitForMultipleObjects() returned unknown result";
+      return -1;
+  }
+}
+
+extern "C" JNIEXPORT jint JNICALL
+Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeGetProcessPid(
+    JNIEnv *env, jclass clazz, jlong process_long) {
+  NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long);
+  process->error_ = "";
+  return GetProcessId(process->process_);  // MSDN says that this cannot fail
+}
+
+extern "C" JNIEXPORT jboolean JNICALL
+Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeTerminate(
+    JNIEnv *env, jclass clazz, jlong process_long) {
+  NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long);
+  if (!TerminateJobObject(process->job_, 0)) {
+    process->error_ = GetLastErrorString("TerminateJobObject()");
+    return JNI_FALSE;
+  }
+
+  process->error_ = "";
+  return JNI_TRUE;
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeDelete(
+    JNIEnv *env, jclass clazz, jlong process_long) {
+  NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long);
+
+  if (process->stdin_ != INVALID_HANDLE_VALUE) {
+    CloseHandle(process->stdin_);
+  }
+
+  if (process->stdout_ != INVALID_HANDLE_VALUE) {
+    CloseHandle(process->stdout_);
+  }
+
+  if (process->stderr_ != INVALID_HANDLE_VALUE) {
+    CloseHandle(process->stderr_);
+  }
+
+  if (process->process_ != INVALID_HANDLE_VALUE) {
+    CloseHandle(process->process_);
+  }
+
+  if (process->job_ != INVALID_HANDLE_VALUE) {
+    CloseHandle(process->job_);
+  }
+
+  delete process;
+}
+
+extern "C" JNIEXPORT jstring JNICALL
+Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeGetLastError(
+    JNIEnv *env, jclass clazz, jlong process_long) {
+  NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long);
+  jstring result = env->NewStringUTF(process->error_.c_str());
+  process->error_ = "";
+  return result;
+}
diff --git a/src/test/java/com/google/devtools/build/lib/BUILD b/src/test/java/com/google/devtools/build/lib/BUILD
index 6f890c6..45763d0 100644
--- a/src/test/java/com/google/devtools/build/lib/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/BUILD
@@ -133,6 +133,8 @@
     ],
 )
 
+# Tests that test Windows-specific functionality that run on other operating
+# systems
 java_test(
     name = "windows_test",
     srcs = [
@@ -151,6 +153,7 @@
         "//src/main/java/com/google/devtools/build/lib:inmemoryfs",
         "//src/main/java/com/google/devtools/build/lib:util",
         "//src/main/java/com/google/devtools/build/lib:vfs",
+        "//src/main/java/com/google/devtools/build/lib:windows",
         "//src/main/java/com/google/devtools/common/options",
         "//third_party:guava",
         "//third_party:guava-testlib",
@@ -159,6 +162,32 @@
     ],
 )
 
+# Tests that need to run on Windows
+java_test(
+    name = "windows-tests",
+    srcs = glob(
+        ["windows/*.java"],
+        exclude = ["windows/MockSubprocess.java"],
+    ),
+    data = [
+        ":MockSubprocess_deploy.jar",
+    ] + select({
+        "//src:windows": ["//src/main/native:windows_jni.dll"],
+        "//conditions:default": [
+            "//src/main/native:libunix.dylib",
+            "//src/main/native:libunix.so",
+        ],
+    }),
+    test_class = "com.google.devtools.build.lib.AllTests",
+    deps = [
+        ":test_runner",
+        ":testutil",
+        "//src/main/java/com/google/devtools/build/lib:os_util",
+        "//src/main/java/com/google/devtools/build/lib:windows",
+        "//third_party:truth",
+    ],
+)
+
 java_library(
     name = "actions_testutil",
     srcs = glob([
@@ -980,6 +1009,11 @@
     ],
 )
 
+java_binary(
+    name = "MockSubprocess",
+    srcs = ["windows/MockSubprocess.java"],
+)
+
 java_library(
     name = "ExampleWorker-lib",
     srcs = glob(["worker/ExampleWorker*.java"]),
diff --git a/src/test/java/com/google/devtools/build/lib/windows/MockSubprocess.java b/src/test/java/com/google/devtools/build/lib/windows/MockSubprocess.java
new file mode 100644
index 0000000..7e2d90c
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/windows/MockSubprocess.java
@@ -0,0 +1,94 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.windows;
+
+import java.io.PrintStream;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Mock subprocess to be used for testing Windows process management. Command line usage:
+ *
+ * <ul>
+ *   <li><code>I&lt;register&gt;&lt;count&gt;</code>: Read count bytes to the specified register
+ *   <li><code>O-&lt;string&gt;</code>: Write a string to stdout</li>
+ *   <li><code>E-&lt;string&gt;</code>: Write a string to stderr</li>
+ *   <li><code>O$&lt;variable&gt;</code>: Write an environment variable to stdout</li>
+ *   <li><code>E$&lt;variable&gt;</code>: Write an environment variable to stderr</li>
+ *   <li><code>O&lt;register&gt;</code>: Write the contents of a register to stdout</li>
+ *   <li><code>E&lt;register&gt;</code>: Write the contents of a register to stderr</li>
+ *   <li><code>X&lt;exit code%gt;</code>: Exit with the specified exit code</li>
+ * </ul>
+ *
+ * <p>Registers are single characters. Each command line argument is interpreted as a single
+ * operation. Example:
+ *
+ * <code>
+ *   Ia10 Oa Oa Ea E-OVER X42
+ * </code>
+ *
+ * Means: read 10 bytes from stdin, write them back twice to stdout and once to stderr, write
+ * the string "OVER" to stderr then exit with exit code 42.
+ */
+public class MockSubprocess {
+  private static Map<Character, byte[]> registers = new HashMap<>();
+
+  private static void writeBytes(PrintStream stream, String arg) throws Exception {
+    byte[] buf;
+    switch (arg.charAt(1)) {
+      case '-':
+        // Immediate string
+        buf = arg.substring(2).getBytes(Charset.forName("UTF-8"));
+        break;
+
+      case '$':
+        // Environment variable
+        buf = System.getenv(arg.substring(2)).getBytes(Charset.forName("UTF-8"));
+        break;
+
+      default:
+        buf = registers.get(arg.charAt(1));
+        break;
+    }
+
+    stream.write(buf, 0, buf.length);
+}
+
+  public static void main(String[] args) throws Exception {
+    for (String arg : args) {
+      switch (arg.charAt(0)) {
+        case 'I':
+          char register = arg.charAt(1);
+          int length = Integer.parseInt(arg.substring(2));
+          byte[] buf = new byte[length];
+          registers.put(register, buf);
+          System.in.read(buf, 0, length);
+          break;
+
+        case 'E':
+          writeBytes(System.err, arg);
+          break;
+
+        case 'O':
+          writeBytes(System.out, arg);
+          break;
+
+        case 'X':
+          System.exit(Integer.parseInt(arg.substring(1)));
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/windows/WindowsProcessesTest.java b/src/test/java/com/google/devtools/build/lib/windows/WindowsProcessesTest.java
new file mode 100644
index 0000000..ecd7c26
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/windows/WindowsProcessesTest.java
@@ -0,0 +1,358 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.windows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.util.concurrent.Uninterruptibles;
+import com.google.devtools.build.lib.testutil.TestSpec;
+import com.google.devtools.build.lib.util.OS;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Unit tests for {@link WindowsProcesses}.
+ */
+@RunWith(JUnit4.class)
+@TestSpec(localOnly = true, supportedOs = OS.WINDOWS)
+public class WindowsProcessesTest {
+  private static final Charset UTF8 = Charset.forName("UTF-8");
+  private String mockSubprocess;
+  private String javaHome;
+  private long process;
+
+  @Before
+  public void loadJni() throws Exception {
+    String jniDllPath = WindowsTestUtil.getRunfile("io_bazel/src/main/native/windows_jni.dll");
+    mockSubprocess = WindowsTestUtil.getRunfile(
+        "io_bazel/src/test/java/com/google/devtools/build/lib/MockSubprocess_deploy.jar");
+    javaHome = System.getProperty("java.home");
+
+    WindowsJniLoader.loadJniForTesting(jniDllPath);
+
+    process = -1;
+  }
+
+  @After
+  public void terminateProcess() throws Exception {
+    if (process != -1) {
+      WindowsProcesses.nativeTerminate(process);
+      WindowsProcesses.nativeDelete(process);
+      process = -1;
+    }
+  }
+  private String mockArgs(String... args) {
+    List<String> argv = new ArrayList<>();
+
+    argv.add(javaHome + "/bin/java");
+    argv.add("-jar");
+    argv.add(mockSubprocess);
+    for (String arg : args) {
+      argv.add(arg);
+    }
+
+    return WindowsProcesses.quoteCommandLine(argv);
+  }
+
+  private void assertNoError() throws Exception {
+    assertThat(WindowsProcesses.nativeGetLastError(process)).isEmpty();
+  }
+
+  @Test
+  public void testSmoke() throws Exception {
+    process = WindowsProcesses.nativeCreateProcess(mockArgs("Ia5", "Oa"), null, null, null);
+    assertNoError();
+
+    byte[] input = "HELLO".getBytes(UTF8);
+    byte[] output = new byte[5];
+    WindowsProcesses.nativeWriteStdin(process, input, 0, 5);
+    assertNoError();
+    WindowsProcesses.nativeReadStdout(process, output, 0, 5);
+    assertNoError();
+    assertThat(new String(output, UTF8)).isEqualTo("HELLO");
+  }
+
+  @Test
+  public void testPingpong() throws Exception {
+    List<String> args = new ArrayList<>();
+    for (int i = 0; i < 100; i++) {
+      args.add("Ia3");
+      args.add("Oa");
+    }
+
+    process = WindowsProcesses.nativeCreateProcess(mockArgs(args.toArray(new String[] {})), null,
+        null, null);
+    for (int i = 0; i < 100; i++) {
+      byte[] input = String.format("%03d", i).getBytes(UTF8);
+      assertThat(input.length).isEqualTo(3);
+      assertThat(WindowsProcesses.nativeWriteStdin(process, input, 0, 3)).isEqualTo(3);
+      byte[] output = new byte[3];
+      assertThat(WindowsProcesses.nativeReadStdout(process, output, 0, 3)).isEqualTo(3);
+      assertThat(Integer.parseInt(new String(output, UTF8))).isEqualTo(i);
+    }
+  }
+
+  private void startInterruptThread(final long delayMilliseconds) {
+    Thread thread = new Thread(new Runnable() {
+      @Override
+      public void run() {
+        while (true) {
+          Uninterruptibles.sleepUninterruptibly(delayMilliseconds, TimeUnit.MILLISECONDS);
+          WindowsProcesses.nativeInterrupt(process);
+        }
+      }
+    });
+
+    thread.setDaemon(true);
+    thread.start();
+  }
+
+  @Test
+  public void testInterruption() throws Exception {
+    process = WindowsProcesses.nativeCreateProcess(mockArgs("Ia1"), null, null, null);  // hang
+    startInterruptThread(1000);
+    // If the interruption doesn't work, this will hang indefinitely, but there isn't a lot
+    // we can do in that case because we can't just tell native code to stop whatever it's doing
+    // from Java.
+    assertThat(WindowsProcesses.nativeWaitFor(process)).isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeIsInterrupted(process)).isTrue();
+  }
+
+  @Test
+  public void testExitCode() throws Exception {
+    process = WindowsProcesses.nativeCreateProcess(mockArgs("X42"), null, null, null);
+    assertThat(WindowsProcesses.nativeWaitFor(process)).isEqualTo(42);
+    assertNoError();
+  }
+
+  @Test
+  public void testPartialRead() throws Exception {
+    process = WindowsProcesses.nativeCreateProcess(mockArgs("O-HELLO"), null, null, null);
+    byte[] one = new byte[2];
+    byte[] two = new byte[3];
+
+    assertThat(WindowsProcesses.nativeReadStdout(process, one, 0, 2)).isEqualTo(2);
+    assertNoError();
+    assertThat(WindowsProcesses.nativeReadStdout(process, two, 0, 3)).isEqualTo(3);
+    assertNoError();
+
+    assertThat(new String(one, UTF8)).isEqualTo("HE");
+    assertThat(new String(two, UTF8)).isEqualTo("LLO");
+  }
+
+  @Test
+  public void testArrayOutOfBounds() throws Exception {
+    process = WindowsProcesses.nativeCreateProcess(mockArgs("O-oob"), null, null, null);
+    byte[] buf = new byte[3];
+    assertThat(WindowsProcesses.nativeReadStdout(process, buf, -1, 3)).isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 5)).isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeReadStdout(process, buf, 4, 1)).isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeReadStdout(process, buf, 2, -1)).isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeReadStdout(process, buf, Integer.MAX_VALUE, 2))
+        .isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeReadStdout(process, buf, 2, Integer.MAX_VALUE))
+        .isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeReadStderr(process, buf, -1, 3)).isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeReadStderr(process, buf, 0, 5)).isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeReadStderr(process, buf, 4, 1)).isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeReadStderr(process, buf, 2, -1)).isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeReadStderr(process, buf, Integer.MAX_VALUE, 2))
+        .isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeReadStderr(process, buf, 2, Integer.MAX_VALUE))
+        .isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeWriteStdin(process, buf, -1, 3)).isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeWriteStdin(process, buf, 0, 5)).isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeWriteStdin(process, buf, 4, 1)).isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeWriteStdin(process, buf, 2, -1)).isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeWriteStdin(process, buf, Integer.MAX_VALUE, 2))
+        .isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeWriteStdin(process, buf, 2, Integer.MAX_VALUE))
+        .isEqualTo(-1);
+
+    assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 3)).isEqualTo(3);
+    assertThat(new String(buf, UTF8)).isEqualTo("oob");
+  }
+
+  @Test
+  public void testOffsetedOps() throws Exception {
+    process = WindowsProcesses.nativeCreateProcess(mockArgs("Ia3", "Oa"), null, null, null);
+    byte[] input = "01234".getBytes(UTF8);
+    byte[] output = "abcde".getBytes(UTF8);
+
+    assertThat(WindowsProcesses.nativeWriteStdin(process, input, 1, 3)).isEqualTo(3);
+    assertNoError();
+    int rv = WindowsProcesses.nativeReadStdout(process, output, 1, 3);
+    assertNoError();
+    assertThat(rv).isEqualTo(3);
+
+    assertThat(new String(output, UTF8)).isEqualTo("a123e");
+  }
+
+  @Test
+  public void testParallelStdoutAndStderr() throws Exception {
+    process = WindowsProcesses.nativeCreateProcess(mockArgs(
+        "O-out1", "E-err1", "O-out2", "E-err2", "E-err3", "O-out3", "E-err4", "O-out4"),
+        null, null, null);
+
+    byte[] buf = new byte[4];
+    assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 4)).isEqualTo(4);
+    assertThat(new String(buf, UTF8)).isEqualTo("out1");
+    assertThat(WindowsProcesses.nativeReadStderr(process, buf, 0, 4)).isEqualTo(4);
+    assertThat(new String(buf, UTF8)).isEqualTo("err1");
+
+    assertThat(WindowsProcesses.nativeReadStderr(process, buf, 0, 4)).isEqualTo(4);
+    assertThat(new String(buf, UTF8)).isEqualTo("err2");
+    assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 4)).isEqualTo(4);
+    assertThat(new String(buf, UTF8)).isEqualTo("out2");
+
+    assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 4)).isEqualTo(4);
+    assertThat(new String(buf, UTF8)).isEqualTo("out3");
+    assertThat(WindowsProcesses.nativeReadStderr(process, buf, 0, 4)).isEqualTo(4);
+    assertThat(new String(buf, UTF8)).isEqualTo("err3");
+
+    assertThat(WindowsProcesses.nativeReadStderr(process, buf, 0, 4)).isEqualTo(4);
+    assertThat(new String(buf, UTF8)).isEqualTo("err4");
+    assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 4)).isEqualTo(4);
+    assertThat(new String(buf, UTF8)).isEqualTo("out4");
+  }
+
+  @Test
+  public void testExecutableNotFound() throws Exception {
+    process = WindowsProcesses.nativeCreateProcess("ThisExecutableDoesNotExist", null, null, null);
+    assertThat(WindowsProcesses.nativeGetLastError(process))
+        .contains("The system cannot find the file specified.");
+    byte[] buf = new byte[1];
+    assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 1)).isEqualTo(-1);
+  }
+
+  @Test
+  public void testReadingAndWritingAfterTermination() throws Exception {
+    process = WindowsProcesses.nativeCreateProcess("X42", null, null, null);
+    byte[] buf = new byte[1];
+    assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 1)).isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeReadStderr(process, buf, 0, 1)).isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeWriteStdin(process, buf, 0, 1)).isEqualTo(-1);
+  }
+
+  @Test
+  public void testNewEnvironmentVariables() throws Exception {
+    byte[] data = "ONE=one\0TWO=twotwo\0\0".getBytes(UTF8);
+    process = WindowsProcesses.nativeCreateProcess(mockArgs("O$ONE", "O$TWO"), data, null, null);
+    assertNoError();
+    byte[] buf = new byte[3];
+    assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 3)).isEqualTo(3);
+    assertThat(new String(buf, UTF8)).isEqualTo("one");
+    buf = new byte[6];
+    assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 6)).isEqualTo(6);
+    assertThat(new String(buf, UTF8)).isEqualTo("twotwo");
+  }
+
+  @Test
+  public void testNoZeroInEnvBuffer() throws Exception {
+    byte[] data = "clown".getBytes(UTF8);
+    process = WindowsProcesses.nativeCreateProcess(mockArgs(), data, null, null);
+    assertThat(WindowsProcesses.nativeGetLastError(process)).isNotEmpty();
+  }
+
+  @Test
+  public void testOneZeroInEnvBuffer() throws Exception {
+    byte[] data = "FOO=bar\0".getBytes(UTF8);
+    process = WindowsProcesses.nativeCreateProcess(mockArgs(), data, null, null);
+    assertThat(WindowsProcesses.nativeGetLastError(process)).isNotEmpty();
+  }
+
+  @Test
+  public void testOneByteEnvBuffer() throws Exception {
+    byte[] data = "a".getBytes(UTF8);
+    process = WindowsProcesses.nativeCreateProcess(mockArgs(), data, null, null);
+    assertThat(WindowsProcesses.nativeGetLastError(process)).isNotEmpty();
+  }
+
+  @Test
+  public void testRedirect() throws Exception {
+    String stdoutFile = System.getenv("TEST_TMPDIR") + "\\stdout_redirect";
+    String stderrFile = System.getenv("TEST_TMPDIR") + "\\stderr_redirect";
+
+    process = WindowsProcesses.nativeCreateProcess(mockArgs("O-one", "E-two"),
+        null, stdoutFile, stderrFile);
+    assertThat(process).isGreaterThan(0L);
+    assertNoError();
+    WindowsProcesses.nativeWaitFor(process);
+    assertNoError();
+    byte[] stdout = Files.readAllBytes(Paths.get(stdoutFile));
+    byte[] stderr = Files.readAllBytes(Paths.get(stderrFile));
+    assertThat(new String(stdout, UTF8)).isEqualTo("one");
+    assertThat(new String(stderr, UTF8)).isEqualTo("two");
+  }
+
+  @Test
+  public void testRedirectToSameFile() throws Exception {
+    String file = System.getenv("TEST_TMPDIR") + "\\captured_";
+
+    process = WindowsProcesses.nativeCreateProcess(mockArgs("O-one", "E-two"),
+        null, file, file);
+    assertThat(process).isGreaterThan(0L);
+    assertNoError();
+    WindowsProcesses.nativeWaitFor(process);
+    assertNoError();
+    byte[] bytes = Files.readAllBytes(Paths.get(file));
+    assertThat(new String(bytes, UTF8)).isEqualTo("onetwo");
+  }
+
+  @Test
+  public void testErrorWhenReadingFromRedirectedStreams() throws Exception {
+    String stdoutFile = System.getenv("TEST_TMPDIR") + "\\captured_stdout";
+    String stderrFile = System.getenv("TEST_TMPDIR") + "\\captured_stderr";
+
+    process = WindowsProcesses.nativeCreateProcess(mockArgs("O-one", "E-two"), null,
+        stdoutFile, stderrFile);
+    assertNoError();
+    byte[] buf = new byte[1];
+    assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 1)).isEqualTo(-1);
+    assertThat(WindowsProcesses.nativeReadStderr(process, buf, 0, 1)).isEqualTo(-1);
+    WindowsProcesses.nativeWaitFor(process);
+  }
+
+  @Test
+  public void testAppendToExistingFile() throws Exception {
+    String stdoutFile = System.getenv("TEST_TMPDIR") + "\\stdout_atef";
+    String stderrFile = System.getenv("TEST_TMPDIR") + "\\stderr_atef";
+    Path stdout = Paths.get(stdoutFile);
+    Path stderr = Paths.get(stderrFile);
+    Files.write(stdout, "out1".getBytes(UTF8));
+    Files.write(stderr, "err1".getBytes(UTF8));
+
+    process = WindowsProcesses.nativeCreateProcess(mockArgs("O-out2", "E-err2"), null,
+        stdoutFile, stderrFile);
+    assertNoError();
+    WindowsProcesses.nativeWaitFor(process);
+    assertNoError();
+    byte[] stdoutBytes = Files.readAllBytes(Paths.get(stdoutFile));
+    byte[] stderrBytes = Files.readAllBytes(Paths.get(stderrFile));
+    assertThat(new String(stdoutBytes, UTF8)).isEqualTo("out1out2");
+    assertThat(new String(stderrBytes, UTF8)).isEqualTo("err1err2");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/windows/WindowsTestUtil.java b/src/test/java/com/google/devtools/build/lib/windows/WindowsTestUtil.java
new file mode 100644
index 0000000..6440d78
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/windows/WindowsTestUtil.java
@@ -0,0 +1,55 @@
+// Copyright 2016 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.windows;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Utilities for running Java tests on Windows.
+ */
+public class WindowsTestUtil {
+  private static Map<String, String> runfiles;
+  public static String getRunfile(String runfilesPath) throws IOException {
+    ensureRunfilesParsed();
+    return runfiles.get(runfilesPath);
+  }
+
+  private static synchronized void ensureRunfilesParsed() throws IOException {
+    if (runfiles != null) {
+      return;
+    }
+
+    runfiles = new HashMap<>();
+    InputStream fis = new FileInputStream(System.getenv("RUNFILES_MANIFEST_FILE"));
+    InputStreamReader isr = new InputStreamReader(fis, Charset.forName("UTF-8"));
+    BufferedReader br = new BufferedReader(isr);
+    String line;
+    while ((line = br.readLine()) != null) {
+      String[] splitLine = line.split(" ");  // This is buggy when the path contains spaces
+      if (splitLine.length != 2) {
+        continue;
+      }
+
+      runfiles.put(splitLine[0], splitLine[1]);
+    }
+  }
+}