From fb10c6388267110510a574f05a3f0a5ebacfc77d Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Sun, 23 Feb 2025 08:49:49 -0600 Subject: [PATCH] add dmon and nota into source tree, out of subprojects; build on macos --- meson.build | 24 +- source/dmon.h | 1748 +++++++++++++++++++++++++++++++++++++ source/jsffi.c | 7 + source/kim.h | 122 +++ source/nota.h | 357 ++++++++ source/qjs_dmon.c | 150 ++++ source/qjs_dmon.h | 8 + source/qjs_nota.c | 180 ++++ source/qjs_nota.h | 8 + subprojects/qjs-dmon.wrap | 7 - subprojects/qjs-nota.wrap | 7 - tests/nota.js | 126 +++ 12 files changed, 2725 insertions(+), 19 deletions(-) create mode 100644 source/dmon.h create mode 100755 source/kim.h create mode 100755 source/nota.h create mode 100644 source/qjs_dmon.c create mode 100644 source/qjs_dmon.h create mode 100755 source/qjs_nota.c create mode 100644 source/qjs_nota.h delete mode 100644 subprojects/qjs-dmon.wrap delete mode 100644 subprojects/qjs-nota.wrap create mode 100644 tests/nota.js diff --git a/meson.build b/meson.build index 9d3a1b30..16861951 100644 --- a/meson.build +++ b/meson.build @@ -39,7 +39,24 @@ deps = [] if host_machine.system() == 'darwin' add_project_arguments('-x', 'objective-c', language: 'c') - fworks = ['foundation', 'metal', 'audiotoolbox', 'metalkit', 'avfoundation', 'quartzcore', 'cocoa'] + fworks = [ + 'foundation', + 'metal', + 'audiotoolbox', + 'metalkit', + 'avfoundation', + 'quartzcore', + 'cocoa', + 'coreaudio', + 'coremedia', + 'gamecontroller', + 'forcefeedback', + 'iokit', + 'corefoundation', + 'corehaptics', + 'carbon', + 'uniformtypeidentifiers' + ] foreach fkit : fworks deps += dependency('appleframeworks', modules: fkit) endforeach @@ -106,7 +123,6 @@ if storefront == 'steam' endif deps += dependency('qjs-layout',static:true) -deps += dependency('qjs-nota',static:true) deps += dependency('qjs-miniz',static:true) deps += dependency('qjs-soloud',static:true) deps += dependency('physfs', static:true) @@ -127,7 +143,7 @@ if get_option('enet') endif sources = [] -src += ['anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c','render.c','script.c','simplex.c','spline.c', 'timer.c', 'transform.c','prosperon.c', 'wildmatch.c', 'sprite.c', 'rtree.c'] +src += ['anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c','render.c','script.c','simplex.c','spline.c', 'timer.c', 'transform.c','prosperon.c', 'wildmatch.c', 'sprite.c', 'rtree.c', 'qjs_dmon.c', 'qjs_nota.c'] imsrc = ['GraphEditor.cpp','ImCurveEdit.cpp','ImGradient.cpp','imgui_draw.cpp','imgui_tables.cpp','imgui_widgets.cpp','imgui.cpp','ImGuizmo.cpp','imnodes.cpp','implot_items.cpp','implot.cpp', 'imgui_impl_sdlrenderer3.cpp', 'imgui_impl_sdl3.cpp', 'imgui_impl_sdlgpu3.cpp'] @@ -146,8 +162,6 @@ if get_option('editor') foreach imgui : imsrc sources += tp / 'imgui' / imgui endforeach -# sub_dmon = subproject('qjs-dmon') -# dmon_dep = sub_dmon.get_variable('qjs_dmon_dep') endif includers = [] diff --git a/source/dmon.h b/source/dmon.h new file mode 100644 index 00000000..ead58241 --- /dev/null +++ b/source/dmon.h @@ -0,0 +1,1748 @@ +#ifndef __DMON_H__ +#define __DMON_H__ + +// +// Copyright 2023 Sepehr Taghdisian (septag@github). All rights reserved. +// License: https://github.com/septag/dmon#license-bsd-2-clause +// +// Portable directory monitoring library +// watches directories for file or directory changes. +// +// Usage: +// define DMON_IMPL and include this file to use it: +// #define DMON_IMPL +// #include "dmon.h" +// +// dmon_init(): +// Call this once at the start of your program. +// This will start a low-priority monitoring thread +// dmon_deinit(): +// Call this when your work with dmon is finished, usually on program terminate +// This will free resources and stop the monitoring thread +// dmon_watch: +// Watch for directories +// You can watch multiple directories by calling this function multiple times +// rootdir: root directory to monitor +// watch_cb: callback function to receive events. +// NOTE that this function is called from another thread, so you should +// beware of data races in your application when accessing data within this +// callback +// flags: watch flags, see dmon_watch_flags_t +// user_data: user pointer that is passed to callback function +// Returns the Id of the watched directory after successful call, or returns Id=0 if error +// dmon_unwatch: +// Remove the directory from watch list +// +// see test.c for the basic example +// +// Configuration: +// You can customize some low-level functionality like malloc and logging by overriding macros: +// +// DMON_MALLOC, DMON_FREE, DMON_REALLOC: +// define these macros to override memory allocations +// default is 'malloc', 'free' and 'realloc' +// DMON_ASSERT: +// define this to provide your own assert +// default is 'assert' +// DMON_LOG_ERROR: +// define this to provide your own logging mechanism +// default implementation logs to stdout and breaks the program +// DMON_LOG_DEBUG +// define this to provide your own extra debug logging mechanism +// default implementation logs to stdout in DEBUG and does nothing in other builds +// DMON_API_DECL, DMON_API_IMPL +// define these to provide your own API declarations. (for example: static) +// default is nothing (which is extern in C language ) +// DMON_MAX_PATH +// Maximum size of path characters +// default is 260 characters +// DMON_MAX_WATCHES +// Maximum number of watch directories +// default is 64 +// DMON_SLEEP_INTERVAL +// Number of milliseconds to pause between polling for file changes +// default is 10 ms +// +// TODO: +// - Use FSEventStreamSetDispatchQueue instead of FSEventStreamScheduleWithRunLoop on MacOS +// - DMON_WATCHFLAGS_FOLLOW_SYMLINKS does not resolve files +// - implement DMON_WATCHFLAGS_OUTOFSCOPE_LINKS +// - implement DMON_WATCHFLAGS_IGNORE_DIRECTORIES +// +// History: +// 1.0.0 First version. working Win32/Linux backends +// 1.1.0 MacOS backend +// 1.1.1 Minor fixes, eliminate gcc/clang warnings with -Wall +// 1.1.2 Eliminate some win32 dead code +// 1.1.3 Fixed select not resetting causing high cpu usage on linux +// 1.2.1 inotify (linux) fixes and improvements, added extra functionality header for linux +// to manually add/remove directories manually to the watch handle, in case of large file sets +// 1.2.2 Name refactoring +// 1.3.0 Fixing bugs and proper watch/unwatch handles with freelists. Lower memory consumption, especially on Windows backend +// 1.3.1 Fix in MacOS event grouping + +#include +#include + +#ifndef DMON_API_DECL +# define DMON_API_DECL +#endif + +#ifndef DMON_API_IMPL +# define DMON_API_IMPL +#endif + +typedef struct { uint32_t id; } dmon_watch_id; + +// Pass these flags to `dmon_watch` +typedef enum dmon_watch_flags_t { + DMON_WATCHFLAGS_RECURSIVE = 0x1, // monitor all child directories + DMON_WATCHFLAGS_FOLLOW_SYMLINKS = 0x2, // resolve symlinks (linux only) + DMON_WATCHFLAGS_OUTOFSCOPE_LINKS = 0x4, // TODO: not implemented yet + DMON_WATCHFLAGS_IGNORE_DIRECTORIES = 0x8 // TODO: not implemented yet +} dmon_watch_flags; + +// Action is what operation performed on the file. this value is provided by watch callback +typedef enum dmon_action_t { + DMON_ACTION_CREATE = 1, + DMON_ACTION_DELETE, + DMON_ACTION_MODIFY, + DMON_ACTION_MOVE +} dmon_action; + +#ifdef __cplusplus +extern "C" { +#endif + +DMON_API_DECL void dmon_init(void); +DMON_API_DECL void dmon_deinit(void); + +DMON_API_DECL dmon_watch_id dmon_watch(const char* rootdir, + void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, + const char* rootdir, const char* filepath, + const char* oldfilepath, void* user), + uint32_t flags, void* user_data); +DMON_API_DECL void dmon_unwatch(dmon_watch_id id); + +#ifdef __cplusplus +} +#endif + +#ifdef DMON_IMPL + +#define DMON_OS_WINDOWS 0 +#define DMON_OS_MACOS 0 +#define DMON_OS_LINUX 0 + +#if defined(_WIN32) || defined(_WIN64) +# undef DMON_OS_WINDOWS +# define DMON_OS_WINDOWS 1 +#elif defined(__linux__) +# undef DMON_OS_LINUX +# define DMON_OS_LINUX 1 +#elif defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) +# undef DMON_OS_MACOS +# define DMON_OS_MACOS __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ +#else +# define DMON_OS 0 +# error "unsupported platform" +#endif + +#if DMON_OS_WINDOWS +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# ifndef NOMINMAX +# define NOMINMAX +# endif +# include +# include +# ifdef _MSC_VER +# pragma intrinsic(_InterlockedExchange) +# endif +#elif DMON_OS_LINUX +# ifndef __USE_MISC +# define __USE_MISC +# endif +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +#elif DMON_OS_MACOS +# include +# include +# include +# include +# include +#endif + +#ifndef DMON_MALLOC +# include +# define DMON_MALLOC(size) malloc(size) +# define DMON_FREE(ptr) free(ptr) +# define DMON_REALLOC(ptr, size) realloc(ptr, size) +#endif + +#ifndef DMON_ASSERT +# include +# define DMON_ASSERT(e) assert(e) +#endif + +#ifndef DMON_LOG_ERROR +# include +# define DMON_LOG_ERROR(s) do { puts(s); DMON_ASSERT(0); } while(0) +#endif + +#ifndef DMON_LOG_DEBUG +# ifndef NDEBUG +# include +# define DMON_LOG_DEBUG(s) do { puts(s); } while(0) +# else +# define DMON_LOG_DEBUG(s) +# endif +#endif + +#ifndef DMON_MAX_WATCHES +# define DMON_MAX_WATCHES 64 +#endif + +#ifndef DMON_MAX_PATH +# define DMON_MAX_PATH 260 +#endif + +#define _DMON_UNUSED(x) (void)(x) + +#ifndef _DMON_PRIVATE +# if defined(__GNUC__) || defined(__clang__) +# define _DMON_PRIVATE __attribute__((unused)) static +# else +# define _DMON_PRIVATE static +# endif +#endif + +#ifndef DMON_SLEEP_INTERVAL +# define DMON_SLEEP_INTERVAL 10 +#endif + +#include + +#ifndef _DMON_LOG_ERRORF +# define _DMON_LOG_ERRORF(str, ...) do { char msg[512]; snprintf(msg, sizeof(msg), str, __VA_ARGS__); DMON_LOG_ERROR(msg); } while(0); +#endif + +#ifndef _DMON_LOG_DEBUGF +# define _DMON_LOG_DEBUGF(str, ...) do { char msg[512]; snprintf(msg, sizeof(msg), str, __VA_ARGS__); DMON_LOG_DEBUG(msg); } while(0); +#endif + +#ifndef _dmon_min +# define _dmon_min(a, b) ((a) < (b) ? (a) : (b)) +#endif + +#ifndef _dmon_max +# define _dmon_max(a, b) ((a) > (b) ? (a) : (b)) +#endif + +#ifndef _dmon_swap +# define _dmon_swap(a, b, _type) \ + do { \ + _type tmp = a; \ + a = b; \ + b = tmp; \ + } while (0) +#endif + +#ifndef _dmon_make_id +# ifdef __cplusplus +# define _dmon_make_id(id) {id} +# else +# define _dmon_make_id(id) (dmon_watch_id) {id} +# endif +#endif // _dmon_make_id + +_DMON_PRIVATE bool _dmon_isrange(char ch, char from, char to) +{ + return (uint8_t)(ch - from) <= (uint8_t)(to - from); +} + +_DMON_PRIVATE bool _dmon_isupperchar(char ch) +{ + return _dmon_isrange(ch, 'A', 'Z'); +} + +_DMON_PRIVATE char _dmon_tolowerchar(char ch) +{ + return ch + (_dmon_isupperchar(ch) ? 0x20 : 0); +} + +_DMON_PRIVATE char* _dmon_tolower(char* dst, int dst_sz, const char* str) +{ + int offset = 0; + int dst_max = dst_sz - 1; + while (*str && offset < dst_max) { + dst[offset++] = _dmon_tolowerchar(*str); + ++str; + } + dst[offset] = '\0'; + return dst; +} + +_DMON_PRIVATE char* _dmon_strcpy(char* dst, int dst_sz, const char* src) +{ + DMON_ASSERT(dst); + DMON_ASSERT(src); + + const int32_t len = (int32_t)strlen(src); + const int32_t _max = dst_sz - 1; + const int32_t num = (len < _max ? len : _max); + memcpy(dst, src, num); + dst[num] = '\0'; + + return dst; +} + +_DMON_PRIVATE char* _dmon_unixpath(char* dst, int size, const char* path) +{ + size_t len = strlen(path), i; + len = _dmon_min(len, (size_t)size - 1); + + for (i = 0; i < len; i++) { + if (path[i] != '\\') + dst[i] = path[i]; + else + dst[i] = '/'; + } + dst[len] = '\0'; + return dst; +} + +#if DMON_OS_LINUX || DMON_OS_MACOS +_DMON_PRIVATE char* _dmon_strcat(char* dst, int dst_sz, const char* src) +{ + int len = (int)strlen(dst); + return _dmon_strcpy(dst + len, dst_sz - len, src); +} +#endif // DMON_OS_LINUX || DMON_OS_MACOS + +// stretchy buffer: https://github.com/nothings/stb/blob/master/stretchy_buffer.h +#define stb_sb_free(a) ((a) ? DMON_FREE(stb__sbraw(a)),0 : 0) +#define stb_sb_push(a,v) (stb__sbmaybegrow(a,1), (a)[stb__sbn(a)++] = (v)) +#define stb_sb_pop(a) (stb__sbn(a)--) +#define stb_sb_count(a) ((a) ? stb__sbn(a) : 0) +#define stb_sb_add(a,n) (stb__sbmaybegrow(a,n), stb__sbn(a)+=(n), &(a)[stb__sbn(a)-(n)]) +#define stb_sb_last(a) ((a)[stb__sbn(a)-1]) +#define stb_sb_reset(a) ((a) ? (stb__sbn(a) = 0) : 0) + +#define stb__sbraw(a) ((int *) (a) - 2) +#define stb__sbm(a) stb__sbraw(a)[0] +#define stb__sbn(a) stb__sbraw(a)[1] + +#define stb__sbneedgrow(a,n) ((a)==0 || stb__sbn(a)+(n) >= stb__sbm(a)) +#define stb__sbmaybegrow(a,n) (stb__sbneedgrow(a,(n)) ? stb__sbgrow(a,n) : 0) +#define stb__sbgrow(a,n) (*((void **)&(a)) = stb__sbgrowf((a), (n), sizeof(*(a)))) + +static void * stb__sbgrowf(void *arr, int increment, int itemsize) +{ + int dbl_cur = arr ? 2*stb__sbm(arr) : 0; + int min_needed = stb_sb_count(arr) + increment; + int m = dbl_cur > min_needed ? dbl_cur : min_needed; + int *p = (int *) DMON_REALLOC(arr ? stb__sbraw(arr) : 0, itemsize * m + sizeof(int)*2); + if (p) { + if (!arr) + p[1] = 0; + p[0] = m; + return p+2; + } else { + return (void *) (2*sizeof(int)); // try to force a NULL pointer exception later + } +} + +// watcher callback (same as dmon.h's declaration) +typedef void (_dmon_watch_cb)(dmon_watch_id, dmon_action, const char*, const char*, const char*, void*); + +#if DMON_OS_WINDOWS +// --------------------------------------------------------------------------------------------------------------------- +// @Windows +// IOCP +#ifdef UNICODE +# define _DMON_WINAPI_STR(name, size) wchar_t _##name[size]; MultiByteToWideChar(CP_UTF8, 0, name, -1, _##name, size) +#else +# define _DMON_WINAPI_STR(name, size) const char* _##name = name +#endif + +typedef struct dmon__win32_event { + char filepath[DMON_MAX_PATH]; + DWORD action; + dmon_watch_id watch_id; + bool skip; +} dmon__win32_event; + +typedef struct dmon__watch_state { + dmon_watch_id id; + OVERLAPPED overlapped; + HANDLE dir_handle; + uint8_t buffer[64512]; // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365465(v=vs.85).aspx + DWORD notify_filter; + _dmon_watch_cb* watch_cb; + uint32_t watch_flags; + void* user_data; + char rootdir[DMON_MAX_PATH]; + char old_filepath[DMON_MAX_PATH]; +} dmon__watch_state; + +typedef struct dmon__state { + int num_watches; + dmon__watch_state* watches[DMON_MAX_WATCHES]; + int freelist[DMON_MAX_WATCHES]; + HANDLE thread_handle; + CRITICAL_SECTION mutex; + volatile LONG modify_watches; + dmon__win32_event* events; + bool quit; +} dmon__state; + +static bool _dmon_init; +static dmon__state _dmon; + +_DMON_PRIVATE bool _dmon_refresh_watch(dmon__watch_state* watch) +{ + return ReadDirectoryChangesW(watch->dir_handle, watch->buffer, sizeof(watch->buffer), + (watch->watch_flags & DMON_WATCHFLAGS_RECURSIVE) ? TRUE : FALSE, + watch->notify_filter, NULL, &watch->overlapped, NULL) != 0; +} + +_DMON_PRIVATE void _dmon_unwatch(dmon__watch_state* watch) +{ + CancelIo(watch->dir_handle); + CloseHandle(watch->overlapped.hEvent); + CloseHandle(watch->dir_handle); +} + +_DMON_PRIVATE void _dmon_win32_process_events(void) +{ + int i, c; + for (i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__win32_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + + if (ev->action == FILE_ACTION_MODIFIED || ev->action == FILE_ACTION_ADDED) { + // remove duplicate modifies on a single file + int j; + for (j = i + 1; j < c; j++) { + dmon__win32_event* check_ev = &_dmon.events[j]; + if (check_ev->action == FILE_ACTION_MODIFIED && + strcmp(ev->filepath, check_ev->filepath) == 0) { + check_ev->skip = true; + } + } + } + } + + // trigger user callbacks + for (i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__win32_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + dmon__watch_state* watch = _dmon.watches[ev->watch_id.id - 1]; + + if(watch == NULL || watch->watch_cb == NULL) { + continue; + } + + switch (ev->action) { + case FILE_ACTION_ADDED: + watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, + watch->user_data); + break; + case FILE_ACTION_MODIFIED: + watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir, ev->filepath, NULL, + watch->user_data); + break; + case FILE_ACTION_RENAMED_OLD_NAME: { + // find the first occurrence of the NEW_NAME + // this is somewhat API flaw that we have no reference for relating old and new files + int j; + for (j = i + 1; j < c; j++) { + dmon__win32_event* check_ev = &_dmon.events[j]; + if (check_ev->action == FILE_ACTION_RENAMED_NEW_NAME) { + watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir, + check_ev->filepath, ev->filepath, watch->user_data); + break; + } + } + } break; + case FILE_ACTION_REMOVED: + watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir, ev->filepath, NULL, + watch->user_data); + break; + } + } + stb_sb_reset(_dmon.events); +} + +_DMON_PRIVATE DWORD WINAPI _dmon_thread(LPVOID arg) +{ + _DMON_UNUSED(arg); + HANDLE wait_handles[DMON_MAX_WATCHES]; + dmon__watch_state* watch_states[DMON_MAX_WATCHES]; + + SYSTEMTIME starttm; + GetSystemTime(&starttm); + uint64_t msecs_elapsed = 0; + + while (!_dmon.quit) { + int i; + if (_dmon.modify_watches || !TryEnterCriticalSection(&_dmon.mutex)) { + Sleep(DMON_SLEEP_INTERVAL); + continue; + } + + if (_dmon.num_watches == 0) { + Sleep(DMON_SLEEP_INTERVAL); + LeaveCriticalSection(&_dmon.mutex); + continue; + } + + for (i = 0; i < DMON_MAX_WATCHES; i++) { + if (_dmon.watches[i]) { + dmon__watch_state* watch = _dmon.watches[i]; + watch_states[i] = watch; + wait_handles[i] = watch->overlapped.hEvent; + } + } + + DWORD wait_result = WaitForMultipleObjects(_dmon.num_watches, wait_handles, FALSE, 10); + DMON_ASSERT(wait_result != WAIT_FAILED); + if (wait_result != WAIT_TIMEOUT) { + dmon__watch_state* watch = watch_states[wait_result - WAIT_OBJECT_0]; + DMON_ASSERT(HasOverlappedIoCompleted(&watch->overlapped)); + + DWORD bytes; + if (GetOverlappedResult(watch->dir_handle, &watch->overlapped, &bytes, FALSE)) { + char filepath[DMON_MAX_PATH]; + PFILE_NOTIFY_INFORMATION notify; + size_t offset = 0; + + if (bytes == 0) { + _dmon_refresh_watch(watch); + LeaveCriticalSection(&_dmon.mutex); + continue; + } + + do { + notify = (PFILE_NOTIFY_INFORMATION)&watch->buffer[offset]; + + int count = WideCharToMultiByte(CP_UTF8, 0, notify->FileName, + notify->FileNameLength / sizeof(WCHAR), + filepath, DMON_MAX_PATH - 1, NULL, NULL); + filepath[count] = TEXT('\0'); + _dmon_unixpath(filepath, sizeof(filepath), filepath); + + // TODO: ignore directories if flag is set + + if (stb_sb_count(_dmon.events) == 0) { + msecs_elapsed = 0; + } + dmon__win32_event wev = { { 0 }, notify->Action, watch->id, false }; + _dmon_strcpy(wev.filepath, sizeof(wev.filepath), filepath); + stb_sb_push(_dmon.events, wev); + + offset += notify->NextEntryOffset; + } while (notify->NextEntryOffset > 0); + + if (!_dmon.quit) { + _dmon_refresh_watch(watch); + } + } + } // if (WaitForMultipleObjects) + + SYSTEMTIME tm; + GetSystemTime(&tm); + LONG dt =(tm.wSecond - starttm.wSecond) * 1000 + (tm.wMilliseconds - starttm.wMilliseconds); + starttm = tm; + msecs_elapsed += dt; + if (msecs_elapsed > 100 && stb_sb_count(_dmon.events) > 0) { + _dmon_win32_process_events(); + msecs_elapsed = 0; + } + + LeaveCriticalSection(&_dmon.mutex); + } + return 0; +} + + +DMON_API_IMPL void dmon_init(void) +{ + DMON_ASSERT(!_dmon_init); + InitializeCriticalSection(&_dmon.mutex); + + _dmon.thread_handle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)_dmon_thread, NULL, 0, NULL); + DMON_ASSERT(_dmon.thread_handle); + + for (int i = 0; i < DMON_MAX_WATCHES; i++) + _dmon.freelist[i] = DMON_MAX_WATCHES - i - 1; + + _dmon_init = true; +} + + +DMON_API_IMPL void dmon_deinit(void) +{ + DMON_ASSERT(_dmon_init); + _dmon.quit = true; + if (_dmon.thread_handle != INVALID_HANDLE_VALUE) { + WaitForSingleObject(_dmon.thread_handle, INFINITE); + CloseHandle(_dmon.thread_handle); + } + + { + int i; + for (i = 0; i < DMON_MAX_WATCHES; i++) { + if (_dmon.watches[i]) { + _dmon_unwatch(_dmon.watches[i]); + DMON_FREE(_dmon.watches[i]); + } + } + } + + DeleteCriticalSection(&_dmon.mutex); + stb_sb_free(_dmon.events); + memset(&_dmon, 0x0, sizeof(_dmon)); + _dmon_init = false; +} + +DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, + void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, + const char* dirname, const char* filename, + const char* oldname, void* user), + uint32_t flags, void* user_data) +{ + DMON_ASSERT(_dmon_init); + DMON_ASSERT(watch_cb); + DMON_ASSERT(rootdir && rootdir[0]); + + _InterlockedExchange(&_dmon.modify_watches, 1); + EnterCriticalSection(&_dmon.mutex); + + DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES); + if (_dmon.num_watches >= DMON_MAX_WATCHES) { + DMON_LOG_ERROR("Exceeding maximum number of watches"); + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); + return _dmon_make_id(0); + } + + int num_freelist = DMON_MAX_WATCHES - _dmon.num_watches; + int index = _dmon.freelist[num_freelist - 1]; + uint32_t id = (uint32_t)(index + 1); + + if (_dmon.watches[index] == NULL) { + dmon__watch_state* state = (dmon__watch_state*)DMON_MALLOC(sizeof(dmon__watch_state)); + DMON_ASSERT(state); + if (state == NULL) { + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); + return _dmon_make_id(0); + } + memset(state, 0x0, sizeof(dmon__watch_state)); + _dmon.watches[index] = state; + } + + ++_dmon.num_watches; + + dmon__watch_state* watch = _dmon.watches[index]; + watch->id = _dmon_make_id(id); + watch->watch_flags = flags; + watch->watch_cb = watch_cb; + watch->user_data = user_data; + + _dmon_strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir); + _dmon_unixpath(watch->rootdir, sizeof(watch->rootdir), rootdir); + size_t rootdir_len = strlen(watch->rootdir); + if (watch->rootdir[rootdir_len - 1] != '/') { + watch->rootdir[rootdir_len] = '/'; + watch->rootdir[rootdir_len + 1] = '\0'; + } + + _DMON_WINAPI_STR(rootdir, DMON_MAX_PATH); + watch->dir_handle = + CreateFile(_rootdir, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL); + if (watch->dir_handle != INVALID_HANDLE_VALUE) { + watch->notify_filter = FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_LAST_WRITE | + FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | + FILE_NOTIFY_CHANGE_SIZE; + watch->overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + DMON_ASSERT(watch->overlapped.hEvent != INVALID_HANDLE_VALUE); + + if (!_dmon_refresh_watch(watch)) { + _dmon_unwatch(watch); + DMON_LOG_ERROR("ReadDirectoryChanges failed"); + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); + return _dmon_make_id(0); + } + } else { + _DMON_LOG_ERRORF("Could not open: %s", rootdir); + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); + return _dmon_make_id(0); + } + + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); + return _dmon_make_id(id); +} + +DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) +{ + DMON_ASSERT(_dmon_init); + DMON_ASSERT(id.id > 0); + int index = id.id - 1; + DMON_ASSERT(index < DMON_MAX_WATCHES); + DMON_ASSERT(_dmon.watches[index]); + DMON_ASSERT(_dmon.num_watches > 0); + + if (_dmon.watches[index]) { + _InterlockedExchange(&_dmon.modify_watches, 1); + EnterCriticalSection(&_dmon.mutex); + + _dmon_unwatch(_dmon.watches[index]); + DMON_FREE(_dmon.watches[index]); + _dmon.watches[index] = NULL; + + --_dmon.num_watches; + int num_freelist = DMON_MAX_WATCHES - _dmon.num_watches; + _dmon.freelist[num_freelist - 1] = index; + + LeaveCriticalSection(&_dmon.mutex); + _InterlockedExchange(&_dmon.modify_watches, 0); + } +} + +#elif DMON_OS_LINUX +// --------------------------------------------------------------------------------------------------------------------- +// @Linux +// inotify linux backend +#define _DMON_TEMP_BUFFSIZE ((sizeof(struct inotify_event) + PATH_MAX) * 1024) + +typedef struct dmon__watch_subdir { + char rootdir[DMON_MAX_PATH]; +} dmon__watch_subdir; + +typedef struct dmon__inotify_event { + char filepath[DMON_MAX_PATH]; + uint32_t mask; + uint32_t cookie; + dmon_watch_id watch_id; + bool skip; +} dmon__inotify_event; + +typedef struct dmon__watch_state { + dmon_watch_id id; + int fd; + uint32_t watch_flags; + _dmon_watch_cb* watch_cb; + void* user_data; + char rootdir[DMON_MAX_PATH]; + dmon__watch_subdir* subdirs; + int* wds; +} dmon__watch_state; + +typedef struct dmon__state { + dmon__watch_state* watches[DMON_MAX_WATCHES]; + int freelist[DMON_MAX_WATCHES]; + dmon__inotify_event* events; + int num_watches; + pthread_t thread_handle; + pthread_mutex_t mutex; + bool quit; +} dmon__state; + +static bool _dmon_init; +static dmon__state _dmon; + +_DMON_PRIVATE void _dmon_watch_recursive(const char* dirname, int fd, uint32_t mask, + bool followlinks, dmon__watch_state* watch) +{ + struct dirent* entry; + DIR* dir = opendir(dirname); + DMON_ASSERT(dir); + + char watchdir[DMON_MAX_PATH]; + + while ((entry = readdir(dir)) != NULL) { + bool entry_valid = false; + if (entry->d_type == DT_DIR) { + if (strcmp(entry->d_name, "..") != 0 && strcmp(entry->d_name, ".") != 0) { + _dmon_strcpy(watchdir, sizeof(watchdir), dirname); + _dmon_strcat(watchdir, sizeof(watchdir), entry->d_name); + entry_valid = true; + } + } else if (followlinks && entry->d_type == DT_LNK) { + char linkpath[PATH_MAX]; + _dmon_strcpy(watchdir, sizeof(watchdir), dirname); + _dmon_strcat(watchdir, sizeof(watchdir), entry->d_name); + char* r = realpath(watchdir, linkpath); + _DMON_UNUSED(r); + DMON_ASSERT(r); + _dmon_strcpy(watchdir, sizeof(watchdir), linkpath); + entry_valid = true; + } + + // add sub-directory to watch dirs + if (entry_valid) { + int watchdir_len = (int)strlen(watchdir); + if (watchdir[watchdir_len - 1] != '/') { + watchdir[watchdir_len] = '/'; + watchdir[watchdir_len + 1] = '\0'; + } + int wd = inotify_add_watch(fd, watchdir, mask); + _DMON_UNUSED(wd); + DMON_ASSERT(wd != -1); + + dmon__watch_subdir subdir; + _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); + if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { + _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir)); + } + + stb_sb_push(watch->subdirs, subdir); + stb_sb_push(watch->wds, wd); + + // recurse + _dmon_watch_recursive(watchdir, fd, mask, followlinks, watch); + } + } + closedir(dir); +} + +_DMON_PRIVATE const char* _dmon_find_subdir(const dmon__watch_state* watch, int wd) +{ + const int* wds = watch->wds; + int i, c; + for (i = 0, c = stb_sb_count(wds); i < c; i++) { + if (wd == wds[i]) { + return watch->subdirs[i].rootdir; + } + } + + return NULL; +} + +_DMON_PRIVATE void _dmon_gather_recursive(dmon__watch_state* watch, const char* dirname) +{ + struct dirent* entry; + DIR* dir = opendir(dirname); + DMON_ASSERT(dir); + + char newdir[DMON_MAX_PATH]; + while ((entry = readdir(dir)) != NULL) { + bool entry_valid = false; + bool is_dir = false; + if (strcmp(entry->d_name, "..") != 0 && strcmp(entry->d_name, ".") != 0) { + _dmon_strcpy(newdir, sizeof(newdir), dirname); + _dmon_strcat(newdir, sizeof(newdir), entry->d_name); + is_dir = (entry->d_type == DT_DIR); + entry_valid = true; + } + + // add sub-directory to watch dirs + if (entry_valid) { + dmon__watch_subdir subdir; + _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), newdir); + if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { + _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), newdir + strlen(watch->rootdir)); + } + + dmon__inotify_event dev = { { 0 }, IN_CREATE|(is_dir ? IN_ISDIR : 0U), 0, watch->id, false }; + _dmon_strcpy(dev.filepath, sizeof(dev.filepath), subdir.rootdir); + stb_sb_push(_dmon.events, dev); + } + } + closedir(dir); +} + +_DMON_PRIVATE void _dmon_inotify_process_events(void) +{ + int i, c; + for (i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__inotify_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + + // remove redundant modify events on a single file + if (ev->mask & IN_MODIFY) { + int j; + for (j = i + 1; j < c; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) { + ev->skip = true; + break; + } else if ((ev->mask & IN_ISDIR) && (check_ev->mask & (IN_ISDIR|IN_MODIFY))) { + // in some cases, particularly when created files under sub directories + // there can be two modify events for a single subdir one with trailing slash and one without + // remove trailing slash from both cases and test + int l1 = (int)strlen(ev->filepath); + int l2 = (int)strlen(check_ev->filepath); + if (ev->filepath[l1-1] == '/') ev->filepath[l1-1] = '\0'; + if (check_ev->filepath[l2-1] == '/') check_ev->filepath[l2-1] = '\0'; + if (strcmp(ev->filepath, check_ev->filepath) == 0) { + ev->skip = true; + break; + } + } + } + } else if (ev->mask & IN_CREATE) { + int j; + bool loop_break = false; + for (j = i + 1; j < c && !loop_break; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if ((check_ev->mask & IN_MOVED_FROM) && strcmp(ev->filepath, check_ev->filepath) == 0) { + // there is a case where some programs (like gedit): + // when we save, it creates a temp file, and moves it to the file being modified + // search for these cases and remove all of them + int k; + for (k = j + 1; k < c; k++) { + dmon__inotify_event* third_ev = &_dmon.events[k]; + if (third_ev->mask & IN_MOVED_TO && check_ev->cookie == third_ev->cookie) { + third_ev->mask = IN_MODIFY; // change to modified + ev->skip = check_ev->skip = true; + loop_break = true; + break; + } + } + } else if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) { + // Another case is that file is copied. CREATE and MODIFY happens sequentially + // so we ignore MODIFY event + check_ev->skip = true; + } + } + } else if (ev->mask & IN_MOVED_FROM) { + bool move_valid = false; + int j; + for (j = i + 1; j < c; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if (check_ev->mask & IN_MOVED_TO && ev->cookie == check_ev->cookie) { + move_valid = true; + break; + } + } + + // in some environments like nautilus file explorer: + // when a file is deleted, it is moved to recycle bin + // so if the destination of the move is not valid, it's probably DELETE + if (!move_valid) { + ev->mask = IN_DELETE; + } + } else if (ev->mask & IN_MOVED_TO) { + bool move_valid = false; + int j; + for (j = 0; j < i; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if (check_ev->mask & IN_MOVED_FROM && ev->cookie == check_ev->cookie) { + move_valid = true; + break; + } + } + + // in some environments like nautilus file explorer: + // when a file is deleted, it is moved to recycle bin, on undo it is moved back it + // so if the destination of the move is not valid, it's probably CREATE + if (!move_valid) { + ev->mask = IN_CREATE; + } + } else if (ev->mask & IN_DELETE) { + int j; + for (j = i + 1; j < c; j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + // if the file is DELETED and then MODIFIED after, just ignore the modify event + if ((check_ev->mask & IN_MODIFY) && strcmp(ev->filepath, check_ev->filepath) == 0) { + check_ev->skip = true; + break; + } + } + } + } + + // trigger user callbacks + for (i = 0; i < stb_sb_count(_dmon.events); i++) { + dmon__inotify_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + dmon__watch_state* watch = _dmon.watches[ev->watch_id.id - 1]; + + if(watch == NULL || watch->watch_cb == NULL) { + continue; + } + + if (ev->mask & IN_CREATE) { + if (ev->mask & IN_ISDIR) { + if (watch->watch_flags & DMON_WATCHFLAGS_RECURSIVE) { + char watchdir[DMON_MAX_PATH]; + _dmon_strcpy(watchdir, sizeof(watchdir), watch->rootdir); + _dmon_strcat(watchdir, sizeof(watchdir), ev->filepath); + _dmon_strcat(watchdir, sizeof(watchdir), "/"); + uint32_t mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; + int wd = inotify_add_watch(watch->fd, watchdir, mask); + _DMON_UNUSED(wd); + DMON_ASSERT(wd != -1); + + dmon__watch_subdir subdir; + _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); + if (strstr(subdir.rootdir, watch->rootdir) == subdir.rootdir) { + _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir + strlen(watch->rootdir)); + } + + stb_sb_push(watch->subdirs, subdir); + stb_sb_push(watch->wds, wd); + + // some directories may be already created, for instance, with the command: mkdir -p + // so we will enumerate them manually and add them to the events + _dmon_gather_recursive(watch, watchdir); + ev = &_dmon.events[i]; // gotta refresh the pointer because it may be relocated + } + } + watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir, ev->filepath, NULL, watch->user_data); + } + else if (ev->mask & IN_MODIFY) { + watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir, ev->filepath, NULL, watch->user_data); + } + else if (ev->mask & IN_MOVED_FROM) { + int j; + for (j = i + 1; j < stb_sb_count(_dmon.events); j++) { + dmon__inotify_event* check_ev = &_dmon.events[j]; + if (check_ev->mask & IN_MOVED_TO && ev->cookie == check_ev->cookie) { + watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir, + check_ev->filepath, ev->filepath, watch->user_data); + break; + } + } + } + else if (ev->mask & IN_DELETE) { + watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir, ev->filepath, NULL, watch->user_data); + } + } + + stb_sb_reset(_dmon.events); +} + +static void* _dmon_thread(void* arg) +{ + _DMON_UNUSED(arg); + + static uint8_t buff[_DMON_TEMP_BUFFSIZE]; + struct timespec req = { (time_t)DMON_SLEEP_INTERVAL / 1000, (long)(DMON_SLEEP_INTERVAL * 1000000) }; + struct timespec rem = { 0, 0 }; + struct timeval timeout; + uint64_t usecs_elapsed = 0; + + struct timeval starttm; + gettimeofday(&starttm, 0); + + while (!_dmon.quit) { + nanosleep(&req, &rem); + if (_dmon.num_watches == 0 || pthread_mutex_trylock(&_dmon.mutex) != 0) { + continue; + } + + // Create read FD set + fd_set rfds; + FD_ZERO(&rfds); + { + int i; + for (i = 0; i < _dmon.num_watches; i++) { + dmon__watch_state* watch = _dmon.watches[i]; + FD_SET(watch->fd, &rfds); + } + } + + timeout.tv_sec = 0; + timeout.tv_usec = 100000; + if (select(FD_SETSIZE, &rfds, NULL, NULL, &timeout)) { + int i; + for (i = 0; i < _dmon.num_watches; i++) { + dmon__watch_state* watch = _dmon.watches[i]; + if (FD_ISSET(watch->fd, &rfds)) { + ssize_t offset = 0; + ssize_t len = read(watch->fd, buff, _DMON_TEMP_BUFFSIZE); + if (len <= 0) { + continue; + } + + while (offset < len) { + struct inotify_event* iev = (struct inotify_event*)&buff[offset]; + + const char *subdir = _dmon_find_subdir(watch, iev->wd); + if (subdir) { + char filepath[DMON_MAX_PATH]; + _dmon_strcpy(filepath, sizeof(filepath), subdir); + _dmon_strcat(filepath, sizeof(filepath), iev->name); + + // TODO: ignore directories if flag is set + + if (stb_sb_count(_dmon.events) == 0) { + usecs_elapsed = 0; + } + dmon__inotify_event dev = { { 0 }, iev->mask, iev->cookie, watch->id, false }; + _dmon_strcpy(dev.filepath, sizeof(dev.filepath), filepath); + stb_sb_push(_dmon.events, dev); + } + + offset += sizeof(struct inotify_event) + iev->len; + } + } + } + } + + struct timeval tm; + gettimeofday(&tm, 0); + long dt = (tm.tv_sec - starttm.tv_sec) * 1000000 + tm.tv_usec - starttm.tv_usec; + starttm = tm; + usecs_elapsed += dt; + if (usecs_elapsed > 100000 && stb_sb_count(_dmon.events) > 0) { + _dmon_inotify_process_events(); + usecs_elapsed = 0; + } + + pthread_mutex_unlock(&_dmon.mutex); + } + return 0x0; +} + +_DMON_PRIVATE void _dmon_unwatch(dmon__watch_state* watch) +{ + close(watch->fd); + stb_sb_free(watch->subdirs); + stb_sb_free(watch->wds); +} + +DMON_API_IMPL void dmon_init(void) +{ + DMON_ASSERT(!_dmon_init); + pthread_mutex_init(&_dmon.mutex, NULL); + + int r = pthread_create(&_dmon.thread_handle, NULL, _dmon_thread, NULL); + _DMON_UNUSED(r); + DMON_ASSERT(r == 0 && "pthread_create failed"); + + for (int i = 0; i < DMON_MAX_WATCHES; i++) + _dmon.freelist[i] = DMON_MAX_WATCHES - i - 1; + + _dmon_init = true; +} + +DMON_API_IMPL void dmon_deinit(void) +{ + DMON_ASSERT(_dmon_init); + _dmon.quit = true; + pthread_join(_dmon.thread_handle, NULL); + + { + int i; + for (i = 0; i < _dmon.num_watches; i++) { + if (_dmon.watches[i]) { + _dmon_unwatch(_dmon.watches[i]); + DMON_FREE(_dmon.watches[i]); + } + } + } + + pthread_mutex_destroy(&_dmon.mutex); + stb_sb_free(_dmon.events); + memset(&_dmon, 0x0, sizeof(_dmon)); + _dmon_init = false; +} + +DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, + void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, + const char* dirname, const char* filename, + const char* oldname, void* user), + uint32_t flags, void* user_data) +{ + DMON_ASSERT(_dmon_init); + DMON_ASSERT(watch_cb); + DMON_ASSERT(rootdir && rootdir[0]); + + pthread_mutex_lock(&_dmon.mutex); + + DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES); + if (_dmon.num_watches >= DMON_MAX_WATCHES) { + DMON_LOG_ERROR("Exceeding maximum number of watches"); + pthread_mutex_unlock(&_dmon.mutex); + return _dmon_make_id(0); + } + + int num_freelist = DMON_MAX_WATCHES - _dmon.num_watches; + int index = _dmon.freelist[num_freelist - 1]; + uint32_t id = (uint32_t)(index + 1); + + if (_dmon.watches[index] == NULL) { + dmon__watch_state* state = (dmon__watch_state*)DMON_MALLOC(sizeof(dmon__watch_state)); + DMON_ASSERT(state); + if (state == NULL) { + pthread_mutex_unlock(&_dmon.mutex); + return _dmon_make_id(0); + } + memset(state, 0x0, sizeof(dmon__watch_state)); + _dmon.watches[index] = state; + } + + ++_dmon.num_watches; + + dmon__watch_state* watch = _dmon.watches[index]; + DMON_ASSERT(watch); + watch->id = _dmon_make_id(id); + watch->watch_flags = flags; + watch->watch_cb = watch_cb; + watch->user_data = user_data; + + struct stat root_st; + if (stat(rootdir, &root_st) != 0 || !S_ISDIR(root_st.st_mode) || (root_st.st_mode & S_IRUSR) != S_IRUSR) { + _DMON_LOG_ERRORF("Could not open/read directory: %s", rootdir); + pthread_mutex_unlock(&_dmon.mutex); + return _dmon_make_id(0); + } + + if (S_ISLNK(root_st.st_mode)) { + if (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) { + char linkpath[PATH_MAX]; + char* r = realpath(rootdir, linkpath); + _DMON_UNUSED(r); + DMON_ASSERT(r); + + _dmon_strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, linkpath); + } else { + _DMON_LOG_ERRORF("symlinks are unsupported: %s. use DMON_WATCHFLAGS_FOLLOW_SYMLINKS", + rootdir); + pthread_mutex_unlock(&_dmon.mutex); + return _dmon_make_id(0); + } + } else { + _dmon_strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir); + } + + // add trailing slash + int rootdir_len = (int)strlen(watch->rootdir); + if (watch->rootdir[rootdir_len - 1] != '/') { + watch->rootdir[rootdir_len] = '/'; + watch->rootdir[rootdir_len + 1] = '\0'; + } + + watch->fd = inotify_init(); + if (watch->fd < -1) { + DMON_LOG_ERROR("could not create inotify instance"); + pthread_mutex_unlock(&_dmon.mutex); + return _dmon_make_id(0); + } + + uint32_t inotify_mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; + int wd = inotify_add_watch(watch->fd, watch->rootdir, inotify_mask); + if (wd < 0) { + _DMON_LOG_ERRORF("Error watching directory '%s'. (inotify_add_watch:err=%d)", watch->rootdir, errno); + pthread_mutex_unlock(&_dmon.mutex); + return _dmon_make_id(0); + } + dmon__watch_subdir subdir; + _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), ""); // root dir is just a dummy entry + stb_sb_push(watch->subdirs, subdir); + stb_sb_push(watch->wds, wd); + + // recursive mode: enumerate all child directories and add them to watch + if (flags & DMON_WATCHFLAGS_RECURSIVE) { + _dmon_watch_recursive(watch->rootdir, watch->fd, inotify_mask, + (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) ? true : false, watch); + } + + + pthread_mutex_unlock(&_dmon.mutex); + return _dmon_make_id(id); +} + +DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) +{ + DMON_ASSERT(_dmon_init); + DMON_ASSERT(id.id > 0); + int index = id.id - 1; + DMON_ASSERT(index < DMON_MAX_WATCHES); + DMON_ASSERT(_dmon.watches[index]); + DMON_ASSERT(_dmon.num_watches > 0); + + if (_dmon.watches[index]) { + pthread_mutex_lock(&_dmon.mutex); + + _dmon_unwatch(_dmon.watches[index]); + DMON_FREE(_dmon.watches[index]); + _dmon.watches[index] = NULL; + + --_dmon.num_watches; + int num_freelist = DMON_MAX_WATCHES - _dmon.num_watches; + _dmon.freelist[num_freelist - 1] = index; + + pthread_mutex_unlock(&_dmon.mutex); + } +} +#elif DMON_OS_MACOS +// --------------------------------------------------------------------------------------------------------------------- +// @MacOS +// FSEvents MacOS backend +typedef struct dmon__fsevent_event { + char filepath[DMON_MAX_PATH]; + uint64_t event_id; + long event_flags; + dmon_watch_id watch_id; + bool skip; + bool move_valid; +} dmon__fsevent_event; + +typedef struct dmon__watch_state { + dmon_watch_id id; + uint32_t watch_flags; + FSEventStreamRef fsev_stream_ref; + _dmon_watch_cb* watch_cb; + void* user_data; + char rootdir[DMON_MAX_PATH]; + char rootdir_unmod[DMON_MAX_PATH]; + bool init; +} dmon__watch_state; + +typedef struct dmon__state { + dmon__watch_state* watches[DMON_MAX_WATCHES]; + int freelist[DMON_MAX_WATCHES]; + dmon__fsevent_event* events; + int num_watches; + volatile int modify_watches; + pthread_t thread_handle; + dispatch_semaphore_t thread_sem; + pthread_mutex_t mutex; + CFRunLoopRef cf_loop_ref; + CFAllocatorRef cf_alloc_ref; + bool quit; +} dmon__state; + +union dmon__cast_userdata { + void* ptr; + uint32_t id; +}; + +static bool _dmon_init; +static dmon__state _dmon; + +_DMON_PRIVATE void* _dmon_cf_malloc(CFIndex size, CFOptionFlags hints, void* info) +{ + _DMON_UNUSED(hints); + _DMON_UNUSED(info); + return DMON_MALLOC(size); +} + +_DMON_PRIVATE void _dmon_cf_free(void* ptr, void* info) +{ + _DMON_UNUSED(info); + DMON_FREE(ptr); +} + +_DMON_PRIVATE void* _dmon_cf_realloc(void* ptr, CFIndex newsize, CFOptionFlags hints, void* info) +{ + _DMON_UNUSED(hints); + _DMON_UNUSED(info); + return DMON_REALLOC(ptr, (size_t)newsize); +} + +_DMON_PRIVATE void _dmon_fsevent_process_events(void) +{ + int i, c; + for (i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__fsevent_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + + // remove redundant modify events on a single file + if (ev->event_flags & kFSEventStreamEventFlagItemModified) { + int j; + for (j = i + 1; j < c; j++) { + dmon__fsevent_event* check_ev = &_dmon.events[j]; + if ((check_ev->event_flags & kFSEventStreamEventFlagItemModified) && + strcmp(ev->filepath, check_ev->filepath) == 0) { + ev->skip = true; + break; + } + } + } else if ((ev->event_flags & kFSEventStreamEventFlagItemRenamed) && !ev->move_valid) { + int j; + for (j = i + 1; j < c; j++) { + dmon__fsevent_event* check_ev = &_dmon.events[j]; + if ((check_ev->event_flags & kFSEventStreamEventFlagItemRenamed) && + check_ev->event_id == (ev->event_id + 1)) { + ev->move_valid = check_ev->move_valid = true; + break; + } + } + + // in some environments like finder file explorer: + // when a file is deleted, it is moved to recycle bin + // so if the destination of the move is not valid, it's probably DELETE or CREATE + // decide CREATE if file exists + if (!ev->move_valid) { + ev->event_flags &= ~kFSEventStreamEventFlagItemRenamed; + + char abs_filepath[DMON_MAX_PATH]; + dmon__watch_state* watch = _dmon.watches[ev->watch_id.id-1]; + _dmon_strcpy(abs_filepath, sizeof(abs_filepath), watch->rootdir); + _dmon_strcat(abs_filepath, sizeof(abs_filepath), ev->filepath); + + struct stat root_st; + if (stat(abs_filepath, &root_st) != 0) { + ev->event_flags |= kFSEventStreamEventFlagItemRemoved; + } else { + ev->event_flags |= kFSEventStreamEventFlagItemCreated; + } + } + } + } + + // trigger user callbacks + for (i = 0, c = stb_sb_count(_dmon.events); i < c; i++) { + dmon__fsevent_event* ev = &_dmon.events[i]; + if (ev->skip) { + continue; + } + dmon__watch_state* watch = _dmon.watches[ev->watch_id.id - 1]; + + if(watch == NULL || watch->watch_cb == NULL) { + continue; + } + + if (ev->event_flags & kFSEventStreamEventFlagItemCreated) { + watch->watch_cb(ev->watch_id, DMON_ACTION_CREATE, watch->rootdir_unmod, ev->filepath, NULL, + watch->user_data); + } + + if (ev->event_flags & kFSEventStreamEventFlagItemModified) { + watch->watch_cb(ev->watch_id, DMON_ACTION_MODIFY, watch->rootdir_unmod, ev->filepath, NULL, watch->user_data); + } else if (ev->event_flags & kFSEventStreamEventFlagItemRenamed) { + int j; + for (j = i + 1; j < c; j++) { + dmon__fsevent_event* check_ev = &_dmon.events[j]; + if (check_ev->event_flags & kFSEventStreamEventFlagItemRenamed) { + watch->watch_cb(check_ev->watch_id, DMON_ACTION_MOVE, watch->rootdir_unmod, + check_ev->filepath, ev->filepath, watch->user_data); + break; + } + } + } else if (ev->event_flags & kFSEventStreamEventFlagItemRemoved) { + watch->watch_cb(ev->watch_id, DMON_ACTION_DELETE, watch->rootdir_unmod, ev->filepath, NULL, + watch->user_data); + } + } + + stb_sb_reset(_dmon.events); +} + +_DMON_PRIVATE void* _dmon_thread(void* arg) +{ + _DMON_UNUSED(arg); + + struct timespec req = { (time_t)DMON_SLEEP_INTERVAL / 1000, (long)(DMON_SLEEP_INTERVAL * 1000000) }; + struct timespec rem = { 0, 0 }; + + _dmon.cf_loop_ref = CFRunLoopGetCurrent(); + dispatch_semaphore_signal(_dmon.thread_sem); + + while (!_dmon.quit) { + int i; + if (_dmon.modify_watches || pthread_mutex_trylock(&_dmon.mutex) != 0) { + nanosleep(&req, &rem); + continue; + } + + if (_dmon.num_watches == 0) { + nanosleep(&req, &rem); + pthread_mutex_unlock(&_dmon.mutex); + continue; + } + + for (i = 0; i < _dmon.num_watches; i++) { + dmon__watch_state* watch = _dmon.watches[i]; + if (!watch->init) { + DMON_ASSERT(watch->fsev_stream_ref); + FSEventStreamScheduleWithRunLoop(watch->fsev_stream_ref, _dmon.cf_loop_ref, kCFRunLoopDefaultMode); + FSEventStreamStart(watch->fsev_stream_ref); + + watch->init = true; + } + } + + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, kCFRunLoopRunTimedOut); + _dmon_fsevent_process_events(); + + pthread_mutex_unlock(&_dmon.mutex); + } + + CFRunLoopStop(_dmon.cf_loop_ref); + _dmon.cf_loop_ref = NULL; + return 0x0; +} + +_DMON_PRIVATE void _dmon_unwatch(dmon__watch_state* watch) +{ + if (watch->fsev_stream_ref) { + FSEventStreamStop(watch->fsev_stream_ref); + FSEventStreamInvalidate(watch->fsev_stream_ref); + FSEventStreamRelease(watch->fsev_stream_ref); + watch->fsev_stream_ref = NULL; + } +} + +DMON_API_IMPL void dmon_init(void) +{ + DMON_ASSERT(!_dmon_init); + pthread_mutex_init(&_dmon.mutex, NULL); + + CFAllocatorContext cf_alloc_ctx = { 0 }; + cf_alloc_ctx.allocate = _dmon_cf_malloc; + cf_alloc_ctx.deallocate = _dmon_cf_free; + cf_alloc_ctx.reallocate = _dmon_cf_realloc; + _dmon.cf_alloc_ref = CFAllocatorCreate(NULL, &cf_alloc_ctx); + + _dmon.thread_sem = dispatch_semaphore_create(0); + DMON_ASSERT(_dmon.thread_sem); + + int r = pthread_create(&_dmon.thread_handle, NULL, _dmon_thread, NULL); + _DMON_UNUSED(r); + DMON_ASSERT(r == 0 && "pthread_create failed"); + + // wait for thread to initialize loop object + dispatch_semaphore_wait(_dmon.thread_sem, DISPATCH_TIME_FOREVER); + + for (int i = 0; i < DMON_MAX_WATCHES; i++) + _dmon.freelist[i] = DMON_MAX_WATCHES - i - 1; + + _dmon_init = true; +} + +DMON_API_IMPL void dmon_deinit(void) +{ + DMON_ASSERT(_dmon_init); + _dmon.quit = true; + pthread_join(_dmon.thread_handle, NULL); + + dispatch_release(_dmon.thread_sem); + + { + int i; + for (i = 0; i < _dmon.num_watches; i++) { + if (_dmon.watches[i]) { + _dmon_unwatch(_dmon.watches[i]); + DMON_FREE(_dmon.watches[i]); + } + } + } + + pthread_mutex_destroy(&_dmon.mutex); + stb_sb_free(_dmon.events); + if (_dmon.cf_alloc_ref) + CFRelease(_dmon.cf_alloc_ref); + + memset(&_dmon, 0x0, sizeof(_dmon)); + _dmon_init = false; +} + +_DMON_PRIVATE void _dmon_fsevent_callback(ConstFSEventStreamRef stream_ref, void* user_data, + size_t num_events, void* event_paths, + const FSEventStreamEventFlags event_flags[], + const FSEventStreamEventId event_ids[]) +{ + _DMON_UNUSED(stream_ref); + + union dmon__cast_userdata _userdata; + _userdata.ptr = user_data; + dmon_watch_id watch_id = _dmon_make_id(_userdata.id); + DMON_ASSERT(watch_id.id > 0); + dmon__watch_state* watch = _dmon.watches[watch_id.id - 1]; + char abs_filepath[DMON_MAX_PATH]; + char abs_filepath_lower[DMON_MAX_PATH]; + + { + size_t i; + for (i = 0; i < num_events; i++) { + const char *filepath = ((const char **) event_paths)[i]; + long flags = (long) event_flags[i]; + uint64_t event_id = (uint64_t) event_ids[i]; + dmon__fsevent_event ev; + memset(&ev, 0x0, sizeof(ev)); + + _dmon_strcpy(abs_filepath, sizeof(abs_filepath), filepath); + _dmon_unixpath(abs_filepath, sizeof(abs_filepath), abs_filepath); + + // normalize path, so it would be the same on both MacOS file-system types (case/nocase) + _dmon_tolower(abs_filepath_lower, sizeof(abs_filepath), abs_filepath); + DMON_ASSERT(strstr(abs_filepath_lower, watch->rootdir) == abs_filepath_lower); + + // strip the root dir from the beginning + _dmon_strcpy(ev.filepath, sizeof(ev.filepath), abs_filepath + strlen(watch->rootdir)); + + ev.event_flags = flags; + ev.event_id = event_id; + ev.watch_id = watch_id; + stb_sb_push(_dmon.events, ev); + } + } +} + +DMON_API_IMPL dmon_watch_id dmon_watch(const char* rootdir, + void (*watch_cb)(dmon_watch_id watch_id, dmon_action action, + const char* dirname, const char* filename, + const char* oldname, void* user), + uint32_t flags, void* user_data) +{ + DMON_ASSERT(_dmon_init); + DMON_ASSERT(watch_cb); + DMON_ASSERT(rootdir && rootdir[0]); + + __sync_lock_test_and_set(&_dmon.modify_watches, 1); + pthread_mutex_lock(&_dmon.mutex); + + DMON_ASSERT(_dmon.num_watches < DMON_MAX_WATCHES); + if (_dmon.num_watches >= DMON_MAX_WATCHES) { + DMON_LOG_ERROR("Exceeding maximum number of watches"); + pthread_mutex_unlock(&_dmon.mutex); + return _dmon_make_id(0); + } + + + int num_freelist = DMON_MAX_WATCHES - _dmon.num_watches; + int index = _dmon.freelist[num_freelist - 1]; + uint32_t id = (uint32_t)(index + 1); + + if (_dmon.watches[index] == NULL) { + dmon__watch_state* state = (dmon__watch_state*)DMON_MALLOC(sizeof(dmon__watch_state)); + DMON_ASSERT(state); + if (state == NULL) { + pthread_mutex_unlock(&_dmon.mutex); + return _dmon_make_id(0); + } + memset(state, 0x0, sizeof(dmon__watch_state)); + _dmon.watches[index] = state; + } + + ++_dmon.num_watches; + + dmon__watch_state* watch = _dmon.watches[id - 1]; + DMON_ASSERT(watch); + watch->id = _dmon_make_id(id); + watch->watch_flags = flags; + watch->watch_cb = watch_cb; + watch->user_data = user_data; + + struct stat root_st; + if (stat(rootdir, &root_st) != 0 || !S_ISDIR(root_st.st_mode) || + (root_st.st_mode & S_IRUSR) != S_IRUSR) { + _DMON_LOG_ERRORF("Could not open/read directory: %s", rootdir); + pthread_mutex_unlock(&_dmon.mutex); + __sync_lock_test_and_set(&_dmon.modify_watches, 0); + return _dmon_make_id(0); + } + + if (S_ISLNK(root_st.st_mode)) { + if (flags & DMON_WATCHFLAGS_FOLLOW_SYMLINKS) { + char linkpath[PATH_MAX]; + char* r = realpath(rootdir, linkpath); + _DMON_UNUSED(r); + DMON_ASSERT(r); + + _dmon_strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, linkpath); + } else { + _DMON_LOG_ERRORF("symlinks are unsupported: %s. use DMON_WATCHFLAGS_FOLLOW_SYMLINKS", rootdir); + pthread_mutex_unlock(&_dmon.mutex); + __sync_lock_test_and_set(&_dmon.modify_watches, 0); + return _dmon_make_id(0); + } + } else { + char rootdir_abspath[DMON_MAX_PATH]; + if (realpath(rootdir, rootdir_abspath) != NULL) { + _dmon_strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir_abspath); + } else { + _dmon_strcpy(watch->rootdir, sizeof(watch->rootdir) - 1, rootdir); + } + } + + _dmon_unixpath(watch->rootdir, sizeof(watch->rootdir), watch->rootdir); + + // add trailing slash + int rootdir_len = (int)strlen(watch->rootdir); + if (watch->rootdir[rootdir_len - 1] != '/') { + watch->rootdir[rootdir_len] = '/'; + watch->rootdir[rootdir_len + 1] = '\0'; + } + + _dmon_strcpy(watch->rootdir_unmod, sizeof(watch->rootdir_unmod), watch->rootdir); + _dmon_tolower(watch->rootdir, sizeof(watch->rootdir), watch->rootdir); + + // create FS objects + CFStringRef cf_dir = CFStringCreateWithCString(NULL, watch->rootdir_unmod, kCFStringEncodingUTF8); + CFArrayRef cf_dirarr = CFArrayCreate(NULL, (const void**)&cf_dir, 1, NULL); + + FSEventStreamContext ctx; + union dmon__cast_userdata userdata; + userdata.id = id; + ctx.version = 0; + ctx.info = userdata.ptr; + ctx.retain = NULL; + ctx.release = NULL; + ctx.copyDescription = NULL; + watch->fsev_stream_ref = FSEventStreamCreate(_dmon.cf_alloc_ref, _dmon_fsevent_callback, &ctx, + cf_dirarr, kFSEventStreamEventIdSinceNow, 0.25, + kFSEventStreamCreateFlagFileEvents); + + + CFRelease(cf_dirarr); + CFRelease(cf_dir); + + pthread_mutex_unlock(&_dmon.mutex); + __sync_lock_test_and_set(&_dmon.modify_watches, 0); + return _dmon_make_id(id); +} + +DMON_API_IMPL void dmon_unwatch(dmon_watch_id id) +{ + DMON_ASSERT(_dmon_init); + DMON_ASSERT(id.id > 0); + int index = id.id - 1; + DMON_ASSERT(index < DMON_MAX_WATCHES); + DMON_ASSERT(_dmon.watches[index]); + DMON_ASSERT(_dmon.num_watches > 0); + + if (_dmon.watches[index]) { + __sync_lock_test_and_set(&_dmon.modify_watches, 1); + pthread_mutex_lock(&_dmon.mutex); + + _dmon_unwatch(_dmon.watches[index]); + DMON_FREE(_dmon.watches[index]); + _dmon.watches[index] = NULL; + + --_dmon.num_watches; + int num_freelist = DMON_MAX_WATCHES - _dmon.num_watches; + _dmon.freelist[num_freelist - 1] = index; + + pthread_mutex_unlock(&_dmon.mutex); + __sync_lock_test_and_set(&_dmon.modify_watches, 0); + } +} + +#endif + +#endif // DMON_IMPL +#endif // __DMON_H__ diff --git a/source/jsffi.c b/source/jsffi.c index 6968b430..98abbdfa 100644 --- a/source/jsffi.c +++ b/source/jsffi.c @@ -7584,8 +7584,12 @@ MISTUSE(video) MISTUSE(event) MISTUSE(camera) MISTUSE(debug) + JSValue js_imgui_use(JSContext *js); +#include "qjs_dmon.h" +#include "qjs_nota.h" + #define MISTLINE(NAME) (ModuleEntry){#NAME, js_##NAME##_use} void ffi_load(JSContext *js, int argc, char **argv) { @@ -7607,6 +7611,9 @@ void ffi_load(JSContext *js, int argc, char **argv) { arrput(module_registry, MISTLINE(imgui)); arrput(module_registry, MISTLINE(camera)); arrput(module_registry, MISTLINE(debug)); + arrput(module_registry, MISTLINE(dmon)); + arrput(module_registry, MISTLINE(nota)); + #ifdef TRACY_ENABLE arrput(module_registry, MISTLINE(tracy)); #endif diff --git a/source/kim.h b/source/kim.h new file mode 100755 index 00000000..216650e8 --- /dev/null +++ b/source/kim.h @@ -0,0 +1,122 @@ +#ifndef KIM_H +#define KIM_H + +// write number of runes from a kim stream int a utf8 stream +void utf8_to_kim(const char **utf, char **kim); + +// write number of runes from a kim stream int a utf8 stream +void kim_to_utf8(char **kim, char **utf, int runes); + +// Return the number of bytes a given utf-8 rune will have +int utf8_bytes(char c); + +// Return the number of runes in a utf8 string +int utf8_count(const char *utf8); + +#ifdef KIM_IMPLEMENTATION + +#define KIM_CONT 0x80 +#define KIM_DATA 0x7f +#define CONTINUE(CHAR) (CHAR>>7) + +int decode_utf8(char **s); +void encode_utf8(char **s, int code); +static void encode_kim(char **s, int code); +int decode_kim(char **s); + +int utf8_bytes(char c) +{ + int bytes = __builtin_clz(~(c)); + if (!bytes) return 1; + return bytes-24; +} + +int utf8_count(const char *utf8) +{ + int count = 0; + + while(*utf8) { + count++; + utf8 += utf8_bytes(*utf8); + } + + return count; +} + +// decode and advance s, returning the character rune +int decode_utf8(char **s) { + int k = **s ? __builtin_clz(~(**s << 24)) : 0; // Count # of leading 1 bits. + int mask = (1 << (8 - k)) - 1; // All 1's with k leading 0's. + int value = **s & mask; + for (++(*s), --k; k > 0 && **s; --k, ++(*s)) { // Note that k = #total bytes, or 0. + value <<= 6; + value += (**s & 0x3F); + } + return value; +} + +// Write and advance s with rune in utf-8 +void encode_utf8(char **s, int rune) { + char val[4]; + int lead_byte_max = 0x7F; + int val_index = 0; + while (rune > lead_byte_max) { + val[val_index++] = (rune & 0x3F) | 0x80; + rune >>= 6; + lead_byte_max >>= (val_index == 1 ? 2 : 1); + } + val[val_index++] = (rune & lead_byte_max) | (~lead_byte_max << 1); + while (val_index--) { + **s = val[val_index]; + (*s)++; + } +} + +// write and advance s with rune in kim +static inline void encode_kim(char **s, int rune) +{ + if (rune < KIM_CONT) { + **s = 0 | (KIM_DATA & rune); + (*s)++; + return; + } + + int bits = ((32 - __builtin_clz(rune) + 6) / 7) * 7; + + while (bits > 7) { + bits -= 7; + **s = KIM_CONT | KIM_DATA & (rune >> bits); + (*s)++; + } + **s = KIM_DATA & rune; + (*s)++; +} + +// decode and advance s, returning the character rune +int decode_kim(char **s) +{ + int rune = **s & KIM_DATA; + while (CONTINUE(**s)) { + rune <<= 7; + (*s)++; + rune |= **s & KIM_DATA; + } + (*s)++; + return rune; +} + +void utf8_to_kim(const char **utf, char **kim) +{ + char * str = *utf; + while (*str) + encode_kim(kim, decode_utf8(&str)); +} + +void kim_to_utf8(char **kim, char **utf, int runes) +{ + for (int i = 0; i < runes; i++) + encode_utf8(utf, decode_kim(kim)); +} + +#endif +#endif diff --git a/source/nota.h b/source/nota.h new file mode 100755 index 00000000..c6feb59c --- /dev/null +++ b/source/nota.h @@ -0,0 +1,357 @@ +#ifndef NOTA_H +#define NOTA_H + +#define NOTA_BLOB 0x00 // C 0 0 0 +#define NOTA_TEXT 0x10 // +#define NOTA_ARR 0x20 // 0 1 0 +#define NOTA_REC 0x30 // C 0 1 1 +#define NOTA_FLOAT 0x40 // C 1 0 +#define NOTA_INT 0x60 // C 1 1 0 +#define NOTA_SYM 0x70 // C 1 1 1 + +#define NOTA_FALSE 0x00 +#define NOTA_TRUE 0x01 +#define NOTA_NULL 0x02 +#define NOTA_INF 0x03 +#define NOTA_PRIVATE 0x08 +#define NOTA_SYSTEM 0x09 + +// Returns the type NOTA_ of the byte at *nota +int nota_type(char *nota); + +// Functions take a pointer to a buffer *nota, read or write the value, and then return a pointer to the next byte of the stream + +// Pass NULL into the read in variable to skip over it + +char *nota_read_blob(long long *len, char *nota); +// ALLOCATES! Uses strdup to return it via the text pointer +char *nota_read_text(char **text, char *nota); +char *nota_read_array(long long *len, char *nota); +char *nota_read_record(long long *len, char *nota); +char *nota_read_float(double *d, char *nota); +char *nota_read_int(long long *l, char *nota); +char *nota_read_sym(int *sym, char *nota); + +char *nota_write_blob(unsigned long long n, char *nota); +char *nota_write_text(const char *s, char *nota); +char *nota_write_array(unsigned long long n, char *nota); +char *nota_write_record(unsigned long long n, char *nota); +char *nota_write_number(double n, char *nota); +char *nota_write_sym(int sym, char *nota); + +void print_nota_hex(char *nota); + +#ifdef NOTA_IMPLEMENTATION + +#include "stdio.h" +#include "math.h" +#include "string.h" +#include "stdlib.h" +#include "limits.h" +#include "kim.h" + +#define NOTA_CONT 0x80 +#define NOTA_DATA 0x7f +#define NOTA_INT_DATA 0x07 +#define NOTA_INT_SIGN(CHAR) (CHAR & (1<<3)) +#define NOTA_SIG_SIGN(CHAR) (CHAR & (1<<3)) +#define NOTA_EXP_SIGN(CHAR) (CHAR & (1<<4)) +#define NOTA_TYPE 0x70 +#define NOTA_HEAD_DATA 0x0f +#define CONTINUE(CHAR) (CHAR>>7) + +#define UTF8_DATA 0x3f + +/* define this to use native string instead of kim. Bytes are encoded instead of runes */ +#define NOTA_UTF8 + +int nota_type(char *nota) { return *nota & NOTA_TYPE; } + +char *nota_skip(char *nota) +{ + while (CONTINUE(*nota)) + nota++; + + return nota+1; +} + +char *nota_read_num(long long *n, char *nota) +{ + if (!n) + return nota_skip(nota); + + *n = 0; + *n |= (*nota) & NOTA_HEAD_DATA; + + while (CONTINUE(*(nota++))) + *n = (*n<<7) | (*nota) & NOTA_DATA; + + return nota; +} + +// Given a number n, and bits used in the first char sb, how many bits are needed +int nota_bits(long long n, int sb) +{ + if (n == 0) return sb; + int bits = sizeof(n)*CHAR_BIT - __builtin_clzll(n); + bits-=sb; + int needed = ((bits + 6) / 7)*7 + sb; + return needed; +} + +// write a number from n into *nota, with sb bits in the first char +char *nota_continue_num(long long n, char *nota, int sb) +{ + int bits = nota_bits(n, sb); + bits -= sb; + if (bits > 0) + nota[0] |= NOTA_CONT; + else + nota[0] &= ~NOTA_CONT; + + int shex = (~0) << sb; + nota[0] &= shex; /* clear shex bits */ + nota[0] |= (~shex) & (n>>bits); + + int i = 1; + while (bits > 0) { + bits -= 7; + int head = bits == 0 ? 0 : NOTA_CONT; + nota[i] = head | (NOTA_DATA & (n >> bits)); + i++; + } + + return ¬a[i]; +} + + +void print_nota_hex(char *nota) +{ + do { + printf("%02X ", (unsigned char)(*nota)); + } while(CONTINUE(*(nota++))); + printf("\n"); + + return; + long long chars = 0; + if (!((*nota>>4 & 0x07) ^ NOTA_TEXT>>4)) + nota_read_num(&chars, nota); + + if ((*nota>>5) == 2 || (*nota>>5) == 6) + chars = 1; + + for (int i = 0; i < chars+1; i++) { + do { + printf("%02X ", (unsigned char)(*nota)); + } while(CONTINUE(*(nota++))); + } + + printf("\n"); +} + +char *nota_write_int(long long n, char *nota) +{ + char sign = 0; + + if (n < 0) { + sign = 0x08; + n *= -1; + } + + *nota = NOTA_INT | sign; + + return nota_continue_num(n, nota, 3); +} + +#define NOTA_DBL_PREC 6 +#define xstr(s) str(s) +#define str(s) #s + +#include +#include + +void extract_mantissa_coefficient(double num, long *mantissa, long* coefficient) { + char buf[64]; + char *p, *dec_point; + int exp = 0, coeff = 0; + + // Convert double to string with maximum precision + snprintf(buf, sizeof(buf), "%.17g", num); + + // Find if 'e' or 'E' is present (scientific notation) + p = strchr(buf, 'e'); + if (!p) p = strchr(buf, 'E'); + if (p) { + // There is an exponent part + exp = atol(p + 1); + *p = '\0'; // Remove exponent part from the string + } + + // Find decimal point + dec_point = strchr(buf, '.'); + if (dec_point) { + // Count number of digits after decimal point + int digits_after_point = strlen(dec_point + 1); + coeff = digits_after_point; + // Remove decimal point by shifting characters + memmove(dec_point, dec_point + 1, strlen(dec_point)); + } else + coeff = 0; + + // Adjust coefficient with exponent from scientific notation + coeff -= exp; + + // Copy the mantissa + *mantissa = atol(buf); + + // Set coefficient + *coefficient = coeff; +} + +char *nota_write_float(double n, char *nota) +{ + int neg = n < 0; + long digits; + long coef; + extract_mantissa_coefficient(n, &digits, &coef); + + printf("Values of %g are %ld e %ld\n", n, digits, coef); + if (coef == 0) + return nota_write_int(coef * (neg ? -1 : 1), nota); + + int expsign = coef < 0 ? ~0 : 0; + coef = llabs(coef); + + nota[0] = NOTA_FLOAT; + nota[0] |= 0x10 & expsign; + nota[0] |= 0x08 & neg; + + char *c = nota_continue_num(coef, nota, 3); + + return nota_continue_num(digits, c, 7); +} + +char *nota_read_float(double *d, char *nota) +{ + long long sig = 0; + long long e = 0; + + char *c = nota; + e = (*c) & NOTA_INT_DATA; /* first three bits */ + + while (CONTINUE(*c)) { + e = (e<<7) | (*c) & NOTA_DATA; + c++; + } + + c++; + + do + sig = (sig<<7) | *c & NOTA_DATA; + while (CONTINUE(*(c++))); + + if (NOTA_SIG_SIGN(*nota)) sig *= -1; + if (NOTA_EXP_SIGN(*nota)) e *= -1; + + *d = (double)sig * pow(10.0, e); + return c; +} + +char *nota_write_number(double n, char *nota) +{ + if (n < (double)INT64_MIN || n > (double)INT64_MAX) return nota_write_float(n, nota); + if (floor(n) == n) + return nota_write_int(n, nota); + return nota_write_float(n, nota); +} + +char *nota_read_int(long long *n, char *nota) +{ + if (!n) + return nota_skip(nota); + + *n = 0; + char *c = nota; + *n |= (*c) & NOTA_INT_DATA; /* first three bits */ + while (CONTINUE(*(c++))) + *n = (*n<<7) | (*c) & NOTA_DATA; + + if (NOTA_INT_SIGN(*nota)) *n *= -1; + + return c; +} + +/* n is the number of bits */ +char *nota_write_blob(unsigned long long n, char *nota) +{ + nota[0] = NOTA_BLOB; + return nota_continue_num(n, nota, 4); +} + +char *nota_write_array(unsigned long long n, char *nota) +{ + nota[0] = NOTA_ARR; + return nota_continue_num(n, nota, 4); +} + +char *nota_read_array(long long *len, char *nota) +{ + if (!len) return nota; + return nota_read_num(len, nota); +} + +char *nota_read_record(long long *len, char *nota) +{ + if (!len) return nota; + return nota_read_num(len, nota); +} + +char *nota_read_blob(long long *len, char *nota) +{ + if (!len) return nota; + return nota_read_num(len, nota); +} + +char *nota_write_record(unsigned long long n, char *nota) +{ + nota[0] = NOTA_REC; + return nota_continue_num(n, nota, 4); +} + +char *nota_write_sym(int sym, char *nota) +{ + *nota = NOTA_SYM | sym; + return nota+1; +} + +char *nota_read_sym(int *sym, char *nota) +{ + if (*sym) *sym = (*nota) & 0x0f; + return nota+1; +} + +char *nota_read_text(char **text, char *nota) +{ + long long chars; + nota = nota_read_num(&chars, nota); + + char utf[chars*4]; // enough for the worst case scenario + char *pp = utf; + kim_to_utf8(¬a, &pp, chars); + *pp = 0; + *text = strdup(utf); + + return nota; +} + +char *nota_write_text(const char *s, char *nota) +{ + nota[0] = NOTA_TEXT; + long long n = utf8_count(s); + nota = nota_continue_num(n,nota,4); + utf8_to_kim(&s, ¬a); + return nota; +} + +#endif +#endif diff --git a/source/qjs_dmon.c b/source/qjs_dmon.c new file mode 100644 index 00000000..5adaf9fa --- /dev/null +++ b/source/qjs_dmon.c @@ -0,0 +1,150 @@ +#include "quickjs.h" + +#define DMON_IMPL +#include "dmon.h" + +// Define the file event structure and completion queue +typedef struct { + dmon_action action; + char rootdir[256]; + char filepath[256]; + char oldfilepath[256]; +} FileEvent; + +typedef struct EventNode { + FileEvent event; + struct EventNode *next; +} EventNode; + +typedef struct { + EventNode *head; + EventNode *tail; +} CompletionQueue; + +CompletionQueue completionQueue = { NULL, NULL }; + +// Helper functions for the completion queue +void enqueue_event(FileEvent event) { + EventNode *node = malloc(sizeof(EventNode)); + node->event = event; + node->next = NULL; + + if (completionQueue.tail) { + completionQueue.tail->next = node; + } else { + completionQueue.head = node; + } + completionQueue.tail = node; +} + +int dequeue_event(FileEvent *event) { + if (!completionQueue.head) { + return 0; // No event + } + EventNode *node = completionQueue.head; + *event = node->event; + completionQueue.head = node->next; + if (!completionQueue.head) { + completionQueue.tail = NULL; + } + free(node); + return 1; +} + +void watch_cb(dmon_watch_id id, dmon_action action, const char *rootdir, const char *filepath, const char *oldfilepath, void *user) +{ + FileEvent event; + event.action = action; + strncpy(event.rootdir, rootdir, sizeof(event.rootdir) - 1); + strncpy(event.filepath, filepath, sizeof(event.filepath) - 1); + if (oldfilepath) { + strncpy(event.oldfilepath, oldfilepath, sizeof(event.oldfilepath) - 1); + } else { + event.oldfilepath[0] = '\0'; + } + enqueue_event(event); // Add event to completion queue +} + +static dmon_watch_id watched = {0}; + +JSValue js_dmon_watch(JSContext *js, JSValue self, int argc, JSValue *argv) +{ + if (watched.id) + return JS_ThrowReferenceError(js, "Already watching a directory."); + + const char *dir = JS_ToCString(js,argv[0]); + watched = dmon_watch(dir,watch_cb, DMON_WATCHFLAGS_RECURSIVE, NULL); + return JS_UNDEFINED; +} + +JSValue js_dmon_unwatch(JSContext *js, JSValue self, int argc, JSValue *argv) +{ + if (!watched.id) + return JS_ThrowReferenceError(js, "Not watching a directory."); + + dmon_unwatch(watched); + watched.id = 0; + return JS_UNDEFINED; +} + +JSValue js_dmon_poll(JSContext *js, JSValueConst this_val, int argc, JSValueConst *argv) { + FileEvent event; + while (dequeue_event(&event)) { + JSValue jsevent = JS_NewObject(js); + JSValue action; + switch(event.action) { + case DMON_ACTION_CREATE: + action = JS_NewAtomString(js, "create"); + break; + case DMON_ACTION_DELETE: + action = JS_NewAtomString(js, "delete"); + break; + case DMON_ACTION_MODIFY: + action = JS_NewAtomString(js, "modify"); + break; + case DMON_ACTION_MOVE: + action = JS_NewAtomString(js, "move"); + break; + } + JS_SetPropertyStr(js, jsevent, "action", action); + JS_SetPropertyStr(js, jsevent, "root", JS_NewString(js, event.rootdir)); + JS_SetPropertyStr(js, jsevent, "file", JS_NewString(js, event.filepath)); + JS_SetPropertyStr(js, jsevent, "old", JS_NewString(js, event.oldfilepath)); + JS_Call(js, argv[0], JS_UNDEFINED, 1, &jsevent); + } + return JS_UNDEFINED; +} + +static const JSCFunctionListEntry js_dmon_funcs[] = { + JS_CFUNC_DEF("watch", 1, js_dmon_watch), + JS_CFUNC_DEF("unwatch", 0, js_dmon_unwatch), + JS_CFUNC_DEF("poll", 1, js_dmon_poll) +}; + +JSValue js_dmon_use(JSContext *js) +{ + JSValue export = JS_NewObject(js); + JS_SetPropertyFunctionList(js, export, js_dmon_funcs, sizeof(js_dmon_funcs)/sizeof(JSCFunctionListEntry)); + + dmon_init(); + + return export; +} + +static int js_dmon_init(JSContext *js, JSModuleDef *m) { + JS_SetModuleExport(js, m, "default",js_dmon_use(js)); + return 0; +} + +#ifdef JS_SHARED_LIBRARY +#define JS_INIT_MODULE js_init_module +#else +#define JS_INIT_MODULE js_init_module_dmon +#endif + +JSModuleDef *JS_INIT_MODULE(JSContext *js, const char *module_name) { + JSModuleDef *m = JS_NewCModule(js, module_name, js_dmon_init); + if (!m) return NULL; + JS_AddModuleExport(js, m, "default"); + return m; +} diff --git a/source/qjs_dmon.h b/source/qjs_dmon.h new file mode 100644 index 00000000..27b0e0b4 --- /dev/null +++ b/source/qjs_dmon.h @@ -0,0 +1,8 @@ +#ifndef QJS_DMON_H +#define QJS_DMON_H + +#include "quickjs.h" + +JSValue js_dmon_use(JSContext *js); + +#endif \ No newline at end of file diff --git a/source/qjs_nota.c b/source/qjs_nota.c new file mode 100755 index 00000000..c236840b --- /dev/null +++ b/source/qjs_nota.c @@ -0,0 +1,180 @@ +#include "quickjs.h" + +#define KIM_IMPLEMENTATION +#define NOTA_IMPLEMENTATION +#include "nota.h" + +char *js_do_nota_decode(JSContext *js, JSValue *tmp, char *nota) +{ + int type = nota_type(nota); + JSValue ret2; + long long n; + double d; + int b; + char *str; + + switch(type) { + case NOTA_BLOB: + break; + case NOTA_TEXT: + nota = nota_read_text(&str, nota); + *tmp = JS_NewString(js, str); + /* TODO: Avoid malloc and free here */ + free(str); + break; + case NOTA_ARR: + nota = nota_read_array(&n, nota); + *tmp = JS_NewArray(js); + for (int i = 0; i < n; i++) { + nota = js_do_nota_decode(js, &ret2, nota); + JS_SetPropertyInt64(js, *tmp, i, ret2); + } + break; + case NOTA_REC: + nota = nota_read_record(&n, nota); + *tmp = JS_NewObject(js); + for (int i = 0; i < n; i++) { + nota = nota_read_text(&str, nota); + nota = js_do_nota_decode(js, &ret2, nota); + JS_SetPropertyStr(js, *tmp, str, ret2); + free(str); + } + break; + case NOTA_INT: + nota = nota_read_int(&n, nota); + *tmp = JS_NewInt64(js,n); + break; + case NOTA_SYM: + nota = nota_read_sym(&b, nota); + if (b == NOTA_NULL) *tmp = JS_UNDEFINED; + else + *tmp = JS_NewBool(js,b); + break; + default: + case NOTA_FLOAT: + nota = nota_read_float(&d, nota); + *tmp = JS_NewFloat64(js,d); + break; + } + + return nota; +} + +// Writers the JSValue v into the buffer of char *nota, returning a pointer to the next byte in nota to be written +char *js_do_nota_encode(JSContext *js, JSValue v, char *nota) +{ + int tag = JS_VALUE_GET_TAG(v); + const char *str = NULL; + JSPropertyEnum *ptab; + uint32_t plen; + int n; + double nval; + JSValue val; + + switch(tag) { + case JS_TAG_FLOAT64: + case JS_TAG_INT: + JS_ToFloat64(js, &nval, v); + return nota_write_number(nval, nota); + case JS_TAG_STRING: + str = JS_ToCString(js, v); + nota = nota_write_text(str, nota); + JS_FreeCString(js, str); + return nota; + case JS_TAG_BOOL: + return nota_write_sym(JS_VALUE_GET_BOOL(v), nota); + case JS_TAG_UNDEFINED: + return nota_write_sym(NOTA_NULL, nota); + case JS_TAG_NULL: + return nota_write_sym(NOTA_NULL, nota); + case JS_TAG_OBJECT: + if (JS_IsArray(js, v)) { + int n; + JS_ToInt32(js, &n, JS_GetPropertyStr(js, v, "length")); + nota = nota_write_array(n, nota); + for (int i = 0; i < n; i++) + nota = js_do_nota_encode(js, JS_GetPropertyUint32(js, v, i), nota); + return nota; + } + n = JS_GetOwnPropertyNames(js, &ptab, &plen, v, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK); + nota = nota_write_record(plen, nota); + + for (int i = 0; i < plen; i++) { + val = JS_GetProperty(js,v,ptab[i].atom); + str = JS_AtomToCString(js, ptab[i].atom); + JS_FreeAtom(js, ptab[i].atom); + + nota = nota_write_text(str, nota); + JS_FreeCString(js, str); + + nota = js_do_nota_encode(js, val, nota); + JS_FreeValue(js,val); + } + js_free(js, ptab); + return nota; + default: + return nota; + } + return nota; +} + +JSValue js_nota_encode(JSContext *js, JSValue self, int argc, JSValue *argv) +{ + if (argc < 1) + JS_ThrowInternalError(js, "Expected at least one argument to encode."); + + JSValue obj = argv[0]; + char nota[1024*1024]; + char *e = js_do_nota_encode(js, obj, nota); + return JS_NewArrayBufferCopy(js, (unsigned char*)nota, e-nota); +} + +JSValue js_nota_decode(JSContext *js, JSValue self, int argc, JSValue *argv) +{ + if (argc < 1) return JS_UNDEFINED; + + size_t len; + unsigned char *nota = JS_GetArrayBuffer(js, &len, argv[0]); + JSValue ret; + js_do_nota_decode(js, &ret, (char*)nota); + return ret; +} + +JSValue js_nota_hex(JSContext *js, JSValue self, int argc, JSValue *argv) +{ + size_t len; + unsigned char *nota = JS_GetArrayBuffer(js, &len, argv[0]); + print_nota_hex(nota); + return JS_UNDEFINED; +} + +static const JSCFunctionListEntry js_nota_funcs[] = { + JS_CFUNC_DEF("encode", 1, js_nota_encode), + JS_CFUNC_DEF("decode", 1, js_nota_decode), + JS_CFUNC_DEF("hex", 1, js_nota_hex), +}; + +static int js_nota_init(JSContext *ctx, JSModuleDef *m) { + JS_SetModuleExportList(ctx, m, js_nota_funcs, sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry)); + return 0; +} + +JSValue js_nota_use(JSContext *js) +{ + JSValue export = JS_NewObject(js); + JS_SetPropertyFunctionList(js, export, js_nota_funcs, sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry)); + return export; +} + +#ifdef JS_SHARED_LIBRARY +#define JS_INIT_MODULE js_init_module +#else +#define JS_INIT_MODULE js_init_module_nota +#endif + +JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name) { + JSModuleDef *m = JS_NewCModule(ctx, module_name, js_nota_init); + if (!m) return NULL; + JS_AddModuleExportList(ctx, m, js_nota_funcs, sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry)); + return m; +} diff --git a/source/qjs_nota.h b/source/qjs_nota.h new file mode 100644 index 00000000..85c14822 --- /dev/null +++ b/source/qjs_nota.h @@ -0,0 +1,8 @@ +#ifndef QJS_NOTA_H +#define QJS_NOTA_H + +#include "quickjs.h" + +JSValue js_nota_use(JSContext*); + +#endif \ No newline at end of file diff --git a/subprojects/qjs-dmon.wrap b/subprojects/qjs-dmon.wrap deleted file mode 100644 index 5c05dadd..00000000 --- a/subprojects/qjs-dmon.wrap +++ /dev/null @@ -1,7 +0,0 @@ -[wrap-git] -url = https://github.com/johnalanbrook/qjs-dmon.git -revision = head -depth = 1 - -[provide] -qjs-dmon = qjs_dmon_dep diff --git a/subprojects/qjs-nota.wrap b/subprojects/qjs-nota.wrap deleted file mode 100644 index 8d0d7d08..00000000 --- a/subprojects/qjs-nota.wrap +++ /dev/null @@ -1,7 +0,0 @@ -[wrap-git] -url = https://github.com/johnalanbrook/cnota.git -revision = head -depth = 1 - -[provide] -qjs-nota = qjs_nota_dep diff --git a/tests/nota.js b/tests/nota.js new file mode 100644 index 00000000..b0cd68cb --- /dev/null +++ b/tests/nota.js @@ -0,0 +1,126 @@ +var nota = use('nota'); +var os = use('os'); + +// Helper function to convert hex string to ArrayBuffer +function hexToBuffer(hex) { + let bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i/2] = parseInt(hex.substr(i, 2), 16); + } + return bytes.buffer; +} + +// Helper function to convert ArrayBuffer to hex string +function bufferToHex(buffer) { + return Array.from(new Uint8Array(buffer)) + .map(b => b.toString(16).padStart(2, '0')) + .join('') + .toLowerCase(); +} + +// Test cases +var testCases = [ + { input: 0, expectedHex: "60" }, + { input: 2023, expectedHex: "e08f67" }, + { input: -1, expectedHex: "69" }, + { input: true, expectedHex: "71" }, + { input: false, expectedHex: "72" }, + { input: null, expectedHex: "73" }, + { input: -1.01, expectedHex: "5a65" }, + { input: 98.6, expectedHex: "51875a" }, + { input: "", expectedHex: "10" }, + { input: "cat", expectedHex: "13636174" }, + { input: [], expectedHex: "20" }, + { input: [1, 2, 3], expectedHex: "23616263" }, + { input: {}, expectedHex: "30" }, + { input: {a: 1, b: 2}, expectedHex: "32116161116262" }, + { input: new Uint8Array([0xFF, 0xAA]).buffer, expectedHex: "020280ffaa" }, + { input: { + num: 42, + arr: [1, -1, 2.5], + str: "test", + obj: {x: true} + }, + expectedHex: "34216e756d622a2173747214746573742161727223616965235840216f626a21117873" + } +]; + +// Run tests and collect results +let testCount = 0; +let failedCount = 0; +let results = []; + +for (let test of testCases) { + testCount++; + let testName = `Test ${testCount}: ${JSON.stringify(test.input)}`; + let passed = true; + let messages = []; + + console.log(`Running ${testName}`); + + // Test encoding + let encoded = nota.encode(test.input); + if (!(encoded instanceof ArrayBuffer)) { + passed = false; + messages.push("Encode should return ArrayBuffer"); + } else { + let encodedHex = bufferToHex(encoded); + if (encodedHex !== test.expectedHex.toLowerCase()) { + passed = false; + messages.push( + `Encoding failed\n` + + `Expected: ${test.expectedHex}\n` + + `Got: ${encodedHex}` + ); + } + + // Test decoding + let decoded = nota.decode(encoded); + let inputStr = JSON.stringify(test.input instanceof ArrayBuffer ? + Array.from(new Uint8Array(test.input)) : test.input); + let decodedStr = JSON.stringify(decoded); + + if (inputStr !== decodedStr) { + passed = false; + messages.push( + `Decoding failed\n` + + `Expected: ${inputStr}\n` + + `Got: ${decodedStr}` + ); + } + } + + // Record result + let status = passed ? "PASSED" : "FAILED"; + results.push({ testName, status, messages }); + if (!passed) failedCount++; + + // Print immediate feedback + console.log(`${testName} - ${status}`); + if (!passed) { + console.log(messages.join("\n")); + } + console.log(""); // Empty line between tests +} + +// Summary +console.log("Test Summary:"); +console.log(`Total tests: ${testCount}`); +console.log(`Passed: ${testCount - failedCount}`); +console.log(`Failed: ${failedCount}`); +console.log("\nDetailed Results:"); +results.forEach(result => { + console.log(`${result.testName} - ${result.status}`); + if (result.messages.length > 0) { + console.log(result.messages.join("\n")); + console.log(""); + } +}); + +if (failedCount > 0) { + console.log("Overall result: FAILED"); + os.exit(1); +} else { + console.log("Overall result: PASSED"); + os.exit(0); +} \ No newline at end of file