From 42f6af4bf1f48e6e048b20af82f7df31ef4a1052 Mon Sep 17 00:00:00 2001 From: Calvin Rose Date: Sat, 17 Aug 2024 12:37:58 -0700 Subject: [PATCH] First working version of filewatch on windows. --- src/core/filewatch.c | 168 +++++++++++++++++++++++++++++-------------- 1 file changed, 114 insertions(+), 54 deletions(-) diff --git a/src/core/filewatch.c b/src/core/filewatch.c index 2ac73ed3..d9c2165f 100644 --- a/src/core/filewatch.c +++ b/src/core/filewatch.c @@ -50,6 +50,7 @@ typedef struct { JanetTable watch_descriptors; JanetChannel *channel; uint32_t default_flags; + int is_watching; } JanetWatcher; /* Reject certain filename events without sending anything to the channel @@ -117,6 +118,7 @@ static void janet_watcher_init(JanetWatcher *watcher, JanetChannel *channel, uin janet_table_init_raw(&watcher->watch_descriptors, 0); watcher->channel = channel; watcher->default_flags = default_flags; + watcher->is_watching = 0; watcher->stream = janet_stream(fd, JANET_STREAM_READABLE, NULL); } @@ -248,20 +250,22 @@ static void watcher_callback_read(JanetFiber *fiber, JanetAsyncEvent event) { } static void janet_watcher_listen(JanetWatcher *watcher) { + if (watcher->is_watching) janet_panic("already watching"); + watcher->is_watching = 1; janet_async_start(watcher->stream, JANET_ASYNC_LISTEN_READ, watcher_callback_read, watcher); } #elif JANET_WINDOWS static const JanetWatchFlagName watcher_flags_windows[] = { - {"file-name", FILE_NOTIFY_CHANGE_FILE_NAME}, - {"dir-name", FILE_NOTIFY_CHANGE_DIR_NAME}, {"attributes", FILE_NOTIFY_CHANGE_ATTRIBUTES}, - {"size", FILE_NOTIFY_CHANGE_SIZE}, - {"last-write", FILE_NOTIFY_CHANGE_LAST_WRITE}, - {"last-access", FILE_NOTIFY_CHANGE_LAST_ACCESS}, {"creation", FILE_NOTIFY_CHANGE_CREATION}, - {"security", FILE_NOTIFY_CHANGE_SECURITY} + {"dir-name", FILE_NOTIFY_CHANGE_DIR_NAME}, + {"file-name", FILE_NOTIFY_CHANGE_FILE_NAME}, + {"last-access", FILE_NOTIFY_CHANGE_LAST_ACCESS}, + {"last-write", FILE_NOTIFY_CHANGE_LAST_WRITE}, + {"security", FILE_NOTIFY_CHANGE_SECURITY}, + {"size", FILE_NOTIFY_CHANGE_SIZE}, }; static uint32_t decode_watch_flags(const Janet *options, int32_t n) { @@ -289,30 +293,118 @@ static void janet_watcher_init(JanetWatcher *watcher, JanetChannel *channel, uin watcher->default_flags = default_flags; } +/* Since the file info padding includes embedded file names, we want to include more space for data. + * We also need to handle manually calculating changes if path names are too long, but ideally just avoid + * that scenario as much as possible */ +#define FILE_INFO_PADDING (4096 * 4) + typedef struct { - JanetStream stream; OVERLAPPED overlapped; + JanetStream *stream; + JanetWatcher *watcher; uint32_t flags; - FILE_NOTIFY_INFORMATION fni; + uint64_t buf[FILE_INFO_PADDING / sizeof(uint64_t)]; /* Ensure alignment */ } OverlappedWatch; +static void read_dir_changes(OverlappedWatch *ow) { + BOOL result = ReadDirectoryChangesExW(ow->stream->handle, + (FILE_NOTIFY_EXTENDED_INFORMATION *) ow->buf, + FILE_INFO_PADDING, + TRUE, + ow->flags, + NULL, + (OVERLAPPED *) ow, + NULL, + ReadDirectoryNotifyExtendedInformation); + if (!result) { + janet_panicv(janet_ev_lasterr()); + } +} + +static const char* watcher_actions_windows[] = { + "unknown", + "added", + "removed", + "modified", + "renamed-old", + "renamed-new", +}; + +static void watcher_callback_read(JanetFiber *fiber, JanetAsyncEvent event) { + OverlappedWatch *ow = (OverlappedWatch *) fiber->ev_state; + JanetWatcher *watcher = ow->watcher; + switch (event) { + default: + break; + case JANET_ASYNC_EVENT_MARK: + janet_mark(janet_wrap_abstract(ow->stream)); + janet_mark(janet_wrap_abstract(watcher)); + break; + case JANET_ASYNC_EVENT_CLOSE: + janet_schedule(fiber, janet_wrap_nil()); + janet_async_end(fiber); + break; + case JANET_ASYNC_EVENT_ERR: + janet_schedule(fiber, janet_wrap_nil()); + janet_async_end(fiber); + break; + case JANET_ASYNC_EVENT_COMPLETE: + { + FILE_NOTIFY_EXTENDED_INFORMATION *fni = (FILE_NOTIFY_EXTENDED_INFORMATION *) ow->buf; + + while (1) { + /* Got an event */ + + /* Extract name */ + uint8_t tempbuf[FILE_INFO_PADDING]; + int32_t nbytes = (int32_t) WideCharToMultiByte(CP_UTF8, WC_SEPCHARS, fni->FileName, fni->FileNameLength / 2, tempbuf, sizeof(tempbuf), NULL, NULL); + JanetString filename = janet_string(tempbuf, nbytes); + + JanetKV *event = janet_struct_begin(2); + janet_struct_put(event, janet_ckeywordv("action"), janet_ckeywordv(watcher_actions_windows[fni->Action])); + janet_struct_put(event, janet_ckeywordv("file-name"), janet_wrap_string(filename)); + Janet eventv = janet_wrap_struct(janet_struct_end(event)); + + janet_channel_give(watcher->channel, eventv); + + /* Next event */ + if (!fni->NextEntryOffset) break; + fni = (FILE_NOTIFY_EXTENDED_INFORMATION *) ((char *)fni + fni->NextEntryOffset); + } + + /* Make another call to read directory changes */ + read_dir_changes(ow); + } + break; + } +} + static void janet_watcher_add(JanetWatcher *watcher, const char *path, uint32_t flags) { - HANDLE handle = CreateFileA(path, GENERIC_READ, + HANDLE handle = CreateFileA(path, + FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, - FILE_FLAG_OVERLAPPED | FILE_ATTRIBUTE_NORMAL, - 0); + FILE_FLAG_OVERLAPPED | FILE_FLAG_BACKUP_SEMANTICS, + NULL); if (handle == INVALID_HANDLE_VALUE) { janet_panicv(janet_ev_lasterr()); } - JanetStream *stream = janet_stream_ext(handle, 0, NULL, sizeof(OverlappedWatch)); + JanetStream *stream = janet_stream(handle, JANET_STREAM_READABLE, NULL); + OverlappedWatch *ow = janet_malloc(sizeof(OverlappedWatch)); + memset(ow, 0, sizeof(OverlappedWatch)); + ow->stream = stream; Janet pathv = janet_cstringv(path); - stream->flags = flags | watcher->default_flags; - Janet streamv = janet_wrap_abstract(stream); + ow->flags = flags | watcher->default_flags; + ow->watcher = watcher; + ow->overlapped.hEvent = CreateEvent(NULL, FALSE, 0, NULL); /* Do we need this */ + Janet streamv = janet_wrap_pointer(ow); janet_table_put(&watcher->watch_descriptors, pathv, streamv); janet_table_put(&watcher->watch_descriptors, streamv, pathv); - /* TODO - if listening, also listen for this new path */ + if (watcher->is_watching) { + read_dir_changes(ow); + janet_async_start(stream, JANET_ASYNC_LISTEN_READ, watcher_callback_read, ow); + } } static void janet_watcher_remove(JanetWatcher *watcher, const char *path) { @@ -323,51 +415,19 @@ static void janet_watcher_remove(JanetWatcher *watcher, const char *path) { } janet_table_remove(&watcher->watch_descriptors, pathv); janet_table_remove(&watcher->watch_descriptors, streamv); - OverlappedWatch *ow = janet_unwrap_abstract(streamv); - janet_stream_close((JanetStream *) ow); -} - -static void watcher_callback_read(JanetFiber *fiber, JanetAsyncEvent event) { - JanetWatcher *watcher = (JanetWatcher *) fiber->ev_state; - switch (event) { - default: - break; - case JANET_ASYNC_EVENT_MARK: - janet_mark(janet_wrap_abstract(watcher)); - break; - case JANET_ASYNC_EVENT_CLOSE: - janet_schedule(fiber, janet_wrap_nil()); - fiber->ev_state = NULL; - janet_async_end(fiber); - break; - case JANET_ASYNC_EVENT_ERR: - { - janet_schedule(fiber, janet_wrap_nil()); - fiber->ev_state = NULL; - janet_async_end(fiber); - break; - } - break; - } + OverlappedWatch *ow = janet_unwrap_pointer(streamv); + janet_stream_close(ow->stream); } static void janet_watcher_listen(JanetWatcher *watcher) { + if (watcher->is_watching) janet_panic("already watching"); + watcher->is_watching = 1; for (int32_t i = 0; i < watcher->watch_descriptors.capacity; i++) { const JanetKV *kv = watcher->watch_descriptors.data + i; - if (!janet_checktype(kv->key, JANET_POINTER)) continue; + if (!janet_checktype(kv->key, JANET_ABSTRACT)) continue; OverlappedWatch *ow = janet_unwrap_pointer(kv->key); - Janet pathv = kv->value; - BOOL result = ReadDirectoryChangesW(ow->stream.handle, - &ow->fni, - sizeof(FILE_NOTIFY_INFORMATION), - TRUE, - ow->flags, - NULL, - &ow->overlapped, - NULL); - if (!result) { - janet_panicv(janet_ev_lasterr()); - } + read_dir_changes(ow); + janet_async_start(ow->stream, JANET_ASYNC_LISTEN_READ, watcher_callback_read, ow); } }