chromium/chrome/browser/ash/system_logs/single_log_file_log_source.cc

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/browser/ash/system_logs/single_log_file_log_source.h"

#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/process/process_info.h"
#include "base/strings/string_split.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "content/public/browser/browser_thread.h"

namespace system_logs {

namespace {

using SupportedSource = SingleLogFileLogSource::SupportedSource;

constexpr char kDefaultSystemLogDirPath[] = "/var/log";
constexpr char kLogTruncated[] = "<earlier logs truncated>\n<partial line>";
constexpr int kMaxNumAllowedLogRotationsDuringFileRead = 3;

// We set a per-read limit of 5 MiB to avoid running out of memory. Clients are
// responsible for further bundling and truncating.
//  * This cap is applied to the read buffer before dropping trailing incomplete
//    lines.
constexpr size_t kMaxReadSize = 5 * 1024 * 1024;

// A custom timestamp for when the current Chrome session started. Used during
// testing to override the actual time.
const base::Time* g_chrome_start_time_for_test = nullptr;

// Converts a logs source type to the corresponding file path, relative to the
// base system log directory path. In the future, if non-file source types are
// added, this function should return an empty file path.
base::FilePath::StringType GetLogFileSourceRelativeFilePathValue(
    SingleLogFileLogSource::SupportedSource source) {
  switch (source) {
    case SupportedSource::kMessages:
      return "messages";
    case SupportedSource::kUiLatest:
      return "ui/ui.LATEST";
    case SupportedSource::kAtrusLog:
      return "atrus.log";
    case SupportedSource::kNetLog:
      return "net.log";
    case SupportedSource::kEventLog:
      return "eventlog.txt";
    case SupportedSource::kUpdateEngineLog:
      return "update_engine.log";
    case SupportedSource::kPowerdLatest:
      return "power_manager/powerd.LATEST";
    case SupportedSource::kPowerdPrevious:
      return "power_manager/powerd.PREVIOUS";
  }
  NOTREACHED_IN_MIGRATION();
  return base::FilePath::StringType();
}

// Returns the inode value of file at |path|, or 0 if it doesn't exist or is
// otherwise unable to be accessed for file system info.
ino_t GetInodeValue(const base::FilePath& path) {
  struct stat file_stats;
  if (stat(path.value().c_str(), &file_stats) != 0)
    return 0;
  return file_stats.st_ino;
}

// Attempts to store a string |value| in |*response| under |key|. If there is
// already a string in |*response| under |key|, appends |value| to the existing
// string value.
void AppendToSystemLogsResponse(SystemLogsResponse* response,
                                const std::string& key,
                                const std::string& value) {
  auto iter = response->find(key);
  if (iter == response->end())
    response->emplace(key, value);
  else
    iter->second += value;
}

}  // namespace

SingleLogFileLogSource::SingleLogFileLogSource(SupportedSource source_type)
    : SystemLogsSource(GetLogFileSourceRelativeFilePathValue(source_type)),
      source_type_(source_type),
      log_file_dir_path_(kDefaultSystemLogDirPath),
      max_read_size_(kMaxReadSize),
      file_cursor_position_(0),
      file_inode_(0) {}

SingleLogFileLogSource::~SingleLogFileLogSource() {}

// static
void SingleLogFileLogSource::SetChromeStartTimeForTesting(
    const base::Time* start_time) {
  g_chrome_start_time_for_test = start_time;
}

void SingleLogFileLogSource::Fetch(SysLogsSourceCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(!callback.is_null());

  auto response = std::make_unique<SystemLogsResponse>();
  auto* response_ptr = response.get();

  base::ThreadPool::PostTaskAndReply(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
      base::BindOnce(&SingleLogFileLogSource::ReadFile,
                     weak_ptr_factory_.GetWeakPtr(),
                     kMaxNumAllowedLogRotationsDuringFileRead, response_ptr),
      base::BindOnce(std::move(callback), std::move(response)));
}

void SingleLogFileLogSource::SetMaxReadSizeForTesting(
    const size_t max_read_size) {
  max_read_size_ = max_read_size;
}

base::FilePath SingleLogFileLogSource::GetLogFilePath() const {
  return log_file_dir_path_.Append(source_name());
}

void SingleLogFileLogSource::ReadFile(size_t num_rotations_allowed,
                                      SystemLogsResponse* response) {
  auto result_string = std::make_unique<std::string>();
  // No bytes have been skipped yet, because the read hasn't started.
  constexpr bool bytes_skipped = false;
  ContinueReadFile(std::move(result_string), bytes_skipped,
                   num_rotations_allowed, response);
}

void SingleLogFileLogSource::ContinueReadFile(
    std::unique_ptr<std::string> result_string,
    bool bytes_skipped,
    size_t num_rotations_allowed,
    SystemLogsResponse* response) {
  // Attempt to open the file if it was not previously opened.
  if (!file_.IsValid()) {
    file_.Initialize(GetLogFilePath(),
                     base::File::FLAG_OPEN | base::File::FLAG_READ);
    if (!file_.IsValid())
      return;

    file_cursor_position_ = 0;
    file_inode_ = GetInodeValue(GetLogFilePath());
  }

  // Check for file size reset.
  const size_t length = file_.GetLength();
  if (length < file_cursor_position_) {
    file_cursor_position_ = 0;
    file_.Seek(base::File::FROM_BEGIN, 0);
  }

  // Check for large read and skip forward to avoid out-of-memory conditions.
  if (length - file_cursor_position_ > max_read_size_) {
    bytes_skipped = true;
    file_.Seek(base::File::FROM_END, -max_read_size_);
    // Update |file_cursor_position_| to support the file size reset check.
    file_cursor_position_ = length - max_read_size_;
  }

  // The calculated amount of data to read, after adjusting for
  // |max_read_size_|.
  const size_t size_to_read = length - file_cursor_position_;

  // Trim down the previously read data before starting a new read.
  const size_t available_previous_read_size = max_read_size_ - size_to_read;
  if (available_previous_read_size < result_string->size()) {
    result_string->erase(0,
                         result_string->size() - available_previous_read_size);
  }

  // Read from file until end.
  std::string new_result_string;
  new_result_string.resize(size_to_read);
  size_t size_read =
      file_.ReadAtCurrentPos(&new_result_string[0], size_to_read);
  new_result_string.resize(size_read);

  const bool file_was_rotated = file_inode_ != GetInodeValue(GetLogFilePath());
  const bool should_handle_file_rotation =
      file_was_rotated && num_rotations_allowed > 0;

  // The reader may only read complete lines. The exception is when there is a
  // rotation, in which case all the remaining contents of the old log file
  // should be read before moving on to read the new log file.
  if ((new_result_string.empty() || new_result_string.back() != '\n') &&
      !should_handle_file_rotation) {
    // If an incomplete line was read, return only the part that includes
    // whole lines.
    size_t last_newline_pos = new_result_string.find_last_of('\n');

    // The part of the string that will be returned includes the newline
    // itself.
    size_t adjusted_size_read =
        last_newline_pos == std::string::npos ? 0 : last_newline_pos + 1;
    file_.Seek(base::File::FROM_CURRENT, -size_read + adjusted_size_read);
    new_result_string.resize(adjusted_size_read);

    // Update |size_read| to reflect that the read was only up to the last
    // newline.
    size_read = adjusted_size_read;
  }

  file_cursor_position_ += size_read;

  result_string->append(new_result_string);

  // If the file was rotated, close the file handle and call this function
  // again, to read from the new file.
  if (should_handle_file_rotation) {
    file_.Close();
    file_cursor_position_ = 0;
    file_inode_ = 0;
    ContinueReadFile(std::move(result_string), bytes_skipped,
                     num_rotations_allowed - 1, response);
  } else {
    // Only write the log truncated sentinel value once we have something to
    // go after it, and any accumulation and rollover has been handled.
    if (bytes_skipped && result_string->size() > 0) {
      AppendToSystemLogsResponse(response, source_name(), kLogTruncated);
    }
    // Pass it back to the callback.
    AppendToSystemLogsResponse(response, source_name(), *result_string.get());
  }
}

}  // namespace system_logs