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