blob: 0b67fe8f400760beb2a2b0b2752be2b29c062bb8 [file] [log] [blame]
// Copyright 2011 The Goma Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "spawner_win.h"
#include <process.h>
#include <psapi.h>
#pragma comment(lib, "psapi.lib")
#include <algorithm>
#include <deque>
#include <memory>
#include <sstream>
#include "absl/strings/match.h"
#include "absl/strings/string_view.h"
#include "client_util.h"
#include "cmdline_parser.h"
#include "compiler_specific.h"
#include "file_dir.h"
#include "filesystem.h"
#include "glog/logging.h"
#include "mypath.h"
#include "path.h"
namespace {
const DWORD kWaitTimeout = 10;
const DWORD kTerminateExitCode = 1;
string GetSubprocTempDirectory() {
std::ostringstream oss;
oss << "goma_temp" << "." << GetCurrentProcessId();
return file::JoinPath(devtools_goma::GetGomaTmpDir(), oss.str());
}
bool IsEnvVar(absl::string_view env_line, absl::string_view env_prefix) {
return absl::StartsWithIgnoreCase(env_line, env_prefix);
}
string EscapeCommandlineArg(const string& arg) {
// TODO: More accurate escape.
// https://msdn.microsoft.com/en-us/library/17w5ykft(v=vs.85).aspx
if (!arg.empty() && arg.find_first_of(" \t\n\v\"") == string::npos) {
return arg;
}
string escaped_arg;
// escaped_arg will be double quoted.
bool quote_end = true;
// construct escaped arg from back to check double quotation is preceded
// by back slash.
for (int i = static_cast<int>(arg.size()) - 1; i >= 0; --i) {
if (arg[i] == '"') {
escaped_arg += "\"\\";
quote_end = true;
continue;
} else if (arg[i] == '\\' && quote_end) {
escaped_arg += '\\';
} else {
quote_end = false;
}
escaped_arg += arg[i];
}
std::reverse(escaped_arg.begin(), escaped_arg.end());
VLOG(1) << "arg: `" << arg << "` -> `" << '"' + escaped_arg + '"' << "`";
return '"' + escaped_arg + '"';
}
// Iter should be an iterator of string containers.
template <typename Iter>
string PrepareCommandLine(const char* cwd, const char* prog,
Iter env_begin, Iter env_end,
Iter argv_begin, Iter argv_end) {
// Check if we have PATH spec and/or PATHEXT spec.
static const size_t kPathLength = 5;
static const char* kPathStr = "PATH=";
static const size_t kPathExtLength = 8;
static const char* kPathExtStr = "PATHEXT=";
string path_spec;
string pathext_spec;
for (Iter i = env_begin; i != env_end; ++i) {
if (IsEnvVar(*i, kPathStr)) {
path_spec = i->substr(kPathLength);
}
if (IsEnvVar(*i, kPathExtStr)) {
pathext_spec = i->substr(kPathExtLength);
}
}
// TODO: remove this when |prog| become full path.
CHECK(!path_spec.empty()) << "PATH env. should be set.";
CHECK(!pathext_spec.empty()) << "PATHEXT env. should be set.";
string command_line;
if (!devtools_goma::GetRealExecutablePath(
nullptr, prog, cwd, path_spec, pathext_spec, &command_line,
nullptr, nullptr)) {
LOG(ERROR) << "Failed GetRealExecutablePath prog=" << prog << " cwd=" << cwd
<< " path_spec=" << path_spec
<< " pathext_spec=" << pathext_spec;
return string();
}
if (command_line[0] != '\"') {
command_line = EscapeCommandlineArg(command_line);
}
for (Iter i = argv_begin; i != argv_end; ++i) {
// argv[0] should be prog.
if (i == argv_begin)
continue;
command_line.append(" ");
command_line.append(EscapeCommandlineArg(*i));
}
return command_line;
}
// Iter should be an iterator of string containers.
template <typename Iter>
void PrepareEnvBlock(Iter begin, Iter end, std::vector<char>* env) {
const size_t kMaxEnv = 32767;
env->resize(kMaxEnv); // max env size
size_t index = 0;
for (Iter i = begin; i != end; i++) {
const string& e = *i;
size_t len = e.size();
strcpy_s(&((*env)[index]), kMaxEnv - index, e.c_str());
index += len + 1;
if (index >= kMaxEnv) {
LOG(WARNING) << "env block exceeds capacity";
index = kMaxEnv - 1;
break;
}
}
env->at(index) = 0;
}
string CreateJobName(DWORD pid, const string& command) {
std::ostringstream ss;
// Get <prog> from |command|.
std::vector<string> args;
devtools_goma::ParseWinCommandLineToArgv(command, &args);
ss << "goma job:"
<< " pid=" << pid
<< " exe=" << file::Basename(args[0]);
string job_name(ss.str());
if (job_name.length() > MAX_PATH)
job_name.erase(MAX_PATH);
return job_name;
}
void SetProcessMemoryUsage(HANDLE child_handle, SIZE_T* mem_bytes) {
PROCESS_MEMORY_COUNTERS pmc;
if (GetProcessMemoryInfo(child_handle, &pmc, sizeof(pmc))) {
*mem_bytes = pmc.PeakWorkingSetSize;
} else {
LOG_SYSRESULT(GetLastError());
}
}
bool WaitThread(devtools_goma::ScopedFd* thread, DWORD timeout) {
if (thread->valid()) {
DWORD r = WaitForSingleObject(thread->handle(), timeout);
switch (r) {
case WAIT_ABANDONED:
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "Wait: join Thread error?"
<< " thread=" << thread->handle();
break;
case WAIT_OBJECT_0:
thread->reset(nullptr);
break;
case WAIT_TIMEOUT:
VLOG(1) << "wait timeout=" << timeout;
return false;
default:
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "Unexpected return value for WaitForSingleObject."
<< " r=" << r;
break;
}
}
return true;
}
} // namespace
namespace devtools_goma {
static const DWORD kInvalidProcessStatus = 0xffffffff;
// On Windows, the common convention of invalid PID is 0 (see
// http://blogs.msdn.com/b/oldnewthing/archive/2004/02/23/78395.aspx for
// discussions, another common invalid pid value is DWORD(-1), which is
// 0xffffffff and not 64-bit friendly).
const int Spawner::kInvalidPid = 0;
string* SpawnerWin::temp_dir_;
/* static */
void SpawnerWin::Setup() {
if (temp_dir_ != nullptr) {
delete temp_dir_;
}
temp_dir_ = new string(GetSubprocTempDirectory());
file::RecursivelyDelete(*temp_dir_, file::Defaults());
CHECK(file::CreateDir(temp_dir_->c_str(), file::CreationMode(0755)).ok())
<< temp_dir_->c_str();
LOG(INFO) << "Create temp dir: " << *temp_dir_;
}
/* static */
void SpawnerWin::TearDown() {
if (temp_dir_ == nullptr) {
return;
}
if (file::RecursivelyDelete(*temp_dir_, file::Defaults()).ok()) {
LOG(INFO) << "Remove temp dir: " << *temp_dir_;
} else {
LOG(ERROR) << "Remove temp dir failed?: " << *temp_dir_;
}
delete temp_dir_;
temp_dir_ = nullptr;
}
SpawnerWin::SpawnerWin()
: input_thread_(nullptr), input_thread_id_(0), stop_input_thread_(false),
output_thread_(nullptr), output_thread_id_(0),
stop_output_thread_(nullptr), process_status_(kInvalidProcessStatus),
process_mem_bytes_(0), is_signaled_(false) {
}
SpawnerWin::~SpawnerWin() {
CleanUp();
}
int SpawnerWin::Run(const string& cmd, const std::vector<string>& args,
const std::vector<string>& envs, const string& cwd) {
DCHECK(!child_process_.valid());
std::vector<string> environs;
if (keep_env_) {
environs = envs;
} else {
// Use own temp dir for subprocess to make it easy to clean up temp file.
// See b/21312000
for (const auto& e : envs) {
if (temp_dir_ != nullptr) {
if (IsEnvVar(e, "TEMP=")) {
environs.push_back("TEMP=" + *temp_dir_);
continue;
}
if (IsEnvVar(e, "TMP=")) {
environs.push_back("TMP=" + *temp_dir_);
continue;
}
}
environs.push_back(e);
}
}
// Having files to redirect or console output should be gathered.
// And do not detach.
bool need_redirect =
(!(stdin_filename_.empty() &&
stdout_filename_.empty() &&
stderr_filename_.empty()) ||
console_output_) && !detach_;
if (need_redirect) {
DCHECK(!console_output_ ||
(stdout_filename_.empty() && stderr_filename_.empty()))
<< "You cannot use SetFileRedirection with SetConsoleOutputBuffer"
<< " console_output_=" << console_output_
<< " stdout_filename_=" << stdout_filename_
<< " stderr_filename_=" << stderr_filename_;
const string command_line =
PrepareCommandLine(cwd.c_str(), cmd.c_str(),
environs.cbegin(), environs.cend(),
args.begin(), args.end());
if (command_line.empty()) {
LOG(ERROR) << "command line is empty."
<< " cwd=" << cwd << " cmd=" << cmd;
return Spawner::kInvalidPid;
}
std::vector<char> env;
PrepareEnvBlock(environs.cbegin(), environs.cend(), &env);
return RunRedirected(command_line, &env, cwd, stdout_filename_,
stdin_filename_);
}
PROCESS_INFORMATION pi;
STARTUPINFOA si;
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
DWORD create_flag = 0;
if (detach_) {
create_flag |= DETACHED_PROCESS;
}
string command_line =
PrepareCommandLine(cwd.c_str(), cmd.c_str(),
environs.cbegin(), environs.cend(),
args.begin(), args.end());
if (command_line.empty()) {
return Spawner::kInvalidPid;
}
VLOG(1) << "Run: command_line:" << command_line
<< " cwd:" << cwd;
std::vector<char> envp;
PrepareEnvBlock(environs.cbegin(), environs.cend(), &envp);
// If environment is empty, use parent process's environment.
LPVOID env_ptr = envp[0] ? &(envp[0]) : nullptr;
const DWORD process_create_flag =
create_flag | CREATE_SUSPENDED | CREATE_BREAKAWAY_FROM_JOB;
if (CreateProcessA(nullptr, &(command_line[0]), nullptr, nullptr, FALSE,
process_create_flag, env_ptr, cwd.c_str(), &si, &pi)) {
child_process_.reset(pi.hProcess);
job_name_ = CreateJobName(pi.dwProcessId, command_line);
VLOG(1) << "Job name:" << job_name_;
// We don't assign the process to a new job, otherwise it's killed when
// compiler_proxy ends. (e.g. auto updater should be alive after
// compiler_proxy.exe is killed.)
if (!detach_) {
child_job_ = AssignProcessToNewJobObject(
child_process_.handle(), job_name_);
}
process_status_ = STILL_ACTIVE;
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
} else {
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "failed to CreateProcess job_name=" << job_name_;
}
VLOG(1) << "Run: pid=" << pi.dwProcessId;
return pi.dwProcessId;
}
void SpawnerWin::UpdateProcessStatus(DWORD timeout) {
DWORD res = WaitForSingleObject(child_process_.handle(), timeout);
if (res == WAIT_TIMEOUT) {
process_status_ = STILL_ACTIVE;
return;
}
if (res == WAIT_FAILED) {
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "Failed to update child process status. job_name="
<< job_name_;
process_status_ = kTerminateExitCode;
return;
}
DCHECK_EQ(res, WAIT_OBJECT_0);
if (!GetExitCodeProcess(child_process_.handle(), &process_status_)) {
// TODO: come up with good way to handle this.
// I expect it temporary error, and return false to make a SpawnerWin user
// ignore this error.
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "Process should be signaled but we cannot get exit code."
<< " job_name=" << job_name_;
// Assume the process is dead, and set kTerminateExitCode.
process_status_ = kTerminateExitCode;
}
}
Spawner::ProcessStatus SpawnerWin::KillAndWait(DWORD timeout) {
if (!is_signaled_) {
if (input_thread_.valid()) {
stop_input_thread_ = true;
}
if (child_job_.valid()) {
if (!TerminateJobObject(child_job_.handle(), kTerminateExitCode))
LOG_SYSRESULT(GetLastError());
} else {
if (!TerminateProcess(child_process_.handle(), kTerminateExitCode))
LOG_SYSRESULT(GetLastError());
}
is_signaled_ = true;
}
std::vector<HANDLE> handles;
if (child_job_.valid())
handles.push_back(child_job_.handle());
handles.push_back(child_process_.handle());
// Wait the process is terminated.
// Since WaitForSingleObject(child_job_.handle()) seems not wait termination
// of |child_process_|, we need to wait it.
VLOG(1) << "Wait: child timeout=" << timeout;
DWORD ret = WaitForMultipleObjects(
handles.size(), &(handles[0]), TRUE, timeout);
if (ret == WAIT_TIMEOUT) {
VLOG(1) << "wait timeout=" << timeout;
return ProcessStatus::RUNNING;
}
if (ret < WAIT_OBJECT_0 || ret > WAIT_OBJECT_0 + handles.size() - 1) {
// Some handlers are abandoned or WAIT_FAILED.
// See: http://msdn.microsoft.com/en-us/library/windows/desktop/ms687025(v=vs.85).aspx
// TODO: come up with good way to handle this.
// I expect it temporary error, and return false to make a SpawnerWin user
// ignore this error.
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "Wait: termiante error? Process seems not signaled."
<< " WaitForMultipleObjects returned:" << ret
<< " nCount=" << handles.size()
<< " timeout=" << timeout
<< " job_name=" << job_name_;
return ProcessStatus::EXITED;
}
UpdateProcessStatus(timeout);
return process_status_ == STILL_ACTIVE ? ProcessStatus::RUNNING
: ProcessStatus::EXITED;
}
void SpawnerWin::FinalizeProcess(DWORD timeout) {
VLOG(1) << "Wait: child_process finished " << process_status_;
if (!WaitThread(&input_thread_, timeout)) {
LOG(WARNING) << "input thread timed out=" << timeout
<< " job_name=" << job_name_;
}
CHECK(child_process_.valid());
SetProcessMemoryUsage(child_process_.handle(), &process_mem_bytes_);
child_process_.reset(nullptr);
if (!child_job_.Close()) {
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "Failed to release child job handle. "
<< "job_name=" << job_name_;
}
// OutputThread should terminate with failing to read from child_stdout and
// child_stderr from child process, which should happen when child process
// has been terminated.
// If the OutputThread doesn't finish with some error, we'll trigger
// stop_output_thread_ in CleanUp to terminate OutputThread.
if (!WaitThread(&output_thread_, INFINITE)) {
LOG(INFO) << "output thread timed out=" << timeout
<< " job_name=" << job_name_;
}
LOG_IF(ERROR, stdout_file_.valid())
<< "stdout_file is still valid. job_name=" << job_name_;
LOG_IF(ERROR, stderr_file_.valid())
<< "stderr_file is still valid. job_name=" << job_name_;
}
Spawner::ProcessStatus SpawnerWin::Kill() {
return KillAndWait(kWaitTimeout);
}
Spawner::ProcessStatus SpawnerWin::Wait(Spawner::WaitPolicy wait_policy) {
const DWORD timeout =
(wait_policy==Spawner::WAIT_INFINITE) ? INFINITE : kWaitTimeout;
const bool need_kill = (wait_policy==Spawner::NEED_KILL);
// child_process_ is valid while subprocess is running.
if (!child_process_.valid()) {
VLOG(1) << "Wait: child_process already invalid";
CHECK_NE(STILL_ACTIVE, process_status_);
LOG_IF(ERROR, stdout_file_.valid())
<< "stdout_file is still valid. job_name=" << job_name_;
LOG_IF(ERROR, stderr_file_.valid())
<< "stderr_file is still valid. job_name=" << job_name_;
return ProcessStatus::EXITED;
}
UpdateProcessStatus(timeout);
if (process_status_ != STILL_ACTIVE) {
FinalizeProcess(timeout);
return ProcessStatus::EXITED;
}
// Process is still active.
if (!need_kill) {
return ProcessStatus::RUNNING;
}
VLOG(1) << "Wait: need kill";
ProcessStatus running = KillAndWait(timeout);
if (running == ProcessStatus::RUNNING) {
return ProcessStatus::RUNNING;
}
FinalizeProcess(timeout);
return ProcessStatus::EXITED;
}
// TODO: make stderr stored to the specified file.
int SpawnerWin::RunRedirected(const string& command_line,
std::vector<char>* env,
const string& cwd,
const string& out_file,
const string& in_file) {
VLOG(1) << "RunRedirect: command_line:" << command_line
<< " cwd:" << cwd
<< " out_file:" << out_file
<< " in_file:" << in_file;
CHECK_GT(command_line.length(), 0U);
stop_output_thread_.reset(CreateEvent(nullptr, TRUE, FALSE, nullptr));
PCHECK(stop_output_thread_.valid());
PROCESS_INFORMATION pi;
STARTUPINFOA si;
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = nullptr;
sa.bInheritHandle = TRUE;
ScopedFd stdout_read_tmp, stderr_read_tmp; // parent stdout/err read handle
ScopedFd stdout_write, stderr_write; // child stdout/err write handle
ScopedFd stdin_write_tmp; // parent stdin write handle
ScopedFd stdin_read; // child stdin read handle
// Create child stdout pipe
if (!CreatePipe(stdout_read_tmp.ptr(), stdout_write.ptr(), &sa, 0)) {
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "Failed to create pipe for stdout. "
<< " cmd: " << command_line
<< " cwd: " << cwd;
return kInvalidPid;
}
switch (console_output_option_) {
case STDOUT_ONLY:
stderr_write.reset(ScopedFd::OpenNull());
if (!stderr_write.valid()) {
LOG(ERROR) << "Failed to open NUL."
<< " cmd: " << command_line
<< " cwd: " << cwd;
return kInvalidPid;
}
break;
case MERGE_STDOUT_STDERR:
// TODO: During development, I found that stderr output are
// not redirected to the pipe as stdout. Both MSDN and
// CodeProject examples redirect out/err to same file.
// I'm not sure if that's a bug on Windows side or my
// end. Due to schedule, I'll just output both to same
// file for now.
if (!DuplicateHandle(GetCurrentProcess(), stdout_write.handle(),
GetCurrentProcess(), stderr_write.ptr(),
0, TRUE, DUPLICATE_SAME_ACCESS)) {
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "Failed to duplicate stderr handle."
<< " cmd: " << command_line
<< " cwd: " << cwd;
return kInvalidPid;
}
break;
default:
LOG(ERROR) << "Unknown console_output_option is set:"
<< console_output_option_;
return kInvalidPid;
}
// Create child stdin pipe
if (!CreatePipe(stdin_read.ptr(), stdin_write_tmp.ptr(), &sa, 0)) {
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "Failed to create pipe for stdin. "
<< " cmd: " << command_line
<< " cwd: " << cwd;
return kInvalidPid;
}
if (!DuplicateHandle(GetCurrentProcess(), stdout_read_tmp.handle(),
GetCurrentProcess(), child_stdout_.ptr(),
0, FALSE, DUPLICATE_SAME_ACCESS)) {
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "Failed to duplicate stdout handle."
<< " cmd: " << command_line
<< " cwd: " << cwd;
return kInvalidPid;
}
if (!DuplicateHandle(GetCurrentProcess(), stdin_write_tmp.handle(),
GetCurrentProcess(), child_stdin_.ptr(),
0, FALSE, DUPLICATE_SAME_ACCESS)) {
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "Failed to duplicate stdin handle."
<< " cmd: " << command_line
<< " cwd: " << cwd;
return kInvalidPid;
}
stdout_read_tmp.reset(nullptr);
stderr_read_tmp.reset(nullptr);
stdin_write_tmp.reset(nullptr);
if (!out_file.empty()) {
string file_path = file::JoinPathRespectAbsolute(cwd, out_file);
stdout_file_.reset(CreateFileA(file_path.c_str(), GENERIC_WRITE,
FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, nullptr));
}
si.hStdOutput = stdout_write.handle();
si.hStdInput = stdin_read.handle();
si.hStdError = stderr_write.handle();
si.wShowWindow = SW_HIDE;
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
// If environment is empty, use parent process's environment.
LPVOID env_ptr = (*env)[0] ? &((*env)[0]) : nullptr;
// compiler_proxy's cwd and build's expected cwd are different. If the
// compiler path (in command_line) is relative, a compiler will be searched
// from compiler_proxy relative path, however, it should be build's expected
// cwd relative. So, we inject "cmd /c" to set cwd to build's expected one.
string cmd = "cmd /c " + command_line;
// TODO: Code around here looks like Run().
// Can we share some code?
const DWORD process_create_flag =
CREATE_NEW_CONSOLE | CREATE_SUSPENDED | CREATE_BREAKAWAY_FROM_JOB;
BOOL result = CreateProcessA(nullptr, &(cmd[0]), nullptr, nullptr, TRUE,
process_create_flag,
env_ptr, cwd.c_str(), &si, &pi);
if (!result) {
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "Failed to create process."
<< " cmd: " << command_line
<< " cwd: " << cwd;
return kInvalidPid;
}
// Child launched, close parent copy of pipe handles.
stdout_write.reset(nullptr);
stderr_write.reset(nullptr);
stdin_read.reset(nullptr);
process_status_ = STILL_ACTIVE;
child_process_.reset(pi.hProcess);
job_name_ = CreateJobName(pi.dwProcessId, command_line);
VLOG(1) << "Job name:" << job_name_;
child_job_ = AssignProcessToNewJobObject(child_process_.handle(), job_name_);
uintptr_t r_output =
_beginthreadex(nullptr, 0, OutputThread, this, 0, &output_thread_id_);
if (r_output == 0) {
LOG(ERROR) << "failed to start spawner output thread";
return kInvalidPid;
}
output_thread_.reset(reinterpret_cast<HANDLE>(r_output));
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
if (!in_file.empty()) {
input_file_ = in_file;
uintptr_t r_input =
_beginthreadex(nullptr, 0, InputThread, this, 0, &input_thread_id_);
if (r_input == 0) {
LOG(ERROR) << "failed to start spawner input thread";
return kInvalidPid;
}
input_thread_.reset(reinterpret_cast<HANDLE>(r_input));
}
VLOG(1) << "Run: pid=" << pi.dwProcessId;
return pi.dwProcessId;
}
// static
ScopedFd SpawnerWin::AssignProcessToNewJobObject(
ScopedFd::FileDescriptor child_process, const string& job_name) {
ScopedFd job_fd(CreateJobObjectA(nullptr, job_name.c_str()));
if (!job_fd.handle()) {
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "failed to CreateJobObject"
<< " job_name=" << job_name;
return ScopedFd();
}
if (GetLastError() == ERROR_ALREADY_EXISTS) {
LOG(ERROR) << "Object already exist."
<< " job_name=" << job_name;
return ScopedFd();
}
// We kill all processes associated with the job when the handle is closed.
// To force it, we prevent child processes from breaking away the job.
// Note that we need to use JOBOBJECT_EXTENDED_LIMIT_INFORMATION to set them.
// See:
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms684161(v=vs.85).aspx#managing_job_objects
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms684147(v=vs.85).aspx
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms684925(v=vs.85).aspx
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms686216(v=vs.85).aspx
JOBOBJECT_EXTENDED_LIMIT_INFORMATION info;
if (!QueryInformationJobObject(job_fd.handle(),
JobObjectExtendedLimitInformation,
&info,
sizeof(info),
nullptr)) {
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "failed to get job extended limit info"
<< " job name=" << job_name;
return ScopedFd();
}
info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
info.BasicLimitInformation.LimitFlags &= ~JOB_OBJECT_LIMIT_BREAKAWAY_OK;
info.BasicLimitInformation.LimitFlags &=
~JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;
if (!SetInformationJobObject(job_fd.handle(),
JobObjectExtendedLimitInformation,
&info, sizeof(info))) {
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "failed to set job extended limit info"
<< " job name=" << job_name;
return ScopedFd();
}
if (!AssignProcessToJobObject(job_fd.handle(), child_process)) {
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "failed to AssignProcessToJobObject"
<< " job_name=" << job_name;
return ScopedFd();
}
return job_fd;
}
void SpawnerWin::CleanUp() {
VLOG(1) << "CleanUp";
if (input_thread_.valid()) {
LOG(ERROR) << "input_thread still valid. job_name=" << job_name_;
CHECK_NE(::GetCurrentThreadId(), input_thread_id_);
stop_input_thread_ = true;
WaitForSingleObject(input_thread_.handle(), INFINITE);
input_thread_.reset(nullptr);
}
process_status_ = kInvalidProcessStatus;
child_process_.reset(nullptr);
if (!child_job_.Close()) {
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "Failed to release child job handle."
<< " job_name=" << job_name_;
}
if (output_thread_.valid()) {
LOG(ERROR) << "output_thread still valid. job_name=" << job_name_;
CHECK_NE(::GetCurrentThreadId(), output_thread_id_);
DCHECK(stop_output_thread_.handle());
SetEvent(stop_output_thread_.handle());
VLOG(2) << "Join OutputThread";
WaitForSingleObject(output_thread_.handle(), INFINITE);
output_thread_.reset(nullptr);
}
stop_output_thread_.reset(nullptr);
stdout_file_.reset(nullptr);
stderr_file_.reset(nullptr);
child_stdin_.reset(nullptr);
child_stdout_.reset(nullptr);
child_stderr_.reset(nullptr);
output_thread_id_ = 0;
}
bool SpawnerWin::WriteToPipe() {
const char* filepath = input_file_.c_str();
VLOG(1) << "WriteToPipe from " << filepath;
ScopedFd input(CreateFileA(filepath, GENERIC_READ, 0, nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_READONLY, nullptr));
DWORD read, written;
CHAR buf[4096];
BOOL success = FALSE;
for (;;) {
if (stop_input_thread_)
break;
success = ReadFile(input.handle(), buf, 4096, &read, nullptr);
// End of file under synchronous read operation.
// See: http://msdn.microsoft.com/en-us/library/windows/desktop/aa365690(v=vs.85).aspx
if (success && read == 0) {
break;
}
if (!success) {
DWORD error = GetLastError();
LOG_SYSRESULT(error);
LOG(ERROR) << "ReadFile failed:"
<< " filepath=" << filepath
<< " read=" << read
<< " job_name=" << job_name_;
return false;
}
if (stop_input_thread_)
break;
success = WriteFile(child_stdin_.handle(), buf, read, &written, nullptr);
// Since this is an anonymous pipe, WriteFile blocks until |read| bytes has
// been written.
// See "Remarks" section:
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa365152(v=vs.85).aspx
if (!success) {
DWORD error = GetLastError();
// When the child is killed, WriteFile would fail with ERROR_BROKEN_PIPE.
if (stop_input_thread_ && error == ERROR_BROKEN_PIPE) {
VLOG(1) << "broken pipe caused by process termination."
<< " filepath=" << filepath
<< " read=" << read
<< " written=" << written;
return false;
}
LOG_SYSRESULT(error);
LOG(ERROR) << "WriteFile failed:"
<< " filepath=" << filepath
<< " read=" << read
<< " written=" << written
<< " job_name=" << job_name_;
return false;
}
if (read != written) {
LOG(ERROR) << "Failed to WriteFile |read| length."
<< " The execution result may strange."
<< " filepath=" << filepath
<< " read=" << read
<< " written=" << written
<< " success=" << success
<< " job_name=" << job_name_;
return false;
}
VLOG(2) << "WriteToPipe read=" << read << " written=" << written;
}
// close the pipe handle so the child process stops reading.
if (child_stdin_.Close()) {
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "close stdin handler failed."
<< " job_name=" << job_name_;
return false;
}
VLOG(1) << "WriteToPipe finished";
return true;
}
bool SpawnerWin::Redirect() {
bool stdout_open = false;
bool stderr_open = false;
VLOG(1) << "Redirect";
if (child_stdout_.valid()) {
VLOG(2) << "ReadFromStdout";
stdout_open = ReadFromPipe(child_stdout_.handle(), stdout_file_.handle());
}
if (child_stderr_.valid()) {
VLOG(2) << "ReadFromStderr";
stderr_open = ReadFromPipe(child_stderr_.handle(), stderr_file_.handle());
}
return stdout_open || stderr_open;
}
bool SpawnerWin::ReadFromPipe(HANDLE pipe, HANDLE file) {
DWORD avail = 0;
if (!PeekNamedPipe(pipe, nullptr, 0, nullptr, &avail, nullptr)) {
DWORD err = GetLastError();
if (err != ERROR_HANDLE_EOF && err != ERROR_BROKEN_PIPE) {
LOG_SYSRESULT(err);
LOG(ERROR) << "PeekNamedPipe error:" << err
<< " job_name=" << job_name_;
}
return false;
}
if (avail) {
VLOG(2) << "ReadFromPipe avail=" << avail;
std::unique_ptr<char[]> buffer(new char[avail + 1]);
memset(buffer.get(), 0, avail + 1);
DWORD read = 0, written = 0;
DWORD r = ReadFile(pipe, buffer.get(), avail, &read, nullptr);
if (!r) {
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "ReadFile err avail=" << avail
<< " job_name=" << job_name_;
return false;
} else if (read == 0) {
// reached EOF, but avail > 0 ?
LOG(ERROR) << "ReadFile read 0 avail=" << avail
<< " job_name=" << job_name_;
return false;
}
if (file != INVALID_HANDLE_VALUE && file != 0) {
r = WriteFile(file, buffer.get(), read, &written, nullptr);
if (!r) {
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "WriteFile err size=" << read << " written=" << written
<< " job_name=" << job_name_;
return false;
}
LOG_IF(ERROR, read != written)
<< "WriteFile size=" << read << " written=" << written
<< " job_name=" << job_name_;
} else {
VLOG(1) << "ignored to output to log file";
}
VLOG(2) << "ReadFromPipe read=" << read << " written=" << written;
if (console_output_) {
console_output_->append(buffer.get(), read);
}
}
return true;
}
void SpawnerWin::Flush() {
VLOG(1) << "Flush";
stdout_file_.reset(nullptr);
stderr_file_.reset(nullptr);
}
/* static */
unsigned __stdcall SpawnerWin::InputThread(void* thread_params) {
SpawnerWin* self = reinterpret_cast<SpawnerWin*>(thread_params);
DCHECK(self);
// TODO: handles WriteToPipe error.
self->WriteToPipe();
return 0;
}
/* static */
unsigned __stdcall SpawnerWin::OutputThread(void* thread_params) {
SpawnerWin* self = reinterpret_cast<SpawnerWin*>(thread_params);
DCHECK(self);
HANDLE stop = self->stop_output_thread_.handle();
for (;;) {
bool active = self->Redirect();
if (!active) {
VLOG(1) << "OutputThread: redirect closed";
break;
}
VLOG(2) << "OutputThread: Wait";
DWORD r = WaitForSingleObject(stop, kWaitTimeout);
if (r == WAIT_TIMEOUT) {
continue;
}
switch (r) {
case WAIT_OBJECT_0:
LOG(WARNING) << "OutputThread: Stop before child process ended "
<< "job_name=" << self->job_name_;
break;
case WAIT_ABANDONED:
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "Wait: stop_output_thread error? "
<< " job_name=" << self->job_name_;
break;
default:
LOG_SYSRESULT(GetLastError());
LOG(ERROR) << "Unexpected return value from WaitForSingleObject."
<< " r=" << r
<< " job_name=" << self->job_name_;
break;
}
self->Redirect();
break;
}
self->Flush();
return 0;
}
} // namespace devtools_goma