File: scoped_log_grabber.cc

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (175 lines) | stat: -rw-r--r-- 7,060 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// 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/strings/string_view_util.h"
#include "base/win/windows_handle_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;
}