blob: f0b540130185c4dc61ee9142b8b5b4c3c8a4ffb3 [file] [log] [blame]
// Copyright 2016 The Android Open Source Project
//
// This software is licensed under the terms of the GNU General Public
// License version 2, as published by the Free Software Foundation, and
// may be copied, distributed, and modified under those terms.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
#include "android/metrics/FileMetricsWriter.h"
#include "android/base/files/PathUtils.h"
#include "android/base/Log.h"
#include "android/base/StringFormat.h"
#include "android/base/Uuid.h"
#include "android/metrics/MetricsLogging.h"
#include "android/protobuf/DelimitedSerialization.h"
#include "android/utils/eintr_wrapper.h"
#include "android/utils/path.h"
#include "android/metrics/proto/clientanalytics.pb.h"
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using android::base::AutoLock;
using android::base::Looper;
using android::base::makeCustomScopedPtr;
using android::base::PathUtils;
using android::base::System;
using android::base::StringView;
using android::base::StringFormat;
using android::base::Uuid;
namespace android {
namespace metrics {
// Only Windows has a No-inherit open() flag, and only Windows cares about
// difference between binary/text files.
#ifdef _WIN32
static constexpr int kCommonOpenFlags = O_BINARY | O_NOINHERIT;
#else
static constexpr int kCommonOpenFlags = 0;
#endif
// Extensions for an active temporary logging file and for the finalized one.
static constexpr StringView kTempExtension = ".open";
static constexpr StringView kFinalExtension = ".trx";
// A limit for a counter appended to the log filename to enable multiple files
// with the same session ID.
static constexpr int kMaxFilenameCounter = 1000000;
// Number of retries for a failed file operation, e.g. open(), rename(), etc.
static constexpr int kMaxFileOperationTries = 1000;
// Create a log filename with the passed |sessionId|, |counter| and |extension|.
static std::string formatFilename(StringView sessionId,
int counter,
StringView extension) {
return StringFormat("emulator-metrics-%s-%d-%d%s", sessionId,
(int)System::get()->getCurrentProcessId(), counter,
extension);
}
// A cross-platform function to rename an |from| file to |to| name only if |to|
// doesn't exist.
// On POSIX one needs to make sure the |to| doesn't exist, otherwise it will be
// owerwritten. On Windows rename() never overwrites target.
static bool renameIfNotExists(StringView from, StringView to) {
#ifdef _WIN32
return rename(from.c_str(), to.c_str()) == 0;
#else
if (System::get()->pathExists(to)) {
return false;
}
// There's a small chance of race condition here, but we don't actually
// rename files in several threads, so it's OK.
// TODO(zyy): use renameat2() on Linux to make it atomic.
if (HANDLE_EINTR(rename(from.c_str(), to.c_str())) != 0) {
return false;
}
return true;
#endif
}
FileMetricsWriter::FileMetricsWriter(StringView spoolDir,
const std::string& sessionId,
int recordCountLimit,
Looper* looper,
System::Duration timeLimitMs)
: MetricsWriter(sessionId),
mSpoolDir(spoolDir),
mTimeLimitMs(timeLimitMs),
mRecordCountLimit(recordCountLimit),
mLooper(looper),
mActiveFileLock(
makeCustomScopedPtr<FileLock*>(nullptr, filelock_release)) {
D("created a FileMetricsWriter");
assert(strlen(spoolDir.data()) == spoolDir.size());
path_mkdir_if_needed(spoolDir.data(), 0744);
assert(System::get()->pathIsDir(spoolDir));
// It's OK to call this without a lock in ctor - there's no external
// references to the object yet.
openNewFileNoLock();
}
FileMetricsWriter::Ptr FileMetricsWriter::create(StringView spoolDir,
const std::string& sessionId,
int recordCountLimit,
Looper* looper,
System::Duration timeLimitMs) {
return Ptr(new FileMetricsWriter(spoolDir, sessionId, recordCountLimit,
looper, timeLimitMs));
}
MetricsWriter::AbandonedSessions
FileMetricsWriter::finalizeAbandonedSessionFiles(StringView spoolDir) {
AbandonedSessions abandonedSessions;
const std::vector<std::string> files =
System::get()->scanDirEntries(spoolDir);
for (const std::string& file : files) {
if (PathUtils::extension(file) != kTempExtension) {
continue;
}
// try to lock the file to find out if it's abandoned
const auto lock = makeCustomScopedPtr(
filelock_create(PathUtils::join(spoolDir, file).c_str()),
filelock_release);
if (!lock) {
continue;
}
// parse the file name to get session ID and the filename counter
int filenameCounter = 0;
std::string sessionId = Uuid::nullUuidStr;
assert(sessionId.size() >= 36);
int scanned = sscanf(file.c_str(), "emulator-metrics-%36s-%*d-%d",
&sessionId[0], &filenameCounter);
assert(scanned == 2);
(void)scanned;
// now rename it
if (!runFileOperationWithRetries(&filenameCounter, [spoolDir,
&sessionId,
&filenameCounter,
&file]() -> bool {
const std::string finalName = PathUtils::join(
spoolDir, formatFilename(sessionId, filenameCounter,
kFinalExtension));
return renameIfNotExists(PathUtils::join(spoolDir, file),
finalName);
})) {
W("failed to rename an abandoned log file '%s'", file.c_str());
}
abandonedSessions.insert(std::move(sessionId));
}
return abandonedSessions;
}
FileMetricsWriter::~FileMetricsWriter() {
AutoLock lock(mLock);
if (mTimer) {
mTimer->stop();
mTimer.reset();
}
finalizeActiveFileNoLock();
}
void FileMetricsWriter::finalizeActiveFileNoLock() {
if (!mActiveFile) {
return;
}
mActiveFile.reset();
if (!runFileOperationWithRetries(&mFilenameCounter, [this]() -> bool {
const std::string finalName = PathUtils::join(
mSpoolDir, formatFilename(sessionId(), mFilenameCounter,
kFinalExtension));
return renameIfNotExists(mActiveFileName, finalName);
})) {
W("failed to rename an active log file '%s'", mActiveFileName.c_str());
} else {
D("finalized active log file '%s'", mActiveFileName.c_str());
}
mActiveFileLock.reset();
mActiveFileName.clear();
mRecordCount = 0;
// We've done with the current file no, make sure the next one gets a
// new counter value.
advanceFilenameCounter(&mFilenameCounter);
}
void FileMetricsWriter::openNewFileNoLock() {
std::string newName;
int fd = -1;
if (!runFileOperationWithRetries(&mFilenameCounter, [this, &newName,
&fd]() -> bool {
std::string testName = PathUtils::join(
mSpoolDir, formatFilename(sessionId(), mFilenameCounter,
kTempExtension));
mActiveFileLock.reset(filelock_create(testName.c_str()));
if (!mActiveFileLock) {
return false;
}
const int testFd = HANDLE_EINTR(
open(testName.c_str(),
O_WRONLY | O_CREAT | O_EXCL | kCommonOpenFlags,
S_IREAD | S_IWRITE));
if (testFd < 0) {
mActiveFileLock.reset();
return false;
}
newName = std::move(testName);
fd = testFd;
return true;
})) {
W("failed to open a new log file");
return;
}
assert(fd >= 0);
assert(!newName.empty());
mActiveFile.reset(new google::protobuf::io::FileOutputStream(fd));
if (!mActiveFile) {
E("memory allocation failed");
close(fd);
HANDLE_EINTR(unlink(newName.c_str()));
return;
}
mActiveFile->SetCloseOnDelete(true);
mActiveFileName = std::move(newName);
mRecordCount = 0;
D("opened new metrics file %s", mActiveFileName.c_str());
}
bool FileMetricsWriter::countLimitReached() const {
// Only check the count limit if there is one.
return mRecordCountLimit > 0 && mRecordCount >= mRecordCountLimit;
}
void FileMetricsWriter::advanceFilenameCounter(int* counter) {
*counter = (*counter + 1) % kMaxFilenameCounter;
}
void FileMetricsWriter::ensureTimerStarted() {
if (mTimer || mTimeLimitMs <= 0 || !mLooper) {
return;
}
mTimer.reset(mLooper->createTimer(
[](void* weakSelf, Looper::Timer* timer) {
auto weakPtr =
static_cast<FileMetricsWriter::WPtr*>(weakSelf);
if (auto ptr = weakPtr->lock()) {
ptr->onTimer();
} else {
delete weakPtr; // we're done
}
},
new WPtr(shared_from_this())));
if (mTimer) {
mTimer->startRelative(mTimeLimitMs);
D("started metrics timer with a period of %d ms",
mTimeLimitMs);
}
}
void FileMetricsWriter::onTimer() {
D("timer tick");
AutoLock lock(mLock);
if (mRecordCount > 0) {
finalizeActiveFileNoLock();
openNewFileNoLock();
}
mTimer->startRelative(mTimeLimitMs);
}
template <class Operation>
bool FileMetricsWriter::runFileOperationWithRetries(int* filenameCounter,
Operation op) {
for (int i = 0; i < kMaxFileOperationTries;
++i, advanceFilenameCounter(filenameCounter)) {
if (op()) {
return true;
}
}
return false;
}
void FileMetricsWriter::write(
const wireless_android_play_playlog::LogEvent& event) {
D("writing a log event with uptime %ld ms", event.event_uptime_ms());
AutoLock lock(mLock);
if (!mActiveFile) {
openNewFileNoLock();
}
if (!mActiveFile) {
W("No active log file during write(), event lost:\n%s",
event.DebugString().c_str());
return;
}
ensureTimerStarted();
protobuf::writeOneDelimited(event, mActiveFile.get());
// FileOutputStream buffers everything it can, but it means we may lose
// a message on a crash, so let's just flush it every time.
// Shouldn't affect performance as it's expected that only asynchronous
// metrics reporter is used in the emulator process.
mActiveFile->Flush();
++mRecordCount;
if (countLimitReached()) {
finalizeActiveFileNoLock();
openNewFileNoLock();
}
}
} // namespace metrics
} // namespace android