| // Copyright 2015 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include <algorithm> | |
| #include <fcntl.h> | |
| #include <sstream> | |
| #include <string> | |
| #include <vector> | |
| #include <sysexits.h> | |
| #include <sys/types.h> | |
| #include <base/check.h> | |
| #include <base/files/file_path.h> | |
| #include <base/files/file_util.h> | |
| #include <base/functional/bind.h> | |
| #include <base/functional/callback_helpers.h> | |
| #include <base/logging.h> | |
| #include <base/strings/string_number_conversions.h> | |
| #include <base/time/time.h> | |
| #include <brillo/daemons/daemon.h> | |
| #include <libec/ec_command.h> | |
| #include <libec/get_features_command.h> | |
| #include "timberslide/log_listener_factory.h" | |
| #include "timberslide/timberslide.h" | |
| namespace timberslide { | |
| namespace { | |
| const char kCurrentLogExt[] = ".log"; | |
| const char kPreviousLogExt[] = ".previous"; | |
| const int kMaxCurrentLogSize = 10 * 1024 * 1024; | |
| // This line fails to compile with a static_assert if the database is invalid. | |
| constexpr pw::tokenizer::TokenDatabase kDefaultDatabase = | |
| pw::tokenizer::TokenDatabase(); | |
| // String proxy class for dealing with lines. | |
| class LineExtractor { | |
| public: | |
| // Extract one line from beginning of string stream. | |
| friend std::istream& operator>>(std::istream& is, LineExtractor& l) { | |
| std::getline(is, l.str_); | |
| return is; | |
| } | |
| // Transparently convert to std::string. | |
| operator std::string() const { return str_; } | |
| private: | |
| std::string str_; | |
| }; | |
| } // namespace | |
| TimberSlide::TimberSlide(const std::string& ec_type, | |
| base::File device_file, | |
| base::File uptime_file, | |
| const base::FilePath& log_dir, | |
| const base::FilePath& token_db) | |
| : device_file_(std::move(device_file)), | |
| tokens_db_(token_db), | |
| detokenizer_(kDefaultDatabase), | |
| total_size_(0), | |
| uptime_file_(std::move(uptime_file)), | |
| uptime_file_valid_(uptime_file_.IsValid()) { | |
| current_log_ = log_dir.Append(ec_type + kCurrentLogExt); | |
| previous_log_ = log_dir.Append(ec_type + kPreviousLogExt); | |
| log_listener_ = LogListenerFactory::Create(ec_type); | |
| xfrm_ = std::make_unique<StringTransformer>(); | |
| } | |
| TimberSlide::TimberSlide(std::unique_ptr<LogListener> log_listener, | |
| std::unique_ptr<StringTransformer> xfrm) | |
| : detokenizer_(kDefaultDatabase), | |
| log_listener_(std::move(log_listener)), | |
| xfrm_(std::move(xfrm)) {} | |
| int TimberSlide::OnInit() { | |
| LOG(INFO) << "Starting timberslide daemon"; | |
| int ret = brillo::Daemon::OnInit(); | |
| if (ret != EX_OK) | |
| return ret; | |
| int64_t ec_uptime_ms; | |
| if (uptime_file_valid_) { | |
| LOG(INFO) << "EC uptime file is valid"; | |
| if (GetEcUptime(&ec_uptime_ms)) | |
| xfrm_->UpdateTimestamps(ec_uptime_ms, base::Time::Now()); | |
| } else { | |
| LOG(WARNING) << "EC uptime file is not valid; ignoring"; | |
| } | |
| RotateLogs(previous_log_, current_log_); | |
| auto cros_fd = base::ScopedFD(open(ec::kCrosEcPath, O_RDWR | O_CLOEXEC)); | |
| ec::GetFeaturesCommand getFeaturesCmd; | |
| if (getFeaturesCmd.Run(cros_fd.get())) { | |
| tokenized_logging_ = getFeaturesCmd.IsFeatureSupported( | |
| ec_feature_code::EC_FEATURE_TOKENIZED_LOGGING); | |
| } | |
| if (tokenized_logging_) { | |
| LOG(INFO) << "EC logging: tokenized"; | |
| if (base::PathExists(tokens_db_)) { | |
| token_watcher_ = std::make_unique<base::FilePathWatcher>(); | |
| token_watcher_->Watch( | |
| tokens_db_, base::FilePathWatcher::Type::kNonRecursive, | |
| base::BindRepeating(&TimberSlide::OnEventTokenChange, | |
| base::Unretained(this))); | |
| detokenizer_ = OpenDatabase(tokens_db_); | |
| } else { | |
| LOG(ERROR) << "EC token database not found"; | |
| return EX_OSERR; | |
| } | |
| } else { | |
| LOG(INFO) << "EC logging: raw text"; | |
| } | |
| watcher_ = base::FileDescriptorWatcher::WatchReadable( | |
| device_file_.GetPlatformFile(), | |
| base::BindRepeating(&TimberSlide::OnEventReadable, | |
| base::Unretained(this))); | |
| return watcher_ ? EX_OK : EX_OSERR; | |
| } | |
| // | |
| // From kernel's Documentation/filesystems/sysfs.txt: If userspace seeks back | |
| // to zero or does a pread(2) with an offset of '0' the show() method will | |
| // be called again, rearmed, to fill the buffer. | |
| // | |
| // Therefore, the 'uptime' file will be kept open and just seeked back to | |
| // 0 when new uptime is needed. | |
| // | |
| bool TimberSlide::GetEcUptime(int64_t* ec_uptime_ms) { | |
| char uptime_buf[64] = {0}; | |
| if (!uptime_file_valid_ || uptime_file_.Seek(base::File::FROM_BEGIN, 0) != 0) | |
| return false; | |
| // Read single line from file and parse as a number. | |
| int count = uptime_file_.ReadAtCurrentPos(uptime_buf, sizeof(uptime_buf) - 1); | |
| if (count <= 0) | |
| return false; | |
| uptime_buf[count] = '\0'; | |
| base::StringToInt64(uptime_buf, ec_uptime_ms); | |
| // If the 'uptime' file contains zero, that means the kernel patch is | |
| // available, but the EC doesn't support EC_CMD_GET_UPTIME_INFO. In | |
| // that case, this returns false so that incorrect times aren't reported | |
| // in the EC log file. | |
| return (*ec_uptime_ms > 0); | |
| } | |
| pw::tokenizer::Detokenizer TimberSlide::OpenDatabase( | |
| const base::FilePath& token_db) { | |
| LOG(INFO) << "Loading tokens: " << token_db; | |
| base::File tokens(token_db, base::File::FLAG_OPEN | base::File::FLAG_READ); | |
| std::vector<uint8_t> data(tokens.GetLength()); | |
| tokens.ReadAndCheck(0, base::make_span(data)); | |
| pw::tokenizer::TokenDatabase database = | |
| pw::tokenizer::TokenDatabase::Create(data); | |
| // This checks if the file contained a valid database. It is safe to use a | |
| // TokenDatabase that failed to load (it will be empty), but it may be | |
| // desirable to provide a default database or otherwise handle the error. | |
| return pw::tokenizer::Detokenizer(database.ok() ? database | |
| : kDefaultDatabase); | |
| } | |
| std::string TimberSlide::ProcessLogBuffer(const std::string& buffer, | |
| const base::Time& now) { | |
| int64_t ec_current_uptime_ms = 0; | |
| std::string log = | |
| (tokenized_logging_ ? detokenizer_.DetokenizeBase64(buffer) : buffer); | |
| std::istringstream iss(log); | |
| if (GetEcUptime(&ec_current_uptime_ms)) | |
| xfrm_->UpdateTimestamps(ec_current_uptime_ms, now); | |
| auto fn_xfrm = [this](const std::string& line) { | |
| if (log_listener_) { | |
| log_listener_->OnLogLine(line); | |
| } | |
| return xfrm_->AddHostTs(line); | |
| }; | |
| // Iterate over each line and prepend the corresponding host timestamp if we | |
| // have it | |
| std::ostringstream oss; | |
| std::transform(std::istream_iterator<LineExtractor>(iss), | |
| std::istream_iterator<LineExtractor>(), | |
| std::ostream_iterator<std::string>(oss, "\n"), fn_xfrm); | |
| return oss.str(); | |
| } | |
| void TimberSlide::OnEventReadable() { | |
| char buffer[4096]; | |
| int ret; | |
| ret = TEMP_FAILURE_RETRY( | |
| device_file_.ReadAtCurrentPosNoBestEffort(buffer, sizeof(buffer))); | |
| if (ret == 0) | |
| return; | |
| if (ret < 0) { | |
| PLOG(ERROR) << "Read error"; | |
| Quit(); | |
| return; | |
| } | |
| std::string str = | |
| ProcessLogBuffer(std::string(buffer, ret), base::Time::Now()); | |
| ret = str.size(); | |
| if (!base::AppendToFile(current_log_, str)) { | |
| PLOG(ERROR) << "Could not append to log file"; | |
| Quit(); | |
| return; | |
| } | |
| total_size_ += ret; | |
| if (total_size_ >= kMaxCurrentLogSize) { | |
| RotateLogs(previous_log_, current_log_); | |
| total_size_ = 0; | |
| } | |
| } | |
| void TimberSlide::OnEventTokenChange(const base::FilePath& file_path, | |
| bool error) { | |
| // Refresh tokens. | |
| LOG(INFO) << "Token DB changed: " << file_path; | |
| detokenizer_ = OpenDatabase(file_path); | |
| } | |
| void TimberSlide::RotateLogs(const base::FilePath& previous_log, | |
| const base::FilePath& current_log) { | |
| CHECK(base::DeleteFile(previous_log)); | |
| if (base::PathExists(current_log)) | |
| CHECK(base::Move(current_log, previous_log)); | |
| base::WriteFile(current_log, "", 0); | |
| } | |
| } // namespace timberslide |