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