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,50 @@ 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. +/// +/// @note Care should be taken when using this function in a multithreaded +/// context, as it may not prevent other threads in the same process from +/// obtaining a lock on the same file, even if they are using a different file +/// descriptor. +std::error_code tryLockFileWithTimeout(int FD, + std::chrono::milliseconds Timeout); + +/// Try locking file. +/// +/// This function acts as @ref tryLockFileWithTimeout but it returns +/// immediately. +inline std::error_code tryLockFile(int FD) { + return tryLockFileWithTimeout(FD, std::chrono::milliseconds(0)); +} + +/// Lock the file. +/// +/// This function acts as @ref tryLockFileWithTimeout 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 @@ -33,6 +33,7 @@ #include #include +#include #ifdef __APPLE__ #include @@ -1047,6 +1048,51 @@ return NumRead; } +std::error_code tryLockFileWithTimeout(int FD, + std::chrono::milliseconds Timeout) { + auto Start = std::chrono::steady_clock::now(); + auto End = Start + Timeout; + do { + struct flock Lock; + memset(&Lock, 0, sizeof(Lock)); + Lock.l_type = F_WRLCK; + Lock.l_whence = SEEK_SET; + Lock.l_start = 0; + Lock.l_len = 0; + if (::fcntl(FD, F_SETLK, &Lock) != -1) + return std::error_code(); + int Error = errno; + if (Error != EACCES && Error != EAGAIN) + return std::error_code(Error, std::generic_category()); + usleep(1000); + } while (std::chrono::steady_clock::now() < End); + return make_error_code(errc::no_lock_available); +} + +std::error_code lockFile(int FD) { + struct flock Lock; + memset(&Lock, 0, sizeof(Lock)); + Lock.l_type = F_WRLCK; + Lock.l_whence = SEEK_SET; + Lock.l_start = 0; + Lock.l_len = 0; + if (::fcntl(FD, F_SETLKW, &Lock) != -1) + return std::error_code(); + int Error = errno; + return std::error_code(Error, std::generic_category()); +} + +std::error_code unlockFile(int FD) { + struct flock Lock; + Lock.l_type = F_UNLCK; + Lock.l_whence = SEEK_SET; + Lock.l_start = 0; + Lock.l_len = 0; + if (::fcntl(FD, F_SETLK, &Lock) != -1) + return std::error_code(); + return std::error_code(errno, std::generic_category()); +} + 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,44 @@ return readNativeFileImpl(FileHandle, Buf, &Overlapped); } +std::error_code tryLockFileWithTimeout(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/ProgramTest.cpp b/llvm/unittests/Support/ProgramTest.cpp --- a/llvm/unittests/Support/ProgramTest.cpp +++ b/llvm/unittests/Support/ProgramTest.cpp @@ -14,6 +14,7 @@ #include "llvm/Support/Path.h" #include "gtest/gtest.h" #include +#include #if defined(__APPLE__) # include #elif !defined(_MSC_VER) @@ -336,4 +337,57 @@ ASSERT_NO_ERROR(fs::remove(TestDirectory.str())); } +TEST_F(ProgramEnvTest, TestLockFile) { + using namespace llvm::sys; + + if (const char *LockedFile = getenv("LLVM_PROGRAM_TEST_LOCKED_FILE")) { + // Child process. + int FD2; + ASSERT_NO_ERROR(fs::openFileForReadWrite(LockedFile, FD2, + fs::CD_OpenExisting, fs::OF_None)); + + std::error_code ErrC = fs::tryLockFileWithTimeout(FD2, std::chrono::seconds(5)); + ASSERT_NO_ERROR(ErrC); + ASSERT_NO_ERROR(fs::unlockFile(FD2)); + close(FD2); + exit(0); + } + + // Create file that will be locked. + SmallString<64> LockedFile; + int FD1; + ASSERT_NO_ERROR( + fs::createTemporaryFile("TestLockFile", "temp", FD1, LockedFile)); + + std::string Executable = + sys::fs::getMainExecutable(TestMainArgv0, &ProgramTestStringArg1); + StringRef argv[] = {Executable, "--gtest_filter=ProgramEnvTest.TestLockFile"}; + + // Add LLVM_PROGRAM_TEST_LOCKED_FILE to the environment of the child. + std::string EnvVar = "LLVM_PROGRAM_TEST_LOCKED_FILE="; + EnvVar += LockedFile.str(); + addEnvVar(EnvVar); + + // Lock the file. + ASSERT_NO_ERROR(fs::tryLockFile(FD1)); + + std::string Error; + bool ExecutionFailed; + ProcessInfo PI2 = ExecuteNoWait(Executable, argv, getEnviron(), {}, 0, &Error, + &ExecutionFailed); + ASSERT_FALSE(ExecutionFailed) << Error; + ASSERT_TRUE(Error.empty()); + ASSERT_NE(PI2.Pid, ProcessInfo::InvalidPid) << "Invalid process id"; + + // Wait some time to give the child process a chance to start. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + ASSERT_NO_ERROR(fs::unlockFile(FD1)); + ProcessInfo WaitResult = llvm::sys::Wait(PI2, 5 /* seconds */, true, &Error); + ASSERT_TRUE(Error.empty()); + ASSERT_EQ(0, WaitResult.ReturnCode); + ASSERT_EQ(WaitResult.Pid, PI2.Pid); + sys::fs::remove(LockedFile); +} + } // end anonymous namespace