blob: fa3e22c79c9088f2a4ea450be7d82bde7902e2f2 [file] [log] [blame]
// Copyright 2015 The Chromium 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 "chromecast/crash/linux/synchronized_minidump_manager.h"
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h> // perror
#include <stdlib.h>
#include <sys/file.h>
#include <sys/stat.h> // mkdir
#include <sys/types.h>
#include <time.h>
#include <fstream>
#include <memory>
#include <utility>
#include "base/base_paths.h"
#include "base/bind.h"
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/ptr_util.h"
#include "base/memory/scoped_vector.h"
#include "base/process/launch.h"
#include "base/test/scoped_path_override.h"
#include "base/threading/platform_thread.h"
#include "base/threading/thread.h"
#include "chromecast/base/scoped_temp_file.h"
#include "chromecast/crash/linux/crash_testing_utils.h"
#include "chromecast/crash/linux/dump_info.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace chromecast {
namespace {
const char kLockfileName[] = "lockfile";
const char kMetadataName[] = "metadata";
const char kMinidumpSubdir[] = "minidumps";
// A trivial implementation of SynchronizedMinidumpManager, which does no work
// to the minidump and exposes its protected members for testing. This simply
// adds an entry to the lockfile.
class SynchronizedMinidumpManagerSimple : public SynchronizedMinidumpManager {
public:
SynchronizedMinidumpManagerSimple()
: SynchronizedMinidumpManager(),
work_done_(false),
add_entry_return_code_(false),
lockfile_path_(dump_path_.Append(kLockfileName).value()) {}
~SynchronizedMinidumpManagerSimple() override {}
void SetDumpInfoToWrite(std::unique_ptr<DumpInfo> dump_info) {
dump_info_ = std::move(dump_info);
}
bool DoWorkLocked() { return AcquireLockAndDoWork(); }
// SynchronizedMinidumpManager implementation:
bool DoWork() override {
if (dump_info_)
add_entry_return_code_ = AddEntryToLockFile(*dump_info_);
work_done_ = true;
return true;
}
// Accessors for testing.
bool HasDumps() { return SynchronizedMinidumpManager::HasDumps(); }
const std::string& dump_path() { return dump_path_.value(); }
const std::string& lockfile_path() { return lockfile_path_; }
bool work_done() { return work_done_; }
bool add_entry_return_code() { return add_entry_return_code_; }
private:
bool work_done_;
bool add_entry_return_code_;
std::string lockfile_path_;
std::unique_ptr<DumpInfo> dump_info_;
};
void DoWorkLockedTask(SynchronizedMinidumpManagerSimple* manager) {
manager->DoWorkLocked();
}
// Simple SynchronizedMinidumpManager consumer. Checks if a dump can be uploaded
// then removes it from the lockfile.
class FakeSynchronizedMinidumpUploader : public SynchronizedMinidumpManager {
public:
FakeSynchronizedMinidumpUploader()
: SynchronizedMinidumpManager(), can_upload_return_val_(false) {}
~FakeSynchronizedMinidumpUploader() override {}
bool DoWorkLocked() { return AcquireLockAndDoWork(); }
// SynchronizedMinidumpManager implementation:
bool DoWork() override {
can_upload_return_val_ = CanUploadDump();
if (!RemoveEntryFromLockFile(0))
return false;
if (!IncrementNumDumpsInCurrentPeriod())
return false;
return true;
}
// Accessors for testing.
bool HasDumps() { return SynchronizedMinidumpManager::HasDumps(); }
bool can_upload_return_val() { return can_upload_return_val_; }
private:
bool can_upload_return_val_;
};
class SleepySynchronizedMinidumpManagerSimple
: public SynchronizedMinidumpManagerSimple {
public:
SleepySynchronizedMinidumpManagerSimple(int sleep_duration_ms)
: SynchronizedMinidumpManagerSimple(),
sleep_duration_ms_(sleep_duration_ms) {}
~SleepySynchronizedMinidumpManagerSimple() override {}
// SynchronizedMinidumpManager implementation:
bool DoWork() override {
// The lock has been acquired. Fall asleep for |kSleepDurationMs|, then
// write the file.
base::PlatformThread::Sleep(
base::TimeDelta::FromMilliseconds(sleep_duration_ms_));
return SynchronizedMinidumpManagerSimple::DoWork();
}
private:
const int sleep_duration_ms_;
};
class SynchronizedMinidumpManagerTest : public testing::Test {
public:
SynchronizedMinidumpManagerTest() {}
~SynchronizedMinidumpManagerTest() override {}
void SetUp() override {
// Set up a temporary directory which will be used as our fake home dir.
ASSERT_TRUE(fake_home_dir_.CreateUniqueTempDir());
path_override_.reset(
new base::ScopedPathOverride(base::DIR_HOME, fake_home_dir_.path()));
minidump_dir_ = fake_home_dir_.path().Append(kMinidumpSubdir);
lockfile_ = minidump_dir_.Append(kLockfileName);
metadata_ = minidump_dir_.Append(kMetadataName);
// Create a minidump directory.
ASSERT_TRUE(base::CreateDirectory(minidump_dir_));
ASSERT_TRUE(base::IsDirectoryEmpty(minidump_dir_));
// Create a lockfile in that directory.
base::File lockfile(
lockfile_, base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
ASSERT_TRUE(lockfile.IsValid());
}
protected:
base::FilePath minidump_dir_; // Path the the minidump directory.
base::FilePath lockfile_; // Path to the lockfile in |minidump_dir_|.
base::FilePath metadata_; // Path to the metadata in |minidump_dir_|.
private:
base::ScopedTempDir fake_home_dir_;
std::unique_ptr<base::ScopedPathOverride> path_override_;
};
// Have |producer| generate |num_dumps| while checking there are no errors.
void produce_dumps(SynchronizedMinidumpManagerSimple& producer, int num_dumps) {
for (int i = 0; i < num_dumps; ++i) {
ASSERT_TRUE(producer.DoWorkLocked());
ASSERT_TRUE(producer.add_entry_return_code());
}
}
// Have |consumer| remove and process |num_dumps| while checking there are no
// errors.
void consume_dumps(FakeSynchronizedMinidumpUploader& consumer, int num_dumps) {
for (int i = 0; i < num_dumps; ++i) {
ASSERT_TRUE(consumer.DoWorkLocked());
ASSERT_TRUE(consumer.can_upload_return_val());
}
}
} // namespace
TEST_F(SynchronizedMinidumpManagerTest, FilePathsAreCorrect) {
SynchronizedMinidumpManagerSimple manager;
// Verify file paths for directory and lock file.
ASSERT_EQ(minidump_dir_.value(), manager.dump_path());
ASSERT_EQ(lockfile_.value(), manager.lockfile_path());
}
TEST_F(SynchronizedMinidumpManagerTest, AcquireLockOnNonExistentDirectory) {
// The directory was created in SetUp(). Delete it and its contents.
ASSERT_TRUE(base::DeleteFile(minidump_dir_, true));
ASSERT_FALSE(base::PathExists(minidump_dir_));
SynchronizedMinidumpManagerSimple manager;
ASSERT_TRUE(manager.DoWorkLocked());
ASSERT_TRUE(manager.work_done());
// Verify the directory and the lockfile both exist.
ASSERT_TRUE(base::DirectoryExists(minidump_dir_));
ASSERT_TRUE(base::PathExists(lockfile_));
}
TEST_F(SynchronizedMinidumpManagerTest, AcquireLockOnExistingEmptyDirectory) {
// The lockfile was created in SetUp(). Delete it.
ASSERT_TRUE(base::DeleteFile(lockfile_, false));
ASSERT_FALSE(base::PathExists(lockfile_));
SynchronizedMinidumpManagerSimple manager;
ASSERT_TRUE(manager.DoWorkLocked());
ASSERT_TRUE(manager.work_done());
// Verify the directory and the lockfile both exist.
ASSERT_TRUE(base::DirectoryExists(minidump_dir_));
ASSERT_TRUE(base::PathExists(lockfile_));
}
TEST_F(SynchronizedMinidumpManagerTest,
AcquireLockOnExistingDirectoryWithLockfile) {
SynchronizedMinidumpManagerSimple manager;
ASSERT_TRUE(manager.DoWorkLocked());
ASSERT_TRUE(manager.work_done());
// Verify the directory and the lockfile both exist.
ASSERT_TRUE(base::DirectoryExists(minidump_dir_));
ASSERT_TRUE(base::PathExists(lockfile_));
}
TEST_F(SynchronizedMinidumpManagerTest,
AddEntryToLockFile_FailsWithInvalidEntry) {
// Create invalid dump info value
base::DictionaryValue val;
// Test that the manager tried to log the entry and failed.
SynchronizedMinidumpManagerSimple manager;
manager.SetDumpInfoToWrite(base::WrapUnique(new DumpInfo(&val)));
ASSERT_TRUE(manager.DoWorkLocked());
ASSERT_FALSE(manager.add_entry_return_code());
// Verify the lockfile is untouched.
ScopedVector<DumpInfo> dumps;
ASSERT_TRUE(FetchDumps(lockfile_.value(), &dumps));
ASSERT_EQ(0u, dumps.size());
}
TEST_F(SynchronizedMinidumpManagerTest,
AddEntryToLockFile_SucceedsWithValidEntries) {
// Sample parameters.
base::Time now = base::Time::Now();
MinidumpParams params;
params.process_name = "process";
// Write the first entry.
SynchronizedMinidumpManagerSimple manager;
manager.SetDumpInfoToWrite(
base::WrapUnique(new DumpInfo("dump1", "log1", now, params)));
ASSERT_TRUE(manager.DoWorkLocked());
ASSERT_TRUE(manager.add_entry_return_code());
// Test that the manager was successful in logging the entry.
ScopedVector<DumpInfo> dumps;
ASSERT_TRUE(FetchDumps(lockfile_.value(), &dumps));
ASSERT_EQ(1u, dumps.size());
// Write the second entry.
manager.SetDumpInfoToWrite(
base::WrapUnique(new DumpInfo("dump2", "log2", now, params)));
ASSERT_TRUE(manager.DoWorkLocked());
ASSERT_TRUE(manager.add_entry_return_code());
// Test that the second entry is also valid.
ASSERT_TRUE(FetchDumps(lockfile_.value(), &dumps));
ASSERT_EQ(2u, dumps.size());
}
TEST_F(SynchronizedMinidumpManagerTest, AcquireLockFile_WaitsForOtherThread) {
// Create some parameters for a minidump.
base::Time now = base::Time::Now();
MinidumpParams params;
params.process_name = "process";
// Create a manager that grabs the lock then sleeps. Post a DoWork task to
// another thread. |sleepy_manager| will grab the lock and hold it for
// |sleep_time_ms|. It will then write a dump and release the lock.
const int sleep_time_ms = 100;
SleepySynchronizedMinidumpManagerSimple sleepy_manager(sleep_time_ms);
sleepy_manager.SetDumpInfoToWrite(
base::WrapUnique(new DumpInfo("dump", "log", now, params)));
base::Thread sleepy_thread("sleepy");
sleepy_thread.Start();
sleepy_thread.task_runner()->PostTask(
FROM_HERE,
base::Bind(&DoWorkLockedTask, base::Unretained(&sleepy_manager)));
// Meanwhile, this thread should wait brielfy to allow the other thread to
// grab the lock.
const int concurrency_delay = 50;
base::PlatformThread::Sleep(
base::TimeDelta::FromMilliseconds(concurrency_delay));
// |sleepy_manager| has the lock by now, but has not released it. Attempt to
// grab it. DoWorkLocked() should block until |manager| has a chance to write
// the dump.
SynchronizedMinidumpManagerSimple manager;
manager.SetDumpInfoToWrite(
base::WrapUnique(new DumpInfo("dump", "log", now, params)));
EXPECT_TRUE(manager.DoWorkLocked());
EXPECT_TRUE(manager.add_entry_return_code());
EXPECT_TRUE(manager.work_done());
// Check that the other manager was also successful.
EXPECT_TRUE(sleepy_manager.add_entry_return_code());
EXPECT_TRUE(sleepy_manager.work_done());
// Test that both entries were logged.
ScopedVector<DumpInfo> dumps;
ASSERT_TRUE(FetchDumps(lockfile_.value(), &dumps));
EXPECT_EQ(2u, dumps.size());
}
// TODO(slan): These tests are passing but forking them is creating duplicates
// of all tests in this thread. Figure out how to lock the file more cleanly
// from another process.
TEST_F(SynchronizedMinidumpManagerTest,
DISABLED_AcquireLockFile_WaitsForOtherProcess) {
// Create some parameters for a minidump.
base::Time now = base::Time::Now();
MinidumpParams params;
params.process_name = "process";
// Fork the process.
pid_t pid = base::ForkWithFlags(0u, nullptr, nullptr);
if (pid != 0) {
// The child process should instantiate a manager which immediately grabs
// the lock, and falls aleep for some period of time, then writes a dump,
// and finally releases the lock.
SleepySynchronizedMinidumpManagerSimple sleepy_manager(100);
sleepy_manager.SetDumpInfoToWrite(
base::WrapUnique(new DumpInfo("dump", "log", now, params)));
ASSERT_TRUE(sleepy_manager.DoWorkLocked());
ASSERT_TRUE(sleepy_manager.work_done());
return;
}
// Meanwhile, this process should wait brielfy to allow the other thread to
// grab the lock.
const int concurrency_delay = 50;
base::PlatformThread::Sleep(
base::TimeDelta::FromMilliseconds(concurrency_delay));
// |sleepy_manager| has the lock by now, but has not released it. Attempt to
// grab it. DoWorkLocked() should block until |manager| has a chance to write
// the dump.
SynchronizedMinidumpManagerSimple manager;
manager.SetDumpInfoToWrite(
base::WrapUnique(new DumpInfo("dump", "log", now, params)));
EXPECT_TRUE(manager.DoWorkLocked());
EXPECT_TRUE(manager.add_entry_return_code());
EXPECT_TRUE(manager.work_done());
// Test that both entries were logged.
ScopedVector<DumpInfo> dumps;
ASSERT_TRUE(FetchDumps(lockfile_.value(), &dumps));
EXPECT_EQ(2u, dumps.size());
}
TEST_F(SynchronizedMinidumpManagerTest,
Upload_SucceedsWhenDumpLimitsNotExceeded) {
// Sample parameters.
base::Time now = base::Time::Now();
MinidumpParams params;
params.process_name = "process";
FakeSynchronizedMinidumpUploader uploader;
SynchronizedMinidumpManagerSimple producer;
producer.SetDumpInfoToWrite(
base::WrapUnique(new DumpInfo("dump1", "log1", now, params)));
const int max_dumps = SynchronizedMinidumpManager::kRatelimitPeriodMaxDumps;
produce_dumps(producer, max_dumps);
consume_dumps(uploader, max_dumps);
}
TEST_F(SynchronizedMinidumpManagerTest, Upload_FailsWhenTooManyRecentDumps) {
// Sample parameters.
base::Time now = base::Time::Now();
MinidumpParams params;
params.process_name = "process";
FakeSynchronizedMinidumpUploader uploader;
SynchronizedMinidumpManagerSimple producer;
producer.SetDumpInfoToWrite(
base::WrapUnique(new DumpInfo("dump1", "log1", now, params)));
const int max_dumps = SynchronizedMinidumpManager::kRatelimitPeriodMaxDumps;
produce_dumps(producer, max_dumps + 1);
consume_dumps(uploader, max_dumps);
// Should fail with too many dumps
ASSERT_TRUE(uploader.DoWorkLocked());
ASSERT_FALSE(uploader.can_upload_return_val());
}
TEST_F(SynchronizedMinidumpManagerTest, UploadSucceedsAfterRateLimitPeriodEnd) {
// Sample parameters.
base::Time now = base::Time::Now();
MinidumpParams params;
params.process_name = "process";
FakeSynchronizedMinidumpUploader uploader;
SynchronizedMinidumpManagerSimple producer;
producer.SetDumpInfoToWrite(
base::WrapUnique(new DumpInfo("dump1", "log1", now, params)));
const int iters = 3;
const int max_dumps = SynchronizedMinidumpManager::kRatelimitPeriodMaxDumps;
for (int i = 0; i < iters; ++i) {
produce_dumps(producer, max_dumps + 1);
consume_dumps(uploader, max_dumps);
// Should fail with too many dumps
ASSERT_TRUE(uploader.DoWorkLocked());
ASSERT_FALSE(uploader.can_upload_return_val());
base::TimeDelta period = base::TimeDelta::FromSeconds(
SynchronizedMinidumpManager::kRatelimitPeriodSeconds);
base::Time now = base::Time::Now();
// Half period shouldn't trigger reset
produce_dumps(producer, 1);
SetRatelimitPeriodStart(metadata_.value(), now - period / 2);
ASSERT_TRUE(uploader.DoWorkLocked());
ASSERT_FALSE(uploader.can_upload_return_val());
// Set period starting time to trigger a reset
SetRatelimitPeriodStart(metadata_.value(), now - period);
}
produce_dumps(producer, 1);
consume_dumps(uploader, 1);
}
TEST_F(SynchronizedMinidumpManagerTest, HasDumpsWithoutDumps) {
FakeSynchronizedMinidumpUploader uploader;
ASSERT_FALSE(uploader.HasDumps());
}
TEST_F(SynchronizedMinidumpManagerTest, HasDumpsWithDumps) {
// Sample parameters.
base::Time now = base::Time::Now();
MinidumpParams params;
params.process_name = "process";
SynchronizedMinidumpManagerSimple producer;
FakeSynchronizedMinidumpUploader uploader;
producer.SetDumpInfoToWrite(
base::WrapUnique(new DumpInfo("dump1", "log1", now, params)));
const int kNumDumps = 3;
for (int i = 0; i < kNumDumps; ++i) {
produce_dumps(producer, 1);
ASSERT_TRUE(uploader.HasDumps());
}
for (int i = 0; i < kNumDumps; ++i) {
ASSERT_TRUE(uploader.HasDumps());
consume_dumps(uploader, 1);
}
ASSERT_FALSE(uploader.HasDumps());
}
TEST_F(SynchronizedMinidumpManagerTest, HasDumpsNotInLockFile) {
SynchronizedMinidumpManagerSimple manager;
ASSERT_FALSE(manager.HasDumps());
// Create file in dump path.
const base::FilePath path =
base::FilePath(manager.dump_path()).Append("hello123");
const char kFileContents[] = "foobar";
ASSERT_EQ(static_cast<int>(sizeof(kFileContents)),
WriteFile(path, kFileContents, sizeof(kFileContents)));
ASSERT_TRUE(manager.HasDumps());
}
} // namespace chromecast