blob: c8639073e7c91a0d07db0273397dfb1715b1d573 [file] [log] [blame]
// Copyright 2019 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.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include "src/main/native/windows/process.h"
#include <windows.h>
#include <VersionHelpers.h>
#include <memory>
#include <sstream>
namespace bazel {
namespace windows {
template <typename T>
static std::wstring ToString(const T& e) {
std::wstringstream s;
s << e;
return s.str();
}
static bool DupeHandle(HANDLE h, AutoHandle* out, std::wstring* error) {
HANDLE dup;
if (!DuplicateHandle(GetCurrentProcess(), h, GetCurrentProcess(), &dup, 0,
TRUE, DUPLICATE_SAME_ACCESS)) {
DWORD err = GetLastError();
*error =
MakeErrorMessage(WSTR(__FILE__), __LINE__, L"DupeHandle", L"", err);
return false;
}
*out = dup;
return true;
}
bool WaitableProcess::Create(const std::wstring& argv0,
const std::wstring& argv_rest, void* env,
const std::wstring& wcwd, std::wstring* error) {
// On Windows 7, the subprocess cannot inherit console handles via the
// attribute list (CreateProcessW fails with ERROR_NO_SYSTEM_RESOURCES).
// If we pass only invalid handles here, the Create method creates an
// AutoAttributeList with 0 handles and initializes the STARTUPINFOEXW as
// empty and disallowing handle inheritance. At the same time, CreateProcessW
// specifies neither CREATE_NEW_CONSOLE nor DETACHED_PROCESS, so the
// subprocess *does* inherit the console, which is exactly what we want, and
// it works on all supported Windows versions.
// Fixes https://github.com/bazelbuild/bazel/issues/8676
return Create(argv0, argv_rest, env, wcwd, INVALID_HANDLE_VALUE,
INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, nullptr, true,
error);
}
bool WaitableProcess::Create(const std::wstring& argv0,
const std::wstring& argv_rest, void* env,
const std::wstring& wcwd, HANDLE stdin_process,
HANDLE stdout_process, HANDLE stderr_process,
LARGE_INTEGER* opt_out_start_time,
std::wstring* error) {
return Create(argv0, argv_rest, env, wcwd, stdin_process, stdout_process,
stderr_process, opt_out_start_time, false, error);
}
bool WaitableProcess::Create(const std::wstring& argv0,
const std::wstring& argv_rest, void* env,
const std::wstring& wcwd, HANDLE stdin_process,
HANDLE stdout_process, HANDLE stderr_process,
LARGE_INTEGER* opt_out_start_time,
bool create_window, std::wstring* error) {
std::wstring cwd;
std::wstring error_msg(AsShortPath(wcwd, &cwd));
if (!error_msg.empty()) {
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"WaitableProcess::Create", argv0, error_msg);
return false;
}
std::wstring argv0short;
error_msg = AsExecutablePathForCreateProcess(argv0, &argv0short);
if (!error_msg.empty()) {
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"WaitableProcess::Create", argv0, error_msg);
return false;
}
std::wstring commandline =
argv_rest.empty() ? argv0short : (argv0short + L" " + argv_rest);
std::unique_ptr<WCHAR[]> mutable_commandline(
new WCHAR[commandline.size() + 1]);
wcsncpy(mutable_commandline.get(), commandline.c_str(),
commandline.size() + 1);
// 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.
job_ = CreateJobObject(NULL, NULL);
if (!job_.IsValid()) {
DWORD err_code = GetLastError();
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"WaitableProcess::Create", argv0, err_code);
return false;
}
JOBOBJECT_EXTENDED_LIMIT_INFORMATION job_info = {0};
job_info.BasicLimitInformation.LimitFlags =
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!SetInformationJobObject(job_, JobObjectExtendedLimitInformation,
&job_info, sizeof(job_info))) {
DWORD err_code = GetLastError();
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"WaitableProcess::Create", argv0, err_code);
return false;
}
ioport_ = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 1);
if (!ioport_.IsValid()) {
DWORD err_code = GetLastError();
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"WaitableProcess::Create", argv0, err_code);
return false;
}
JOBOBJECT_ASSOCIATE_COMPLETION_PORT port;
port.CompletionKey = job_;
port.CompletionPort = ioport_;
if (!SetInformationJobObject(job_,
JobObjectAssociateCompletionPortInformation,
&port, sizeof(port))) {
DWORD err_code = GetLastError();
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"WaitableProcess::Create", argv0, err_code);
return false;
}
std::unique_ptr<AutoAttributeList> attr_list;
if (!AutoAttributeList::Create(stdin_process, stdout_process, stderr_process,
&attr_list, &error_msg)) {
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"WaitableProcess::Create", L"", error_msg);
return false;
}
// kMaxCmdline value: see lpCommandLine parameter of CreateProcessW.
static constexpr size_t kMaxCmdline = 32767;
std::wstring cmd_sample = mutable_commandline.get();
if (cmd_sample.size() > 500) {
cmd_sample = cmd_sample.substr(0, 495) + L"(...)";
}
if (wcsnlen_s(mutable_commandline.get(), kMaxCmdline) == kMaxCmdline) {
std::wstringstream error_msg;
error_msg << L"command is longer than CreateProcessW's limit ("
<< kMaxCmdline << L" characters)";
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"CreateProcessWithExplicitHandles", cmd_sample,
error_msg.str().c_str());
return false;
}
PROCESS_INFORMATION process_info = {0};
STARTUPINFOEXW info;
attr_list->InitStartupInfoExW(&info);
if (!CreateProcessW(
/* lpApplicationName */ NULL,
/* lpCommandLine */ mutable_commandline.get(),
/* lpProcessAttributes */ NULL,
/* lpThreadAttributes */ NULL,
/* bInheritHandles */ attr_list->InheritAnyHandles() ? TRUE : FALSE,
/* dwCreationFlags */ (create_window ? 0 : CREATE_NO_WINDOW) |
CREATE_NEW_PROCESS_GROUP // So that Ctrl-Break isn't propagated
| CREATE_SUSPENDED // So that it doesn't start a new job itself
| EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT,
/* lpEnvironment */ env,
/* lpCurrentDirectory */ cwd.empty() ? NULL : cwd.c_str(),
/* lpStartupInfo */ &info.StartupInfo,
/* lpProcessInformation */ &process_info)) {
DWORD err = GetLastError();
std::wstring errmsg;
if (err == ERROR_NO_SYSTEM_RESOURCES && !IsWindows8OrGreater() &&
attr_list->HasConsoleHandle()) {
errmsg =
L"Unrecoverable error: host OS is Windows 7 and subprocess"
" got an inheritable console handle";
} else {
errmsg = GetLastErrorString(err) + L" (error: " + ToString(err) + L")";
}
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__, L"CreateProcessW",
cmd_sample, errmsg);
return false;
}
pid_ = process_info.dwProcessId;
process_ = process_info.hProcess;
AutoHandle thread(process_info.hThread);
if (!AssignProcessToJobObject(job_, process_)) {
BOOL is_in_job = false;
if (IsProcessInJob(process_, NULL, &is_in_job) && is_in_job &&
!IsWindows8OrGreater()) {
// Pre-Windows 8 systems don't support nested jobs, and 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.
job_ = INVALID_HANDLE_VALUE;
ioport_ = INVALID_HANDLE_VALUE;
} else {
DWORD err_code = GetLastError();
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"WaitableProcess::Create", argv0, err_code);
return false;
}
}
// Now that we put the process in a new job object, we can start executing
// it
if (ResumeThread(thread) == -1) {
DWORD err_code = GetLastError();
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"WaitableProcess::Create", argv0, err_code);
return false;
}
if (opt_out_start_time) {
QueryPerformanceCounter(opt_out_start_time);
}
*error = L"";
return true;
}
int WaitableProcess::WaitFor(int64_t timeout_msec,
LARGE_INTEGER* opt_out_end_time,
std::wstring* error) {
struct Defer {
LARGE_INTEGER* t;
Defer(LARGE_INTEGER* cnt) : t(cnt) {}
~Defer() {
if (t) {
QueryPerformanceCounter(t);
}
}
} defer_query_end_time(opt_out_end_time);
DWORD win32_timeout = timeout_msec < 0 ? INFINITE : timeout_msec;
int result;
switch (WaitForSingleObject(process_, win32_timeout)) {
case WAIT_OBJECT_0:
result = kWaitSuccess;
break;
case WAIT_TIMEOUT:
result = kWaitTimeout;
break;
// Any other case is an error and should be reported back to Bazel.
default:
DWORD err_code = GetLastError();
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"WaitableProcess::WaitFor", ToString(pid_),
err_code);
return kWaitError;
}
// Ensure that the process is really terminated (if WaitForSingleObject
// above timed out, we have to explicitly kill it) and that it doesn't
// leave behind any subprocesses.
if (!Terminate(error)) {
return kWaitError;
}
if (job_.IsValid()) {
// Wait for the job object to complete, signalling that all subprocesses
// have exited.
DWORD CompletionCode;
ULONG_PTR CompletionKey;
LPOVERLAPPED Overlapped;
while (GetQueuedCompletionStatus(ioport_, &CompletionCode,
&CompletionKey, &Overlapped, INFINITE) &&
!((HANDLE)CompletionKey == (HANDLE)job_ &&
CompletionCode == JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO)) {
// Still waiting...
}
job_ = INVALID_HANDLE_VALUE;
ioport_ = INVALID_HANDLE_VALUE;
}
// Fetch and store the exit code in case Bazel asks us for it later,
// because we cannot do this anymore after we closed the handle.
GetExitCode(error);
if (process_.IsValid()) {
process_ = INVALID_HANDLE_VALUE;
}
return result;
}
int WaitableProcess::GetExitCode(std::wstring* error) {
if (exit_code_ == STILL_ACTIVE) {
if (!GetExitCodeProcess(process_, &exit_code_)) {
DWORD err_code = GetLastError();
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"WaitableProcess::GetExitCode", ToString(pid_),
err_code);
return -1;
}
}
return exit_code_;
}
bool WaitableProcess::Terminate(std::wstring* error) {
static constexpr UINT exit_code = 130; // 128 + SIGINT, like on Linux
if (job_.IsValid()) {
if (!TerminateJobObject(job_, exit_code)) {
DWORD err_code = GetLastError();
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"WaitableProcess::Terminate", ToString(pid_),
err_code);
return false;
}
} else if (process_.IsValid()) {
if (!TerminateProcess(process_, exit_code)) {
DWORD err_code = GetLastError();
std::wstring our_error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"WaitableProcess::Terminate",
ToString(pid_), err_code);
// If the process exited, despite TerminateProcess having failed, we're
// still happy and just ignore the error. It might have been a race
// where the process exited by itself just before we tried to kill it.
// However, if the process is *still* running at this point (evidenced
// by its exit code still being STILL_ACTIVE) then something went
// really unexpectedly wrong and we should report that error.
if (GetExitCode(error) == STILL_ACTIVE) {
// Restore the error message from TerminateProcess - it will be much
// more helpful for debugging in case something goes wrong here.
*error = our_error;
return false;
}
}
if (WaitForSingleObject(process_, INFINITE) != WAIT_OBJECT_0) {
DWORD err_code = GetLastError();
*error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
L"WaitableProcess::Terminate", ToString(pid_),
err_code);
return false;
}
}
*error = L"";
return true;
}
} // namespace windows
} // namespace bazel