diff options
| -rw-r--r-- | build/visual-studio/slang-unit-test-tool/slang-unit-test-tool.vcxproj | 1 | ||||
| -rw-r--r-- | build/visual-studio/slang-unit-test-tool/slang-unit-test-tool.vcxproj.filters | 3 | ||||
| -rw-r--r-- | premake5.lua | 3 | ||||
| -rw-r--r-- | source/core/slang-io.cpp | 164 | ||||
| -rw-r--r-- | source/core/slang-io.h | 73 | ||||
| -rw-r--r-- | tools/slang-unit-test/unit-test-lock-file.cpp | 128 |
6 files changed, 342 insertions, 30 deletions
diff --git a/build/visual-studio/slang-unit-test-tool/slang-unit-test-tool.vcxproj b/build/visual-studio/slang-unit-test-tool/slang-unit-test-tool.vcxproj index 9253b2930..79617d702 100644 --- a/build/visual-studio/slang-unit-test-tool/slang-unit-test-tool.vcxproj +++ b/build/visual-studio/slang-unit-test-tool/slang-unit-test-tool.vcxproj @@ -283,6 +283,7 @@ <ClCompile Include="..\..\..\tools\slang-unit-test\unit-test-io.cpp" />
<ClCompile Include="..\..\..\tools\slang-unit-test\unit-test-json-native.cpp" />
<ClCompile Include="..\..\..\tools\slang-unit-test\unit-test-json.cpp" />
+ <ClCompile Include="..\..\..\tools\slang-unit-test\unit-test-lock-file.cpp" />
<ClCompile Include="..\..\..\tools\slang-unit-test\unit-test-md5.cpp" />
<ClCompile Include="..\..\..\tools\slang-unit-test\unit-test-memory-arena.cpp" />
<ClCompile Include="..\..\..\tools\slang-unit-test\unit-test-offset-container.cpp" />
diff --git a/build/visual-studio/slang-unit-test-tool/slang-unit-test-tool.vcxproj.filters b/build/visual-studio/slang-unit-test-tool/slang-unit-test-tool.vcxproj.filters index b4ce5c734..20e28f2f8 100644 --- a/build/visual-studio/slang-unit-test-tool/slang-unit-test-tool.vcxproj.filters +++ b/build/visual-studio/slang-unit-test-tool/slang-unit-test-tool.vcxproj.filters @@ -50,6 +50,9 @@ <ClCompile Include="..\..\..\tools\slang-unit-test\unit-test-json.cpp">
<Filter>Source Files</Filter>
</ClCompile>
+ <ClCompile Include="..\..\..\tools\slang-unit-test\unit-test-lock-file.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
<ClCompile Include="..\..\..\tools\slang-unit-test\unit-test-md5.cpp">
<Filter>Source Files</Filter>
</ClCompile>
diff --git a/premake5.lua b/premake5.lua index 92243263a..85532a07f 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1441,6 +1441,9 @@ tool "slangd" includedirs { "." } addSourceDir "tools/unit-test" links { "lz4", "miniz", "core", "compiler-core", "slang" } + if not targetInfo.isWindows then + links { "pthread" } + end if enableProfile then tool "slang-profile" diff --git a/source/core/slang-io.cpp b/source/core/slang-io.cpp index 345a2c353..d8ef48ee3 100644 --- a/source/core/slang-io.cpp +++ b/source/core/slang-io.cpp @@ -29,6 +29,7 @@ # include <dirent.h> # include <sys/stat.h> +# include <sys/file.h> #endif #if SLANG_APPLE_FAMILY @@ -272,7 +273,7 @@ namespace Slang else return ""; } - + /* static */void Path::append(StringBuilder& ioBuilder, const UnownedStringSlice& path) { if (ioBuilder.getLength() == 0) @@ -287,7 +288,7 @@ namespace Slang { ioBuilder.append(kPathDelimiter); } - // Check that path doesn't start with a path delimiter + // Check that path doesn't start with a path delimiter SLANG_ASSERT(!isDelimiter(path[0])); // Append the path ioBuilder.append(path); @@ -325,7 +326,7 @@ namespace Slang { switch (element.getLength()) { - case 0: + case 0: { // We'll just assume it is return true; @@ -333,7 +334,7 @@ namespace Slang case 2: { // Look for a windows like drive spec - const char firstChar = element[0]; + const char firstChar = element[0]; return element[1] == ':' && ((firstChar >= 'a' && firstChar <= 'z') || (firstChar >= 'A' && firstChar <= 'Z')); } default: return false; @@ -363,7 +364,7 @@ namespace Slang return true; } - // Check for drive + // Check for drive if (isDriveSpecification(getFirstElement(path))) { return true; @@ -387,19 +388,19 @@ namespace Slang while (cur < end && !isDelimiter(*cur)) cur++; splitOut.add(UnownedStringSlice(start, cur)); - + // Next start = cur + 1; } - // Okay if the end is empty. And we aren't with a spec like // or c:/ , then drop the final slash + // Okay if the end is empty. And we aren't with a spec like // or c:/ , then drop the final slash if (splitOut.getCount() > 1 && splitOut.getLast().getLength() == 0) { if (splitOut.getCount() == 2 && isDriveSpecification(splitOut[0])) { return; } - // Remove the last + // Remove the last splitOut.removeLast(); } } @@ -440,7 +441,7 @@ namespace Slang return SLANG_E_NOT_FOUND; } - // We allow splitPath.getCount() == 0, because + // We allow splitPath.getCount() == 0, because // the original path could have been '.' or './.' // // Special handling this case is in Path::join @@ -470,7 +471,7 @@ namespace Slang const UnownedStringSlice& cur = ioSplit[i]; if (cur == "." && ioSplit.getCount() > 1) { - // Just remove it + // Just remove it ioSplit.removeAt(i); i--; } @@ -525,7 +526,7 @@ namespace Slang { #if defined(_WIN32) return _wmkdir(path.toWString()) == 0; -#else +#else return mkdir(path.getBuffer(), 0777) == 0; #endif } @@ -581,14 +582,14 @@ namespace Slang if (!absPath) { return SLANG_FAIL; - } + } canonicalPathOut = String::fromWString(absPath); ::free(absPath); return SLANG_OK; #else # if 1 - + // http://man7.org/linux/man-pages/man3/realpath.3.html char* canonicalPath = ::realpath(path.begin(), nullptr); if (canonicalPath) @@ -637,7 +638,7 @@ namespace Slang SlangPathType pathType; SLANG_RETURN_ON_FAIL(getPathType(path, &pathType)); - + switch (pathType) { case SLANG_PATH_TYPE_FILE: @@ -706,7 +707,7 @@ namespace Slang /* static */SlangResult Path::find(const String& directoryPath, const char* pattern, Visitor* visitor) { DIR* directory = opendir(directoryPath.getBuffer()); - + if (!directory) { return SLANG_E_NOT_FOUND; @@ -766,7 +767,7 @@ namespace Slang /// Gets the path to the executable that was invoked that led to the current threads execution /// If run from a shared library/dll will be the path of the executable that loaded said library /// @param outPath Pointer to buffer to hold the path. - /// @param ioPathSize Size of the buffer to hold the path (including zero terminator). + /// @param ioPathSize Size of the buffer to hold the path (including zero terminator). /// @return SLANG_OK on success, SLANG_E_BUFFER_TOO_SMALL if buffer is too small. If ioPathSize is changed it will be the required size static SlangResult _calcExectuablePath(char* outPath, size_t* ioSize) { @@ -776,7 +777,7 @@ namespace Slang #if SLANG_WINDOWS_FAMILY // https://docs.microsoft.com/en-us/windows/desktop/api/libloaderapi/nf-libloaderapi-getmodulefilenamea - + DWORD res = ::GetModuleFileNameA(::GetModuleHandle(nullptr), outPath, DWORD(bufferSize)); // If it fits it's the size not including terminator. So must be less than bufferSize if (res < bufferSize) @@ -801,7 +802,7 @@ namespace Slang // Zero terminate outPath[resSize - 1] = 0; return SLANG_OK; -# else +# else String text = Slang::File::readAllText("/proc/self/maps"); Index startIndex = text.indexOf('/'); if (startIndex == Index(-1)) @@ -963,7 +964,7 @@ namespace Slang SLANG_RETURN_ON_FAIL(stream.write(data, size)); return SLANG_OK; } - + SlangResult File::writeAllText(const Slang::String& fileName, const Slang::String& text) { RefPtr<FileStream> stream = new FileStream; @@ -975,7 +976,7 @@ namespace Slang return SLANG_OK; } - + /* static */SlangResult File::writeNativeText(const String& path, const void* data, size_t size) { FILE* file = fopen(path.getBuffer(), "w"); @@ -1075,4 +1076,127 @@ namespace Slang return uri; } + + SlangResult LockFile::open(const String& fileName) + { +#if SLANG_WINDOWS_FAMILY + m_fileHandle = ::CreateFileW( + fileName.toWString(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, + NULL + ); + m_isOpen = m_fileHandle != INVALID_HANDLE_VALUE; +#else + m_fileHandle = ::open(fileName.getBuffer(), O_RDWR | O_CREAT, 0600); + m_isOpen = m_fileHandle != -1; +#endif + return m_isOpen ? SLANG_OK : SLANG_E_CANNOT_OPEN; + } + + void LockFile::close() + { + if (!m_isOpen) + return; + +#if SLANG_WINDOWS_FAMILY + ::CloseHandle(m_fileHandle); +#else + ::close(m_fileHandle); +#endif + + m_isOpen = false; + } + + SlangResult LockFile::tryLock(LockType lockType) + { + if (!m_isOpen) + return SLANG_E_CANNOT_OPEN; + + SlangResult result = SLANG_OK; +#if SLANG_WINDOWS_FAMILY + OVERLAPPED overlapped = {0}; + DWORD flags = lockType == LockType::Shared ? LOCKFILE_FAIL_IMMEDIATELY : (LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY); + if (::LockFileEx(m_fileHandle, flags, DWORD(0), ~DWORD(0), ~DWORD(0), &overlapped) == 0) + { + result = SLANG_E_TIME_OUT; + } +#else + int operation = lockType == LockType::Shared ? (LOCK_SH | LOCK_NB) : (LOCK_EX | LOCK_NB); + if (::flock(m_fileHandle, operation) != 0) + { + result = SLANG_E_TIME_OUT; + } +#endif + return result; + } + + SlangResult LockFile::lock(LockType lockType) + { + if (!m_isOpen) + return SLANG_E_CANNOT_OPEN; + + SlangResult result = SLANG_OK; +#if SLANG_WINDOWS_FAMILY + OVERLAPPED overlapped = {0}; + overlapped.hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL); + DWORD flags = lockType == LockType::Shared ? 0 : LOCKFILE_EXCLUSIVE_LOCK; + if (::LockFileEx(m_fileHandle, flags, DWORD(0), ~DWORD(0), ~DWORD(0), &overlapped) == 0) + { + auto err = ::GetLastError(); + if (err == ERROR_IO_PENDING) + { + DWORD bytes; + if (::GetOverlappedResult(m_fileHandle, &overlapped, &bytes, TRUE) == 0) + { + result = SLANG_E_INTERNAL_FAIL; + } + } + else + { + result = SLANG_E_INTERNAL_FAIL; + } + } + ::CloseHandle(overlapped.hEvent); +#else + int operation = lockType == LockType::Shared ? LOCK_SH : LOCK_EX; + if (::flock(m_fileHandle, operation) != 0) + { + result = SLANG_E_INTERNAL_FAIL; + } +#endif + return result; +} + + SlangResult LockFile::unlock() + { + if (!m_isOpen) + return SLANG_E_CANNOT_OPEN; + +#if SLANG_WINDOWS_FAMILY + OVERLAPPED overlapped = {0}; + if (::UnlockFileEx(m_fileHandle, DWORD(0), ~DWORD(0), ~DWORD(0), &overlapped) == 0) + { + return SLANG_E_INTERNAL_FAIL; + } +#else + if (::flock(m_fileHandle, LOCK_UN) != 0) + { + return SLANG_E_INTERNAL_FAIL; + } +#endif + return SLANG_OK; +} + + LockFile::LockFile() + : m_isOpen(false) + {} + + LockFile::~LockFile() + { + close(); + } } diff --git a/source/core/slang-io.h b/source/core/slang-io.h index 292328603..fc5cbfa9d 100644 --- a/source/core/slang-io.h +++ b/source/core/slang-io.h @@ -25,14 +25,14 @@ namespace Slang static SlangResult writeNativeText(const String& filename, const void* data, size_t size); static SlangResult writeAllBytes(const String& fileName, const void* data, size_t size); - + static SlangResult remove(const String& fileName); static SlangResult makeExecutable(const String& fileName); /// Creates a temporary file typically in some way based on the prefix /// The file will be *created* with the outFileName, on success. - /// It's creation in necessary to lock that particular name. + /// It's creation in necessary to lock that particular name. static SlangResult generateTemporary(const UnownedStringSlice& prefix, String& outFileName); }; @@ -47,15 +47,15 @@ namespace Slang { /// Can only simplify to an absolute path. Will return an error if not possible. /// Useful to constrain a path, such as when wanting something like 'chroot'. - AbsoluteOnly = 0x1, - /// If the simplified path is a root path, remove the root. + AbsoluteOnly = 0x1, + /// If the simplified path is a root path, remove the root. /// Will mean that for example /// "/" -> "." /// "/a/.." -> "." /// "/a" -> "a" - /// Its worth noting that a path prefixed "/" will never be returned and if *just* the root it specified + /// Its worth noting that a path prefixed "/" will never be returned and if *just* the root it specified /// it will return as ".". - NoRoot = 0x2, + NoRoot = 0x2, }; }; @@ -136,7 +136,7 @@ namespace Slang /// Combine path sections and store the result in outBuilder static void combineIntoBuilder(const UnownedStringSlice& path1, const UnownedStringSlice& path2, StringBuilder& outBuilder); - /// Append a path, taking into account path separators onto the end of ioBuilder + /// Append a path, taking into account path separators onto the end of ioBuilder static void append(StringBuilder& ioBuilder, const UnownedStringSlice& path); static bool createDirectory(const String& path); @@ -155,12 +155,12 @@ namespace Slang /// Relative paths that are in effect "." will become [] static void split(const UnownedStringSlice& path, List<UnownedStringSlice>& splitOut); - /// Strips .. and . as much as it can + /// Strips .. and . as much as it can static String simplify(const UnownedStringSlice& path); static String simplify(const String& path) { return simplify(path.getUnownedSlice()); } /// Given a path simplifies it such the the resultant path is absolute (ie contains no . or ..) - /// Same behavior as simplify around the root + /// Same behavior as simplify around the root static SlangResult simplify(const UnownedStringSlice& path, SimplifyStyle style, StringBuilder& outPath); static SlangResult simplify(const String& path, SimplifyStyle style, StringBuilder& outPath) { return simplify(path.getUnownedSlice(), style, outPath); } static SlangResult simplify(const char* path, SimplifyStyle style, StringBuilder& outPath) { return simplify(UnownedStringSlice(path), style, outPath); } @@ -199,7 +199,7 @@ namespace Slang /// Returns the first element of the path or an empty slice if there is none /// This broadly equivalent to returning the first element of split /// @param path Path to extract first element from - /// @return The first element of the path, or empty + /// @return The first element of the path, or empty static UnownedStringSlice getFirstElement(const UnownedStringSlice& path); /// Remove a file or directory at specified path. The directory must be empty for it to be removed @@ -225,6 +225,59 @@ namespace Slang static bool isSafeURIChar(char ch); }; + /// Helper class abstracting lock files. + /// Uses LockFileEx() on windows systems and flock() on POSIX systems. + class LockFile + { + public: + enum class LockType + { + Exclusive, + Shared, + }; + + /// Open the lock file. This will create the file if it doesn't exist yet. + /// @param fileName File name to open. + /// @return SLANG_OK on success. + SlangResult open(const String& fileName); + + /// Closes the lock file. + void close(); + + /// Returns true if the lock file is open. + bool isOpen() const { return m_isOpen; } + + /// Acquire the lock in non-blocking mode. + /// @param lockType Lock type (Exclusive or Shared). + /// @return SLANG_OK on success. SLANG_E_TIME_OUT if the lock is already held. + SlangResult tryLock(LockType lockType = LockType::Exclusive); + + /// Acquire the lock in blocking mode. + /// @param lockType Lock type (Exclusive or Shared). + /// @return SLANG_OK on success. + SlangResult lock(LockType lockType = LockType::Exclusive); + + /// Release the lock. + /// @return SLANG_OK on success. + SlangResult unlock(); + + LockFile(); + ~LockFile(); + + private: + LockFile(const LockFile&) = delete; + LockFile(LockFile&) = delete; + LockFile& operator=(const LockFile&) = delete; + LockFile& operator=(const LockFile&&) = delete; + +#if SLANG_WINDOWS_FAMILY + void* m_fileHandle; +#else + int m_fileHandle; +#endif + bool m_isOpen; + }; + } #endif diff --git a/tools/slang-unit-test/unit-test-lock-file.cpp b/tools/slang-unit-test/unit-test-lock-file.cpp new file mode 100644 index 000000000..c5709242d --- /dev/null +++ b/tools/slang-unit-test/unit-test-lock-file.cpp @@ -0,0 +1,128 @@ +// unit-test-lock-file.cpp +#include "tools/unit-test/slang-unit-test.h" + +#include "../../source/core/slang-io.h" + +#include <atomic> +#include <future> +#include <thread> +#include <vector> + +using namespace Slang; + +SLANG_UNIT_TEST(lockFile) +{ + static const String fileName = "test_lock_file"; + + // Open/close lock file. + { + LockFile file; + SLANG_CHECK(file.isOpen() == false); + SLANG_CHECK(file.open(fileName) == SLANG_OK); + SLANG_CHECK(file.isOpen() == true); + SLANG_CHECK(File::exists(fileName) == true); + file.close(); + SLANG_CHECK(file.isOpen() == false); + } + + // Test using multiple threads. + { + static std::atomic<uint32_t> lockCounter; + static std::atomic<uint32_t> unlockCounter; + + struct LockTask + { + std::thread thread; + std::promise<void> startPromise; + std::future<void> startFuture; + LockFile lockFile; + SlangResult openResult = false; + SlangResult tryLockSharedResult = false; + SlangResult tryLockExclusiveResult = false; + SlangResult lockResult = false; + SlangResult unlockResult = false; + uint32_t lockIteration = 0; + uint32_t unlockIteration = 0; + + LockTask() + : startFuture(startPromise.get_future()) + { + openResult = lockFile.open(fileName); + } + + void run() + { + tryLockSharedResult = lockFile.tryLock(LockFile::LockType::Shared); + tryLockExclusiveResult = lockFile.tryLock(LockFile::LockType::Exclusive); + startPromise.set_value(); + lockResult = lockFile.lock(LockFile::LockType::Exclusive); + lockIteration = lockCounter.fetch_add(1); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + unlockIteration = unlockCounter.fetch_add(1); + unlockResult = lockFile.unlock(); + } + }; + + // Acquire lock from main thread. + LockFile lockFile; + SLANG_CHECK(lockFile.open(fileName) == SLANG_OK); + SLANG_CHECK(lockFile.lock(LockFile::LockType::Exclusive) == SLANG_OK); + + // Make sure we cannot acquire the lock in non-blocking mode from a second instance. + LockFile lockFile2; + SLANG_CHECK(lockFile2.open(fileName) == SLANG_OK); + SLANG_CHECK(lockFile2.tryLock(LockFile::LockType::Shared) == SLANG_E_TIME_OUT); + SLANG_CHECK(lockFile2.tryLock(LockFile::LockType::Exclusive) == SLANG_E_TIME_OUT); + + // Start a number of threads and wait for them to start up. + // Each thread immediately tries to acquire the lock in non-blocking mode (expected to fail). + // Next each thread acquires the lock in blocking mode. + std::vector<LockTask> tasks(32); + for (auto& task : tasks) + { + task.thread = std::thread(&LockTask::run, &task); + task.startFuture.wait(); + } + + // Make sure none of the threads were able to acquire the lock yet. + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + SLANG_CHECK(lockCounter == 0); + + // Release the lock from the main thread. This will allow all the other + // threads to acquire the lock, one after the other. + SLANG_CHECK(lockFile.unlock() == SLANG_OK); + + // Wait for all threads to finish and make sure they behaved as expected. + std::vector<bool> lockIterationUsed(tasks.size(), false); + std::vector<bool> unlockIterationUsed(tasks.size(), false); + for (auto& task : tasks) + { + task.thread.join(); + + SLANG_CHECK(task.openResult == SLANG_OK); + SLANG_CHECK(task.tryLockSharedResult == SLANG_E_TIME_OUT); + SLANG_CHECK(task.tryLockExclusiveResult == SLANG_E_TIME_OUT); + SLANG_CHECK(task.lockResult == SLANG_OK); + SLANG_CHECK(task.unlockResult == SLANG_OK); + SLANG_CHECK(task.lockIteration < lockIterationUsed.size()); + SLANG_CHECK(task.unlockIteration < unlockIterationUsed.size()); + SLANG_CHECK(task.unlockIteration == task.lockIteration); + SLANG_CHECK(lockIterationUsed[task.lockIteration] == false); + SLANG_CHECK(unlockIterationUsed[task.unlockIteration] == false); + lockIterationUsed[task.lockIteration] = true; + unlockIterationUsed[task.unlockIteration] = true; + } + + // Ensure all threads did manage to acquire the lock. + SLANG_CHECK(lockCounter == tasks.size()); + SLANG_CHECK(unlockCounter == tasks.size()); + + // Check that we can now acquire the lock in non-blocking mode. + SLANG_CHECK(lockFile2.tryLock(LockFile::LockType::Exclusive) == SLANG_OK); + SLANG_CHECK(lockFile2.unlock() == SLANG_OK); + } + + // Cleanup. + File::remove(fileName); + SLANG_CHECK(File::exists(fileName) == false); +} |
