From 748790e82395fe67a6bf740ef8003fa7c6460e8a Mon Sep 17 00:00:00 2001 From: Calvin Rose Date: Wed, 25 Mar 2026 19:36:40 -0500 Subject: [PATCH] Add filewatch example. --- examples/filewatch.janet | 14 ++++++++++++ src/core/filewatch.c | 46 ++++++++++++++++++++++++++++++++------ test/suite-filewatch.janet | 16 +++++++++++-- 3 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 examples/filewatch.janet diff --git a/examples/filewatch.janet b/examples/filewatch.janet new file mode 100644 index 00000000..0f0c99b1 --- /dev/null +++ b/examples/filewatch.janet @@ -0,0 +1,14 @@ +### +### example/filewatch.janet ...files +### +### Watch for all changes in a list of files and directories. Behavior +### depends on the filewatch module, and different operating systems will +### report different events. + +(def chan (ev/chan 1000)) +(def fw (filewatch/new chan)) +(each arg (drop 1 (dyn *args* [])) + (filewatch/add fw arg :all)) +(filewatch/listen fw) + +(forever (let [event (ev/take chan)] (pp event))) diff --git a/src/core/filewatch.c b/src/core/filewatch.c index 33ef9885..d1e0b39b 100644 --- a/src/core/filewatch.c +++ b/src/core/filewatch.c @@ -651,6 +651,7 @@ static void janet_watcher_remove(JanetWatcher *watcher, const char *path) { typedef struct { JanetWatcher *watcher; + uint32_t cookie; } KqueueWatcherState; static void watcher_callback_read(JanetFiber *fiber, JanetAsyncEvent event) { @@ -688,18 +689,49 @@ static void watcher_callback_read(JanetFiber *fiber, JanetAsyncEvent event) { break; } for (int i = 0; i < status; i++) { + state->cookie += 6700417; struct kevent kev = events[i]; Janet ident = janet_wrap_integer(kev.ident); + /* TODO - avoid stat call here, maybe just when adding listener? */ + struct stat stat_buf = {0}; + int status; + do { + status = fstat(ident, &stat_buf); + } while (status == -1 && errno == EINTR); + if (status == -1) continue; + int is_dir = S_ISDIR(stat_buf.st_mode); Janet path = janet_table_get(watcher->watch_descriptors, ident); int32_t ev_index = 0; for (unsigned int j = 1; j < (sizeof(watcher_flags_kqueue) / sizeof(watcher_flags_kqueue[0])); j++) { uint32_t flagcheck = watcher_flags_kqueue[j].flag; if (kev.fflags & flagcheck) { - JanetKV *event = janet_struct_begin(4); - janet_struct_put(event, janet_ckeywordv("fd"), ident); - janet_struct_put(event, janet_ckeywordv("file-name"), path); - janet_struct_put(event, janet_ckeywordv("ev-index"), janet_wrap_integer(ev_index++)); + JanetKV *event = janet_struct_begin(6); + janet_struct_put(event, janet_ckeywordv("wd"), ident); + janet_struct_put(event, janet_ckeywordv("wd-path"), path); + janet_struct_put(event, janet_ckeywordv("cookie"), janet_wrap_number((double) state->cookie)); janet_struct_put(event, janet_ckeywordv("type"), janet_ckeywordv(watcher_flags_kqueue[j].name)); + if (is_dir) { + /* Pass in directly */ + janet_struct_put(event, janet_ckeywordv("file-name"), janet_cstringv("")); + janet_struct_put(event, janet_ckeywordv("dir-name"), path); + } else { + /* Split path */ + JanetString spath = janet_unwrap_string(path); + const uint8_t *cursor = spath + janet_string_length(spath); + const uint8_t *cursor_end = cursor; + while (cursor > spath && cursor[0] != '/') { + cursor--; + } + if (cursor == spath) { + /* No path separators */ + janet_struct_put(event, janet_ckeywordv("dir-name"), janet_cstringv(".")); + janet_struct_put(event, janet_ckeywordv("file-name"), janet_wrap_string(spath)); + } else { + /* Found path separator */ + janet_struct_put(event, janet_ckeywordv("dir-name"), janet_wrap_string(janet_string(spath, (cursor - spath)))); + janet_struct_put(event, janet_ckeywordv("file-name"), janet_wrap_string(janet_string(cursor + 1, (cursor_end - cursor - 1)))); + } + } Janet eventv = janet_wrap_struct(janet_struct_end(event)); janet_channel_give(watcher->channel, eventv); } @@ -812,10 +844,10 @@ JANET_CORE_FN(cfun_filewatch_make, "* `:dir-name` -- the directory name of the file that triggered the event.\n\n" "Events also will contain keys specific to the host OS.\n\n" "Windows has no extra properties on events.\n\n" - "Linux has the following extra properties on events:\n\n" - "* `:wd` -- the integer key returned by `filewatch/add` for the path that triggered this.\n\n" + "Linux and the BSDs have the following extra properties on events:\n\n" + "* `:wd` -- the integer key returned by `filewatch/add` for the path that triggered this. This is a file descriptor integer on BSD and macos.\n\n" "* `:wd-path` -- the string path for watched directory of file. For files, will be the same as `:file-name`, and for directories, will be the same as `:dir-name`.\n\n" - "* `:cookie` -- a randomized integer used to associate related events, such as :moved-from and :moved-to events.\n\n" + "* `:cookie` -- a semi-randomized integer used to associate related events, such as :moved-from and :moved-to events.\n\n" "") { janet_sandbox_assert(JANET_SANDBOX_FS_READ); janet_arity(argc, 1, -1); diff --git a/test/suite-filewatch.janet b/test/suite-filewatch.janet index b59b1266..73aeac53 100644 --- a/test/suite-filewatch.janet +++ b/test/suite-filewatch.janet @@ -210,9 +210,21 @@ # This could be ammended with some heavier-weight functionality in userspace, though. (when is-kqueue (spit-file td1 "file1.txt") - (expect :ev-index 0 :file-name td1 :type :write) + (expect :wd-path td1 :type :write) (expect-empty) - (gccollect)) + (gccollect) + (spit-file td1 "file1.txt") + # Currently, only operations that modify the parent vnode do anything + (expect-empty) + (gccollect) + # Check that we don't get anymore events from test directory 2 + (spit-file td2 "file2.txt") + (expect :wd-path td2 :type :write) + (expect-empty) + # Remove a file, then wait for remove event + (rmrf (string td1 "/file1.txt")) + (expect :type :delete) + (expect-empty)) (assert-no-error "filewatch/unlisten no error" (filewatch/unlisten fw)) (assert-no-error "cleanup 1" (rmrf td1))