blob: 93620655710b3c2619cf31844cb27d1af93c858d [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/windows_services/service_program/test_support/scoped_log_grabber.h"
#include <windows.h>
#include <utility>
#include "base/check.h"
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/containers/heap_array.h"
#include "base/containers/span.h"
#include "base/logging.h"
#include "base/process/process.h"
#include "base/strings/string_number_conversions.h"
#include "base/win/win_util.h"
#include "chrome/windows_services/service_program/switches.h"
ScopedLogGrabber::ScopedLogGrabber() {
// Create a pipe to give to the child as stderr.
HANDLE read_handle;
HANDLE write_handle;
PCHECK(::CreatePipe(&read_handle, &write_handle,
/*lpPipeAttributes=*/nullptr, /*nSize=*/0));
read_pipe_ = base::File(std::exchange(read_handle, nullptr));
write_pipe_ = base::File(std::exchange(write_handle, nullptr));
// Spin off a thread that will stream from `read_pipe_` to this process's
// stderr (its log destination). `base::ThreadPool` cannot be used because a
// test using this class may not yet have created a TaskEnvironment.
CHECK(base::PlatformThread::Create(/*stack_size=*/0, /*delegate=*/this,
&output_thread_));
}
ScopedLogGrabber::~ScopedLogGrabber() {
// Close the write pipe, which will trigger the streaming thread to terminate.
write_pipe_.Close();
// Collect the streaming thread.
base::PlatformThread::Join(output_thread_);
}
void ScopedLogGrabber::AddLoggingSwitches(
base::CommandLine& command_line) const {
command_line.AppendSwitchASCII(switches::kLogFileHandle,
base::NumberToString(base::win::HandleToUint32(
write_pipe_.GetPlatformFile())));
command_line.AppendSwitchASCII(
switches::kLogFileSource,
base::NumberToString(base::Process::Current().Pid()));
}
void ScopedLogGrabber::SetLogMessageCallback(LogMessageCallback callback) {
base::AutoLock callback_lock(lock_);
callback_ = std::move(callback);
}
namespace {
// Processes `current_message` (any unprocessed data from the service); sending
// each newline-delimited log message to `callback`. Messages that are not
// processed by the callback are emitted to `std_err`. Messages are removed from
// `current_message` following processing so that any remaining data is included
// in subsequent invocations.
void ProcessMessages(ScopedLogGrabber::LogMessageCallback& callback,
base::File& std_err,
std::string& current_message) {
// Basic assumption: `current_message` is either empty, or begins with a log
// message. Lines of data that do not begin with a message header (i.e.,
// "[PID:") are assumed to be continuations of a previous multi-line message.
// Lines that precede the first valid message are given to the callback with a
// null PID.
// A view of the unprocessed portion of `current_message`.
std::string_view remaining(current_message);
// The position in `remaining` of the start of a line.
std::string_view::size_type line_start = 0;
// Scan forward to the next distinct message; i.e., a newline at the end of
// the string or a newline followed by a new message header.
while (true) {
const std::string_view::size_type newline =
remaining.find('\n', line_start);
if (newline == std::string_view::npos) {
break; // Incomplete line. Return to read more data.
}
// If this newline is at the end of the data or it is followed by a new
// message, send all data up to and including this newline to the callback
// and emit it if the callback returns false.
std::string_view next_text = remaining.substr(newline + 1);
if (next_text.empty() ||
ScopedLogGrabber::ParseProcessId(next_text) != base::kNullProcessId) {
std::string_view text = remaining.substr(0, newline + 1);
if (!callback.Run(ScopedLogGrabber::ParseProcessId(text), text)) {
PCHECK(std_err.WriteAtCurrentPosAndCheck(base::as_byte_span(text)));
}
// Update `remaining` and look for another message in it.
remaining = next_text;
line_start = 0;
continue;
}
// Otherwise, this line is part of a multi-line message. Continue scanning
// forward for more lines until the next message or the end of the data is
// found.
line_start += newline + 1;
}
// Erase all data before `remaining`, as it has all been processed.
current_message.erase(0, remaining.data() - current_message.data());
}
} // namespace
void ScopedLogGrabber::ThreadMain() {
base::PlatformThread::SetName("ScopedLogGrabber");
// Duplicate this process's stderr handle for use below.
HANDLE stderr_handle = ::GetStdHandle(STD_ERROR_HANDLE);
PCHECK(stderr_handle != INVALID_HANDLE_VALUE);
HANDLE duplicate;
PCHECK(::DuplicateHandle(
::GetCurrentProcess(), std::exchange(stderr_handle, nullptr),
::GetCurrentProcess(), &duplicate,
/*dwDesiredAccess=*/0, /*bInheritHandle=*/FALSE, DUPLICATE_SAME_ACCESS));
base::File std_err(std::exchange(duplicate, nullptr));
// A buffer to hold text to be handled by `ProcessMessages()` if a callback
// has been set.
std::string current_message;
// Read logs from the service until the write-side of the pipe is closed by
// both any running service and the test's ScopedLogGrabber instance.
auto buffer = base::HeapArray<uint8_t>::Uninit(4096);
while (true) {
std::optional<size_t> bytes_read =
read_pipe_.ReadAtCurrentPos(buffer.as_span());
if (!bytes_read.has_value()) {
const auto error = ::GetLastError();
CHECK_EQ(error, static_cast<DWORD>(ERROR_BROKEN_PIPE)) << error;
break;
}
base::span<const uint8_t> bytes = buffer.first(*bytes_read);
base::AutoLock callback_lock(lock_);
if (!callback_) {
// No callback, so simply emit all data read
PCHECK(std_err.WriteAtCurrentPosAndCheck(bytes));
} else {
// Accumulate data into `current_message` and pass all individual messages
// to the callback, emitting only if the callback returns false.
current_message += base::as_string_view(bytes);
ProcessMessages(callback_, std_err, current_message);
}
}
}
// static
base::ProcessId ScopedLogGrabber::ParseProcessId(std::string_view message) {
constexpr size_t kMinMessageHeaderSize = 3; // '[', N, ':'
if (message.size() >= kMinMessageHeaderSize && message.front() == '[') {
std::string_view rest = message.substr(1);
if (std::string_view::size_type colon_pos = rest.find(':');
colon_pos != std::string_view::npos) {
unsigned pid = base::kNullProcessId;
if (base::StringToUint(rest.substr(0, colon_pos), &pid)) {
return static_cast<base::ProcessId>(pid);
}
}
}
return base::kNullProcessId;
}