summaryrefslogtreecommitdiff
path: root/Source/Asura.Editor/System/FileWatcher.h
diff options
context:
space:
mode:
Diffstat (limited to 'Source/Asura.Editor/System/FileWatcher.h')
-rw-r--r--Source/Asura.Editor/System/FileWatcher.h517
1 files changed, 517 insertions, 0 deletions
diff --git a/Source/Asura.Editor/System/FileWatcher.h b/Source/Asura.Editor/System/FileWatcher.h
new file mode 100644
index 0000000..43821da
--- /dev/null
+++ b/Source/Asura.Editor/System/FileWatcher.h
@@ -0,0 +1,517 @@
+// MIT License
+//
+// Copyright(c) 2017 Thomas Monkman
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files(the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions :
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+#ifndef FILEWATCHER_H
+#define FILEWATCHER_H
+
+#ifdef _WIN32
+#define WIN32_LEAN_AND_MEAN
+#define NOMINMAX
+#include <windows.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <tchar.h>
+#include <Pathcch.h>
+#include <shlwapi.h>
+#endif // WIN32
+
+#if __unix__
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/inotify.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#endif // __unix__
+
+#include <functional>
+#include <atomic>
+#include <thread>
+#include <mutex>
+#include <condition_variable>
+#include <utility>
+#include <vector>
+#include <array>
+#include <map>
+#include <system_error>
+#include <string>
+#include <algorithm>
+#include <type_traits>
+#include <future>
+#include <regex>
+
+namespace filewatch {
+ enum class Event {
+ added,
+ removed,
+ modified,
+ renamed_old,
+ renamed_new
+ };
+
+ /**
+ * \class FileWatch
+ *
+ * \brief Watches a folder or file, and will notify of changes via function callback.
+ *
+ * \author Thomas Monkman
+ *
+ */
+ template<class T>
+ class FileWatch
+ {
+ typedef std::basic_string<typename T::value_type, std::char_traits<typename T::value_type>> UnderpinningString;
+ typedef std::basic_regex<typename T::value_type, std::regex_traits<typename T::value_type>> UnderpinningRegex;
+
+ public:
+
+ FileWatch(T path, UnderpinningRegex pattern, std::function<void(const T& file, const Event event_type)> callback) :
+ _path(path),
+ _pattern(pattern),
+ _callback(callback),
+ _directory(get_directory(path))
+ {
+ init();
+ }
+
+#if defined _WIN32 && (defined UNICODE || defined _UNICODE)
+ FileWatch(T path, std::function<void(const T& file, const Event event_type)> callback) :
+ FileWatch<T>(path, UnderpinningRegex(L".*"), callback) {}
+#else // _WIN32 && (UNICODE || _UNICODE)
+ FileWatch(T path, std::function<void(const T& file, const Event event_type)> callback) :
+ FileWatch<T>(path, UnderpinningRegex(".*"), callback) {}
+#endif
+
+ ~FileWatch() {
+ destroy();
+ }
+
+ FileWatch(const FileWatch<T>& other) : FileWatch<T>(other._path, other._callback) {}
+
+ FileWatch<T>& operator=(const FileWatch<T>& other)
+ {
+ if (this == &other) { return *this; }
+
+ destroy();
+ _path = other._path;
+ _callback = other._callback;
+ _directory = get_directory(other._path);
+ init();
+ return *this;
+ }
+
+ // Const memeber varibles don't let me implent moves nicely, if moves are really wanted std::unique_ptr should be used and move that.
+ FileWatch<T>(FileWatch<T>&&) = delete;
+ FileWatch<T>& operator=(FileWatch<T>&&) & = delete;
+
+ private:
+ struct PathParts
+ {
+ PathParts(T directory, T filename) : directory(directory), filename(filename) {}
+ T directory;
+ T filename;
+ };
+ const T _path;
+
+ UnderpinningRegex _pattern;
+
+ static constexpr std::size_t _buffer_size = { 1024 * 256 };
+
+ // only used if watch a single file
+ bool _watching_single_file = { false };
+ T _filename;
+
+ std::atomic<bool> _destory = { false };
+ std::function<void(const T& file, const Event event_type)> _callback;
+
+ std::thread _watch_thread;
+
+ std::condition_variable _cv;
+ std::mutex _callback_mutex;
+ std::vector<std::pair<T, Event>> _callback_information;
+ std::thread _callback_thread;
+
+ std::promise<void> _running;
+#ifdef _WIN32
+ HANDLE _directory = { nullptr };
+ HANDLE _close_event = { nullptr };
+
+ const DWORD _listen_filters =
+ FILE_NOTIFY_CHANGE_SECURITY |
+ FILE_NOTIFY_CHANGE_CREATION |
+ FILE_NOTIFY_CHANGE_LAST_ACCESS |
+ FILE_NOTIFY_CHANGE_LAST_WRITE |
+ FILE_NOTIFY_CHANGE_SIZE |
+ FILE_NOTIFY_CHANGE_ATTRIBUTES |
+ FILE_NOTIFY_CHANGE_DIR_NAME |
+ FILE_NOTIFY_CHANGE_FILE_NAME;
+
+ const std::map<DWORD, Event> _event_type_mapping = {
+ { FILE_ACTION_ADDED, Event::added },
+ { FILE_ACTION_REMOVED, Event::removed },
+ { FILE_ACTION_MODIFIED, Event::modified },
+ { FILE_ACTION_RENAMED_OLD_NAME, Event::renamed_old },
+ { FILE_ACTION_RENAMED_NEW_NAME, Event::renamed_new }
+ };
+#endif // WIN32
+
+#if __unix__
+ struct FolderInfo {
+ int folder;
+ int watch;
+ };
+
+ FolderInfo _directory;
+
+ const std::uint32_t _listen_filters = IN_MODIFY | IN_CREATE | IN_DELETE;
+
+ const static std::size_t event_size = (sizeof(struct inotify_event));
+#endif // __unix__
+
+ void init()
+ {
+#ifdef _WIN32
+ _close_event = CreateEvent(NULL, TRUE, FALSE, NULL);
+ if (!_close_event) {
+ throw std::system_error(GetLastError(), std::system_category());
+ }
+#endif // WIN32
+ _callback_thread = std::move(std::thread([this]() {
+ try {
+ callback_thread();
+ }
+ catch (...) {
+ try {
+ _running.set_exception(std::current_exception());
+ }
+ catch (...) {} // set_exception() may throw too
+ }
+ }));
+ _watch_thread = std::move(std::thread([this]() {
+ try {
+ monitor_directory();
+ }
+ catch (...) {
+ try {
+ _running.set_exception(std::current_exception());
+ }
+ catch (...) {} // set_exception() may throw too
+ }
+ }));
+
+ std::future<void> future = _running.get_future();
+ future.get(); //block until the monitor_directory is up and running
+ }
+
+ void destroy()
+ {
+ _destory = true;
+ _running = std::promise<void>();
+#ifdef _WIN32
+ SetEvent(_close_event);
+#elif __unix__
+ inotify_rm_watch(_directory.folder, _directory.watch);
+#endif // __unix__
+ _cv.notify_all();
+ _watch_thread.join();
+ _callback_thread.join();
+#ifdef _WIN32
+ CloseHandle(_directory);
+#elif __unix__
+ close(_directory.folder);
+#endif // __unix__
+ }
+
+ const PathParts split_directory_and_file(const T& path) const
+ {
+ const auto predict = [](typename T::value_type character) {
+#ifdef _WIN32
+ return character == _T('\\') || character == _T('/');
+#elif __unix__
+ return character == '/';
+#endif // __unix__
+ };
+#ifdef _WIN32
+#define _UNICODE
+ const UnderpinningString this_directory = _T("./");
+#elif __unix__
+ const UnderpinningString this_directory = "./";
+#endif // __unix__
+
+ const auto pivot = std::find_if(path.rbegin(), path.rend(), predict).base();
+ //if the path is something like "test.txt" there will be no directoy part, however we still need one, so insert './'
+ const T directory = [&]() {
+ const auto extracted_directory = UnderpinningString(path.begin(), pivot);
+ return (extracted_directory.size() > 0) ? extracted_directory : this_directory;
+ }();
+ const T filename = UnderpinningString(pivot, path.end());
+ return PathParts(directory, filename);
+ }
+
+ bool pass_filter(const UnderpinningString& file_path)
+ {
+ if (_watching_single_file) {
+ const UnderpinningString extracted_filename = { split_directory_and_file(file_path).filename };
+ //if we are watching a single file, only that file should trigger action
+ return extracted_filename == _filename;
+ }
+ return std::regex_match(file_path, _pattern);
+ }
+
+#ifdef _WIN32
+ HANDLE get_directory(const T& path)
+ {
+ auto file_info = GetFileAttributes(path.c_str());
+
+ if (file_info == INVALID_FILE_ATTRIBUTES)
+ {
+ throw std::system_error(GetLastError(), std::system_category());
+ }
+ _watching_single_file = (file_info & FILE_ATTRIBUTE_DIRECTORY) == false;
+
+ const T watch_path = [this, &path]() {
+ if (_watching_single_file)
+ {
+ const auto parsed_path = split_directory_and_file(path);
+ _filename = parsed_path.filename;
+ return parsed_path.directory;
+ }
+ else
+ {
+ return path;
+ }
+ }();
+
+ HANDLE directory = ::CreateFile(
+ watch_path.c_str(), // pointer to the file name
+ FILE_LIST_DIRECTORY, // access (read/write) mode
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // share mode
+ NULL, // security descriptor
+ OPEN_EXISTING, // how to create
+ FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, // file attributes
+ NULL); // file with attributes to copy
+
+ if (directory == INVALID_HANDLE_VALUE)
+ {
+ throw std::system_error(GetLastError(), std::system_category());
+ }
+ return directory;
+ }
+ void monitor_directory()
+ {
+ std::vector<BYTE> buffer(_buffer_size);
+ DWORD bytes_returned = 0;
+ OVERLAPPED overlapped_buffer{ 0 };
+
+ overlapped_buffer.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
+ if (!overlapped_buffer.hEvent) {
+ std::cerr << "Error creating monitor event" << std::endl;
+ }
+
+ std::array<HANDLE, 2> handles{ overlapped_buffer.hEvent, _close_event };
+
+ auto async_pending = false;
+ _running.set_value();
+ do {
+ std::vector<std::pair<T, Event>> parsed_information;
+ ReadDirectoryChangesW(
+ _directory,
+ buffer.data(), buffer.size(),
+ TRUE,
+ _listen_filters,
+ &bytes_returned,
+ &overlapped_buffer, NULL);
+
+ async_pending = true;
+
+ switch (WaitForMultipleObjects(2, handles.data(), FALSE, INFINITE))
+ {
+ case WAIT_OBJECT_0:
+ {
+ if (!GetOverlappedResult(_directory, &overlapped_buffer, &bytes_returned, TRUE)) {
+ throw std::system_error(GetLastError(), std::system_category());
+ }
+ async_pending = false;
+
+ if (bytes_returned == 0) {
+ break;
+ }
+
+ FILE_NOTIFY_INFORMATION *file_information = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(&buffer[0]);
+ do
+ {
+ UnderpinningString changed_file{ file_information->FileName, file_information->FileNameLength / 2 };
+ if (pass_filter(changed_file))
+ {
+ parsed_information.emplace_back(T{ changed_file }, _event_type_mapping.at(file_information->Action));
+ }
+
+ if (file_information->NextEntryOffset == 0) {
+ break;
+ }
+
+ file_information = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(reinterpret_cast<BYTE*>(file_information) + file_information->NextEntryOffset);
+ } while (true);
+ break;
+ }
+ case WAIT_OBJECT_0 + 1:
+ // quit
+ break;
+ case WAIT_FAILED:
+ break;
+ }
+ //dispatch callbacks
+ {
+ std::lock_guard<std::mutex> lock(_callback_mutex);
+ _callback_information.insert(_callback_information.end(), parsed_information.begin(), parsed_information.end());
+ }
+ _cv.notify_all();
+ } while (_destory == false);
+
+ if (async_pending)
+ {
+ //clean up running async io
+ CancelIo(_directory);
+ GetOverlappedResult(_directory, &overlapped_buffer, &bytes_returned, TRUE);
+ }
+ }
+#endif // WIN32
+
+#if __unix__
+
+ bool is_file(const T& path) const
+ {
+ struct stat statbuf = {};
+ if (stat(path.c_str(), &statbuf) != 0)
+ {
+ throw std::system_error(errno, std::system_category());
+ }
+ return S_ISREG(statbuf.st_mode);
+ }
+
+ FolderInfo get_directory(const T& path)
+ {
+ const auto folder = inotify_init();
+ if (folder < 0)
+ {
+ throw std::system_error(errno, std::system_category());
+ }
+ const auto listen_filters = _listen_filters;
+
+ _watching_single_file = is_file(path);
+
+ const T watch_path = [this, &path]() {
+ if (_watching_single_file)
+ {
+ const auto parsed_path = split_directory_and_file(path);
+ _filename = parsed_path.filename;
+ return parsed_path.directory;
+ }
+ else
+ {
+ return path;
+ }
+ }();
+
+ const auto watch = inotify_add_watch(folder, watch_path.c_str(), IN_MODIFY | IN_CREATE | IN_DELETE);
+ if (watch < 0)
+ {
+ throw std::system_error(errno, std::system_category());
+ }
+ return { folder, watch };
+ }
+
+ void monitor_directory()
+ {
+ std::vector<char> buffer(_buffer_size);
+
+ _running.set_value();
+ while (_destory == false)
+ {
+ const auto length = read(_directory.folder, static_cast<void*>(buffer.data()), buffer.size());
+ if (length > 0)
+ {
+ int i = 0;
+ std::vector<std::pair<T, Event>> parsed_information;
+ while (i < length)
+ {
+ struct inotify_event *event = reinterpret_cast<struct inotify_event *>(&buffer[i]); // NOLINT
+ if (event->len)
+ {
+ const UnderpinningString changed_file{ event->name };
+ if (pass_filter(changed_file))
+ {
+ if (event->mask & IN_CREATE)
+ {
+ parsed_information.emplace_back(T{ changed_file }, Event::added);
+ }
+ else if (event->mask & IN_DELETE)
+ {
+ parsed_information.emplace_back(T{ changed_file }, Event::removed);
+ }
+ else if (event->mask & IN_MODIFY)
+ {
+ parsed_information.emplace_back(T{ changed_file }, Event::modified);
+ }
+ }
+ }
+ i += event_size + event->len;
+ }
+ //dispatch callbacks
+ {
+ std::lock_guard<std::mutex> lock(_callback_mutex);
+ _callback_information.insert(_callback_information.end(), parsed_information.begin(), parsed_information.end());
+ }
+ _cv.notify_all();
+ }
+ }
+ }
+#endif // __unix__
+
+ void callback_thread()
+ {
+ while (_destory == false) {
+ std::unique_lock<std::mutex> lock(_callback_mutex);
+ if (_callback_information.empty() && _destory == false) {
+ _cv.wait(lock, [this] { return _callback_information.size() > 0 || _destory; });
+ }
+ decltype(_callback_information) callback_information = {};
+ std::swap(callback_information, _callback_information);
+ lock.unlock();
+
+ for (const auto& file : callback_information) {
+ if (_callback) {
+ try
+ {
+ _callback(file.first, file.second);
+ }
+ catch (const std::exception&)
+ {
+ }
+ }
+ }
+ }
+ }
+ };
+}
+#endif \ No newline at end of file