| // 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 |