| // 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 |