/************************************************************************* * * Copyright 2016 Realm Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * **************************************************************************/ #ifndef REALM_UTIL_INTERPROCESS_MUTEX #define REALM_UTIL_INTERPROCESS_MUTEX #include <realm/util/features.h> #include <realm/util/thread.hpp> #include <realm/util/file.hpp> #include <realm/utilities.hpp> #include <mutex> #include <map> #include <thread> #if REALM_PLATFORM_APPLE #include <dispatch/dispatch.h> #endif // Enable this only on platforms where it might be needed #if REALM_PLATFORM_APPLE || REALM_ANDROID #define REALM_ROBUST_MUTEX_EMULATION 1 #else #define REALM_ROBUST_MUTEX_EMULATION 0 #endif namespace realm::util { // fwd decl to support friend decl below class InterprocessCondVar; // A wrapper for a semaphore to expose a mutex interface. Unlike a real mutex, // this can be locked and unlocked from different threads. Currently only // implemented on Apple platforms class SemaphoreMutex { public: SemaphoreMutex() noexcept; ~SemaphoreMutex() noexcept; SemaphoreMutex(const SemaphoreMutex&) = delete; SemaphoreMutex& operator=(const SemaphoreMutex&) = delete; void lock() noexcept; void unlock() noexcept; bool try_lock() noexcept; private: #if REALM_PLATFORM_APPLE dispatch_semaphore_t m_semaphore; #endif }; /// Emulation of a Robust Mutex. /// A Robust Mutex is an interprocess mutex which will automatically /// release any locks held by a process when it crashes. Contrary to /// Posix robust mutexes, this robust mutex is not capable of informing /// participants that they have been granted a lock after a crash of /// the process holding it (though it could be added if needed). class InterprocessMutex { public: InterprocessMutex(); ~InterprocessMutex() noexcept; // Disable copying. Copying a locked Mutex will create a scenario // where the same file descriptor will be locked once but unlocked twice. InterprocessMutex(const InterprocessMutex&) = delete; InterprocessMutex& operator=(const InterprocessMutex&) = delete; #if REALM_ROBUST_MUTEX_EMULATION || defined(_WIN32) struct SharedPart { }; #else using SharedPart = RobustMutex; #endif /// You need to bind the emulation to a SharedPart in shared/mmapped memory. /// The SharedPart is assumed to have been initialized (possibly by another process) /// elsewhere. void set_shared_part(SharedPart& shared_part, const std::string& path, const std::string& mutex_name); void set_shared_part(SharedPart& shared_part, File&& lock_file); /// Destroy shared object. Potentially release system resources. Caller must /// ensure that the shared_part is not in use at the point of call. void release_shared_part(); /// Lock the mutex. If the mutex is already locked, wait for it to be unlocked. void lock(); /// Non-blocking attempt to lock the mutex. Returns true if the lock is obtained. /// If the lock can not be obtained return false immediately. bool try_lock(); /// Unlock the mutex void unlock(); /// Attempt to check if the mutex is valid (only relevant if not emulating) bool is_valid() noexcept; #if REALM_ROBUST_MUTEX_EMULATION constexpr static bool is_robust_on_this_platform = true; // we're faking it! #else constexpr static bool is_robust_on_this_platform = RobustMutex::is_robust_on_this_platform; #endif #if REALM_PLATFORM_APPLE // On Apple platforms we support locking and unlocking InterprocessMutex on // different threads, while on other platforms the locking thread owns the // mutex. The non-thread-confined version should be implementable on more // platforms if desired. constexpr static bool is_thread_confined = false; #else constexpr static bool is_thread_confined = true; #endif private: #if REALM_ROBUST_MUTEX_EMULATION struct LockInfo { File m_file; #if REALM_PLATFORM_APPLE SemaphoreMutex m_local_mutex; #else Mutex m_local_mutex; #endif LockInfo() {} ~LockInfo() noexcept; // Disable copying. LockInfo(const LockInfo&) = delete; LockInfo& operator=(const LockInfo&) = delete; }; /// InterprocessMutex created on the same file (same inode on POSIX) share the same LockInfo. /// LockInfo will be saved in a static map as a weak ptr and use the UniqueID as the key. /// Operations on the map need to be protected by s_mutex static std::map<File::UniqueID, std::weak_ptr<LockInfo>>* s_info_map; static Mutex* s_mutex; /// We manually initialize these static variables when first needed, /// creating them on the heap so that they last for the entire lifetime /// of the process. The destructor of these is never called; the /// process will clean up their memory when exiting. It is not enough /// to count instances of InterprocessMutex and clean up these statics when /// the count reaches zero because the program can create more /// InterprocessMutex instances before the process ends, so we really need /// these variables for the entire lifetime of the process. static std::once_flag s_init_flag; static void initialize_statics(); /// Only used for release_shared_part std::string m_filename; File::UniqueID m_fileuid; std::shared_ptr<LockInfo> m_lock_info; /// Free the lock info hold by this instance. /// If it is the last reference, underly resources will be freed as well. void free_lock_info(); #else SharedPart* m_shared_part = nullptr; #ifdef _WIN32 HANDLE m_handle = 0; #endif #endif friend class InterprocessCondVar; }; inline InterprocessMutex::InterprocessMutex() { #if REALM_ROBUST_MUTEX_EMULATION std::call_once(s_init_flag, initialize_statics); #endif } inline InterprocessMutex::~InterprocessMutex() noexcept { #ifdef _WIN32 if (m_handle) { bool b = CloseHandle(m_handle); REALM_ASSERT_RELEASE(b); } #endif #if REALM_ROBUST_MUTEX_EMULATION free_lock_info(); #endif } #if REALM_ROBUST_MUTEX_EMULATION inline InterprocessMutex::LockInfo::~LockInfo() noexcept { if (m_file.is_attached()) { m_file.close(); } } inline void InterprocessMutex::free_lock_info() { // It has not been initialized yet. if (!m_lock_info) return; std::lock_guard<Mutex> guard(*s_mutex); m_lock_info.reset(); if ((*s_info_map)[m_fileuid].expired()) { s_info_map->erase(m_fileuid); } m_filename.clear(); } inline void InterprocessMutex::initialize_statics() { s_mutex = new Mutex(); s_info_map = new std::map<File::UniqueID, std::weak_ptr<LockInfo>>(); } #endif inline void InterprocessMutex::set_shared_part(SharedPart& shared_part, const std::string& path, const std::string& mutex_name) { #if REALM_ROBUST_MUTEX_EMULATION static_cast<void>(shared_part); free_lock_info(); if (path.size() == 0) { m_filename = make_temp_file(mutex_name.c_str()); } else { m_filename = path + "." + mutex_name + ".mx"; } std::lock_guard<Mutex> guard(*s_mutex); // Try to get the file uid if the file exists if (auto uid = File::get_unique_id(m_filename)) { m_fileuid = std::move(*uid); auto result = s_info_map->find(m_fileuid); if (result != s_info_map->end()) { // File exists and the lock info has been created in the map. m_lock_info = result->second.lock(); return; } } // LockInfo has not been created yet. m_lock_info = std::make_shared<LockInfo>(); // Always use mod_Write to open file and retreive the uid in case other process // deletes the file. m_lock_info->m_file.open(m_filename, File::mode_Write); // exFAT does not allocate a unique id for the file until it's non-empty m_lock_info->m_file.resize(1); m_fileuid = m_lock_info->m_file.get_unique_id(); (*s_info_map)[m_fileuid] = m_lock_info; #elif defined(_WIN32) if (m_handle) { bool b = CloseHandle(m_handle); REALM_ASSERT_RELEASE(b); } // replace backslashes because they're significant in object namespace names std::string path_escaped = path; std::replace(path_escaped.begin(), path_escaped.end(), '\\', '/'); std::string name = "Local\\realm_named_intermutex_" + path_escaped + mutex_name; std::wstring wname(name.begin(), name.end()); m_handle = CreateMutexW(0, false, wname.c_str()); if (!m_handle) { throw std::system_error(GetLastError(), std::system_category(), "Error opening mutex"); } #else m_shared_part = &shared_part; static_cast<void>(path); static_cast<void>(mutex_name); #endif } inline void InterprocessMutex::set_shared_part(SharedPart& shared_part, File&& lock_file) { #if REALM_ROBUST_MUTEX_EMULATION static_cast<void>(shared_part); free_lock_info(); std::lock_guard<Mutex> guard(*s_mutex); m_fileuid = lock_file.get_unique_id(); auto result = s_info_map->find(m_fileuid); if (result == s_info_map->end()) { m_lock_info = std::make_shared<LockInfo>(); m_lock_info->m_file = std::move(lock_file); (*s_info_map)[m_fileuid] = m_lock_info; } else { // File exists and the lock info has been created in the map. m_lock_info = result->second.lock(); lock_file.close(); } #else m_shared_part = &shared_part; static_cast<void>(lock_file); #endif } inline void InterprocessMutex::release_shared_part() { #if REALM_ROBUST_MUTEX_EMULATION if (!m_filename.empty()) File::try_remove(m_filename); free_lock_info(); #else m_shared_part = nullptr; #endif } inline void InterprocessMutex::lock() { #if REALM_ROBUST_MUTEX_EMULATION std::unique_lock mutex_lock(m_lock_info->m_local_mutex); m_lock_info->m_file.lock(); mutex_lock.release(); #else #ifdef _WIN32 DWORD d = WaitForSingleObject(m_handle, INFINITE); REALM_ASSERT_RELEASE(d != WAIT_FAILED); #else REALM_ASSERT(m_shared_part); m_shared_part->lock([]() {}); #endif #endif } inline bool InterprocessMutex::try_lock() { #if REALM_ROBUST_MUTEX_EMULATION std::unique_lock mutex_lock(m_lock_info->m_local_mutex, std::try_to_lock_t()); if (!mutex_lock.owns_lock()) { return false; } bool success = m_lock_info->m_file.try_lock(); if (success) { mutex_lock.release(); return true; } else { return false; } #else #ifdef _WIN32 DWORD ret = WaitForSingleObject(m_handle, 0); REALM_ASSERT_RELEASE(ret != WAIT_FAILED); if (ret == WAIT_OBJECT_0) { return true; } else { return false; } #else REALM_ASSERT(m_shared_part); return m_shared_part->try_lock([]() {}); #endif #endif } inline void InterprocessMutex::unlock() { #if REALM_ROBUST_MUTEX_EMULATION m_lock_info->m_file.unlock(); m_lock_info->m_local_mutex.unlock(); #else #ifdef _WIN32 bool b = ReleaseMutex(m_handle); REALM_ASSERT_RELEASE(b); #else REALM_ASSERT(m_shared_part); m_shared_part->unlock(); #endif #endif } inline bool InterprocessMutex::is_valid() noexcept { #if REALM_ROBUST_MUTEX_EMULATION return true; #elif defined(_WIN32) // There is no safe way of testing if the m_handle mutex handle is valid on Windows, without having bad side // effects for the cases where it is indeed invalid. If m_handle contains an arbitrary value, it might by // coincidence be equal to a real live handle of another kind. This excludes a try_lock implementation and many // other ideas. return true; #else REALM_ASSERT(m_shared_part); return m_shared_part->is_valid(); #endif } } // namespace realm::util #endif // #ifndef REALM_UTIL_INTERPROCESS_MUTEX