1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "base/command_line.h"
6 #include "base/files/file.h"
7 #include "base/files/file_util.h"
8 #include "base/files/scoped_temp_dir.h"
9 #include "base/macros.h"
10 #include "base/test/multiprocess_test.h"
11 #include "base/test/test_timeouts.h"
12 #include "base/threading/platform_thread.h"
13 #include "base/time/time.h"
14 #include "build/build_config.h"
15 #include "testing/gtest/include/gtest/gtest.h"
16 #include "testing/multiprocess_func_list.h"
17 
18 using base::File;
19 using base::FilePath;
20 
21 namespace {
22 
23 // Flag for the parent to share a temp dir to the child.
24 const char kTempDirFlag[] = "temp-dir";
25 
26 // Flags to control how the subprocess unlocks the file.
27 const char kFileUnlock[] = "file-unlock";
28 const char kCloseUnlock[] = "close-unlock";
29 const char kExitUnlock[] = "exit-unlock";
30 
31 // File to lock in temp dir.
32 const char kLockFile[] = "lockfile";
33 
34 // Constants for various requests and responses, used as |signal_file| parameter
35 // to signal/wait helpers.
36 const char kSignalLockFileLocked[] = "locked.signal";
37 const char kSignalLockFileClose[] = "close.signal";
38 const char kSignalLockFileClosed[] = "closed.signal";
39 const char kSignalLockFileUnlock[] = "unlock.signal";
40 const char kSignalLockFileUnlocked[] = "unlocked.signal";
41 const char kSignalExit[] = "exit.signal";
42 
43 // Signal an event by creating a file which didn't previously exist.
SignalEvent(const FilePath & signal_dir,const char * signal_file)44 bool SignalEvent(const FilePath& signal_dir, const char* signal_file) {
45   File file(signal_dir.AppendASCII(signal_file),
46             File::FLAG_CREATE | File::FLAG_WRITE);
47   return file.IsValid();
48 }
49 
50 // Check whether an event was signaled.
CheckEvent(const FilePath & signal_dir,const char * signal_file)51 bool CheckEvent(const FilePath& signal_dir, const char* signal_file) {
52   File file(signal_dir.AppendASCII(signal_file),
53             File::FLAG_OPEN | File::FLAG_READ);
54   return file.IsValid();
55 }
56 
57 // Busy-wait for an event to be signaled, returning false for timeout.
WaitForEventWithTimeout(const FilePath & signal_dir,const char * signal_file,const base::TimeDelta & timeout)58 bool WaitForEventWithTimeout(const FilePath& signal_dir,
59                              const char* signal_file,
60                              const base::TimeDelta& timeout) {
61   const base::Time finish_by = base::Time::Now() + timeout;
62   while (!CheckEvent(signal_dir, signal_file)) {
63     if (base::Time::Now() > finish_by)
64       return false;
65     base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(10));
66   }
67   return true;
68 }
69 
70 // Wait forever for the event to be signaled (should never return false).
WaitForEvent(const FilePath & signal_dir,const char * signal_file)71 bool WaitForEvent(const FilePath& signal_dir, const char* signal_file) {
72   return WaitForEventWithTimeout(signal_dir, signal_file,
73                                  base::TimeDelta::Max());
74 }
75 
76 // Keep these in sync so StartChild*() can refer to correct test main.
77 #define ChildMain ChildLockUnlock
78 #define ChildMainString "ChildLockUnlock"
79 
80 // Subprocess to test getting a file lock then releasing it.  |kTempDirFlag|
81 // must pass in an existing temporary directory for the lockfile and signal
82 // files.  One of the following flags must be passed to determine how to unlock
83 // the lock file:
84 // - |kFileUnlock| calls Unlock() to unlock.
85 // - |kCloseUnlock| calls Close() while the lock is held.
86 // - |kExitUnlock| exits while the lock is held.
MULTIPROCESS_TEST_MAIN(ChildMain)87 MULTIPROCESS_TEST_MAIN(ChildMain) {
88   base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
89   const FilePath temp_path = command_line->GetSwitchValuePath(kTempDirFlag);
90   CHECK(base::DirectoryExists(temp_path));
91 
92   // Immediately lock the file.
93   File file(temp_path.AppendASCII(kLockFile),
94             File::FLAG_OPEN | File::FLAG_READ | File::FLAG_WRITE);
95   CHECK(file.IsValid());
96   CHECK_EQ(File::FILE_OK, file.Lock());
97   CHECK(SignalEvent(temp_path, kSignalLockFileLocked));
98 
99   if (command_line->HasSwitch(kFileUnlock)) {
100     // Wait for signal to unlock, then unlock the file.
101     CHECK(WaitForEvent(temp_path, kSignalLockFileUnlock));
102     CHECK_EQ(File::FILE_OK, file.Unlock());
103     CHECK(SignalEvent(temp_path, kSignalLockFileUnlocked));
104   } else if (command_line->HasSwitch(kCloseUnlock)) {
105     // Wait for the signal to close, then close the file.
106     CHECK(WaitForEvent(temp_path, kSignalLockFileClose));
107     file.Close();
108     CHECK(!file.IsValid());
109     CHECK(SignalEvent(temp_path, kSignalLockFileClosed));
110   } else {
111     CHECK(command_line->HasSwitch(kExitUnlock));
112   }
113 
114   // Wait for signal to exit, so that unlock or close can be distinguished from
115   // exit.
116   CHECK(WaitForEvent(temp_path, kSignalExit));
117   return 0;
118 }
119 
120 }  // namespace
121 
122 class FileLockingTest : public testing::Test {
123  public:
124   FileLockingTest() = default;
125 
126  protected:
SetUp()127   void SetUp() override {
128     testing::Test::SetUp();
129 
130     // Setup the temp dir and the lock file.
131     ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
132     lock_file_.Initialize(
133         temp_dir_.GetPath().AppendASCII(kLockFile),
134         File::FLAG_CREATE | File::FLAG_READ | File::FLAG_WRITE);
135     ASSERT_TRUE(lock_file_.IsValid());
136   }
137 
SignalEvent(const char * signal_file)138   bool SignalEvent(const char* signal_file) {
139     return ::SignalEvent(temp_dir_.GetPath(), signal_file);
140   }
141 
WaitForEventOrTimeout(const char * signal_file)142   bool WaitForEventOrTimeout(const char* signal_file) {
143     return ::WaitForEventWithTimeout(temp_dir_.GetPath(), signal_file,
144                                      TestTimeouts::action_timeout());
145   }
146 
147   // Start a child process set to use the specified unlock action, and wait for
148   // it to lock the file.
StartChildAndSignalLock(const char * unlock_action)149   void StartChildAndSignalLock(const char* unlock_action) {
150     // Create a temporary dir and spin up a ChildLockExit subprocess against it.
151     const FilePath temp_path = temp_dir_.GetPath();
152     base::CommandLine child_command_line(
153         base::GetMultiProcessTestChildBaseCommandLine());
154     child_command_line.AppendSwitchPath(kTempDirFlag, temp_path);
155     child_command_line.AppendSwitch(unlock_action);
156     lock_child_ = base::SpawnMultiProcessTestChild(
157         ChildMainString, child_command_line, base::LaunchOptions());
158     ASSERT_TRUE(lock_child_.IsValid());
159 
160     // Wait for the child to lock the file.
161     ASSERT_TRUE(WaitForEventOrTimeout(kSignalLockFileLocked));
162   }
163 
164   // Signal the child to exit cleanly.
ExitChildCleanly()165   void ExitChildCleanly() {
166     ASSERT_TRUE(SignalEvent(kSignalExit));
167     int rv = -1;
168     ASSERT_TRUE(WaitForMultiprocessTestChildExit(
169         lock_child_, TestTimeouts::action_timeout(), &rv));
170     ASSERT_EQ(0, rv);
171   }
172 
173   base::ScopedTempDir temp_dir_;
174   base::File lock_file_;
175   base::Process lock_child_;
176 
177  private:
178   DISALLOW_COPY_AND_ASSIGN(FileLockingTest);
179 };
180 
181 // Test that locks are released by Unlock().
TEST_F(FileLockingTest,LockAndUnlock)182 TEST_F(FileLockingTest, LockAndUnlock) {
183   StartChildAndSignalLock(kFileUnlock);
184 
185   ASSERT_NE(File::FILE_OK, lock_file_.Lock());
186   ASSERT_TRUE(SignalEvent(kSignalLockFileUnlock));
187   ASSERT_TRUE(WaitForEventOrTimeout(kSignalLockFileUnlocked));
188   ASSERT_EQ(File::FILE_OK, lock_file_.Lock());
189   ASSERT_EQ(File::FILE_OK, lock_file_.Unlock());
190 
191   ExitChildCleanly();
192 }
193 
194 // Test that locks are released on Close().
TEST_F(FileLockingTest,UnlockOnClose)195 TEST_F(FileLockingTest, UnlockOnClose) {
196   StartChildAndSignalLock(kCloseUnlock);
197 
198   ASSERT_NE(File::FILE_OK, lock_file_.Lock());
199   ASSERT_TRUE(SignalEvent(kSignalLockFileClose));
200   ASSERT_TRUE(WaitForEventOrTimeout(kSignalLockFileClosed));
201   ASSERT_EQ(File::FILE_OK, lock_file_.Lock());
202   ASSERT_EQ(File::FILE_OK, lock_file_.Unlock());
203 
204   ExitChildCleanly();
205 }
206 
207 // Test that locks are released on exit.
TEST_F(FileLockingTest,UnlockOnExit)208 TEST_F(FileLockingTest, UnlockOnExit) {
209   StartChildAndSignalLock(kExitUnlock);
210 
211   ASSERT_NE(File::FILE_OK, lock_file_.Lock());
212   ExitChildCleanly();
213   ASSERT_EQ(File::FILE_OK, lock_file_.Lock());
214   ASSERT_EQ(File::FILE_OK, lock_file_.Unlock());
215 }
216 
217 // Test that killing the process releases the lock.  This should cover crashing.
218 // Flaky on Android (http://crbug.com/747518)
219 #if defined(OS_ANDROID)
220 #define MAYBE_UnlockOnTerminate DISABLED_UnlockOnTerminate
221 #else
222 #define MAYBE_UnlockOnTerminate UnlockOnTerminate
223 #endif
TEST_F(FileLockingTest,MAYBE_UnlockOnTerminate)224 TEST_F(FileLockingTest, MAYBE_UnlockOnTerminate) {
225   // The child will wait for an exit which never arrives.
226   StartChildAndSignalLock(kExitUnlock);
227 
228   ASSERT_NE(File::FILE_OK, lock_file_.Lock());
229   ASSERT_TRUE(TerminateMultiProcessTestChild(lock_child_, 0, true));
230   ASSERT_EQ(File::FILE_OK, lock_file_.Lock());
231   ASSERT_EQ(File::FILE_OK, lock_file_.Unlock());
232 }
233