blob: 74568823dcdc1cd18bb79f6cf3bd131c42776ec5 [file] [edit]
// Copyright 2012 Google Inc. 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.
#include <assert.h>
#include <stdio.h>
#include <algorithm>
#include "exit_status.h"
#include "process_utils.h"
#include "subprocess.h"
#include "util.h"
using namespace std;
Subprocess::Subprocess(SubprocessSet& subprocess_set, bool use_console)
: subprocess_set_(subprocess_set) {
if (use_console) {
run_mode_ = RunMode::Local;
output_mode_ = OutputMode::Direct;
} else {
run_mode_ = RunMode::Group;
output_mode_ = OutputMode::PipeBuffer;
}
}
Subprocess::~Subprocess() {
Stop();
// Reap child if forgotten.
if (child_)
Finish();
}
void Subprocess::Stop() {
async_pipe_.reset();
}
ScopedHandle Subprocess::SetupPipe() {
char pipe_name[100];
snprintf(pipe_name, sizeof(pipe_name),
"\\\\.\\pipe\\ninja_pid%lu_sp%p", GetCurrentProcessId(), this);
ScopedHandle pipe_handle = ::CreateNamedPipeA(
pipe_name, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE,
PIPE_UNLIMITED_INSTANCES, 0, 0, INFINITE, NULL);
if (!pipe_handle)
Win32Fatal("CreateNamedPipe");
async_pipe_ = AsyncHandle::Create(
std::move(pipe_handle), subprocess_set_.async_loop_,
[this](AsyncError error, size_t size) {
if (error) {
if (error != ERROR_BROKEN_PIPE)
Win32Fatal("SubProcess::GetOverlappedResult", error);
Stop();
subprocess_set_.OnProcessCompletion(this);
return;
}
if (!is_reading_) {
is_reading_ = true;
} else {
buf_.append(read_buf_, size);
}
async_pipe_->StartRead(read_buf_, sizeof(read_buf_));
});
async_pipe_->StartConnectNamedPipe();
// Get the write end of the pipe as a handle inheritable across processes.
ScopedHandle output_write_handle =
CreateFileA(pipe_name, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
output_write_handle.SetInheritable(true);
return output_write_handle;
}
bool Subprocess::Start(const string& command,
const std::string* /* ignored */) {
ScopedHandle child_pipe = SetupPipe();
SECURITY_ATTRIBUTES security_attributes = {};
security_attributes.nLength = sizeof(SECURITY_ATTRIBUTES);
security_attributes.bInheritHandle = TRUE;
// Must be inheritable so subprocesses can dup to children.
ScopedHandle nul =
CreateFileA("NUL", GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
&security_attributes, OPEN_EXISTING, 0, NULL);
if (!nul)
Fatal("couldn't open nul");
STARTUPINFOA startup_info = {};
startup_info.cb = sizeof(STARTUPINFO);
if (output_mode_ == OutputMode::PipeBuffer) {
startup_info.dwFlags = STARTF_USESTDHANDLES;
startup_info.hStdInput = nul.get();
startup_info.hStdOutput = child_pipe.get();
startup_info.hStdError = child_pipe.get();
}
// In the console case, child_pipe is still inherited by the child and closed
// when the subprocess finishes, which then notifies ninja.
PROCESS_INFORMATION process_info = {};
// Ninja handles ctrl-c, except for subprocesses in console pools.
DWORD process_flags =
(run_mode_ == RunMode::Group) ? CREATE_NEW_PROCESS_GROUP : 0;
// Compute environment pointer.
LPVOID environment = NULL;
if (subprocess_set_.environment_)
environment = subprocess_set_.environment_->AsAnsiEnvironmentBlock();
// Do not prepend 'cmd /c' on Windows, this breaks command
// lines greater than 8,191 chars.
if (!CreateProcessA(NULL, (char*)command.c_str(), NULL, NULL,
/* inherit handles */ TRUE, process_flags, environment,
NULL, &startup_info, &process_info)) {
DWORD error = GetLastError();
if (error == ERROR_FILE_NOT_FOUND) {
// File (program) not found error is treated as a normal build
// action failure.
Stop();
// child_ is already NULL;
buf_ = "CreateProcess failed: The system cannot find the file "
"specified.\n";
return true;
} else {
fprintf(stderr, "\nCreateProcess failed. Command attempted:\n\"%s\"\n",
command.c_str());
const char* hint = NULL;
// ERROR_INVALID_PARAMETER means the command line was formatted
// incorrectly. This can be caused by a command line being too long or
// leading whitespace in the command. Give extra context for this case.
if (error == ERROR_INVALID_PARAMETER) {
if (command.length() > 0 && (command[0] == ' ' || command[0] == '\t'))
hint = "command contains leading whitespace";
else
hint = "is the command line too long?";
}
Win32Fatal("CreateProcess", hint);
}
}
CloseHandle(process_info.hThread);
child_ = process_info.hProcess;
return true;
}
ExitStatus Subprocess::Finish() {
if (!child_)
return ExitFailure;
// TODO: add error handling for all of these.
WaitForSingleObject(child_.get(), INFINITE);
DWORD exit_code = 0;
GetExitCodeProcess(child_.get(), &exit_code);
child_.Close();
return exit_code == CONTROL_C_EXIT ? ExitInterrupted
: static_cast<ExitStatus>(exit_code);
}
bool Subprocess::Done() const {
return !async_pipe_;
}
const string& Subprocess::GetOutput() const {
return buf_;
}
SubprocessSet::SubprocessSet() : SubprocessSet(AsyncLoop::Get(), nullptr) {}
SubprocessSet::SubprocessSet(AsyncLoop& async_loop,
const EnvironmentBlock* environment)
: async_loop_(async_loop), interrupt_catcher_(async_loop),
environment_(environment) {}
SubprocessSet::~SubprocessSet() {
Clear();
}
Subprocess* SubprocessSet::Add(const string& command,
const std::string* description,
bool use_console) {
auto subprocess = std::make_unique<Subprocess>(*this, use_console);
if (!subprocess->Start(command, description)) {
return 0;
}
Subprocess* result = subprocess.get();
if (subprocess->child_)
running_.push_back(std::move(subprocess));
else
finished_.push(std::move(subprocess));
return result;
}
void SubprocessSet::OnProcessCompletion(Subprocess* subprocess) {
// Move the subprocess from the running_ vecetor to the finished_ queue.
for (auto it = running_.begin(); it != running_.end(); ++it) {
if (it->get() == subprocess) {
finished_.emplace(it->release());
running_.erase(it);
return;
}
}
assert(false && "Unnknown subprocess pointer");
}
SubprocessSet::WorkResult SubprocessSet::DoWork() {
size_t running_count = running_.size();
if (!running_count)
return WorkResult::NoWork;
do {
AsyncLoop::ExitStatus status = async_loop_.RunOnce(-1);
if (status == AsyncLoop::ExitInterrupted) {
return WorkResult::Interrupted;
}
} while (running_.size() == running_count);
return WorkResult::SubprocFinished;
}
std::unique_ptr<Subprocess> SubprocessSet::NextFinished() {
if (finished_.empty())
return {};
std::unique_ptr<Subprocess> subproc = std::move(finished_.front());
finished_.pop();
return subproc;
}
void SubprocessSet::Clear() {
for (auto& subproc : running_) {
// Since the console process is in our process group, it will receive a
// CTRL_C_EVENT or CTRL_BREAK_EVENT at the same time as us.
if (subproc->child_ && subproc->run_mode_ != Subprocess::RunMode::Local) {
if (!GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT,
GetProcessId(subproc->child_.get()))) {
Win32Fatal("GenerateConsoleCtrlEvent");
}
}
}
running_.clear();
}