#include "file-lock.h" #include "log.h" #include #include #include #include #include #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN #ifndef NOMINMAX #define NOMINMAX #endif #include #include #else #include #include #include #include #endif common_file_lock::common_file_lock(const std::string & lock_path) { this->lock_path = lock_path; } #if defined(_WIN32) // the \\?\ prefix (for paths longer than 255 chars) is only accepted on absolute paths // with backslash separators: normalize first, then check the length of the absolute form static std::wstring as_extended_path(const std::string & path) { // convert from the narrow encoding std::filesystem uses on this system int wlen = MultiByteToWideChar(CP_ACP, 0, path.c_str(), (int) path.size(), nullptr, 0); if (wlen <= 0) { return std::wstring(path.begin(), path.end()); // best effort for exotic encodings } std::wstring wide(wlen, L'\0'); MultiByteToWideChar(CP_ACP, 0, path.c_str(), (int) path.size(), wide.data(), wlen); if (wide.rfind(L"\\\\?\\", 0) == 0) { return wide; } // GetFullPathNameW resolves relative paths and normalizes separators DWORD size = GetFullPathNameW(wide.c_str(), 0, nullptr, nullptr); if (size == 0) { return wide; } std::wstring full(size, L'\0'); DWORD written = GetFullPathNameW(wide.c_str(), size, full.data(), nullptr); if (written == 0 || written >= size) { return wide; } full.resize(written); if (full.size() <= 255) { return wide; // short enough: open the path exactly as given } if (full.rfind(L"\\\\", 0) == 0) { return L"\\\\?\\UNC\\" + full.substr(2); // \\server\share -> \\?\UNC\server\share } return L"\\\\?\\" + full; } #endif common_file_lock::status common_file_lock::try_lock() { if (lock_path.empty()) { return status::acquired; // no lock path, nothing to lock } #if defined(_WIN32) if (fd < 0) { // opened with a wide path: extended paths cannot go through the narrow CRT std::wstring lock_path_w = as_extended_path(lock_path); fd = _wopen(lock_path_w.c_str(), _O_RDWR | _O_CREAT | _O_BINARY | _O_NOINHERIT, _S_IREAD | _S_IWRITE); if (fd < 0) { LOG_ERR("%s: cannot open lock file: %s\n", __func__, lock_path.c_str()); return status::error; } } // lock the first byte, the file itself stays empty OVERLAPPED ov = {}; locked = LockFileEx((HANDLE) _get_osfhandle(fd), LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &ov); if (locked) { return status::acquired; } if (GetLastError() == ERROR_LOCK_VIOLATION) { return status::busy; } LOG_ERR("%s: file locking failed (%d)\n", __func__, (int) GetLastError()); return status::error; #else if (fd < 0) { int open_flags = O_RDWR | O_CREAT | O_CLOEXEC; #ifdef O_NOFOLLOW open_flags |= O_NOFOLLOW; #endif // group-writable, so users sharing a cache can wait on the lock fd = open(lock_path.c_str(), open_flags, 0664); if (fd < 0) { LOG_ERR("%s: cannot open lock file: %s\n", __func__, lock_path.c_str()); return status::error; } // open() filters the mode through the umask (void) fchmod(fd, 0664); } if (flock(fd, LOCK_EX | LOCK_NB) == 0) { locked = true; return status::acquired; } // EACCES is reported as contention on some filesystems if (errno == EWOULDBLOCK || errno == EAGAIN || errno == EACCES) { return status::busy; } // flock is not implemented (some NFS/FUSE mounts): report it instead of racing LOG_ERR("%s: file locking failed (%s)\n", __func__, strerror(errno)); return status::error; #endif } bool common_file_lock::acquire(const std::function & keep_waiting) { // the lock file is opened by try_lock, its parent must exist by then if (!lock_path.empty()) { std::error_code ec; auto lock_dir = std::filesystem::path(lock_path).parent_path(); if (!lock_dir.empty()) { std::filesystem::create_directories(lock_dir, ec); if (ec) { LOG_ERR("%s: unable to create lock directory '%s': %s\n", __func__, lock_dir.string().c_str(), ec.message().c_str()); return false; } } } while (true) { switch (try_lock()) { case status::acquired: return true; case status::busy: if (keep_waiting && !keep_waiting()) { return false; } std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; case status::error: return false; } } } // the lock file is never deleted: another process could then create a fresh // file at the same path and lock that void common_file_lock::close() { #if defined(_WIN32) if (fd >= 0) { if (locked) { OVERLAPPED ov = {}; UnlockFileEx((HANDLE) _get_osfhandle(fd), 0, 1, 0, &ov); } _close(fd); } #else if (fd >= 0) { if (locked) { flock(fd, LOCK_UN); } ::close(fd); } #endif fd = -1; locked = false; } common_file_lock::~common_file_lock() { close(); }