diff --git a/llvm/include/llvm/Support/FileSystem.h b/llvm/include/llvm/Support/FileSystem.h --- a/llvm/include/llvm/Support/FileSystem.h +++ b/llvm/include/llvm/Support/FileSystem.h @@ -1131,6 +1131,39 @@ openNativeFileForRead(const Twine &Name, OpenFlags Flags = OF_None, SmallVectorImpl *RealPath = nullptr); +/// Try to locks the file during the specified time. +/// +/// This function implements advisory locking on entire file. If it returns +/// errc::success, the file is locked by the calling process. Until the +/// process unlocks the file by calling \a unlockFile, all attempts to lock the +/// same file will fail/block. The process that locked the file may assume that +/// none of other processes read or write this file, provided that all processes +/// lock the file prior to accessing its content. +/// +/// @param File The descriptor representing the file to lock. +/// @param Timeout Time in milliseconds that the process should wait before +/// reporting lock failure. Zero value means try to get lock only +/// once. +/// @returns errc::success if lock is successfully obtained, +/// errc::no_lock_available if the file cannot be locked, or platform-specific +/// error_code otherwise. +std::error_code +tryLockFile(int FD, + std::chrono::milliseconds Timeout = std::chrono::milliseconds(0)); + +/// Lock the file. +/// +/// This function acts as @ref tryLockFile(int,std::chrono::milliseconds) but it +/// waits infinitely. +std::error_code lockFile(int FD); + +/// Unlock the file. +/// +/// @param File The descriptor representing the file to unlock. +/// @returns errc::success if lock is successfully released or platform-specific +/// error_code otherwise. +std::error_code unlockFile(int FD); + /// @brief Close the file object. This should be used instead of ::close for /// portability. On error, the caller should assume the file is closed, as is /// the case for Process::SafelyCloseFileDescriptor diff --git a/llvm/lib/Support/Unix/Path.inc b/llvm/lib/Support/Unix/Path.inc --- a/llvm/lib/Support/Unix/Path.inc +++ b/llvm/lib/Support/Unix/Path.inc @@ -31,6 +31,7 @@ #include #endif +#include #include #include @@ -1047,6 +1048,34 @@ return NumRead; } +std::error_code tryLockFile(int FD, std::chrono::milliseconds Timeout) { + auto Start = std::chrono::steady_clock::now(); + auto End = Start + Timeout; + do { + if (::flock(FD, LOCK_EX | LOCK_NB) == 0) + return std::error_code(); + int Error = errno; + if (Error == EWOULDBLOCK) { + usleep(1000); + continue; + } + return std::error_code(Error, std::generic_category()); + } while (std::chrono::steady_clock::now() < End); + return make_error_code(errc::no_lock_available); +} + +std::error_code lockFile(int FD) { + if (::flock(FD, LOCK_EX) == 0) + return std::error_code(); + return std::error_code(errno, std::generic_category()); +} + +std::error_code unlockFile(int FD) { + if (::flock(FD, LOCK_UN) == -1) + return std::error_code(errno, std::generic_category()); + return std::error_code(); +} + std::error_code closeFile(file_t &F) { file_t TmpF = F; F = kInvalidFile; diff --git a/llvm/lib/Support/Windows/Path.inc b/llvm/lib/Support/Windows/Path.inc --- a/llvm/lib/Support/Windows/Path.inc +++ b/llvm/lib/Support/Windows/Path.inc @@ -1255,6 +1255,43 @@ return readNativeFileImpl(FileHandle, Buf, &Overlapped); } +std::error_code tryLockFile(int FD, std::chrono::milliseconds Timeout) { + DWORD Flags = LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY; + OVERLAPPED OV = {0}; + file_t File = convertFDToNativeFile(FD); + auto Start = std::chrono::steady_clock::now(); + auto End = Start + Timeout; + do { + if (::LockFileEx(File, Flags, 0, MAXDWORD, MAXDWORD, &OV)) + return std::error_code(); + DWORD Error = ::GetLastError(); + if (Error == ERROR_LOCK_VIOLATION) { + ::Sleep(1); + continue; + } + return mapWindowsError(Error); + } while (std::chrono::steady_clock::now() < End); + return mapWindowsError(ERROR_LOCK_VIOLATION); +} + +std::error_code lockFile(int FD) { + DWORD Flags = LOCKFILE_EXCLUSIVE_LOCK; + OVERLAPPED OV = {0}; + file_t File = convertFDToNativeFile(FD); + if (::LockFileEx(File, Flags, 0, MAXDWORD, MAXDWORD, &OV)) + return std::error_code(); + DWORD Error = ::GetLastError(); + return mapWindowsError(Error); +} + +std::error_code unlockFile(int FD) { + OVERLAPPED OV = { 0 }; + file_t File = convertFDToNativeFile(FD); + if (::UnlockFileEx(File, 0, MAXDWORD, MAXDWORD, &OV)) + return std::error_code(); + return mapWindowsError(::GetLastError()); +} + std::error_code closeFile(file_t &F) { file_t TmpF = F; F = kInvalidFile; diff --git a/llvm/unittests/Support/Path.cpp b/llvm/unittests/Support/Path.cpp --- a/llvm/unittests/Support/Path.cpp +++ b/llvm/unittests/Support/Path.cpp @@ -39,6 +39,10 @@ #include #endif +#include +#include +#include + using namespace llvm; using namespace llvm::sys; @@ -2038,4 +2042,108 @@ } #endif +TEST_F(FileSystemTest, lockFile) { + int FD1, FD2; + SmallString<64> TempPath; + ASSERT_NO_ERROR(fs::createTemporaryFile("test", "temp", FD1, TempPath)); + FileRemover Cleanup(TempPath); + ASSERT_NO_ERROR(fs::openFileForReadWrite(TempPath, FD2, fs::CD_OpenExisting, + fs::OF_Append)); + ASSERT_NO_ERROR(fs::tryLockFile(FD1)); + + ASSERT_EQ(errc::no_lock_available, + fs::tryLockFile(FD2, std::chrono::milliseconds(5))); + ASSERT_NO_ERROR(fs::unlockFile(FD1)); + ASSERT_NO_ERROR(fs::tryLockFile(FD2)); + ASSERT_NO_ERROR(fs::unlockFile(FD2)); +} + +namespace { +class Event { + std::mutex M; + std::condition_variable CV; + bool Signaling = false; +public: + void reset() { Signaling = false; } + void wait() { + std::unique_lock Lock(M); + if (!Signaling) + CV.wait_for(Lock, std::chrono::seconds(1), [&] { return Signaling; }); + } + void signal() { + std::unique_lock Lock(M); + Signaling = true; + CV.notify_all(); + } +}; +} + +TEST_F(FileSystemTest, lockFileThread) { +#if LLVM_ENABLE_THREADS + int FD1, FD2; + SmallString<64> TempPath; + ASSERT_NO_ERROR(fs::createTemporaryFile("test", "temp", FD1, TempPath)); + FileRemover Cleanup(TempPath); + ASSERT_NO_ERROR(fs::openFileForReadWrite(TempPath, FD2, fs::CD_OpenExisting, + fs::OF_Append)); + + // Threads execute the following sequence: + // + // T1 started + // lock file + // start T2 + // wait T2 started + // | try locking file (failure) + // | / unblock T1 + // unlock file lock file + // unlock file + + Event DoUnlockEvent; + + std::error_code ECT2a, ECT2b; + bool UseBlockingCall = false; + const auto Thread2Body = [&]() { + ECT2a = fs::tryLockFile(FD2); + if (!ECT2a) + return; + DoUnlockEvent.signal(); + if (UseBlockingCall) + ECT2b = fs::lockFile(FD2); + else + ECT2b = fs::tryLockFile(FD2, std::chrono::seconds(5)); + if (ECT2b) + return; + fs::unlockFile(FD2); + }; + + std::error_code ECT1a, ECT1b; + const auto Thread1Body = [&]() { + ECT1a = fs::tryLockFile(FD1); + if (ECT1a) + return; + auto Thread2 = std::thread(Thread2Body); + DoUnlockEvent.wait(); + ECT1b = fs::unlockFile(FD1); + Thread2.join(); + }; + + auto Thread1 = std::thread(Thread1Body); + Thread1.join(); + ASSERT_NO_ERROR(ECT1a); + ASSERT_NO_ERROR(ECT1b); + ASSERT_ERROR(ECT2a); + ASSERT_NO_ERROR(ECT2b); + + UseBlockingCall = true; + DoUnlockEvent.reset(); + auto Thread1a = std::thread(Thread1Body); + Thread1a.join(); + ASSERT_NO_ERROR(ECT1a); + ASSERT_NO_ERROR(ECT1b); + ASSERT_ERROR(ECT2a); + ASSERT_NO_ERROR(ECT2b); + +#endif +} + } // anonymous namespace