blob: c18a53acb06f7ff1f5addf1c171185faedae5fda [file] [log] [blame]
// Copyright 2020 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 "croslog/log_line_reader.h"
#include <algorithm>
#include <string>
#include <utility>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include "base/bind.h"
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/strings/string_util.h"
#include "croslog/file_map_reader.h"
namespace croslog {
namespace {
// Maximum length of line in bytes.
static int64_t g_max_line_length = 1024 * 1024;
} // namespace
// static
void LogLineReader::SetMaxLineLengthForTest(int64_t max_line_length) {
CHECK_LE(max_line_length, FileMapReader::GetChunkSizeInBytes());
CHECK_GE(max_line_length, 0);
g_max_line_length = max_line_length;
}
LogLineReader::LogLineReader(Backend backend_mode)
: backend_mode_(backend_mode) {
// Checks the assumption of this logic.
DCHECK_GE(FileMapReader::GetChunkSizeInBytes(), g_max_line_length);
}
LogLineReader::~LogLineReader() {
if (file_change_watcher_)
file_change_watcher_->RemoveWatch(file_path_);
}
void LogLineReader::OpenFile(const base::FilePath& file_path) {
CHECK(backend_mode_ == Backend::FILE ||
backend_mode_ == Backend::FILE_FOLLOW);
// Ensure the values are not initialized.
CHECK(file_path_.empty());
file_ = base::File(file_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
if (!file_.IsValid()) {
LOG(ERROR) << "Could not open " << file_path;
return;
}
file_path_ = file_path;
pos_ = 0;
// TODO(yoshiki): Use stat_wrapper_t and File::FStat after libchrome uprev.
struct stat file_stat;
if (fstat(file_.GetPlatformFile(), &file_stat) == 0)
file_inode_ = file_stat.st_ino;
DCHECK_NE(0, file_inode_);
if (backend_mode_ == Backend::FILE_FOLLOW) {
// Race may happen when the file rotates just after file opens.
// TODO(yoshiki): detect the race. Maybe we can use /proc/self/fd/$fd.
file_change_watcher_ = FileChangeWatcher::GetInstance();
bool ret = file_change_watcher_->AddWatch(file_path_, this);
if (!ret) {
LOG(ERROR) << "Failed to install FileChangeWatcher for " << file_path_
<< ".";
file_change_watcher_ = nullptr;
}
}
reader_ = FileMapReader::CreateReader(file_.Duplicate());
}
void LogLineReader::OpenMemoryBufferForTest(const char* buffer, size_t size) {
CHECK(backend_mode_ == Backend::MEMORY_FOR_TEST);
reader_ = FileMapReader::CreateFileMapReaderDelegateImplMemoryReaderForTest(
(const uint8_t*)buffer, size);
}
void LogLineReader::SetPositionLast() {
// At first, sets the position to EOF.
pos_ = reader_->GetFileSize();
DCHECK_LE(0, pos_);
// Calculates the maximum traversable range in the file and allocate a buffer.
int64_t pos_traversal_start = std::max(pos_ - g_max_line_length, INT64_C(0));
int64_t traversal_length = std::min(g_max_line_length, pos_);
// Allocates a buffer of the segment from |pos_traversal_start| to EOF.
auto buffer = reader_->MapBuffer(pos_traversal_start, traversal_length);
CHECK(buffer->valid()) << "Mmap failed. Maybe the file has been truncated.";
// Traverses in reverse order to find the last LF.
while (pos_ > pos_traversal_start && buffer->GetChar(pos_ - 1) != '\n')
pos_--;
if (pos_ != 0 && pos_ <= pos_traversal_start) {
LOG(ERROR) << "The last line is too long to handle (more than: "
<< g_max_line_length
<< "bytes). Lines around here may be broken.";
// Sets the position to the last as a sloppy solution.
pos_ = reader_->GetFileSize();
}
}
// Ensure the file path is initialized.
void LogLineReader::ReloadRotatedFile() {
CHECK(backend_mode_ == Backend::FILE_FOLLOW);
DCHECK(rotated_);
DCHECK(PathExists(file_path_));
rotated_ = false;
CHECK(file_change_watcher_);
file_change_watcher_->RemoveWatch(file_path_);
base::FilePath file_path = file_path_;
file_path_.clear();
reader_.reset();
file_inode_ = 0;
pos_ = 0;
OpenFile(file_path);
if (!file_.IsValid()) {
LOG(FATAL) << "File looks rotated, but new file can't be opened.";
}
reader_ = FileMapReader::CreateReader(file_.Duplicate());
}
base::Optional<std::string> LogLineReader::Forward() {
DCHECK_LE(0, pos_);
// Checks the current position is at the beginning of the line.
if (pos_ != 0) {
auto buffer = reader_->MapBuffer(pos_ - 1, 1);
CHECK(buffer->valid()) << "Mmap failed. Maybe the file has been truncated"
<< " and the current read position got invalid.";
if (buffer->GetChar(pos_ - 1) != '\n') {
LOG(WARNING) << "The line is odd. The line is too long or the file is"
<< " unexpectedly changed.";
}
}
// Calculate the maximum traversable size to allocate.
int64_t traversal_length =
std::min(g_max_line_length, reader_->GetFileSize() - pos_);
int64_t pos_traversal_end = pos_ + traversal_length;
// Allocates a buffer of the segment from |pos_| to |pos_traversal_end|.
auto buffer = reader_->MapBuffer(pos_, traversal_length);
CHECK(buffer->valid()) << "Mmap failed. Maybe the file has been truncated"
<< " and the current read position got invalid.";
// Finds the next LF (end of line).
int64_t pos_line_end = pos_;
while (pos_line_end < pos_traversal_end &&
buffer->GetChar(pos_line_end) != '\n') {
pos_line_end++;
}
if (pos_line_end == reader_->GetFileSize()) {
// Reaches EOF without '\n'.
int64_t unread_length = reader_->GetFileSize() - pos_;
if (rotated_ && unread_length == 0 && PathExists(file_path_)) {
// Free the mapped buffer so that another buffer map is allowed.
buffer.reset();
ReloadRotatedFile();
return Forward();
}
// If next file doesn't exist, leave the remaining string.
// If next file exists, read the remaining string.
if (!rotated_)
return base::nullopt;
pos_line_end = reader_->GetFileSize();
} else if (pos_line_end == (pos_ + g_max_line_length)) {
LOG(ERROR) << "A line is too long to handle (more than "
<< g_max_line_length
<< "bytes). Lines around here may be broken.";
}
// Updates the current position.
int64_t pos_line_start = pos_;
int64_t line_length = pos_line_end - pos_;
pos_ = pos_line_end;
if (pos_ < reader_->GetFileSize()) {
// Unless the line is too long, proceed the LF.
if (buffer->GetChar(pos_) == '\n')
pos_ += 1;
}
return GetString(std::move(buffer), pos_line_start, line_length);
}
base::Optional<std::string> LogLineReader::Backward() {
DCHECK_LE(0, pos_);
if (pos_ == 0)
return base::nullopt;
// Calculates the maximum traversable range in the file and allocate a buffer.
int64_t pos_traversal_start = std::max(pos_ - g_max_line_length, INT64_C(0));
int64_t traversal_length = pos_ - pos_traversal_start;
DCHECK_GE(traversal_length, 0);
// Allocates a buffer of the segment from |pos_traversal_start| to |pos_|.
auto buffer = reader_->MapBuffer(pos_traversal_start, traversal_length);
CHECK(buffer->valid()) << "Mmap failed. Maybe the file has been truncated"
<< " and the current read position got invalid.";
// Ensures the current position is the beginning of the previous line.
if (buffer->GetChar(pos_ - 1) != '\n') {
LOG(WARNING) << "The line is too long or the file is unexpectedly changed."
<< " The lines read may be broken.";
}
// Finds the next LF (at the beginning of the line).
int64_t last_start = pos_ - 1;
while (last_start > pos_traversal_start &&
buffer->GetChar(last_start - 1) != '\n') {
last_start--;
}
// Ensures the next LF is found.
if (last_start != 0 && last_start <= pos_traversal_start) {
LOG(ERROR) << "A line is too long to handle (more than "
<< g_max_line_length
<< "bytes). Lines around here may be broken.";
}
// Updates the current position.
int64_t line_length = pos_ - last_start - 1;
pos_ = last_start;
return GetString(std::move(buffer), last_start, line_length);
}
void LogLineReader::AddObserver(Observer* obs) {
observers_.AddObserver(obs);
}
void LogLineReader::RemoveObserver(Observer* obs) {
observers_.RemoveObserver(obs);
}
std::string LogLineReader::GetString(
std::unique_ptr<FileMapReader::MappedBuffer> mapped_buffer,
uint64_t offset,
uint64_t length) const {
std::pair<const uint8_t*, size_t> buffer =
mapped_buffer->GetBuffer(offset, length);
return std::string(reinterpret_cast<const char*>(buffer.first),
buffer.second);
}
void LogLineReader::OnFileContentMaybeChanged() {
CHECK(backend_mode_ == Backend::FILE_FOLLOW);
CHECK(file_.IsValid());
// We didn't consider the case of content change without size change. It
// shouldn't happen with normal log files.
// Previous file size at (or shortly before) the previous mmap.
const int64_t previous_file_size = reader_->GetFileSize();
// Current file size read from the file system.
const int64_t current_file_size = file_.GetLength();
if (previous_file_size != current_file_size) {
reader_->ApplyFileSizeExpansion();
for (Observer& obs : observers_)
obs.OnFileChanged(this);
}
}
void LogLineReader::OnFileNameMaybeChanged() {
CHECK(backend_mode_ == Backend::FILE_FOLLOW);
if (rotated_)
return;
if (!PathExists(file_path_)) {
rotated_ = true;
} else {
// TODO(yoshiki): Use stat_wrapper_t and File::Stat after libchrome uprev.
struct stat file_stat;
bool inode_changed = ((stat(file_path_.value().c_str(), &file_stat) == 0) &&
(file_inode_ != file_stat.st_ino));
if (inode_changed)
rotated_ = true;
}
if (rotated_) {
for (Observer& obs : observers_)
obs.OnFileChanged(this);
}
}
} // namespace croslog