diff options
Diffstat (limited to 'libs/usvfs/src/shared/tree_container.h')
| -rw-r--r-- | libs/usvfs/src/shared/tree_container.h | 630 |
1 files changed, 630 insertions, 0 deletions
diff --git a/libs/usvfs/src/shared/tree_container.h b/libs/usvfs/src/shared/tree_container.h new file mode 100644 index 0000000..a2f61fa --- /dev/null +++ b/libs/usvfs/src/shared/tree_container.h @@ -0,0 +1,630 @@ +#pragma once + +#include "directory_tree.h" +#include "shared_memory.h" + +namespace usvfs::shared +{ + +// smart pointer to DirectoryTrees (only intended for top-level nodes). This +// will transparently switch to new shared memory regions in case they get +// reallocated +// +// +// reassign() is called from a variety of places when the current chunk of +// shared memory is full or is marked as being outdated; its job is to either +// find another chunk that may have been created by another process or to create +// a brand new one +// +// +// if there is only a single process hooked, it will slowly fill up the shared +// memory when adding files, eventually throw a bi::bad_alloc and end up in +// reassign(); a new block will be allocated, the data copied over, and the old +// block will be deallocated +// +// +// when multiple processes are involved, things are more complicated +// +// two processes A and B will start by using the same shared memory, but they +// have their own pointer to it that's local to the process (the `m_TreeMeta` +// member variable) +// +// so when process A fills up the shared memory and reallocates it, process B +// is still pointing to the old shared memory; only when process B does some +// operation that accesses the file tree will the pointer be checked and +// adjusted to point to the new shared memory +// +// in this example, when process A ran out of memory, it set `outdated` to +// `true` in the shared memory block, allocated a new one and copied the data +// over, but it did not deallocate the block because process B is still +// pointing to it +// +// when process B tries to access the block, it checks `outdated` (see get()); +// if it's true, it means that it's pointing to an outdated shared memory block +// and must find the new one that process A created +// +// the new block is not necessarily the very next name, it is possible for +// process A to burn through a series of blocks quickly when adding a bunch of +// new files, and these blocks will all have been deallocated by the time +// process B tries to find the newest one +// +// so when a process sees its block as outdated, it will try to open a bunch of +// names until one exists that is not outdated; if it can't find the block +// (shouldn't happen), it will just create a new one +// +template <typename TreeT> +class TreeContainer +{ +public: + /** + * @brief Constructor + * @param SHMName name of the shared memory holding the tree. This should contain the + * running number + * @param size initial size in bytes of the container. since the tree is resized by + * doubling this should be a power of two. 64k is supposed to be the page size on + * windows so smaller allocations make little sense + * @note size can't be too small. If initial allocations fail automatic growing won't + * work + */ + TreeContainer(const std::string& SHMName, size_t size = 64 * 1024) + : m_TreeMeta(nullptr), m_SHMName(SHMName) + { + std::locale global_loc = std::locale(); + std::locale loc(global_loc, new fs::detail::utf8_codecvt_facet); + fs::path::imbue(loc); + + // append _1 to the name if it doesn't end with _N already + std::regex pattern(R"exp((.*_)(\d+))exp"); + std::smatch match; + std::string shmName(m_SHMName.c_str()); + regex_match(shmName, match, pattern); + + if (match.size() != 3) { + m_SHMName += "_1"; + } + + // creates a new memory block if this is the first process to run or attach + // to an already existing one + createOrOpen(m_SHMName, size); + + spdlog::get("usvfs")->info("attached to {0} with {1} nodes, size {2}", m_SHMName, + m_TreeMeta->tree->numNodesRecursive(), + byte_string(m_SHM->get_size())); + } + + TreeContainer(const TreeContainer&) = delete; + TreeContainer& operator=(const TreeContainer&) = delete; + + ~TreeContainer() + { + if (unassign(m_SHM, m_TreeMeta)) { + bi::shared_memory_object::remove(m_SHMName.c_str()); + } + } + + /** + * @return retrieve an allocator that can be used to create objects in this tree + */ + VoidAllocatorT allocator() { return VoidAllocatorT(m_SHM->get_segment_manager()); } + + template <typename... Arguments> + typename TreeT::DataT create(Arguments&&... args) + { + return TreeT::DataT(std::forward<Arguments>(args)..., allocator()); + } + + TreeT* operator->() { return get(); } + + /** + * @return raw pointer to the managed tree + */ + TreeT* get() + { + if (m_TreeMeta->outdated) { + reassign(); + } + + return m_TreeMeta->tree.get(); + } + + /** + * @return raw const pointer to the managed tree + */ + const TreeT* get() const + { + if (m_TreeMeta->outdated) { + // safe const_cast, TreeContainer are never created const + const_cast<TreeContainer<TreeT>*>(this)->reassign(); + } + + return m_TreeMeta->tree.get(); + } + + const TreeT* operator->() const { return get(); } + + /** + * @return current name of the managed shared memory + */ + std::string shmName() const { return m_SHMName; } + + void clear() { m_TreeMeta->tree->clear(); } + + /** + * @brief add a new file to the tree + * + * @param name name of the file, expected to be relative to this directory + * @param data the file data to attach + * @param flags flags for this files + * @param overwrite if true, the new leaf will overwrite an existing one that compares + *as "equal" + * @return pointer to the new node or a null ptr + **/ + template <typename T> + typename TreeT::NodePtrT addFile(const fs::path& name, const T& data, + TreeFlags flags = 0, bool overwrite = true) + { + for (;;) { + DecomposablePath dp(name.string()); + + try { + return addNode(m_TreeMeta->tree.get(), dp, data, overwrite, flags, allocator()); + } catch (const bi::bad_alloc&) { + } + + reassign(); + } + } + + /** + * @brief add a new directory to the tree + * + * @param name name of the file, expected to be relative to this directory + * @param data the file data to attach + * @param flags flags for this files + * @param overwrite if true, the new leaf will overwrite an existing one that compares + *as "equal" + * @return pointer to the new node or a null ptr + **/ + template <typename T> + typename TreeT::NodePtrT addDirectory(const fs::path& name, const T& data, + TreeFlags flags = 0, bool overwrite = true) + { + for (;;) { + DecomposablePath dp(name.string()); + + try { + return addNode(m_TreeMeta->tree.get(), dp, data, overwrite, + flags | FLAG_DIRECTORY, allocator()); + } catch (const bi::bad_alloc&) { + } + + reassign(); + } + } + + void getBuffer(void*& buffer, size_t& bufferSize) const + { + buffer = m_SHM->get_address(); + bufferSize = m_SHM->get_size(); + } + +private: + struct TreeMeta + { + TreeMeta(const typename TreeT::DataT& data, SegmentManagerT* segmentManager) + : tree(segmentManager->construct<TreeT>(bi::anonymous_instance)( + "", true, TreeT::NodePtrT(), data, VoidAllocatorT(segmentManager))), + referenceCount(0), // reference count only set on top level node + outdated(false) + {} + + OffsetPtrT<TreeT> tree; + long referenceCount; + bool outdated; + bi::interprocess_mutex mutex; + }; + + std::string m_SHMName; + std::shared_ptr<SharedMemoryT> m_SHM; + TreeMeta* m_TreeMeta; + + typename TreeT::DataT createEmpty() + { + return createDataEmpty<typename TreeT::DataT>(allocator()); + } + + template <typename T> + TreeT* createSubNode(const VoidAllocatorT& allocator, std::string_view name, + unsigned long flags, const T& data) + { + auto* manager = allocator.get_segment_manager(); + + return manager->construct<TreeT>(bi::anonymous_instance)( + name, flags, TreeT::NodePtrT(), + createData<typename TreeT::DataT, T>(data, allocator), manager); + } + + typename TreeT::NodePtrT createSubPtr(TreeT* subNode) + { + SharedMemoryT::segment_manager* manager = m_SHM->get_segment_manager(); + return TreeT::NodePtrT(subNode, allocator(), TreeT::DeleterT(manager)); + } + + template <typename T> + typename TreeT::NodePtrT addNode(TreeT* base, DecomposablePath& path, const T& data, + bool overwrite, unsigned int flags, + const VoidAllocatorT& allocator) + { + if (!path.peekNext()) { + typename TreeT::NodePtrT newNode = base->node(path.current()); + + if (!newNode) { + // last name component, should be the filename + TreeT* node = createSubNode(allocator, path.current(), flags, data); + newNode = createSubPtr(node); + newNode->m_Self = TreeT::WeakPtrT(newNode); + newNode->m_Parent = base->m_Self; + base->set(StringT(path.current(), allocator), newNode); + return newNode; + } else if (overwrite) { + newNode->m_Data = createData<typename TreeT::DataT, T>(data, allocator); + newNode->m_Flags = static_cast<usvfs::shared::TreeFlags>(flags); + return newNode; + } else { + // the node is already in the tree, overwrite is false, nothing to do + return {}; + } + } else { + // not last component, continue search in child node + auto subNode = base->m_Nodes.find(path.current()); + + if (subNode == base->m_Nodes.end()) { + typename TreeT::NodePtrT newNode = createSubPtr(createSubNode( + allocator, path.current(), FLAG_DIRECTORY | FLAG_DUMMY, createEmpty())); + + subNode = + base->m_Nodes.emplace(StringT(path.current(), allocator), newNode).first; + subNode->second->m_Self = TreeT::WeakPtrT(subNode->second); + subNode->second->m_Parent = base->m_Self; + } + + path.next(); + + return addNode(subNode->second.get().get(), path, data, overwrite, flags, + allocator); + } + } + + /** + * @brief copy content of one tree to a different tree (in a different shared memory + * segment + * @param destination + * @param reference + * @note at the time this is called, destination needs to refer to the shm of + * "destination" so that objects can be allocated in the new tree + */ + void copyTree(TreeT* destination, const TreeT* reference) + { + VoidAllocatorT allocator = VoidAllocatorT(m_SHM->get_segment_manager()); + destination->m_Flags = reference->m_Flags; + dataAssign(destination->m_Data, reference->m_Data); + destination->m_Name.assign(reference->m_Name.c_str()); + + for (const auto& kv : reference->m_Nodes) { + TreeT* newNode = createSubNode(allocator, "", true, createEmpty()); + typename TreeT::NodePtrT newNodePtr = createSubPtr(newNode); + + // need to set self BEFORE recursively copying the subtree, otherwise + // how would we assign parent pointers? + newNode->m_Self = newNodePtr; + + TreeT* source = reinterpret_cast<TreeT*>(kv.second.get().get()); + copyTree(newNode, source); + destination->set(newNode->m_Name, newNodePtr); + newNode->m_Parent = destination->m_Self; + } + } + + int increaseRefCount(TreeMeta* treeMeta) + { + bi::scoped_lock<bi::interprocess_mutex> lock(treeMeta->mutex); + return ++treeMeta->referenceCount; + } + + int decreaseRefCount(TreeMeta* treeMeta) + { + bi::scoped_lock<bi::interprocess_mutex> lock(treeMeta->mutex); + return --treeMeta->referenceCount; + } + + // see activateSHM() for return value + // + std::optional<std::string> createOrOpen(const std::string& SHMName, size_t size) + { + SharedMemoryT* newSHM = openSHM(SHMName); + + if (newSHM) { + spdlog::get("usvfs")->info("{} opened in process {}", SHMName, + ::GetCurrentProcessId()); + } else { + newSHM = createSHM(SHMName, size); + + if (newSHM) { + spdlog::get("usvfs")->info("{} created in process {}", SHMName, + ::GetCurrentProcessId()); + } + } + + if (!newSHM) { + spdlog::get("usvfs")->error("failed to create or open {} in process {}", SHMName, + ::GetCurrentProcessId()); + + throw std::exception("no shm instance"); + } + + return activateSHM(newSHM, SHMName); + } + + // see activateSHM() for return value + // + SharedMemoryT* createSHM(const std::string& SHMName, size_t size) + { + try { + return new SharedMemoryT(bi::create_only, SHMName.c_str(), + static_cast<unsigned int>(size)); + } catch (const bi::interprocess_exception&) { + } + + return nullptr; + } + + // see activateSHM() for return value + // + SharedMemoryT* openSHM(const std::string& SHMName) + { + try { + return new SharedMemoryT(bi::open_only, SHMName.c_str()); + } catch (const bi::interprocess_exception&) { + } + + return nullptr; + } + + // makes the given shm current, returns the name of the previous shm block + // if it is now unused and must be destroyed; if the block is still used by + // another process, returns empty + // + // see reassign() + // + std::optional<std::string> activateSHM(SharedMemoryT* shm, const std::string& SHMName) + { + std::shared_ptr<SharedMemoryT> oldSHM = m_SHM; + + m_SHM.reset(shm); + std::pair<TreeMeta*, SharedMemoryT::size_type> res = m_SHM->find<TreeMeta>("Meta"); + + if (res.first == nullptr) { + res.first = m_SHM->construct<TreeMeta>("Meta")(createEmpty(), + m_SHM->get_segment_manager()); + if (res.first == nullptr) { + USVFS_THROW_EXCEPTION(bi::bad_alloc()); + } + if (m_TreeMeta != nullptr) { + copyTree(res.first->tree.get(), m_TreeMeta->tree.get()); + } + } + + increaseRefCount(res.first); + + std::optional<std::string> deadSHMName; + + if (oldSHM.get() != nullptr) { + const bool lastUser = unassign(oldSHM, m_TreeMeta); + if (lastUser) { + deadSHMName = m_SHMName; + } + } + + m_TreeMeta = res.first; + m_SHMName = SHMName; + + return deadSHMName; + } + + static std::string followupName(const std::string& currentName) + { + std::regex pattern(R"exp((.*_)(\d+))exp"); + std::smatch match; + regex_match(currentName, match, pattern); + + if (match.size() != 3) { + USVFS_THROW_EXCEPTION(usage_error() << ex_msg("shared memory name invalid")); + } + + const int count = boost::lexical_cast<int>(match[2]); + return match[1].str() + std::to_string(count + 1); + } + + bool unassign(const std::shared_ptr<SharedMemoryT>& shm, TreeMeta* tree) + { + if (tree == nullptr) { + return true; + } + + if (decreaseRefCount(tree) == 0) { + shm->get_segment_manager()->destroy_ptr(tree); + return true; + } else { + return false; + } + } + + // re-entrancy: every time a process switches to a new shared memory block, it + // will know whether it was the last process to have a handle to it; when that + // happens, the block will be destroyed to avoid leaking it + // + // destroying these blocks is somewhat dangerous: it ends up in boost, which + // will try to access the filesystem to see if the name of the shared memory + // corresponds to a file on the drive, which can call hooked functions and end + // up right back here + // + // (note that in usvfs, only the shared memory for the log file uses a real + // file on the filesystem, see shmlogger.cpp; all the tree stuff uses + // anonymous, memory mapped files that live in the Windows pagefile) + // + // so the old blocks can be destroyed, but only after all the shenanigans with + // finding the correct shared memory block are over and `m_TreeMeta` points to + // a valid block, so all the names of the dead shared memory blocks are kept + // in a vector and deallocated at the very end + // + void reassign() + { + // list of all the shared memory blocks that are now unused and can be + // destroyed + std::vector<std::string> deadSHMNames; + + if (m_TreeMeta->outdated) { + // this block was marked as outdated, which should only happen when + // another process has ran out of memory and started allocating blocks + // with higher numbers + + spdlog::get("usvfs")->info("tree {0} is outdated, looking for another one", + m_SHMName); + + if (findNewerBlock(deadSHMNames)) { + // the new block was found and activated + return; + } + } else { + // this block isn't outdated, so reassign() was called because a bad_alloc + // exception was thrown + spdlog::get("usvfs")->info( + "ran out of memory in tree {0}, will create another one", m_SHMName); + } + + // either the block is full or it's outdated, but no higher block was found; + // just create a new one + + createNewBlock(deadSHMNames); + + // remove the old shared memory blocks; this can be recursive and call + // reassign() again, but it's safe at this point + for (const std::string& name : deadSHMNames) { + spdlog::get("usvfs")->info("destroying {0}", name); + bi::shared_memory_object::remove(name.c_str()); + } + } + + // tries a series of shm names above the current one in the hope of finding + // the most recent one + // + // if the new block was found and activated correctly, returns true + // + // when findNewerBlock() returns false, this container will either still be + // pointing to the same, outdated block as it started with, or it might have + // moved up to another outdated block, but it will always be pointing to + // a valid block in memory + // + bool findNewerBlock(std::vector<std::string>& deadSHMNames) + { + // how many blocks are checked above the current one; it's unlikely there + // will ever be more than 15 to 20 blocks allocated, but this is high + // because it's really the only way to keep processes connected to the same + // shm block, so processes must not fail to find the next one + constexpr int Tries = 100; + + std::string nextName = m_SHMName; + + for (int i = 0; i < Tries; ++i) { + // the shm name is something like "mod_organizer_3", which becomes + // "mod_organizer_4" + nextName = followupName(nextName); + spdlog::get("usvfs")->info("opening {0}", nextName); + + // open the shm, see if it exists + SharedMemoryT* shm = openSHM(nextName); + + if (!shm) { + // this is not necessarily an error, another process might have created + // a bunch of blocks and destroyed them before this process had a chance + // to see them, so just keep going + spdlog::get("usvfs")->info("{0} doesn't exist", nextName); + continue; + } + + spdlog::get("usvfs")->info("{0} exists, activating", nextName); + const auto deadSHMName = activateSHM(shm, nextName); + + // if this process was the last user of the previous block, it must be + // deallocated, but only after this whole thing is finished, because it + // can end up calling reassign() again + if (deadSHMName) { + spdlog::get("usvfs")->info("will destroy {0}", *deadSHMName); + deadSHMNames.push_back(*deadSHMName); + } + + // another process might have already created this block, run out of + // memory and created more, so make sure to only stop when finding a block + // that's not outdated + if (m_TreeMeta->outdated) { + spdlog::get("usvfs")->info("{0} is also outdated", nextName); + } else { + // shm opened correctly, activated and not outdated, done + spdlog::get("usvfs")->info("{0} not outdated, taking it, size now {1}", + nextName, byte_string(m_SHM->get_size())); + + return true; + } + } + + // this block is outdated, but no other valid block was found above; this + // shouldn't happen, but just create a new block to make sure programs can + // still run + + spdlog::get("usvfs")->error( + "found no existing tree above {0}, will create a new one", m_SHMName); + + return false; + } + + // creates a new block and activates it, throws on failure + // + void createNewBlock(std::vector<std::string>& deadSHMNames) + { + // the current block is now considered stale, so make sure other processes + // are aware of it and try to find the new block + // + // todo: there really should be some synchronization here, another process + // can pick up this flag before the next block has been created + m_TreeMeta->outdated = true; + + // the shm name is something like "mod_organizer_3", which becomes + // "mod_organizer_4" + const std::string nextName = followupName(m_SHMName); + spdlog::get("usvfs")->info("creating {0}", nextName); + + SharedMemoryT* shm = createSHM(nextName, m_SHM->get_size() * 2); + + if (!shm) { + // this shouldn't happen + spdlog::get("usvfs")->error("failed to create {0}", nextName); + throw std::exception("cannot create block"); + } + + spdlog::get("usvfs")->info("{0} created, activating", nextName); + const auto deadSHMName = activateSHM(shm, nextName); + + // if this process was the last user of the previous block, it must be + // deallocated, but only after this whole thing is finished, because it + // can end up calling reassign() again + if (deadSHMName) { + spdlog::get("usvfs")->info("will destroy {0}", *deadSHMName); + deadSHMNames.push_back(*deadSHMName); + } + + spdlog::get("usvfs")->info("tree {0} size now {1}", m_SHMName, + byte_string(m_SHM->get_size())); + } +}; + +} // namespace usvfs::shared |
