From dc174c400c197983a984b2f28e64eeb66fb92c0a Mon Sep 17 00:00:00 2001 From: Lukacs Berki Date: Thu, 30 Jun 2016 15:46:10 +0000 Subject: Add native process management for Windows and its Java bindings (without a sane Java API for now) -- MOS_MIGRATED_REVID=126306559 --- src/main/cpp/blaze_util_mingw.cc | 2 +- .../build/lib/windows/WindowsJniLoader.java | 4 + .../build/lib/windows/WindowsProcesses.java | 153 +++++++- src/main/native/BUILD | 1 + src/main/native/windows_processes.cc | 436 ++++++++++++++++++++- src/test/java/com/google/devtools/build/lib/BUILD | 34 ++ .../devtools/build/lib/windows/MockSubprocess.java | 94 +++++ .../build/lib/windows/WindowsProcessesTest.java | 358 +++++++++++++++++ .../build/lib/windows/WindowsTestUtil.java | 55 +++ 9 files changed, 1125 insertions(+), 12 deletions(-) create mode 100644 src/test/java/com/google/devtools/build/lib/windows/MockSubprocess.java create mode 100644 src/test/java/com/google/devtools/build/lib/windows/WindowsProcessesTest.java create mode 100644 src/test/java/com/google/devtools/build/lib/windows/WindowsTestUtil.java (limited to 'src') 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& 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. + * + *

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. + * + *

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. + * + *

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. + * + *

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}. + * + *

Should only be called once on per process and only when a {@link #nativeWaitFor(long)} + * call is in progress. Otherwise its behavior is undefined. + * + *

The {@link #nativeWaitFor(long)} call will then return an error. + * + *

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. + * + *

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. + * + *

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. + * + *

Does NOT terminate the process if it is still running. + * + *

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 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 #include -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 + +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(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(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(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(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(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(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(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(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(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(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(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: + * + *

+ * + *

Registers are single characters. Each command line argument is interpreted as a single + * operation. Example: + * + * + * Ia10 Oa Oa Ea E-OVER X42 + * + * + * 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 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 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 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 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]); + } + } +} -- cgit v1.2.3