| // 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()); |
| } |