blob: 08915d6878f8f9adb5712090486ce911be77d065 [file] [log] [blame]
// Copyright 2014 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 "metrics/serialization/serialization_utils.h"
#include <sys/file.h>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/logging.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "metrics/serialization/metric_sample.h"
#define READ_WRITE_ALL_FILE_FLAGS \
(S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH)
namespace metrics {
namespace {
// Magic string that gets written at the beginning of the message file
// when the file has been partially uploaded.
constexpr char kMagicString[] = {'S', 'K', 'I', 'P'};
// Leaves a marker at the beginning of the metrics file, to indicate that the
// first part of the file has been processed. The marker starts with a 4-byte
// magic number ("SKIP") followed by the offset to the remaining samples.
// Returns true on success, false on errors.
//
// Since the file could also start with a regular message, whose 4-byte header
// indicates its size, the magic number must be an invalid size i.e. greater
// than kMessageMaxLength when read as an uint32_t in any byte order.
bool RemovePreviousSamples(int fd) {
off_t offset = lseek(fd, 0, SEEK_CUR);
char marker[sizeof(kMagicString) + sizeof(off_t)];
if (offset == static_cast<off_t>(-1)) {
PLOG(ERROR) << "cannot find offset in metrics log";
return false;
} else if (offset < sizeof(marker)) {
LOG(ERROR) << "metrics log offset is too small: " << offset;
return false;
}
// Fill marker with magic string and offset, then store it.
memcpy(marker, kMagicString, sizeof(kMagicString));
memcpy(marker + sizeof(kMagicString), &offset, sizeof(off_t));
if (pwrite(fd, marker, sizeof(marker), 0) != sizeof(marker)) {
PLOG(ERROR) << "cannot write marker to metrics log";
return false;
}
// Optimization: zero out file content between the marker and the first valid
// sample. NOTE: the fallocate() call writes zeros over the entire range
// specified, and optimizes away zero-filled blocks. We don't care if the
// optimization fails.
int punch_hole_mode = FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE;
if (fallocate(fd, punch_hole_mode, sizeof(marker), offset - sizeof(marker)))
PLOG(WARNING) << "cannot punch hole in metrics log";
return true;
}
// Seeks to the first valid sample in an incompletely-uploaded metrics log, as
// needed.
void SeekToSamples(int fd) {
char marker[sizeof(kMagicString) + sizeof(off_t)];
off_t offset;
// Special case (and, we hope, also the most common): if the beginning of the
// file does not contain a valid marker, nothing else needs to be done.
// Also, we don't need to worry about errors, as we'll hit them again shortly.
if (pread(fd, &marker, sizeof(marker), 0) != sizeof(marker) ||
memcmp(marker, kMagicString, sizeof(kMagicString)) != 0)
return;
memcpy(&offset, marker + sizeof(kMagicString), sizeof(off_t));
if (lseek(fd, offset, SEEK_SET) < 0) {
// This isn't really recoverable, but the earlier pread() did not change
// the offset, and an error will be generated when reading the first sample.
PLOG(WARNING) << "could not seek to samples at offset " << offset;
}
}
// Reads the next message from |file_descriptor| into |message|.
//
// |message| will be set to the empty string if no message could be read (EOF)
// or the message was badly constructed.
//
// Returns false if no message can be read from this file anymore (EOF or
// unrecoverable error).
bool ReadMessage(int fd, std::string* message_out, size_t* bytes_used_out) {
CHECK(message_out);
int result;
uint32_t message_size;
const size_t message_hdr_size = sizeof(message_size);
// The file containing the metrics does not leave the device, so the writer
// and the reader always have the same endianness.
result = HANDLE_EINTR(read(fd, &message_size, sizeof(message_size)));
if (result < 0) {
PLOG(ERROR) << "failed to read message header";
return false;
}
if (result == 0) {
// This indicates a normal EOF.
return false;
}
if (result < message_hdr_size) {
LOG(ERROR) << "bad read size " << result << ", expecting "
<< sizeof(message_size);
return false;
}
// kMessageMaxLength applies to the entire message: the 4-byte
// length field and the content.
if (message_size > SerializationUtils::kMessageMaxLength) {
LOG(ERROR) << "message too long, length = " << message_size;
if (HANDLE_EINTR(lseek(fd, message_size - message_hdr_size, SEEK_CUR)) ==
-1) {
PLOG(ERROR) << "error while skipping message. Aborting.";
return false;
}
// Badly formatted message was skipped. Treat the badly formatted sample as
// an empty sample.
message_out->clear();
return true;
}
if (message_size < message_hdr_size) {
LOG(ERROR) << "message too short, length = " << message_size;
return false;
}
message_size -= message_hdr_size; // The message size includes itself.
char buffer[SerializationUtils::kMessageMaxLength];
if (!base::ReadFromFD(fd, buffer, message_size)) {
LOG(ERROR) << "failed to read message body";
return false;
}
*message_out = std::string(buffer, message_size);
*bytes_used_out = message_size + message_hdr_size;
return true;
}
} // namespace
MetricSample SerializationUtils::ParseSample(const std::string& sample) {
if (sample.empty())
return MetricSample();
// Can't split at \0 anymore, so replace null chars with \n.
std::string sample_copy = sample;
std::replace(sample_copy.begin(), sample_copy.end(), '\0', '\n');
std::vector<std::string> parts = base::SplitString(
sample_copy, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
// We should have two null terminated strings so split should produce
// three chunks.
if (parts.size() != 3) {
LOG(ERROR) << "splitting message on \\0 produced " << parts.size()
<< " parts (expected 3)";
return MetricSample();
}
const std::string& name = parts[0];
const std::string& value = parts[1];
if (base::LowerCaseEqualsASCII(name, "crash")) {
return MetricSample::CrashSample(value);
} else if (base::LowerCaseEqualsASCII(name, "histogram")) {
return MetricSample::ParseHistogram(value);
} else if (base::LowerCaseEqualsASCII(name, "linearhistogram")) {
return MetricSample::ParseLinearHistogram(value);
} else if (base::LowerCaseEqualsASCII(name, "sparsehistogram")) {
return MetricSample::ParseSparseHistogram(value);
} else if (base::LowerCaseEqualsASCII(name, "useraction")) {
return MetricSample::UserActionSample(value);
} else {
LOG(ERROR) << "invalid event type: " << name << ", value: " << value;
}
return MetricSample();
}
bool SerializationUtils::ReadAndTruncateMetricsFromFile(
const std::string& filename,
std::vector<MetricSample>* metrics,
size_t sample_batch_max_length) {
struct stat stat_buf;
int result;
off_t total_length = 0;
result = stat(filename.c_str(), &stat_buf);
if (result < 0) {
if (errno != ENOENT)
PLOG(ERROR) << filename << ": bad metrics file stat";
// Nothing to collect---try later.
return true;
}
if (stat_buf.st_size == 0) {
// Also nothing to collect.
return true;
}
base::ScopedFD fd(open(filename.c_str(), O_RDWR));
if (fd.get() < 0) {
PLOG(ERROR) << filename << ": cannot open";
return true;
}
result = flock(fd.get(), LOCK_EX);
if (result < 0) {
PLOG(ERROR) << filename << ": cannot lock";
return true;
}
// Skip consecutive zeros at the beginning of the file, which may have been
// left by an earlier partial read if the file was too large. Normally there
// are none, but following long stretches of time without connectivity, there
// could be a large number. (They are optimized away by fallocate().)
SeekToSamples(fd.get());
// Try to process all messages in the log, but stop when
// kMaxMetricsBytesCount has been exceeded. If all messages are read and
// processed, or an error occurs, truncate the file to zero size. If the max
// byte count is exceeded, stop processing samples, but set up the file to
// continue at the next call. There are races on daemon crash or system
// crash: resolve them by allowing the loss of samples.
bool skip_truncation = false;
while (true) {
std::string message;
size_t bytes_used = 0;
if (!ReadMessage(fd.get(), &message, &bytes_used))
break;
MetricSample sample = ParseSample(message);
if (sample.IsValid())
metrics->push_back(std::move(sample));
total_length += bytes_used;
if (total_length > sample_batch_max_length) {
// Set up the file to continue processing. Avoid final truncation,
// unless there were errors.
skip_truncation = RemovePreviousSamples(fd.get());
break;
}
}
if (!skip_truncation) {
result = ftruncate(fd.get(), 0);
if (result < 0)
PLOG(ERROR) << "truncate metrics log";
}
result = flock(fd.get(), LOCK_UN);
if (result < 0)
PLOG(ERROR) << "unlock metrics log";
return total_length <= sample_batch_max_length;
}
bool SerializationUtils::WriteMetricsToFile(
const std::vector<MetricSample>& samples, const std::string& filename) {
std::string output;
for (const auto& sample : samples) {
if (!sample.IsValid()) {
return false;
}
std::string msg = sample.ToString();
int32_t size = msg.length() + sizeof(int32_t);
if (size > kMessageMaxLength) {
LOG(ERROR) << "cannot write message: too long, length = " << size;
return false;
}
output.append(reinterpret_cast<char*>(&size), sizeof(size));
output.append(msg);
}
base::ScopedFD file_descriptor(open(filename.c_str(),
O_WRONLY | O_APPEND | O_CREAT,
READ_WRITE_ALL_FILE_FLAGS));
if (file_descriptor.get() < 0) {
PLOG(ERROR) << filename << ": cannot open";
return false;
}
// Grab a lock to avoid chrome truncating the file underneath us. Keep the
// file locked as briefly as possible. Freeing file_descriptor will close the
// file and remove the lock.
if (HANDLE_EINTR(flock(file_descriptor.get(), LOCK_EX)) < 0) {
PLOG(ERROR) << filename << ": cannot lock";
return false;
}
if (!base::WriteFileDescriptor(file_descriptor.get(), output.c_str(),
output.size())) {
PLOG(ERROR) << "error writing output";
return false;
}
return true;
}
} // namespace metrics