Просмотр исходного кода

core: add initial Windows backend for FileMonitor

Daniele Bartolini 5 лет назад
Родитель
Сommit
d840010b5d
2 измененных файлов с 297 добавлено и 3 удалено
  1. 2 3
      src/core/filesystem/file_monitor_noop.cpp
  2. 295 0
      src/core/filesystem/file_monitor_windows.cpp

+ 2 - 3
src/core/filesystem/file_monitor_noop.cpp

@@ -5,7 +5,7 @@
 
 
 #include "core/platform.h"
 #include "core/platform.h"
 
 
-#if !CROWN_PLATFORM_LINUX
+#if !CROWN_PLATFORM_LINUX && !CROWN_PLATFORM_WINDOWS
 
 
 #include "core/filesystem/file_monitor.h"
 #include "core/filesystem/file_monitor.h"
 
 
@@ -30,5 +30,4 @@ void FileMonitor::stop()
 
 
 } // namespace crown
 } // namespace crown
 
 
-#endif // !CROWN_PLATFORM_LINUX
-
+#endif // !CROWN_PLATFORM_LINUX && !CROWN_PLATFORM_WINDOWS

+ 295 - 0
src/core/filesystem/file_monitor_windows.cpp

@@ -0,0 +1,295 @@
+/*
+ * Copyright (c) 2012-2020 Daniele Bartolini and individual contributors.
+ * License: https://github.com/dbartolini/crown/blob/master/LICENSE
+ */
+
+#include "core/platform.h"
+
+#if CROWN_PLATFORM_WINDOWS
+
+#include "core/containers/hash_map.inl"
+#include "core/filesystem/file_monitor.h"
+#include "core/filesystem/path.h"
+#include "core/memory/temp_allocator.inl"
+#include "core/os.h"
+#include "core/strings/dynamic_string.inl"
+#include "core/thread/thread.h"
+#include <windows.h>
+
+namespace crown
+{
+struct FileMonitorImpl
+{
+	struct Watch
+	{
+		ALLOCATOR_AWARE;
+
+		HANDLE _handle;
+		DynamicString _path;
+		char _buffer[4096];
+		OVERLAPPED _overlapped;
+
+		explicit Watch(Allocator& a)
+			: _path(a)
+		{
+			memset(&_overlapped, 0, sizeof(_overlapped));
+		}
+
+		~Watch()
+		{
+			CloseHandle(_handle);
+		}
+	};
+
+	Allocator* _allocator;
+	HANDLE _iocp;
+	HashMap<u32, Watch*> _watches;
+	Thread _thread;
+	bool _exit;
+	bool _recursive;
+	FileMonitorFunction _function;
+	void* _user_data;
+	u32 _key;
+
+	explicit FileMonitorImpl(Allocator& a)
+		: _allocator(&a)
+		, _iocp(0)
+		, _watches(a)
+		, _exit(false)
+		, _recursive(false)
+		, _function(NULL)
+		, _user_data(NULL)
+		, _key(1000)
+	{
+	}
+
+	void add_watch(const char* path, bool recursive)
+	{
+		CE_ENSURE(path != NULL);
+		CE_ASSERT(!path::has_trailing_separator(path), "Malformed path");
+
+		HANDLE fh = CreateFile(path
+			, FILE_LIST_DIRECTORY
+			, FILE_SHARE_DELETE
+			| FILE_SHARE_READ
+			| FILE_SHARE_WRITE
+			, NULL
+			, OPEN_EXISTING
+			, FILE_FLAG_BACKUP_SEMANTICS // Required by directories
+			| FILE_FLAG_OVERLAPPED       // Required by CreateIoCompletionPort
+			, NULL
+			);
+		CE_ASSERT(fh != INVALID_HANDLE_VALUE, "CreateFile: GetLastError: %d", GetLastError());
+
+		// Create new IOCP or return already created one
+		_iocp = CreateIoCompletionPort(fh, _iocp, _key, 0);
+		CE_ASSERT(_iocp != NULL, "CreateIoCompletionPort: GetLastError: %d", GetLastError());
+
+		Watch* watch = CE_NEW(*_allocator, Watch)(*_allocator);
+		watch->_path = path;
+		watch->_handle = fh;
+
+		BOOL rdc = ReadDirectoryChangesW(fh
+			, &watch->_buffer
+			, sizeof(watch->_buffer)
+			, _recursive
+			, FILE_NOTIFY_CHANGE_FILE_NAME
+			| FILE_NOTIFY_CHANGE_DIR_NAME
+			| FILE_NOTIFY_CHANGE_ATTRIBUTES
+			| FILE_NOTIFY_CHANGE_SIZE
+			, NULL
+			, &watch->_overlapped
+			, NULL
+			);
+		CE_ASSERT(rdc != 0, "ReadDirectoryChangesW: GetLastError: %d", GetLastError());
+
+		hash_map::set(_watches, _key, watch);
+		++_key;
+	}
+
+	void start(u32 num, const char** paths, bool recursive, FileMonitorFunction fmf, void* user_data)
+	{
+		CE_ENSURE(NULL != fmf);
+
+		_recursive = recursive;
+		_function = fmf;
+		_user_data = user_data;
+
+		for (u32 i = 0; i < num; ++i)
+			add_watch(paths[i], recursive);
+
+		_thread.start([](void* thiz) { return static_cast<FileMonitorImpl*>(thiz)->watch(); }, this);
+	}
+
+	void stop()
+	{
+		_exit = true;
+		// Post dummy packet to force thread to evaluate _exit
+		PostQueuedCompletionStatus(_iocp, 0, 999, 0);
+		_thread.stop();
+
+		// Release all handles
+		auto cur = hash_map::begin(_watches);
+		auto end = hash_map::end(_watches);
+		for (; cur != end; ++cur)
+		{
+			HASH_MAP_SKIP_HOLE(_watches, cur);
+
+			CE_DELETE(*_allocator, cur->second);
+		}
+
+		CloseHandle(_iocp);
+	}
+
+	int watch()
+	{
+		while (!_exit)
+		{
+			DWORD bytes_transferred;
+			ULONG_PTR key;
+			OVERLAPPED* ov;
+			BOOL ret = GetQueuedCompletionStatus(_iocp
+				, &bytes_transferred
+				, &key
+				, &ov
+				, INFINITE
+				);
+			if (ret == FALSE)
+				continue;
+
+			// Re-evaluate _exit
+			if (bytes_transferred == 0)
+				continue;
+
+			Watch* watch = hash_map::get(_watches, (u32)(uintptr_t)key, (Watch*)NULL);
+
+			// Read packets
+			DWORD last_action = -1;
+			DynamicString path_old_name(default_allocator());
+			char* cur = (char*)watch->_buffer;
+			for(;;)
+			{
+				const FILE_NOTIFY_INFORMATION* fni = (const FILE_NOTIFY_INFORMATION*)cur;
+
+				TempAllocator512 ta;
+				DynamicString path(ta);
+				full_path(path, (u32)(uintptr_t)key, fni->FileName, fni->FileNameLength);
+
+				if (fni->Action == FILE_ACTION_ADDED)
+				{
+					Stat st;
+					os::stat(st, path.c_str());
+
+					_function(_user_data
+						, FileMonitorEvent::CREATED
+						, st.file_type == Stat::FileType::DIRECTORY
+						, path.c_str()
+						, NULL
+					);
+				}
+				else if (fni->Action == FILE_ACTION_REMOVED)
+				{
+					_function(_user_data
+						, FileMonitorEvent::DELETED
+						, false // FIXME: add "unknown" type or always assume file and let client handle that?
+						, path.c_str()
+						, NULL
+					);
+				}
+				else if (fni->Action == FILE_ACTION_MODIFIED)
+				{
+					Stat st;
+					os::stat(st, path.c_str());
+
+					_function(_user_data
+						, FileMonitorEvent::CHANGED
+						, st.file_type == Stat::FileType::DIRECTORY
+						, path.c_str()
+						, NULL
+						);
+				}
+				else if (fni->Action == FILE_ACTION_RENAMED_OLD_NAME)
+				{
+					last_action = fni->Action;
+					full_path(path_old_name, (u32)(uintptr_t)key, fni->FileName, fni->FileNameLength);
+				}
+				else if (fni->Action == FILE_ACTION_RENAMED_NEW_NAME)
+				{
+					if (last_action == FILE_ACTION_RENAMED_OLD_NAME)
+					{
+						Stat st;
+						os::stat(st, path.c_str());
+
+						_function(_user_data
+							, FileMonitorEvent::RENAMED
+							, st.file_type == Stat::FileType::DIRECTORY
+							, path_old_name.c_str()
+							, path.c_str()
+							);
+					}
+
+					last_action = -1;
+				}
+
+				// advance to next entry in buffer (variable length)
+				if (fni->NextEntryOffset == 0)
+					break;
+				cur += fni->NextEntryOffset;
+			}
+
+			BOOL rdc = ReadDirectoryChangesW(watch->_handle
+				, &watch->_buffer
+				, sizeof(watch->_buffer)
+				, _recursive
+				, FILE_NOTIFY_CHANGE_FILE_NAME
+				| FILE_NOTIFY_CHANGE_DIR_NAME
+				| FILE_NOTIFY_CHANGE_ATTRIBUTES
+				| FILE_NOTIFY_CHANGE_SIZE
+				, NULL
+				, &watch->_overlapped
+				, NULL
+				);
+			CE_ASSERT(rdc != 0, "ReadDirectoryChangesW: GetLastError: %d", GetLastError());
+		}
+
+		return 0;
+	}
+
+	void full_path(DynamicString& path, u32 key, const WCHAR* name, u32 name_len)
+	{
+		Watch* watch = hash_map::get(_watches, key, (Watch*)NULL);
+
+		TempAllocator512 ta;
+		DynamicString path_base(ta);
+		path_base = watch->_path;
+		DynamicString filename(ta);
+		for (u32 ii = 0; ii < name_len/2; ++ii)
+			filename += (char)name[ii];
+		path::join(path, path_base.c_str(), filename.c_str());
+	}
+
+};
+
+FileMonitor::FileMonitor(Allocator& a)
+{
+	_impl = CE_NEW(a, FileMonitorImpl)(a);
+}
+
+FileMonitor::~FileMonitor()
+{
+	CE_DELETE(*_impl->_allocator, _impl);
+}
+
+void FileMonitor::start(u32 num, const char** paths, bool recursive, FileMonitorFunction fmf, void* user_data)
+{
+	_impl->start(num, paths, recursive, fmf, user_data);
+}
+
+void FileMonitor::stop()
+{
+	_impl->stop();
+}
+
+} // namespace crown
+
+#endif // CROWN_PLATFORM_WINDOWS