blob: 9210d259aa70a1e621ad902712a9a542d900a37c [file] [log] [blame]
// 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.
#define WINVER 0x0601
#define _WIN32_WINNT 0x0601
#include <jni.h>
#include <string.h>
#include <windows.h>
#include <atomic>
#include <string>
#include "src/main/native/windows_error_handling.h"
extern "C" JNIEXPORT jint JNICALL
Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeGetpid(
JNIEnv* env, jclass clazz) {
return GetCurrentProcessId();
}
struct NativeOutputStream {
HANDLE handle_;
std::string error_;
std::atomic<bool> closed_;
NativeOutputStream()
: handle_(INVALID_HANDLE_VALUE),
error_(""),
closed_(false) {}
void close() {
closed_.store(true);
if (handle_ == INVALID_HANDLE_VALUE) {
return;
}
CancelIoEx(handle_, NULL);
CloseHandle(handle_);
handle_ = INVALID_HANDLE_VALUE;
}
};
struct NativeProcess {
HANDLE stdin_;
NativeOutputStream stdout_;
NativeOutputStream stderr_;
HANDLE process_;
HANDLE job_;
DWORD pid_;
std::string error_;
NativeProcess()
: stdin_(INVALID_HANDLE_VALUE),
stdout_(),
stderr_(),
process_(INVALID_HANDLE_VALUE),
job_(INVALID_HANDLE_VALUE),
error_("") {}
};
static bool NestedJobsSupported() {
OSVERSIONINFOEX version_info;
version_info.dwOSVersionInfoSize = sizeof(version_info);
if (!GetVersionEx(reinterpret_cast<OSVERSIONINFO*>(&version_info))) {
return false;
}
return version_info.dwMajorVersion > 6 ||
version_info.dwMajorVersion == 6 && version_info.dwMinorVersion >= 2;
}
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_cwd, 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;
const char* cwd = 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);
}
if (java_cwd != NULL) {
cwd = env->GetStringUTFChars(java_cwd, 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};
STARTUPINFOA startup_info = {0};
JOBOBJECT_EXTENDED_LIMIT_INFORMATION job_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_ = "The environment must be at least two bytes long";
goto cleanup;
} else 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;
}
}
if (!CreatePipe(&stdin_process, &result->stdin_, &sa, 0)) {
result->error_ = GetLastErrorString("CreatePipe(stdin)");
goto cleanup;
}
if (stdout_redirect != NULL) {
result->stdout_.close();
stdout_process = CreateFileA(
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_.handle_, &stdout_process, &sa, 0)) {
result->error_ = GetLastErrorString("CreatePipe(stdout)");
goto cleanup;
}
}
if (stderr_redirect != NULL) {
result->stderr_.close();
if (!strcmp(stdout_redirect, stderr_redirect)) {
stderr_process = stdout_process;
} else {
stderr_process = CreateFileA(
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_.handle_, &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;
job_info.BasicLimitInformation.LimitFlags =
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!SetInformationJobObject(
job,
JobObjectExtendedLimitInformation,
&job_info,
sizeof(job_info))) {
result->error_ = GetLastErrorString("SetInformationJobObject()");
goto cleanup;
}
startup_info.hStdInput = stdin_process;
startup_info.hStdOutput = stdout_process;
startup_info.hStdError = stderr_process;
startup_info.dwFlags |= STARTF_USESTDHANDLES;
BOOL ok = CreateProcessA(
NULL,
mutable_commandline,
NULL,
NULL,
TRUE,
CREATE_NO_WINDOW // Don't create a console window
| CREATE_NEW_PROCESS_GROUP // So that Ctrl-Break is not propagated
| CREATE_SUSPENDED, // So that it doesn't start a new job itself
env_bytes,
cwd,
&startup_info,
&process_info);
if (!ok) {
result->error_ = GetLastErrorString("CreateProcess()");
goto cleanup;
}
result->pid_ = process_info.dwProcessId;
result->process_ = process_info.hProcess;
thread = process_info.hThread;
if (!AssignProcessToJobObject(result->job_, result->process_)) {
BOOL is_in_job = false;
if (IsProcessInJob(result->process_, NULL, &is_in_job)
&& is_in_job
&& !NestedJobsSupported()) {
// We are on a pre-Windows 8 system and the Bazel is already in a job.
// We can't create nested jobs, so just revert to TerminateProcess() and
// hope for the best. In batch mode, the launcher puts Bazel in a job so
// that will take care of cleanup once the command finishes.
CloseHandle(result->job_);
result->job_ = INVALID_HANDLE_VALUE;
} else {
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);
}
if (cwd != NULL) {
env->ReleaseStringUTFChars(java_cwd, cwd);
}
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;
}
extern "C" JNIEXPORT jlong JNICALL
Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeGetStdout(
JNIEnv* env, jclass clazz, jlong process_long) {
NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long);
return reinterpret_cast<jlong>(&process->stdout_);
}
extern "C" JNIEXPORT jlong JNICALL
Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeGetStderr(
JNIEnv* env, jclass clazz, jlong process_long) {
NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long);
return reinterpret_cast<jlong>(&process->stderr_);
}
extern "C" JNIEXPORT jint JNICALL
Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeReadStream(
JNIEnv* env, jclass clazz, jlong stream_long, jbyteArray java_bytes,
jint offset, jint length) {
NativeOutputStream* stream =
reinterpret_cast<NativeOutputStream*>(stream_long);
jsize array_size = env->GetArrayLength(java_bytes);
if (offset < 0 || length <= 0 || offset > array_size - length) {
stream->error_ = "Array index out of bounds";
return -1;
}
if (stream->handle_ == INVALID_HANDLE_VALUE || stream->closed_.load()) {
stream->error_ = "";
return 0;
}
jbyte* bytes = env->GetByteArrayElements(java_bytes, NULL);
DWORD bytes_read;
if (!::ReadFile(stream->handle_, bytes + offset, length, &bytes_read, NULL)) {
// Check if either the other end closed the pipe or we did it with
// NativeOutputStream.close() . In the latter case, we'll get a "system
// call interrupted" error.
if (GetLastError() == ERROR_BROKEN_PIPE || stream->closed_.load()) {
// End of file.
stream->error_ = "";
bytes_read = 0;
} else {
stream->error_ = GetLastErrorString("ReadFile()");
bytes_read = -1;
}
} else {
stream->error_ = "";
}
env->ReleaseByteArrayElements(java_bytes, bytes, 0);
return bytes_read;
}
extern "C" JNIEXPORT jint JNICALL
Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeGetExitCode(
JNIEnv *env, jclass clazz, jlong process_long) {
NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long);
DWORD exit_code;
if (!GetExitCodeProcess(process->process_, &exit_code)) {
process->error_ = GetLastErrorString("GetExitCodeProcess()");
return -1;
}
return exit_code;
}
// return values:
// 0: Wait completed successfully
// 1: Timeout
// 2: Wait returned with an error
extern "C" JNIEXPORT jint JNICALL
Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeWaitFor(
JNIEnv *env, jclass clazz, jlong process_long, jlong java_timeout) {
NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long);
HANDLE handles[1] = { process->process_ };
DWORD win32_timeout = java_timeout < 0 ? INFINITE : java_timeout;
jint result;
switch (WaitForMultipleObjects(1, handles, FALSE, win32_timeout)) {
case 0:
result = 0;
break;
case WAIT_TIMEOUT:
result = 1;
break;
case WAIT_FAILED:
result = 2;
break;
default:
process->error_ = "WaitForMultipleObjects() returned unknown result";
result = 2;
break;
}
// Close the pipe handles so that any pending nativeReadStream() calls
// return. This will call CancelIoEx() on the file handles in order to make
// ReadFile() in nativeReadStream() return; otherwise, CloseHandle() would
// hang.
//
// This protects against a subprocess being created, it passing the write
// side of the stdout/stderr pipes to a subprocess, then dying. In that case,
// if we didn't do this, the Java side of the code would hang waiting for the
// streams to finish.
//
// An alternative implementation would be to rely on job control terminating
// the subprocesses, but we don't want to assume that it's always available.
process->stdout_.close();
process->stderr_.close();
if (process->stdin_ != INVALID_HANDLE_VALUE) {
CloseHandle(process->stdin_);
process->stdin_ = INVALID_HANDLE_VALUE;
}
return result;
}
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 (process->job_ != INVALID_HANDLE_VALUE) {
// In theory, CloseHandle() on process->job_ would work, too, since we set
// KILL_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, but this is a little more explicit.
if (!TerminateJobObject(process->job_, 0)) {
process->error_ = GetLastErrorString("TerminateJobObject()");
return JNI_FALSE;
}
} else if (process->process_ != INVALID_HANDLE_VALUE) {
if (!TerminateProcess(process->process_, 1)) {
process->error_ = GetLastErrorString("TerminateProcess()");
return JNI_FALSE;
}
}
process->error_ = "";
return JNI_TRUE;
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeDeleteProcess(
JNIEnv* env, jclass clazz, jlong process_long) {
NativeProcess* process = reinterpret_cast<NativeProcess*>(process_long);
if (process->stdin_ != INVALID_HANDLE_VALUE) {
CloseHandle(process->stdin_);
}
process->stdout_.close();
process->stderr_.close();
if (process->process_ != INVALID_HANDLE_VALUE) {
CloseHandle(process->process_);
}
if (process->job_ != INVALID_HANDLE_VALUE) {
CloseHandle(process->job_);
}
delete process;
}
extern "C" JNIEXPORT void JNICALL
Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeCloseStream(
JNIEnv* env, jclass clazz, jlong stream_long) {
NativeOutputStream* stream =
reinterpret_cast<NativeOutputStream*>(stream_long);
stream->close();
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeProcessGetLastError(
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;
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_google_devtools_build_lib_windows_WindowsProcesses_nativeStreamGetLastError(
JNIEnv* env, jclass clazz, jlong stream_long) {
NativeOutputStream* stream =
reinterpret_cast<NativeOutputStream*>(stream_long);
jstring result = env->NewStringUTF(stream->error_.c_str());
stream->error_ = "";
return result;
}