blob: d58d7bd24fa1dff5bf87247d40bec90f7298cf93 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 "perfetto/ext/base/subprocess.h"
#include "perfetto/base/build_config.h"
#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
#include <stdio.h>
#include <algorithm>
#include <mutex>
#include <tuple>
#include <Windows.h>
#include "perfetto/base/logging.h"
#include "perfetto/base/time.h"
#include "perfetto/ext/base/pipe.h"
#include "perfetto/ext/base/utils.h"
namespace perfetto {
namespace base {
// static
const int Subprocess::kTimeoutSignal = static_cast<int>(STATUS_TIMEOUT);
void Subprocess::Start() {
if (args.exec_cmd.empty()) {
PERFETTO_ELOG("Subprocess.exec_cmd cannot be empty on Windows");
return;
}
// Quote arguments but only when ambiguous. When quoting, CreateProcess()
// assumes that the command is an absolute path and does not search in the
// %PATH%. If non quoted, instead, CreateProcess() tries both. This is to
// allow Subprocess("cmd.exe", "/c", "shell command").
std::string cmd;
for (const auto& part : args.exec_cmd) {
if (part.find(" ") != std::string::npos) {
cmd += "\"" + part + "\" ";
} else {
cmd += part + " ";
}
}
// Remove trailing space.
if (!cmd.empty())
cmd.resize(cmd.size() - 1);
s_->stdin_pipe = Pipe::Create();
// Allow the child process to inherit the other end of the pipe.
PERFETTO_CHECK(
::SetHandleInformation(*s_->stdin_pipe.rd, HANDLE_FLAG_INHERIT, 1));
if (args.stderr_mode == kBuffer || args.stdout_mode == kBuffer) {
s_->stdouterr_pipe = Pipe::Create();
PERFETTO_CHECK(
::SetHandleInformation(*s_->stdouterr_pipe.wr, HANDLE_FLAG_INHERIT, 1));
}
ScopedPlatformHandle nul_handle;
if (args.stderr_mode == kDevNull || args.stdout_mode == kDevNull) {
nul_handle.reset(::CreateFileA("NUL", GENERIC_WRITE, FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, nullptr));
PERFETTO_CHECK(::SetHandleInformation(*nul_handle, HANDLE_FLAG_INHERIT, 1));
}
PROCESS_INFORMATION proc_info{};
STARTUPINFOA start_info{};
start_info.cb = sizeof(STARTUPINFOA);
if (args.stderr_mode == kInherit) {
start_info.hStdError = ::GetStdHandle(STD_ERROR_HANDLE);
} else if (args.stderr_mode == kBuffer) {
start_info.hStdError = *s_->stdouterr_pipe.wr;
} else if (args.stderr_mode == kDevNull) {
start_info.hStdError = *nul_handle;
} else if (args.stderr_mode == kFd) {
PERFETTO_CHECK(
::SetHandleInformation(*args.out_fd, HANDLE_FLAG_INHERIT, 1));
start_info.hStdError = *args.out_fd;
} else {
PERFETTO_CHECK(false);
}
if (args.stdout_mode == kInherit) {
start_info.hStdOutput = ::GetStdHandle(STD_OUTPUT_HANDLE);
} else if (args.stdout_mode == kBuffer) {
start_info.hStdOutput = *s_->stdouterr_pipe.wr;
} else if (args.stdout_mode == kDevNull) {
start_info.hStdOutput = *nul_handle;
} else if (args.stdout_mode == kFd) {
PERFETTO_CHECK(
::SetHandleInformation(*args.out_fd, HANDLE_FLAG_INHERIT, 1));
start_info.hStdOutput = *args.out_fd;
} else {
PERFETTO_CHECK(false);
}
start_info.hStdInput = *s_->stdin_pipe.rd;
start_info.dwFlags |= STARTF_USESTDHANDLES;
// Create the child process.
bool success =
::CreateProcessA(nullptr, // App name. Needs to be null to use PATH.
&cmd[0], // Command line.
nullptr, // Process security attributes.
nullptr, // Primary thread security attributes.
true, // Handles are inherited.
0, // Flags.
nullptr, // Use parent's environment.
nullptr, // Use parent's current directory.
&start_info, // STARTUPINFO pointer.
&proc_info); // Receives PROCESS_INFORMATION.
// Close on our side the pipe ends that we passed to the child process.
s_->stdin_pipe.rd.reset();
s_->stdouterr_pipe.wr.reset();
args.out_fd.reset();
if (!success) {
s_->returncode = ERROR_FILE_NOT_FOUND;
s_->status = kTerminated;
s_->stdin_pipe.wr.reset();
s_->stdouterr_pipe.rd.reset();
PERFETTO_ELOG("CreateProcess failed: %lx, cmd: %s", GetLastError(),
&cmd[0]);
return;
}
s_->pid = proc_info.dwProcessId;
s_->win_proc_handle = ScopedPlatformHandle(proc_info.hProcess);
s_->win_thread_handle = ScopedPlatformHandle(proc_info.hThread);
s_->status = kRunning;
MovableState* s = s_.get();
s_->stdin_thread = std::thread(&Subprocess::StdinThread, s, args.input);
if (args.stderr_mode == kBuffer || args.stdout_mode == kBuffer) {
PERFETTO_DCHECK(s_->stdouterr_pipe.rd);
s_->stdouterr_thread = std::thread(&Subprocess::StdoutErrThread, s);
}
}
// static
void Subprocess::StdinThread(MovableState* s, std::string input) {
size_t input_written = 0;
while (input_written < input.size()) {
DWORD wsize = 0;
if (::WriteFile(*s->stdin_pipe.wr, input.data() + input_written,
static_cast<DWORD>(input.size() - input_written), &wsize,
nullptr)) {
input_written += wsize;
} else {
// ERROR_BROKEN_PIPE is WAI when the child just closes stdin and stops
// accepting input.
auto err = ::GetLastError();
if (err != ERROR_BROKEN_PIPE)
PERFETTO_PLOG("Subprocess WriteFile(stdin) failed %lx", err);
break;
}
} // while(...)
std::unique_lock<std::mutex> lock(s->mutex);
s->stdin_pipe.wr.reset();
}
// static
void Subprocess::StdoutErrThread(MovableState* s) {
char buf[4096];
for (;;) {
DWORD rsize = 0;
bool res =
::ReadFile(*s->stdouterr_pipe.rd, buf, sizeof(buf), &rsize, nullptr);
if (!res) {
auto err = GetLastError();
if (err != ERROR_BROKEN_PIPE)
PERFETTO_PLOG("Subprocess ReadFile(stdouterr) failed %ld", err);
}
if (rsize > 0) {
std::unique_lock<std::mutex> lock(s->mutex);
s->locked_outerr_buf.append(buf, static_cast<size_t>(rsize));
} else { // EOF or some error.
break;
}
} // For(..)
// Close the stdouterr_pipe. The main loop looks at the pipe closure to
// determine whether the stdout/err thread has completed.
{
std::unique_lock<std::mutex> lock(s->mutex);
s->stdouterr_pipe.rd.reset();
}
s->stdouterr_done_event.Notify();
}
Subprocess::Status Subprocess::Poll() {
if (s_->status != kRunning)
return s_->status; // Nothing to poll.
Wait(1 /*ms*/);
return s_->status;
}
bool Subprocess::Wait(int timeout_ms) {
PERFETTO_CHECK(s_->status != kNotStarted);
const bool wait_forever = timeout_ms == 0;
const int64_t wait_start_ms = base::GetWallTimeMs().count();
// Break out of the loop only after both conditions are satisfied:
// - All stdout/stderr data has been read (if kBuffer).
// - The process exited.
// Note that the two events can happen arbitrary order. After the process
// exits, there might be still data in the pipe buffer, which we want to
// read fully.
// Note also that stdout/err might be "complete" before starting, if neither
// is operating in kBuffer mode. In that case we just want to wait for the
// process termination.
//
// Instead, don't wait on the stdin to be fully written. The child process
// might exit prematurely (or crash). If that happens, we can end up in a
// state where the write(stdin_pipe_.wr) will never unblock.
bool stdouterr_complete = false;
for (;;) {
HANDLE wait_handles[2]{};
DWORD num_handles = 0;
// Check if the process exited.
bool process_exited = !s_->win_proc_handle;
if (!process_exited) {
DWORD exit_code = STILL_ACTIVE;
PERFETTO_CHECK(::GetExitCodeProcess(*s_->win_proc_handle, &exit_code));
if (exit_code != STILL_ACTIVE) {
s_->returncode = static_cast<int>(exit_code);
s_->status = kTerminated;
s_->win_proc_handle.reset();
s_->win_thread_handle.reset();
process_exited = true;
}
} else {
PERFETTO_DCHECK(s_->status != kRunning);
}
if (!process_exited) {
wait_handles[num_handles++] = *s_->win_proc_handle;
}
// Check if there is more output and if the stdout/err pipe has been closed.
{
std::unique_lock<std::mutex> lock(s_->mutex);
// Move the output from the internal buffer shared with the
// stdouterr_thread to the final buffer exposed to the client.
if (!s_->locked_outerr_buf.empty()) {
s_->output.append(std::move(s_->locked_outerr_buf));
s_->locked_outerr_buf.clear();
}
stdouterr_complete = !s_->stdouterr_pipe.rd;
if (!stdouterr_complete) {
wait_handles[num_handles++] = s_->stdouterr_done_event.fd();
}
} // lock(s_->mutex)
if (num_handles == 0) {
PERFETTO_DCHECK(process_exited && stdouterr_complete);
break;
}
DWORD wait_ms; // Note: DWORD is unsigned.
if (wait_forever) {
wait_ms = INFINITE;
} else {
const int64_t now = GetWallTimeMs().count();
const int64_t wait_left_ms = timeout_ms - (now - wait_start_ms);
if (wait_left_ms <= 0)
return false; // Timed out
wait_ms = static_cast<DWORD>(wait_left_ms);
}
auto wait_res =
::WaitForMultipleObjects(num_handles, wait_handles, false, wait_ms);
PERFETTO_CHECK(wait_res != WAIT_FAILED);
}
PERFETTO_DCHECK(!s_->win_proc_handle);
PERFETTO_DCHECK(!s_->win_thread_handle);
if (s_->stdin_thread.joinable()) // Might not exist if CreateProcess failed.
s_->stdin_thread.join();
if (s_->stdouterr_thread.joinable())
s_->stdouterr_thread.join();
// The stdin pipe is closed by the dedicated stdin thread. However if that is
// not started (e.g. because of no redirection) force close it now. Needs to
// happen after the join() to be thread safe.
s_->stdin_pipe.wr.reset();
s_->stdouterr_pipe.rd.reset();
return true;
}
void Subprocess::KillAndWaitForTermination(int exit_code) {
auto code = exit_code ? static_cast<DWORD>(exit_code) : STATUS_CONTROL_C_EXIT;
::TerminateProcess(*s_->win_proc_handle, code);
Wait();
// TryReadExitStatus must have joined the threads.
PERFETTO_DCHECK(!s_->stdin_thread.joinable());
PERFETTO_DCHECK(!s_->stdouterr_thread.joinable());
}
} // namespace base
} // namespace perfetto
#endif // PERFETTO_OS_WIN