blob: e158b7d06a2ec483d7ae308469aeba30daa916e0 [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 "base/command_line.h"
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/macros.h"
#include "base/test/multiprocess_test.h"
#include "base/test/test_timeouts.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"
using base::File;
using base::FilePath;
namespace {
// Flag for the parent to share a temp dir to the child.
const char kTempDirFlag[] = "temp-dir";
// Flags to control how the subprocess unlocks the file.
const char kFileUnlock[] = "file-unlock";
const char kCloseUnlock[] = "close-unlock";
const char kExitUnlock[] = "exit-unlock";
// File to lock in temp dir.
const char kLockFile[] = "lockfile";
// Constants for various requests and responses, used as |signal_file| parameter
// to signal/wait helpers.
const char kSignalLockFileLocked[] = "locked.signal";
const char kSignalLockFileClose[] = "close.signal";
const char kSignalLockFileClosed[] = "closed.signal";
const char kSignalLockFileUnlock[] = "unlock.signal";
const char kSignalLockFileUnlocked[] = "unlocked.signal";
const char kSignalExit[] = "exit.signal";
// Signal an event by creating a file which didn't previously exist.
bool SignalEvent(const FilePath& signal_dir, const char* signal_file) {
File file(signal_dir.AppendASCII(signal_file),
File::FLAG_CREATE | File::FLAG_WRITE);
return file.IsValid();
}
// Check whether an event was signaled.
bool CheckEvent(const FilePath& signal_dir, const char* signal_file) {
File file(signal_dir.AppendASCII(signal_file),
File::FLAG_OPEN | File::FLAG_READ);
return file.IsValid();
}
// Busy-wait for an event to be signaled, returning false for timeout.
bool WaitForEventWithTimeout(const FilePath& signal_dir,
const char* signal_file,
const base::TimeDelta& timeout) {
const base::Time finish_by = base::Time::Now() + timeout;
while (!CheckEvent(signal_dir, signal_file)) {
if (base::Time::Now() > finish_by)
return false;
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(10));
}
return true;
}
// Wait forever for the event to be signaled (should never return false).
bool WaitForEvent(const FilePath& signal_dir, const char* signal_file) {
return WaitForEventWithTimeout(signal_dir, signal_file,
base::TimeDelta::Max());
}
// Keep these in sync so StartChild*() can refer to correct test main.
#define ChildMain ChildLockUnlock
#define ChildMainString "ChildLockUnlock"
// Subprocess to test getting a file lock then releasing it. |kTempDirFlag|
// must pass in an existing temporary directory for the lockfile and signal
// files. One of the following flags must be passed to determine how to unlock
// the lock file:
// - |kFileUnlock| calls Unlock() to unlock.
// - |kCloseUnlock| calls Close() while the lock is held.
// - |kExitUnlock| exits while the lock is held.
MULTIPROCESS_TEST_MAIN(ChildMain) {
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
const FilePath temp_path = command_line->GetSwitchValuePath(kTempDirFlag);
CHECK(base::DirectoryExists(temp_path));
// Immediately lock the file.
File file(temp_path.AppendASCII(kLockFile),
File::FLAG_OPEN | File::FLAG_READ | File::FLAG_WRITE);
CHECK(file.IsValid());
CHECK_EQ(File::FILE_OK, file.Lock());
CHECK(SignalEvent(temp_path, kSignalLockFileLocked));
if (command_line->HasSwitch(kFileUnlock)) {
// Wait for signal to unlock, then unlock the file.
CHECK(WaitForEvent(temp_path, kSignalLockFileUnlock));
CHECK_EQ(File::FILE_OK, file.Unlock());
CHECK(SignalEvent(temp_path, kSignalLockFileUnlocked));
} else if (command_line->HasSwitch(kCloseUnlock)) {
// Wait for the signal to close, then close the file.
CHECK(WaitForEvent(temp_path, kSignalLockFileClose));
file.Close();
CHECK(!file.IsValid());
CHECK(SignalEvent(temp_path, kSignalLockFileClosed));
} else {
CHECK(command_line->HasSwitch(kExitUnlock));
}
// Wait for signal to exit, so that unlock or close can be distinguished from
// exit.
CHECK(WaitForEvent(temp_path, kSignalExit));
return 0;
}
} // namespace
class FileLockingTest : public testing::Test {
public:
FileLockingTest() = default;
protected:
void SetUp() override {
testing::Test::SetUp();
// Setup the temp dir and the lock file.
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
lock_file_.Initialize(
temp_dir_.GetPath().AppendASCII(kLockFile),
File::FLAG_CREATE | File::FLAG_READ | File::FLAG_WRITE);
ASSERT_TRUE(lock_file_.IsValid());
}
bool SignalEvent(const char* signal_file) {
return ::SignalEvent(temp_dir_.GetPath(), signal_file);
}
bool WaitForEventOrTimeout(const char* signal_file) {
return ::WaitForEventWithTimeout(temp_dir_.GetPath(), signal_file,
TestTimeouts::action_timeout());
}
// Start a child process set to use the specified unlock action, and wait for
// it to lock the file.
void StartChildAndSignalLock(const char* unlock_action) {
// Create a temporary dir and spin up a ChildLockExit subprocess against it.
const FilePath temp_path = temp_dir_.GetPath();
base::CommandLine child_command_line(
base::GetMultiProcessTestChildBaseCommandLine());
child_command_line.AppendSwitchPath(kTempDirFlag, temp_path);
child_command_line.AppendSwitch(unlock_action);
lock_child_ = base::SpawnMultiProcessTestChild(
ChildMainString, child_command_line, base::LaunchOptions());
ASSERT_TRUE(lock_child_.IsValid());
// Wait for the child to lock the file.
ASSERT_TRUE(WaitForEventOrTimeout(kSignalLockFileLocked));
}
// Signal the child to exit cleanly.
void ExitChildCleanly() {
ASSERT_TRUE(SignalEvent(kSignalExit));
int rv = -1;
ASSERT_TRUE(WaitForMultiprocessTestChildExit(
lock_child_, TestTimeouts::action_timeout(), &rv));
ASSERT_EQ(0, rv);
}
base::ScopedTempDir temp_dir_;
base::File lock_file_;
base::Process lock_child_;
private:
DISALLOW_COPY_AND_ASSIGN(FileLockingTest);
};
// Test that locks are released by Unlock().
TEST_F(FileLockingTest, LockAndUnlock) {
StartChildAndSignalLock(kFileUnlock);
ASSERT_NE(File::FILE_OK, lock_file_.Lock());
ASSERT_TRUE(SignalEvent(kSignalLockFileUnlock));
ASSERT_TRUE(WaitForEventOrTimeout(kSignalLockFileUnlocked));
ASSERT_EQ(File::FILE_OK, lock_file_.Lock());
ASSERT_EQ(File::FILE_OK, lock_file_.Unlock());
ExitChildCleanly();
}
// Test that locks are released on Close().
TEST_F(FileLockingTest, UnlockOnClose) {
StartChildAndSignalLock(kCloseUnlock);
ASSERT_NE(File::FILE_OK, lock_file_.Lock());
ASSERT_TRUE(SignalEvent(kSignalLockFileClose));
ASSERT_TRUE(WaitForEventOrTimeout(kSignalLockFileClosed));
ASSERT_EQ(File::FILE_OK, lock_file_.Lock());
ASSERT_EQ(File::FILE_OK, lock_file_.Unlock());
ExitChildCleanly();
}
// Test that locks are released on exit.
TEST_F(FileLockingTest, UnlockOnExit) {
StartChildAndSignalLock(kExitUnlock);
ASSERT_NE(File::FILE_OK, lock_file_.Lock());
ExitChildCleanly();
ASSERT_EQ(File::FILE_OK, lock_file_.Lock());
ASSERT_EQ(File::FILE_OK, lock_file_.Unlock());
}
// Test that killing the process releases the lock. This should cover crashing.
// Flaky on Android (http://crbug.com/747518)
#if defined(OS_ANDROID)
#define MAYBE_UnlockOnTerminate DISABLED_UnlockOnTerminate
#else
#define MAYBE_UnlockOnTerminate UnlockOnTerminate
#endif
TEST_F(FileLockingTest, MAYBE_UnlockOnTerminate) {
// The child will wait for an exit which never arrives.
StartChildAndSignalLock(kExitUnlock);
ASSERT_NE(File::FILE_OK, lock_file_.Lock());
ASSERT_TRUE(TerminateMultiProcessTestChild(lock_child_, 0, true));
ASSERT_EQ(File::FILE_OK, lock_file_.Lock());
ASSERT_EQ(File::FILE_OK, lock_file_.Unlock());
}