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