aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorGravatar Lukacs Berki <lberki@google.com>2016-06-30 15:46:10 +0000
committerGravatar Lukacs Berki <lberki@google.com>2016-07-01 07:09:55 +0000
commitdc174c400c197983a984b2f28e64eeb66fb92c0a (patch)
treeea8ca3c3500453c4ae2455b16310b72617e03d77 /src
parent7e9358786a756fd8362b9577e94946235114b182 (diff)
Add native process management for Windows and its Java bindings (without a sane Java API for now)
-- MOS_MIGRATED_REVID=126306559
Diffstat (limited to 'src')
-rw-r--r--src/main/cpp/blaze_util_mingw.cc2
-rw-r--r--src/main/java/com/google/devtools/build/lib/windows/WindowsJniLoader.java4
-rw-r--r--src/main/java/com/google/devtools/build/lib/windows/WindowsProcesses.java153
-rw-r--r--src/main/native/BUILD1
-rw-r--r--src/main/native/windows_processes.cc436
-rw-r--r--src/test/java/com/google/devtools/build/lib/BUILD34
-rw-r--r--src/test/java/com/google/devtools/build/lib/windows/MockSubprocess.java94
-rw-r--r--src/test/java/com/google/devtools/build/lib/windows/WindowsProcessesTest.java358
-rw-r--r--src/test/java/com/google/devtools/build/lib/windows/WindowsTestUtil.java55
9 files changed, 1125 insertions, 12 deletions
diff --git a/src/main/cpp/blaze_util_mingw.cc b/src/main/cpp/blaze_util_mingw.cc
index d4403fba2c..6faa2d945d 100644
--- a/src/main/cpp/blaze_util_mingw.cc
+++ b/src/main/cpp/blaze_util_mingw.cc
@@ -384,7 +384,7 @@ void ExecuteDaemon(const string& exe, const std::vector<string>& args_vector,
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 ca62fe9052..b95aa615ef 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 class WindowsJniLoader {
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 f6d62602b3..1f0c34e25a 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 @@ public class WindowsProcesses {
// 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 @@ public class WindowsProcesses {
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 5f6bdbf92c..8780377ef4 100644
--- a/src/main/native/BUILD
+++ b/src/main/native/BUILD
@@ -68,6 +68,7 @@ genrule(
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 0de6cecb37..5a70aba85d 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 6f890c67ad..45763d0c69 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 @@ java_test(
],
)
+# Tests that test Windows-specific functionality that run on other operating
+# systems
java_test(
name = "windows_test",
srcs = [
@@ -151,6 +153,7 @@ java_test(
"//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 @@ java_test(
],
)
+# 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_test(
],
)
+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 0000000000..7e2d90cef6
--- /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 0000000000..ecd7c26229
--- /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 0000000000..6440d78134
--- /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]);
+ }
+ }
+}