1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2013 David Faure <faure+bluesystems@kde.org> |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the test suite of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
21 | ** included in the packaging of this file. Please review the following |
22 | ** information to ensure the GNU General Public License requirements will |
23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
24 | ** |
25 | ** $QT_END_LICENSE$ |
26 | ** |
27 | ****************************************************************************/ |
28 | |
29 | |
30 | #include <QtTest/QtTest> |
31 | #include <QtConcurrentRun> |
32 | #include <qlockfile.h> |
33 | #include <qtemporarydir.h> |
34 | #include <qsysinfo.h> |
35 | #if defined(Q_OS_UNIX) && !defined(Q_OS_VXWORKS) |
36 | #include <unistd.h> |
37 | #include <sys/time.h> |
38 | #elif defined(Q_OS_WIN) && !defined(Q_OS_WINRT) |
39 | # include <qt_windows.h> |
40 | #endif |
41 | |
42 | #include <private/qlockfile_p.h> // for getLockFileHandle() |
43 | |
44 | class tst_QLockFile : public QObject |
45 | { |
46 | Q_OBJECT |
47 | |
48 | private slots: |
49 | void initTestCase(); |
50 | void lockUnlock(); |
51 | void lockOutOtherProcess(); |
52 | void lockOutOtherThread(); |
53 | void raceWithOtherThread(); |
54 | void waitForLock_data(); |
55 | void waitForLock(); |
56 | void staleLockFromCrashedProcess_data(); |
57 | void staleLockFromCrashedProcess(); |
58 | void staleLockFromCrashedProcessReusedPid(); |
59 | void staleShortLockFromBusyProcess(); |
60 | void staleLongLockFromBusyProcess(); |
61 | void staleLockRace(); |
62 | void noPermissions(); |
63 | void noPermissionsWindows(); |
64 | void corruptedLockFile(); |
65 | void corruptedLockFileInTheFuture(); |
66 | void hostnameChange(); |
67 | void differentMachines(); |
68 | void reboot(); |
69 | |
70 | private: |
71 | static bool overwriteLineInLockFile(QFile &f, int line, const QString &newLine); |
72 | static bool overwritePidInLockFile(const QString &filePath, qint64 pid); |
73 | |
74 | public: |
75 | QString m_helperApp; |
76 | QTemporaryDir dir; |
77 | }; |
78 | |
79 | void tst_QLockFile::initTestCase() |
80 | { |
81 | #if defined(Q_OS_ANDROID) && !defined(Q_OS_ANDROID_EMBEDDED) |
82 | QSKIP("This test requires deploying and running external console applications" ); |
83 | #elif !QT_CONFIG(process) |
84 | QSKIP("This test requires QProcess support" ); |
85 | #else |
86 | QVERIFY2(dir.isValid(), qPrintable(dir.errorString())); |
87 | // chdir to our testdata path and execute helper apps relative to that. |
88 | QString testdata_dir = QFileInfo(QFINDTESTDATA("qlockfiletesthelper" )).absolutePath(); |
89 | QVERIFY2(QDir::setCurrent(testdata_dir), qPrintable("Could not chdir to " + testdata_dir)); |
90 | m_helperApp = "qlockfiletesthelper/qlockfile_test_helper" ; |
91 | #endif // QT_CONFIG(process) |
92 | } |
93 | |
94 | void tst_QLockFile::lockUnlock() |
95 | { |
96 | const QString fileName = dir.path() + "/lock1" ; |
97 | QVERIFY(!QFile(fileName).exists()); |
98 | QLockFile lockFile(fileName); |
99 | QVERIFY(lockFile.lock()); |
100 | QVERIFY(lockFile.isLocked()); |
101 | QCOMPARE(int(lockFile.error()), int(QLockFile::NoError)); |
102 | QVERIFY(QFile::exists(fileName)); |
103 | |
104 | // Recursive locking is not allowed |
105 | // (can't test lock() here, it would wait forever) |
106 | QVERIFY(!lockFile.tryLock()); |
107 | QCOMPARE(int(lockFile.error()), int(QLockFile::LockFailedError)); |
108 | qint64 pid; |
109 | QString hostname, appname; |
110 | QVERIFY(lockFile.getLockInfo(&pid, &hostname, &appname)); |
111 | QCOMPARE(pid, QCoreApplication::applicationPid()); |
112 | QCOMPARE(appname, qAppName()); |
113 | QVERIFY(!lockFile.tryLock(200)); |
114 | QCOMPARE(int(lockFile.error()), int(QLockFile::LockFailedError)); |
115 | |
116 | // Unlock deletes the lock file |
117 | lockFile.unlock(); |
118 | QCOMPARE(int(lockFile.error()), int(QLockFile::NoError)); |
119 | QVERIFY(!lockFile.isLocked()); |
120 | QVERIFY(!QFile::exists(fileName)); |
121 | } |
122 | |
123 | void tst_QLockFile::lockOutOtherProcess() |
124 | { |
125 | #if !QT_CONFIG(process) |
126 | QSKIP("This test requires QProcess support" ); |
127 | #else |
128 | // Lock |
129 | const QString fileName = dir.path() + "/lockOtherProcess" ; |
130 | QLockFile lockFile(fileName); |
131 | QVERIFY(lockFile.lock()); |
132 | |
133 | // Other process can't acquire lock |
134 | QProcess proc; |
135 | proc.start(program: m_helperApp, arguments: QStringList() << fileName); |
136 | QVERIFY2(proc.waitForStarted(), qPrintable(proc.errorString())); |
137 | QVERIFY(proc.waitForFinished()); |
138 | QCOMPARE(proc.exitCode(), int(QLockFile::LockFailedError)); |
139 | |
140 | // Unlock |
141 | lockFile.unlock(); |
142 | QVERIFY(!QFile::exists(fileName)); |
143 | |
144 | // Other process can now acquire lock |
145 | int ret = QProcess::execute(program: m_helperApp, arguments: QStringList() << fileName); |
146 | QCOMPARE(ret, int(QLockFile::NoError)); |
147 | // Lock doesn't survive process though (on clean exit) |
148 | QVERIFY(!QFile::exists(fileName)); |
149 | #endif // QT_CONFIG(process) |
150 | } |
151 | |
152 | static QLockFile::LockError tryLockFromThread(const QString &fileName) |
153 | { |
154 | QLockFile lockInThread(fileName); |
155 | lockInThread.tryLock(); |
156 | return lockInThread.error(); |
157 | } |
158 | |
159 | void tst_QLockFile::lockOutOtherThread() |
160 | { |
161 | const QString fileName = dir.path() + "/lockOtherThread" ; |
162 | QLockFile lockFile(fileName); |
163 | QVERIFY(lockFile.lock()); |
164 | |
165 | // Other thread can't acquire lock |
166 | QFuture<QLockFile::LockError> ret = QtConcurrent::run<QLockFile::LockError>(functionPointer: tryLockFromThread, arg1: fileName); |
167 | QCOMPARE(ret.result(), QLockFile::LockFailedError); |
168 | |
169 | lockFile.unlock(); |
170 | |
171 | // Now other thread can acquire lock |
172 | QFuture<QLockFile::LockError> ret2 = QtConcurrent::run<QLockFile::LockError>(functionPointer: tryLockFromThread, arg1: fileName); |
173 | QCOMPARE(ret2.result(), QLockFile::NoError); |
174 | } |
175 | |
176 | static QLockFile::LockError lockFromThread(const QString &fileName) |
177 | { |
178 | QLockFile lockInThread(fileName); |
179 | lockInThread.lock(); |
180 | return lockInThread.error(); |
181 | } |
182 | |
183 | // QTBUG-38853, best way to trigger it was to add a QThread::sleep(1) in QLockFilePrivate::getLockInfo() after the first readLine. |
184 | // Then (on Windows), the QFile::remove() in unlock() (called by the first thread who got the lock, in the destructor) |
185 | // would fail due to the existing reader on the file. Fixed by checking the return value of QFile::remove() in unlock(). |
186 | void tst_QLockFile::raceWithOtherThread() |
187 | { |
188 | const QString fileName = dir.path() + "/raceWithOtherThread" ; |
189 | QFuture<QLockFile::LockError> ret = QtConcurrent::run<QLockFile::LockError>(functionPointer: lockFromThread, arg1: fileName); |
190 | QFuture<QLockFile::LockError> ret2 = QtConcurrent::run<QLockFile::LockError>(functionPointer: lockFromThread, arg1: fileName); |
191 | QCOMPARE(ret.result(), QLockFile::NoError); |
192 | QCOMPARE(ret2.result(), QLockFile::NoError); |
193 | } |
194 | |
195 | static bool lockFromThread(const QString &fileName, int sleepMs, QSemaphore *semThreadReady, QSemaphore *semMainThreadDone) |
196 | { |
197 | QLockFile lockFile(fileName); |
198 | if (!lockFile.lock()) { |
199 | qWarning() << "Locking failed" << lockFile.error(); |
200 | return false; |
201 | } |
202 | semThreadReady->release(); |
203 | QThread::msleep(sleepMs); |
204 | semMainThreadDone->acquire(); |
205 | lockFile.unlock(); |
206 | return true; |
207 | } |
208 | |
209 | void tst_QLockFile::waitForLock_data() |
210 | { |
211 | QTest::addColumn<int>(name: "testNumber" ); |
212 | QTest::addColumn<int>(name: "threadSleepMs" ); |
213 | QTest::addColumn<bool>(name: "releaseEarly" ); |
214 | QTest::addColumn<int>(name: "tryLockTimeout" ); |
215 | QTest::addColumn<bool>(name: "expectedResult" ); |
216 | |
217 | int tn = 0; // test number |
218 | QTest::newRow(dataTag: "wait_forever_succeeds" ) << ++tn << 500 << true << -1 << true; |
219 | QTest::newRow(dataTag: "wait_longer_succeeds" ) << ++tn << 500 << true << 1000 << true; |
220 | QTest::newRow(dataTag: "wait_zero_fails" ) << ++tn << 500 << false << 0 << false; |
221 | QTest::newRow(dataTag: "wait_not_enough_fails" ) << ++tn << 500 << false << 100 << false; |
222 | } |
223 | |
224 | void tst_QLockFile::waitForLock() |
225 | { |
226 | QFETCH(int, testNumber); |
227 | QFETCH(int, threadSleepMs); |
228 | QFETCH(bool, releaseEarly); |
229 | QFETCH(int, tryLockTimeout); |
230 | QFETCH(bool, expectedResult); |
231 | |
232 | const QString fileName = dir.path() + "/waitForLock" + QString::number(testNumber); |
233 | QLockFile lockFile(fileName); |
234 | QSemaphore semThreadReady, semMainThreadDone; |
235 | // Lock file from a thread |
236 | QFuture<bool> ret = QtConcurrent::run<bool>(functionPointer: lockFromThread, arg1: fileName, arg2: threadSleepMs, arg3: &semThreadReady, arg4: &semMainThreadDone); |
237 | semThreadReady.acquire(); |
238 | |
239 | if (releaseEarly) // let the thread release the lock after threadSleepMs |
240 | semMainThreadDone.release(); |
241 | |
242 | QCOMPARE(lockFile.tryLock(tryLockTimeout), expectedResult); |
243 | if (expectedResult) |
244 | QCOMPARE(int(lockFile.error()), int(QLockFile::NoError)); |
245 | else |
246 | QCOMPARE(int(lockFile.error()), int(QLockFile::LockFailedError)); |
247 | |
248 | if (!releaseEarly) // only let the thread release the lock now |
249 | semMainThreadDone.release(); |
250 | |
251 | QVERIFY(ret); // waits for the thread to finish |
252 | } |
253 | |
254 | void tst_QLockFile::staleLockFromCrashedProcess_data() |
255 | { |
256 | QTest::addColumn<int>(name: "staleLockTime" ); |
257 | |
258 | // Test both use cases for QLockFile, should make no difference here. |
259 | QTest::newRow(dataTag: "short" ) << 30000; |
260 | QTest::newRow(dataTag: "long" ) << 0; |
261 | } |
262 | |
263 | void tst_QLockFile::staleLockFromCrashedProcess() |
264 | { |
265 | #if !QT_CONFIG(process) |
266 | QSKIP("This test requires QProcess support" ); |
267 | #else |
268 | QFETCH(int, staleLockTime); |
269 | const QString fileName = dir.path() + "/staleLockFromCrashedProcess" ; |
270 | |
271 | int ret = QProcess::execute(program: m_helperApp, arguments: QStringList() << fileName << "-uncleanexit" ); |
272 | QCOMPARE(ret, int(QLockFile::NoError)); |
273 | QTRY_VERIFY(QFile::exists(fileName)); |
274 | |
275 | QLockFile secondLock(fileName); |
276 | secondLock.setStaleLockTime(staleLockTime); |
277 | // tryLock detects and removes the stale lock (since the PID is dead) |
278 | #ifdef Q_OS_WIN |
279 | // It can take a bit of time on Windows, though. |
280 | QVERIFY(secondLock.tryLock(30000)); |
281 | #else |
282 | QVERIFY(secondLock.tryLock()); |
283 | #endif |
284 | QCOMPARE(int(secondLock.error()), int(QLockFile::NoError)); |
285 | #endif // QT_CONFIG(process) |
286 | } |
287 | |
288 | void tst_QLockFile::staleLockFromCrashedProcessReusedPid() |
289 | { |
290 | #if !QT_CONFIG(process) |
291 | QSKIP("This test requires QProcess support" ); |
292 | #elif defined(Q_OS_WINRT) || defined(QT_PLATFORM_UIKIT) |
293 | QSKIP("We cannot retrieve information about other processes on this platform." ); |
294 | #else |
295 | const QString fileName = dir.path() + "/staleLockFromCrashedProcessReusedPid" ; |
296 | |
297 | int ret = QProcess::execute(program: m_helperApp, arguments: QStringList() << fileName << "-uncleanexit" ); |
298 | QCOMPARE(ret, int(QLockFile::NoError)); |
299 | QVERIFY(QFile::exists(fileName)); |
300 | QVERIFY(overwritePidInLockFile(fileName, QCoreApplication::applicationPid())); |
301 | |
302 | QLockFile secondLock(fileName); |
303 | qint64 pid = 0; |
304 | QVERIFY(secondLock.getLockInfo(&pid, 0, 0)); |
305 | QCOMPARE(pid, QCoreApplication::applicationPid()); |
306 | secondLock.setStaleLockTime(0); |
307 | QVERIFY(secondLock.tryLock()); |
308 | QCOMPARE(int(secondLock.error()), int(QLockFile::NoError)); |
309 | #endif // QT_CONFIG(process) |
310 | } |
311 | |
312 | void tst_QLockFile::staleShortLockFromBusyProcess() |
313 | { |
314 | #if !QT_CONFIG(process) |
315 | QSKIP("This test requires QProcess support" ); |
316 | #else |
317 | const QString fileName = dir.path() + "/staleLockFromBusyProcess" ; |
318 | |
319 | QProcess proc; |
320 | proc.start(program: m_helperApp, arguments: QStringList() << fileName << "-busy" ); |
321 | QVERIFY2(proc.waitForStarted(), qPrintable(proc.errorString())); |
322 | QTRY_VERIFY(QFile::exists(fileName)); |
323 | |
324 | QLockFile secondLock(fileName); |
325 | QVERIFY(!secondLock.tryLock()); // held by other process |
326 | QCOMPARE(int(secondLock.error()), int(QLockFile::LockFailedError)); |
327 | qint64 pid; |
328 | QString hostname, appname; |
329 | QTRY_VERIFY(secondLock.getLockInfo(&pid, &hostname, &appname)); |
330 | #ifdef Q_OS_UNIX |
331 | QCOMPARE(pid, proc.processId()); |
332 | #endif |
333 | |
334 | secondLock.setStaleLockTime(100); |
335 | QTest::qSleep(ms: 100); // make the lock stale |
336 | // We can't "steal" (delete+recreate) a lock file from a running process |
337 | // until the file descriptor is closed. |
338 | QVERIFY(!secondLock.tryLock()); |
339 | |
340 | proc.waitForFinished(); |
341 | QVERIFY(secondLock.tryLock()); |
342 | #endif // QT_CONFIG(process) |
343 | } |
344 | |
345 | void tst_QLockFile::staleLongLockFromBusyProcess() |
346 | { |
347 | #if !QT_CONFIG(process) |
348 | QSKIP("This test requires QProcess support" ); |
349 | #else |
350 | const QString fileName = dir.path() + "/staleLockFromBusyProcess" ; |
351 | |
352 | QProcess proc; |
353 | proc.start(program: m_helperApp, arguments: QStringList() << fileName << "-busy" ); |
354 | QVERIFY2(proc.waitForStarted(), qPrintable(proc.errorString())); |
355 | QTRY_VERIFY(QFile::exists(fileName)); |
356 | |
357 | QLockFile secondLock(fileName); |
358 | secondLock.setStaleLockTime(0); |
359 | QVERIFY(!secondLock.tryLock(100)); // never stale |
360 | QCOMPARE(int(secondLock.error()), int(QLockFile::LockFailedError)); |
361 | qint64 pid; |
362 | QTRY_VERIFY(secondLock.getLockInfo(&pid, NULL, NULL)); |
363 | QVERIFY(pid > 0); |
364 | |
365 | // As long as the other process is running, we can't remove the lock file |
366 | QVERIFY(!secondLock.removeStaleLockFile()); |
367 | |
368 | proc.waitForFinished(); |
369 | #endif // QT_CONFIG(process) |
370 | } |
371 | |
372 | static QString tryStaleLockFromThread(const QString &fileName) |
373 | { |
374 | QLockFile lockInThread(fileName + ".lock" ); |
375 | lockInThread.setStaleLockTime(1000); |
376 | if (!lockInThread.lock()) |
377 | return "Error locking: " + QString::number(lockInThread.error()); |
378 | |
379 | // The concurrent use of the file below (write, read, delete) is protected by the lock file above. |
380 | // (provided that it doesn't become stale due to this operation taking too long) |
381 | QFile theFile(fileName); |
382 | if (!theFile.open(flags: QIODevice::WriteOnly)) |
383 | return "Couldn't open for write" ; |
384 | theFile.write(data: "Hello world" ); |
385 | theFile.flush(); |
386 | theFile.close(); |
387 | QFile reader(fileName); |
388 | if (!reader.open(flags: QIODevice::ReadOnly)) |
389 | return "Couldn't open for read" ; |
390 | const QByteArray read = reader.readAll(); |
391 | if (read != "Hello world" ) |
392 | return "File didn't have the expected contents:" + read; |
393 | reader.remove(); |
394 | return QString(); |
395 | } |
396 | |
397 | void tst_QLockFile::staleLockRace() |
398 | { |
399 | #if !QT_CONFIG(process) |
400 | QSKIP("This test requires QProcess support" ); |
401 | #else |
402 | // Multiple threads notice a stale lock at the same time |
403 | // Only one thread should delete it, otherwise a race will ensue |
404 | const QString fileName = dir.path() + "/sharedFile" ; |
405 | const QString lockName = fileName + ".lock" ; |
406 | int ret = QProcess::execute(program: m_helperApp, arguments: QStringList() << lockName << "-uncleanexit" ); |
407 | QCOMPARE(ret, int(QLockFile::NoError)); |
408 | QTRY_VERIFY(QFile::exists(lockName)); |
409 | |
410 | QThreadPool::globalInstance()->setMaxThreadCount(10); |
411 | QFutureSynchronizer<QString> synchronizer; |
412 | for (int i = 0; i < 8; ++i) |
413 | synchronizer.addFuture(future: QtConcurrent::run<QString>(functionPointer: tryStaleLockFromThread, arg1: fileName)); |
414 | synchronizer.waitForFinished(); |
415 | foreach (const QFuture<QString> &future, synchronizer.futures()) |
416 | QVERIFY2(future.result().isEmpty(), qPrintable(future.result())); |
417 | #endif // QT_CONFIG(process) |
418 | } |
419 | |
420 | void tst_QLockFile::noPermissions() |
421 | { |
422 | #if defined(Q_OS_WIN) |
423 | // A readonly directory still allows us to create files, on Windows. |
424 | QSKIP("No permission testing on Windows" ); |
425 | #elif defined(Q_OS_UNIX) && !defined(Q_OS_VXWORKS) |
426 | if (::geteuid() == 0) |
427 | QSKIP("Test is not applicable with root privileges" ); |
428 | #endif |
429 | // Restore permissions so that the QTemporaryDir cleanup can happen |
430 | class PermissionRestorer |
431 | { |
432 | QString m_path; |
433 | public: |
434 | PermissionRestorer(const QString& path) |
435 | : m_path(path) |
436 | {} |
437 | |
438 | ~PermissionRestorer() |
439 | { |
440 | QFile file(m_path); |
441 | file.setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)); |
442 | } |
443 | }; |
444 | |
445 | const QString fileName = dir.path() + "/staleLock" ; |
446 | QFile dirAsFile(dir.path()); // I have to use QFile to change a dir's permissions... |
447 | QVERIFY2(dirAsFile.setPermissions(QFile::Permissions{}), qPrintable(dir.path())); // no permissions |
448 | PermissionRestorer permissionRestorer(dir.path()); |
449 | |
450 | QLockFile lockFile(fileName); |
451 | QVERIFY(!lockFile.lock()); |
452 | QCOMPARE(int(lockFile.error()), int(QLockFile::PermissionError)); |
453 | } |
454 | |
455 | enum ProcessProperty { |
456 | ElevatedProcess = 0x1, |
457 | VirtualStore = 0x2 |
458 | }; |
459 | |
460 | Q_DECLARE_FLAGS(ProcessProperties, ProcessProperty) |
461 | Q_DECLARE_OPERATORS_FOR_FLAGS(ProcessProperties) |
462 | |
463 | static inline ProcessProperties processProperties() |
464 | { |
465 | ProcessProperties result; |
466 | #if defined(Q_OS_WIN) && !defined(Q_OS_WINRT) |
467 | HANDLE processToken = NULL; |
468 | if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &processToken)) { |
469 | DWORD elevation; // struct containing a DWORD, not present in some MinGW headers. |
470 | DWORD cbSize = sizeof(elevation); |
471 | if (GetTokenInformation(processToken, TokenElevation, &elevation, cbSize, &cbSize) |
472 | && elevation) { |
473 | result |= ElevatedProcess; |
474 | } |
475 | // Check for UAC virtualization (compatibility mode for old software |
476 | // allowing it to write to system folders by mirroring them under |
477 | // "\Users\...\AppData\Local\VirtualStore\", which is typically the case |
478 | // for MinGW). |
479 | DWORD virtualStoreEnabled = 0; |
480 | cbSize = sizeof(virtualStoreEnabled); |
481 | if (GetTokenInformation(processToken, TokenVirtualizationEnabled, &virtualStoreEnabled, cbSize, &cbSize) |
482 | && virtualStoreEnabled) { |
483 | result |= VirtualStore; |
484 | } |
485 | CloseHandle(processToken); |
486 | } |
487 | #endif |
488 | return result; |
489 | } |
490 | |
491 | void tst_QLockFile::noPermissionsWindows() |
492 | { |
493 | // Windows: Do the permissions test in a system directory in which |
494 | // files cannot be created. |
495 | #if !defined(Q_OS_WIN) || defined(Q_OS_WINRT) |
496 | QSKIP("This test is for desktop Windows only" ); |
497 | #endif |
498 | #ifdef Q_OS_WIN |
499 | if (QOperatingSystemVersion::current() < QOperatingSystemVersion::Windows7) |
500 | QSKIP("This test requires at least Windows 7" ); |
501 | #endif |
502 | if (const int p = processProperties()) { |
503 | const QByteArray message = "This test cannot be run (properties=0x" |
504 | + QByteArray::number(p, base: 16) + ')'; |
505 | QSKIP(message.constData()); |
506 | } |
507 | |
508 | const QString fileName = QFile::decodeName(localFileName: qgetenv(varName: "ProgramFiles" )) |
509 | + QLatin1Char('/') + QCoreApplication::applicationName() |
510 | + QDateTime::currentDateTime().toString(QStringLiteral("yyMMddhhmm" )); |
511 | QLockFile lockFile(fileName); |
512 | QVERIFY(!lockFile.lock()); |
513 | QCOMPARE(int(lockFile.error()), int(QLockFile::PermissionError)); |
514 | } |
515 | |
516 | void tst_QLockFile::corruptedLockFile() |
517 | { |
518 | const QString fileName = dir.path() + "/corruptedLockFile" ; |
519 | |
520 | { |
521 | // Create a empty file. Typically the result of a computer crash or hard disk full. |
522 | QFile file(fileName); |
523 | QVERIFY(file.open(QFile::WriteOnly)); |
524 | } |
525 | |
526 | QLockFile secondLock(fileName); |
527 | secondLock.setStaleLockTime(100); |
528 | QVERIFY(secondLock.tryLock(10000)); |
529 | QCOMPARE(int(secondLock.error()), int(QLockFile::NoError)); |
530 | } |
531 | |
532 | void tst_QLockFile::corruptedLockFileInTheFuture() |
533 | { |
534 | #if !defined(Q_OS_UNIX) |
535 | QSKIP("This tests needs utimes" ); |
536 | #else |
537 | // This test is the same as the previous one, but the corruption was so there is a corrupted |
538 | // .rmlock whose timestamp is in the future |
539 | |
540 | const QString fileName = dir.path() + "/corruptedLockFile.rmlock" ; |
541 | |
542 | { |
543 | QFile file(fileName); |
544 | QVERIFY(file.open(QFile::WriteOnly)); |
545 | } |
546 | |
547 | struct timeval times[2]; |
548 | gettimeofday(tv: times, tz: 0); |
549 | times[1].tv_sec = (times[0].tv_sec += 600); |
550 | times[1].tv_usec = times[0].tv_usec; |
551 | utimes(file: fileName.toLocal8Bit(), tvp: times); |
552 | |
553 | QTest::ignoreMessage(type: QtInfoMsg, message: "QLockFile: Lock file '" + fileName.toUtf8() + "' has a modification time in the future" ); |
554 | corruptedLockFile(); |
555 | #endif |
556 | } |
557 | |
558 | void tst_QLockFile::hostnameChange() |
559 | { |
560 | const QByteArray hostid = QSysInfo::machineUniqueId(); |
561 | if (hostid.isEmpty()) |
562 | QSKIP("Could not get a unique host ID on this machine" ); |
563 | |
564 | QString lockFile = dir.path() + "/hostnameChangeLock" ; |
565 | QLockFile lock1(lockFile); |
566 | QVERIFY(lock1.lock()); |
567 | |
568 | { |
569 | // now modify it |
570 | QFile f; |
571 | QVERIFY(f.open(QLockFilePrivate::getLockFileHandle(&lock1), |
572 | QIODevice::ReadWrite | QIODevice::Text, |
573 | QFile::DontCloseHandle)); |
574 | QVERIFY(overwriteLineInLockFile(f, 3, "this is not a hostname" )); |
575 | } |
576 | |
577 | { |
578 | // we should fail to lock |
579 | QLockFile lock2(lockFile); |
580 | QVERIFY(!lock2.tryLock(1000)); |
581 | } |
582 | } |
583 | |
584 | void tst_QLockFile::differentMachines() |
585 | { |
586 | const QByteArray hostid = QSysInfo::machineUniqueId(); |
587 | if (hostid.isEmpty()) |
588 | QSKIP("Could not get a unique host ID on this machine" ); |
589 | |
590 | QString lockFile = dir.path() + "/differentMachinesLock" ; |
591 | QLockFile lock1(lockFile); |
592 | QVERIFY(lock1.lock()); |
593 | |
594 | { |
595 | // now modify it |
596 | QFile f; |
597 | QVERIFY(f.open(QLockFilePrivate::getLockFileHandle(&lock1), |
598 | QIODevice::ReadWrite | QIODevice::Text, |
599 | QFile::DontCloseHandle)); |
600 | QVERIFY(overwriteLineInLockFile(f, 1, QT_STRINGIFY(INT_MAX))); |
601 | QVERIFY(overwriteLineInLockFile(f, 4, "this is not a UUID" )); |
602 | } |
603 | |
604 | { |
605 | // we should fail to lock |
606 | QLockFile lock2(lockFile); |
607 | QVERIFY(!lock2.tryLock(1000)); |
608 | } |
609 | } |
610 | |
611 | void tst_QLockFile::reboot() |
612 | { |
613 | const QByteArray bootid = QSysInfo::bootUniqueId(); |
614 | if (bootid.isEmpty()) |
615 | QSKIP("Could not get a unique boot ID on this machine" ); |
616 | |
617 | // create a lock so we can get its contents |
618 | QString lockFile = dir.path() + "/rebootLock" ; |
619 | QLockFile lock1(lockFile); |
620 | QVERIFY(lock1.lock()); |
621 | |
622 | QFile f(lockFile); |
623 | QVERIFY(f.open(QFile::ReadOnly | QFile::Text)); |
624 | auto lines = f.readAll().split(sep: '\n'); |
625 | f.close(); |
626 | |
627 | lock1.unlock(); |
628 | |
629 | // now recreate the file simulating a reboot |
630 | QVERIFY(f.open(QFile::WriteOnly | QFile::Text)); |
631 | lines[4] = "this is not a UUID" ; |
632 | f.write(data: lines.join(sep: '\n')); |
633 | f.close(); |
634 | |
635 | // we should succeed in locking |
636 | QVERIFY(lock1.tryLock(0)); |
637 | } |
638 | |
639 | bool tst_QLockFile::overwritePidInLockFile(const QString &filePath, qint64 pid) |
640 | { |
641 | QFile f(filePath); |
642 | if (!f.open(flags: QFile::ReadWrite | QFile::Text)) { |
643 | qErrnoWarning(msg: "Cannot open %s" , qPrintable(filePath)); |
644 | return false; |
645 | } |
646 | return overwriteLineInLockFile(f, line: 1, newLine: QString::number(pid)); |
647 | } |
648 | |
649 | bool tst_QLockFile::overwriteLineInLockFile(QFile &f, int line, const QString &newLine) |
650 | { |
651 | f.seek(offset: 0); |
652 | QByteArray buf = f.readAll(); |
653 | QStringList lines = QString::fromUtf8(str: buf).split(sep: '\n'); |
654 | if (lines.size() < 3 && lines.size() < line - 1) { |
655 | qWarning(msg: "Unexpected lockfile content." ); |
656 | return false; |
657 | } |
658 | lines[line - 1] = newLine; |
659 | f.seek(offset: 0); |
660 | buf = lines.join(sep: '\n').toUtf8(); |
661 | f.resize(sz: buf.size()); |
662 | return f.write(data: buf) == buf.size(); |
663 | } |
664 | |
665 | struct LockFileUsageInGlobalDtor |
666 | { |
667 | ~LockFileUsageInGlobalDtor() { |
668 | QLockFile lockFile(QDir::currentPath() + "/lastlock" ); |
669 | QVERIFY(lockFile.lock()); |
670 | QVERIFY(lockFile.isLocked()); |
671 | } |
672 | }; |
673 | LockFileUsageInGlobalDtor s_instance; |
674 | |
675 | QTEST_MAIN(tst_QLockFile) |
676 | #include "tst_qlockfile.moc" |
677 | |