blob: 1e77ee30c81607702a351183da95cb277796684a [file] [log] [blame]
// Copyright (c) 2012 The Chromium OS 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 "cros-disks/process.h"
#include <algorithm>
#include <array>
#include <string>
#include <fcntl.h>
#include <poll.h>
#include <base/check.h>
#include <base/check_op.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/posix/eintr_wrapper.h>
#include <base/process/kill.h>
#include <base/strings/strcat.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <base/time/time.h>
#include "cros-disks/quote.h"
#include "cros-disks/sandboxed_init.h"
namespace cros_disks {
namespace {
// Creates a pipe holding the given string and returns a file descriptor to the
// read end of this pipe. If the given string is too big to fit into the pipe's
// buffer, it is truncated.
base::ScopedFD WrapStdIn(const base::StringPiece in) {
SubprocessPipe p(SubprocessPipe::kParentToChild);
const int fd = p.parent_fd.get();
PCHECK(base::SetNonBlocking(fd));
const ssize_t n =
HANDLE_EINTR(write(p.parent_fd.get(), in.data(), in.size()));
if (n < 0) {
PLOG(ERROR) << "Cannot write to pipe " << fd;
} else if (n < in.size()) {
LOG(ERROR) << "Short write to pipe " << fd << ": Wrote " << n
<< " bytes instead of " << in.size() << " bytes";
}
return std::move(p.child_fd);
}
} // namespace
// static
const pid_t Process::kInvalidProcessId = -1;
Process::Process() = default;
Process::~Process() = default;
void Process::AddArgument(std::string argument) {
DCHECK(arguments_array_.empty());
arguments_.push_back(std::move(argument));
if (arguments_.size() == 1)
program_name_ = base::FilePath(arguments_.front()).BaseName().value();
}
char* const* Process::GetArguments() {
if (arguments_array_.empty())
BuildArgumentsArray();
return arguments_array_.data();
}
void Process::BuildArgumentsArray() {
for (std::string& argument : arguments_) {
arguments_array_.push_back(argument.data());
}
arguments_array_.push_back(nullptr);
}
void Process::AddEnvironmentVariable(const base::StringPiece name,
const base::StringPiece value) {
DCHECK(environment_array_.empty());
DCHECK(!name.empty());
std::string s;
s.reserve(name.size() + value.size() + 1);
s.append(name.data(), name.size());
s += '=';
s.append(value.data(), value.size());
environment_.push_back(std::move(s));
}
char* const* Process::GetEnvironment() {
// If there are no extra environment variables, just use the current
// environment.
if (environment_.empty()) {
return environ;
}
if (environment_array_.empty()) {
// Prepare the new environment.
for (std::string& s : environment_) {
// TODO(fdegros) Remove const_cast when using C++17
environment_array_.push_back(const_cast<char*>(s.data()));
}
// Append the current environment.
for (char* const* p = environ; *p; ++p) {
environment_array_.push_back(*p);
}
environment_array_.push_back(nullptr);
}
return environment_array_.data();
}
void Process::SetOutputCallback(OutputCallback callback) {
DCHECK(!output_callback_);
output_callback_ = std::move(callback);
DCHECK(output_callback_);
}
void Process::OnLauncherExit() {
if (!IsFinished()) {
LOG(WARNING) << "Spurious call to OnLauncherExit";
return;
}
// By then, |launcher_exit_callback_| should have been called.
DCHECK(!launcher_exit_callback_);
launcher_watch_.reset();
}
void Process::SetLauncherExitCallback(LauncherExitCallback callback) {
DCHECK(!launcher_exit_callback_);
launcher_exit_callback_ = std::move(callback);
DCHECK(launcher_exit_callback_);
DCHECK(!launcher_watch_);
launcher_watch_ = base::FileDescriptorWatcher::WatchReadable(
launcher_pipe_.parent_fd.get(),
base::BindRepeating(&Process::OnLauncherExit, base::Unretained(this)));
DCHECK(launcher_watch_);
}
bool Process::Start(base::ScopedFD in_fd, base::ScopedFD out_fd) {
CHECK_EQ(kInvalidProcessId, pid_);
CHECK(!finished());
CHECK(!arguments_.empty()) << "No arguments provided";
LOG(INFO) << "Starting program " << quote(program_name_) << " with arguments "
<< quote(arguments_);
LOG_IF(INFO, !environment_.empty())
<< "and extra environment " << quote(environment_);
pid_ = StartImpl(std::move(in_fd), std::move(out_fd));
return pid_ != kInvalidProcessId;
}
bool Process::Start() {
base::ScopedFD out_child_fd;
if (output_callback_) {
SubprocessPipe out_pipe(SubprocessPipe::kChildToParent);
PCHECK(base::SetNonBlocking(out_pipe.parent_fd.get()));
DCHECK(!output_watch_);
output_watch_ = base::FileDescriptorWatcher::WatchReadable(
out_pipe.parent_fd.get(),
base::BindRepeating(&Process::OnOutputAvailable,
base::Unretained(this)));
DCHECK(output_watch_);
DCHECK(!out_fd_.is_valid());
out_fd_ = std::move(out_pipe.parent_fd);
DCHECK(out_fd_.is_valid());
out_child_fd = std::move(out_pipe.child_fd);
} else {
out_child_fd.reset(dup(STDERR_FILENO));
PCHECK(out_child_fd.is_valid());
}
DCHECK(out_child_fd.is_valid());
return Start(WrapStdIn(input_), std::move(out_child_fd));
}
int Process::Wait() {
if (finished())
return exit_code_;
DCHECK_NE(kInvalidProcessId, pid_);
exit_code_ = WaitImpl();
DCHECK(finished());
if (launcher_exit_callback_)
std::move(launcher_exit_callback_).Run(exit_code_);
return exit_code_;
}
bool Process::IsFinished() {
if (finished())
return true;
CHECK_NE(kInvalidProcessId, pid_);
exit_code_ = WaitNonBlockingImpl();
if (!finished())
return false;
if (launcher_exit_callback_)
std::move(launcher_exit_callback_).Run(exit_code_);
return true;
}
void Process::StoreOutputLine(const base::StringPiece line) {
DCHECK(!line.empty());
LOG(INFO) << program_name_ << ": " << line;
captured_output_.emplace_back(line);
if (output_callback_)
output_callback_.Run(line);
}
void Process::SplitOutputIntoLines(base::StringPiece data) {
size_t i;
while ((i = data.find_first_of('\n')) != base::StringPiece::npos) {
remaining_.append(data.data(), i);
data.remove_prefix(i + 1);
StoreOutputLine(remaining_);
remaining_.clear();
}
remaining_.append(data.data(), data.size());
}
bool Process::CaptureOutput() {
if (!out_fd_.is_valid())
return false;
const int fd = out_fd_.get();
while (true) {
char buffer[PIPE_BUF];
const ssize_t n = read(fd, buffer, PIPE_BUF);
if (n < 0) {
// Error reading.
switch (errno) {
case EAGAIN:
case EINTR:
// Nothing for now, but it's Ok to try again later.
VPLOG(2) << "Nothing to read from file descriptor " << fd;
return true;
}
PLOG(ERROR) << "Cannot read from file descriptor " << fd;
FlushOutput();
return false;
}
if (n == 0) {
// End of stream.
VLOG(2) << "End of stream from file descriptor " << fd;
FlushOutput();
return false;
}
VLOG(2) << "Got " << n << " bytes from file descriptor " << fd;
DCHECK_GT(n, 0);
DCHECK_LE(n, PIPE_BUF);
SplitOutputIntoLines(base::StringPiece(buffer, n));
}
}
bool Process::WaitAndCaptureOutput() {
if (!out_fd_.is_valid())
return false;
const int fd = out_fd_.get();
struct pollfd pfd {
fd, POLLIN, 0
};
const int ret = poll(&pfd, 1, 100 /* milliseconds */);
if (ret < 0) {
// Error.
PLOG(ERROR) << "Cannot poll file descriptor " << fd;
CHECK_EQ(errno, EINTR);
return true;
}
if (ret == 0) {
// Nothing to read because of timeout.
VLOG(2) << "Nothing to read from file descriptor " << fd;
return true;
}
return CaptureOutput();
}
void Process::FlushOutput() {
output_watch_.reset();
out_fd_.reset();
if (!remaining_.empty()) {
StoreOutputLine(remaining_);
remaining_.clear();
}
output_callback_.Reset();
}
void Process::OnOutputAvailable() {
VLOG(1) << "Data available from file descriptor " << out_fd_.get();
CaptureOutput();
}
int Process::Run() {
SubprocessPipe out(SubprocessPipe::kChildToParent);
PCHECK(base::SetNonBlocking(out.parent_fd.get()));
DCHECK(!out_fd_.is_valid());
out_fd_ = std::move(out.parent_fd);
DCHECK(out_fd_.is_valid());
if (!Start(WrapStdIn(input_), std::move(out.child_fd)))
return -1;
VLOG(1) << "Collecting output of program " << quote(program_name_) << "...";
// Poll process and pipe. Read from pipe when possible.
while (!IsFinished() && WaitAndCaptureOutput())
continue;
// Really wait for process to finish.
const int exit_code = Wait();
// Final read from pipe after process finished.
CaptureOutput();
FlushOutput();
VLOG(1) << "Finished collecting output of program " << quote(program_name_);
if (exit_code == 0) {
LOG(INFO) << "Program " << quote(program_name_) << " finished successfully";
return exit_code;
}
// Process finished with a non-zero exit code.
DCHECK_GT(exit_code, 0);
// Log the captured output, if it hasn't been already logged as it was getting
// captured.
if (!LOG_IS_ON(INFO)) {
for (const std::string& s : captured_output_) {
LOG(ERROR) << program_name_ << ": " << s;
}
}
LOG(ERROR) << "Program " << quote(program_name_)
<< " finished with exit code " << exit_code;
return exit_code;
}
} // namespace cros_disks