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,38 @@ openNativeFileForRead(const Twine &Name, OpenFlags Flags = OF_None, SmallVectorImpl *RealPath = nullptr); +/// Try to locks the file during the specified time. +/// +/// This function implements advisory locks on entire file. If it returns +/// errc::success, the process may read or write the file assuming no +/// other processes may change it. Any other process that calls this function +/// would get errc::no_lock_available until the process owning the lock +/// calls unlockFile. +/// +/// @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 @@ -1052,6 +1053,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; @@ -2012,4 +2016,112 @@ } #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)); +} + +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)); + + std::mutex Start2M; + std::condition_variable Start2CV; + bool Start2 = false; + std::mutex LockM; + std::condition_variable LockCV; + bool Locked = false; + + std::error_code ECT1a, ECT1b; + const auto Thread1Body = [&]() { + // Lock the file and request thread2 to start. + ECT1a = fs::tryLockFile(FD1); + if (ECT1a) + return; + { + std::unique_lock Locker(Start2M); + Start2 = true; + } + Start2CV.notify_all(); + // Wait command to unlock the file. + { + std::unique_lock Locker(LockM); + if (!Locked) + LockCV.wait_for(Locker, std::chrono::seconds(1), + [&] { return Locked; }); + } + // Unlock the file. + ECT1b = fs::unlockFile(FD1); + }; + + std::error_code ECT2a, ECT2b; + bool UseBlockingCall = false; + const auto Thread2Body = [&]() { + // Wait command to start from thread1. + { + std::unique_lock Locker(Start2M); + if (!Start2) + Start2CV.wait_for(Locker, std::chrono::seconds(1), + [&] { return Start2; }); + } + // Try locking file. + ECT2a = fs::tryLockFile(FD2); + if (!ECT2a) + return; + // Give command to unlock the file. + { + std::unique_lock Locker(LockM); + Locked = true; + } + LockCV.notify_all(); + // Lock the file. + if (UseBlockingCall) + ECT2b = fs::lockFile(FD2); + else + ECT2b = fs::tryLockFile(FD2, std::chrono::seconds(5)); + if (ECT2b) + return; + fs::unlockFile(FD2); + }; + + auto Thread1 = std::thread(Thread1Body); + auto Thread2 = std::thread(Thread2Body); + Thread1.join(); + Thread2.join(); + ASSERT_NO_ERROR(ECT1a); + ASSERT_NO_ERROR(ECT1b); + ASSERT_ERROR(ECT2a); + ASSERT_NO_ERROR(ECT2b); + + UseBlockingCall = true; + Start2 = false; + Locked = false; + auto Thread3 = std::thread(Thread1Body); + auto Thread4 = std::thread(Thread2Body); + Thread3.join(); + Thread4.join(); + ASSERT_NO_ERROR(ECT1a); + ASSERT_NO_ERROR(ECT1b); + ASSERT_ERROR(ECT2a); + ASSERT_NO_ERROR(ECT2b); + +#endif +} + } // anonymous namespace