From 80ebf4d83b9989f7c4130ff85f025c031ebdefa8 Mon Sep 17 00:00:00 2001 From: Jaromil Date: Mon, 9 Dec 2024 01:11:51 +0100 Subject: [PATCH 1/9] feat: add dmon for filesystem monitoring embed and activate dmon in cjit runtime --- LICENSES/BSD-2-Clause.txt | 9 + REUSE.toml | 6 + build/deps.mk | 4 + build/embed-dmon.sh | 49 + build/init.mk | 2 +- lib/dmon/dmon.h | 1912 +++++++++++++++++++++++++++++++++++++ src/cjit.c | 32 +- 7 files changed, 2011 insertions(+), 3 deletions(-) create mode 100644 LICENSES/BSD-2-Clause.txt create mode 100755 build/embed-dmon.sh create mode 100644 lib/dmon/dmon.h diff --git a/LICENSES/BSD-2-Clause.txt b/LICENSES/BSD-2-Clause.txt new file mode 100644 index 0000000..5f662b3 --- /dev/null +++ b/LICENSES/BSD-2-Clause.txt @@ -0,0 +1,9 @@ +Copyright (c) + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/REUSE.toml b/REUSE.toml index cbfd209..0c4c0b0 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -27,6 +27,12 @@ precedence = "aggregate" SPDX-FileCopyrightText = "2001-2004 Fabrice Bellard" SPDX-License-Identifier = "LGPL-2.1-or-later" +[[annotations]] +path = "lib/dmon/**" +precedence = "aggregate" +SPDX-FileCopyrightText = " 2019, Sepehr Taghdisian" +SPDX-License-Identifier = "BSD-2-Clause" + [[annotations]] path = ["docs/**", "**.md"] precedence = "aggregate" diff --git a/build/deps.mk b/build/deps.mk index ad77365..7d534a7 100644 --- a/build/deps.mk +++ b/build/deps.mk @@ -6,6 +6,10 @@ -DVERSION=\"${VERSION}\" \ -DCURRENT_YEAR=\"${CURRENT_YEAR}\" +src/embed-dmon.c: + $(info Embedding dmon headers) + bash build/embed-dmon.sh + src/embed-musl-libc.c: bash build/embed-musl-libc.sh sed -i 's/unsigned char _lib_x86_64_linux_musl_libc_so/const unsigned char musl_libc/' src/embed-musl-libc.c diff --git a/build/embed-dmon.sh b/build/embed-dmon.sh new file mode 100755 index 0000000..11ab76e --- /dev/null +++ b/build/embed-dmon.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +includes=lib/dmon +dst=src/embed-dmon.c + +command -v xxd > /dev/null || { + >&2 echo "Error not found: xxd binary not installed" + exit 1 +} + +([ "$1" = "code" ]) && { + externs=`mktemp` + calls=`mktemp` +} + +echo "// ${includes}" > $dst +ipath=`echo ${includes} | sed 's/\//_/g; s/\./_/g'` +for l in $(ls ${includes}); do + [ -z "$l" ] && continue + [ "${l:0:1}" = "#" ] && continue + + xxd -i ${includes}/${l} >> $dst + hname=`echo $l | sed 's/\//_/g; s/\./_/g'` + ([ "$1" = "code" ]) && { + # print on stderr something handy to paste in code + echo "extern char *${ipath}_${hname};" >> $externs + echo "extern unsigned int ${ipath}_${hname}_len;" >> $externs + echo "if(!write_to_file(tmpdir,\"${l}\",(char*)&${ipath}_${hname},${ipath}_${hname}_len)) goto endgame;" >> $calls + } +done +# must add const in linux or darwin +# sed inplace is not portable +if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i'' -e 's/unsigned char/const char/' $dst + sed -i'' -e 's/unsigned int/const unsigned int/' $dst +else + sed -i -e 's/unsigned char/const char/' $dst + sed -i -e 's/unsigned int/const unsigned int/' $dst +fi +([ "$1" = "code" ]) && { + >&2 echo "Externs to declare:" + cat $externs + >&2 echo + >&2 echo "Calls:" + cat $calls + rm -f $externs $calls +} + +exit 0 diff --git a/build/init.mk b/build/init.mk index ed59b6e..7e4422e 100644 --- a/build/init.mk +++ b/build/init.mk @@ -17,7 +17,7 @@ cflags := ${CFLAGS} ${cflags_includes} SOURCES := src/io.o src/file.o src/cflag.o src/cjit.o \ src/embed-libtcc1.o src/embed-headers.o \ - src/exec-headers.o src/repl.o + src/exec-headers.o src/repl.o src/embed-dmon.o ldadd := lib/tinycc/libtcc.a diff --git a/lib/dmon/dmon.h b/lib/dmon/dmon.h new file mode 100644 index 0000000..18dbe5d --- /dev/null +++ b/lib/dmon/dmon.h @@ -0,0 +1,1912 @@ +#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__ + +#if defined(DMON_OS_LINUX) + +#ifndef __DMON_EXTRA_H__ +#define __DMON_EXTRA_H__ + +// +// Copyright 2023 Sepehr Taghdisian (septag@github). All rights reserved. +// License: https://github.com/septag/dmon#license-bsd-2-clause +// +// Extra header functionality for dmon.h for the backend based on inotify +// +// Add/Remove directory functions: +// dmon_watch_add: Adds a sub-directory to already valid watch_id. sub-directories are assumed to be relative to watch root_dir +// dmon_watch_add: Removes a sub-directory from already valid watch_id. sub-directories are assumed to be relative to watch root_dir +// Reason: The inotify backend does not work well when watching in recursive mode a root directory of a large tree, it may take +// quite a while until all inotify watches are established, and events will not be received in this time. Also, since one +// inotify watch will be established per subdirectory, it is possible that the maximum amount of inotify watches per user +// will be reached. The default maximum is 8192. +// When using inotify backend users may turn off the DMON_WATCHFLAGS_RECURSIVE flag and add/remove selectively the +// sub-directories to be watched based on application-specific logic about which sub-directory actually needs to be watched. +// The function dmon_watch_add and dmon_watch_rm are used to this purpose. +// + +// #ifndef __DMON_H__ +// #error "Include 'dmon.h' before including this file" +// #endif + +#ifdef __cplusplus +extern "C" { +#endif + +DMON_API_DECL bool dmon_watch_add(dmon_watch_id id, const char* subdir); +DMON_API_DECL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir); + +#ifdef __cplusplus +} +#endif + +#ifdef DMON_IMPL +DMON_API_IMPL bool dmon_watch_add(dmon_watch_id id, const char* watchdir) +{ + DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES); + + bool skip_lock = pthread_self() == _dmon.thread_handle; + + if (!skip_lock) + pthread_mutex_lock(&_dmon.mutex); + + dmon__watch_state* watch = _dmon.watches[id.id - 1]; + + int dirlen, i, c; + + // check if the directory exists + // if watchdir contains absolute/root-included path, try to strip the rootdir from it + // else, we assume that watchdir is correct, so save it as it is + struct stat st; + dmon__watch_subdir subdir; + if (stat(watchdir, &st) == 0 && (st.st_mode & S_IFDIR)) { + _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)); + } + } else { + char fullpath[DMON_MAX_PATH]; + _dmon_strcpy(fullpath, sizeof(fullpath), watch->rootdir); + _dmon_strcat(fullpath, sizeof(fullpath), watchdir); + if (stat(fullpath, &st) != 0 || (st.st_mode & S_IFDIR) == 0) { + _DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + _dmon_strcpy(subdir.rootdir, sizeof(subdir.rootdir), watchdir); + } + + dirlen = (int)strlen(subdir.rootdir); + if (subdir.rootdir[dirlen - 1] != '/') { + subdir.rootdir[dirlen] = '/'; + subdir.rootdir[dirlen + 1] = '\0'; + } + + // check that the directory is not already added + for (i = 0, c = stb_sb_count(watch->subdirs); i < c; i++) { + if (strcmp(subdir.rootdir, watch->subdirs[i].rootdir) == 0) { + _DMON_LOG_ERRORF("Error watching directory '%s', because it is already added.", watchdir); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + } + + const uint32_t inotify_mask = IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | IN_DELETE | IN_MODIFY; + char fullpath[DMON_MAX_PATH]; + _dmon_strcpy(fullpath, sizeof(fullpath), watch->rootdir); + _dmon_strcat(fullpath, sizeof(fullpath), subdir.rootdir); + int wd = inotify_add_watch(watch->fd, fullpath, inotify_mask); + if (wd == -1) { + _DMON_LOG_ERRORF("Error watching directory '%s'. (inotify_add_watch:err=%d)", watchdir, errno); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + + stb_sb_push(watch->subdirs, subdir); + stb_sb_push(watch->wds, wd); + + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + + return true; +} + +DMON_API_IMPL bool dmon_watch_rm(dmon_watch_id id, const char* watchdir) +{ + DMON_ASSERT(id.id > 0 && id.id <= DMON_MAX_WATCHES); + + bool skip_lock = pthread_self() == _dmon.thread_handle; + + if (!skip_lock) + pthread_mutex_lock(&_dmon.mutex); + + dmon__watch_state* watch = _dmon.watches[id.id - 1]; + + char subdir[DMON_MAX_PATH]; + _dmon_strcpy(subdir, sizeof(subdir), watchdir); + if (strstr(subdir, watch->rootdir) == subdir) { + _dmon_strcpy(subdir, sizeof(subdir), watchdir + strlen(watch->rootdir)); + } + + int dirlen = (int)strlen(subdir); + if (subdir[dirlen - 1] != '/') { + subdir[dirlen] = '/'; + subdir[dirlen + 1] = '\0'; + } + + int i, c = stb_sb_count(watch->subdirs); + for (i = 0; i < c; i++) { + if (strcmp(watch->subdirs[i].rootdir, subdir) == 0) { + break; + } + } + if (i >= c) { + _DMON_LOG_ERRORF("Watch directory '%s' is not valid", watchdir); + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return false; + } + inotify_rm_watch(watch->fd, watch->wds[i]); + + /* Remove entry from subdirs and wds by swapping position with the last entry */ + watch->subdirs[i] = stb_sb_last(watch->subdirs); + stb_sb_pop(watch->subdirs); + + watch->wds[i] = stb_sb_last(watch->wds); + stb_sb_pop(watch->wds); + + if (!skip_lock) + pthread_mutex_unlock(&_dmon.mutex); + return true; +} +#endif // DMON_IMPL +#endif // __DMON_EXTRA_H__ +#endif // DMON_OS_LINUX diff --git a/src/cjit.c b/src/cjit.c index ba0ca0b..3280a00 100644 --- a/src/cjit.c +++ b/src/cjit.c @@ -42,6 +42,8 @@ extern long file_size(const char *filename); extern char* file_load(const char *filename); extern char *load_stdin(); extern char* dir_load(const char *path); +extern bool write_to_file(char *path, char *filename, + char *buf, unsigned int len); #ifdef LIBC_MINGW32 extern char *win32_mkdtemp(); @@ -64,6 +66,11 @@ extern int cjit_cli_tty(TCCState *TCC); #ifdef KILO_SUPPORTED extern int cjit_cli_kilo(TCCState *TCC); #endif +// from embed-dmon.c +extern char *lib_dmon_dmon_extra_h; +extern unsigned int lib_dmon_dmon_extra_h_len; +extern char *lib_dmon_dmon_h; +extern unsigned int lib_dmon_dmon_h_len; ///////////// void handle_error(void *n, const char *m) { @@ -126,6 +133,7 @@ int main(int argc, char **argv) { // quiet is by default on when cjit's output is redirected // errors will still be printed on stderr bool quiet = isatty(fileno(stdout))?false:true; + bool dmon = false; // activate dmon headers for fs monitoring int arg_separator = 0; int res = 1; int i, c; @@ -146,6 +154,7 @@ int main(int argc, char **argv) { { "help", ko_no_argument, 100 }, { "live", ko_no_argument, 301 }, { "tgen", ko_no_argument, 401 }, + { "dmon", ko_no_argument, 501 }, { NULL, 0, 0 } }; ketopt_t opt = KETOPT_INIT; @@ -198,14 +207,14 @@ int main(int argc, char **argv) { if(entry!=default_main) free(entry); entry = malloc(strlen(opt.arg)+1); strcpy(entry,opt.arg); - } else if (c == 301) { // + } else if (c == 301) { #ifdef LIBC_MINGW32 _err("Live mode not supported in Windows"); #else if(!quiet)_err("Live mode activated"); live_mode = true; #endif - } else if (c == 401) { // + } else if (c == 401) { #ifndef LIBC_MINGW32 tmpdir = posix_mkdtemp(); #else @@ -214,6 +223,8 @@ int main(int argc, char **argv) { if(!quiet)_err("Temporary exec dir: %s",tmpdir); tcc_delete(TCC); exit(0); + } else if (c == 501) { + dmon = true; } else if (c == '?') _err("unknown opt: -%c\n", opt.opt? opt.opt : ':'); else if (c == ':') _err("missing arg: -%c\n", opt.opt? opt.opt : ':'); @@ -257,6 +268,23 @@ int main(int argc, char **argv) { tcc_add_libc_symbols(TCC); #endif + if(dmon) { + _err("Activated DMON extension for filesystem monitoring"); + if(!write_to_file(tmpdir,"dmon.h", + (char*)&lib_dmon_dmon_h, + lib_dmon_dmon_h_len)) goto endgame; + tcc_define_symbol(TCC,"DMON_IMPL",NULL); +#if defined(CJIT_BUILD_LINUX) + tcc_define_symbol(TCC,"DMON_OS_LINUX",NULL); +#endif +#if defined(CJIT_BUILD_OSX) + tcc_define_symbol(TCC,"DMON_OS_MACOS",NULL); +#endif +#if defined(CJIT_BUILD_WIN) + tcc_define_symbol(TCC,"DMON_OS_WINDOWS",NULL); +#endif + } + if (argc == 0 ) { _err("No input file: live mode!"); live_mode = true; From 5bde3554bb656ae84fd697edac0d3d9b8050f63c Mon Sep 17 00:00:00 2001 From: Jaromil Date: Mon, 9 Dec 2024 01:17:29 +0100 Subject: [PATCH 2/9] new CJIT_BUILD flags to specify target platform fix dmon embedding on osx fix embed dmon for win native build --- build/linux.mk | 2 +- build/osx.mk | 12 +++++++++--- build/win-native.mk | 6 +++++- build/win-wsl.mk | 2 +- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/build/linux.mk b/build/linux.mk index fc68062..359264f 100644 --- a/build/linux.mk +++ b/build/linux.mk @@ -2,7 +2,7 @@ include build/init.mk cc := gcc -cflags += -DLIBC_GNU -D_GNU_SOURCE -DKILO_SUPPORTED +cflags += -DLIBC_GNU -D_GNU_SOURCE -DKILO_SUPPORTED -DCJIT_BUILD_LINUX SOURCES += src/kilo.o diff --git a/build/osx.mk b/build/osx.mk index 79a74ad..ff8b40a 100644 --- a/build/osx.mk +++ b/build/osx.mk @@ -1,6 +1,7 @@ include build/init.mk cc := clang +cflags += -DCJIT_BUILD_OSX all: deps cjit.command @@ -14,7 +15,7 @@ cjit.command: ${SOURCES} -DVERSION=\"${VERSION}\" \ -DCURRENT_YEAR=\"${CURRENT_YEAR}\" -deps: lib/tinycc/libtcc.a src/embed-libtcc1.c src/embed-headers.c +deps: lib/tinycc/libtcc.a src/embed-libtcc1.c src/embed-headers.c src/embed-dmon.c ## Custom deps targets for osx due to different sed @@ -32,5 +33,10 @@ src/embed-libtcc1.c: src/embed-headers.c: $(info Embedding tinycc headers) bash build/embed-headers.sh win - sed -i'' -e 's/unsigned char/const char/' src/embed-headers.c - sed -i'' -e 's/unsigned int/const unsigned int/' src/embed-headers.c + +src/embed-dmon.c: + $(info Embedding dmon headers) + bash build/embed-dmon.sh + +# sed -i'' -e 's/unsigned char/const char/' src/embed-headers.c +# sed -i'' -e 's/unsigned int/const unsigned int/' src/embed-headers.c diff --git a/build/win-native.mk b/build/win-native.mk index 626ec49..359b8f1 100755 --- a/build/win-native.mk +++ b/build/win-native.mk @@ -16,7 +16,7 @@ SHELL := C:\Program Files\Git\bin\bash.exe # redefine compilation flags cc := gcc cflags := -O2 -fomit-frame-pointer -Isrc -Ilib/tinycc -cflags += -DLIBC_MINGW32 +cflags += -DLIBC_MINGW32 -DCJIT_BUILD_WIN ldflags := -static-libgcc ldadd := lib/tinycc/libtcc.a -lshlwapi SOURCES += src/win-compat.o @@ -42,3 +42,7 @@ src/embed-headers.c: bash build/embed-headers.sh win sed -i 's/unsigned char/const char/' src/embed-headers.c sed -i 's/unsigned int/const unsigned int/' src/embed-headers.c + +src/embed-dmon.c: + $(info Embedding dmon headers) + bash build/embed-dmon.sh diff --git a/build/win-wsl.mk b/build/win-wsl.mk index 67bd0a8..8b08e28 100644 --- a/build/win-wsl.mk +++ b/build/win-wsl.mk @@ -7,7 +7,7 @@ include build/init.mk cc := x86_64-w64-mingw32-gcc ar := x86_64-w64-mingw32-ar -cflags += -DLIBC_MINGW32 +cflags += -DLIBC_MINGW32 -DCJIT_BUILD_WIN ldadd += -lrpcrt4 -lshlwapi From 7c399ec76275d92fc684776015d047cbdd0d30f7 Mon Sep 17 00:00:00 2001 From: Jaromil Date: Mon, 9 Dec 2024 13:11:47 +0100 Subject: [PATCH 3/9] dmon test unit --- test/bats_setup | 2 ++ test/dmon.bats | 15 +++++++++++++++ test/dmon.c | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 test/dmon.bats create mode 100644 test/dmon.c diff --git a/test/bats_setup b/test/bats_setup index 383fc79..39b64a3 100644 --- a/test/bats_setup +++ b/test/bats_setup @@ -2,11 +2,13 @@ setup() { bats_require_minimum_version 1.5.0 T="$BATS_TEST_DIRNAME" + TMP="$BATS_TEST_TMPDIR" # R=`cd "$T"/.. && pwd` R=`pwd` load "$T"/test_helper/bats_support/load load "$T"/test_helper/bats_assert/load CJIT="${R}/cjit" + rm -rf `${CJIT} --tgen` [ -r "$CJIT" ] || CJIT="${R}/cjit.exe" [ -r "$CJIT" ] || CJIT="${R}/cjit.command" [ -r "$CJIT" ] || { diff --git a/test/dmon.bats b/test/dmon.bats new file mode 100644 index 0000000..76cd139 --- /dev/null +++ b/test/dmon.bats @@ -0,0 +1,15 @@ +load bats_setup + +@test "DMON monitoring of filesystem" { + (sleep .5; + rm -f ${TMP}/dmon_test_create.txt; + touch ${TMP}/dmon_test_create.txt; + rm -f ${TMP}/dmon_test_create.txt; + sleep 2; + kill -HUP `cat ${TMP}/test_dmon.pid`) & + + run ${CJIT} -p ${TMP}/test_dmon.pid --dmon ${T}/dmon.c -- ${TMP} + assert_failure # TODO: cleaner way than kill -HUP + assert_line --regexp '^CREATE:.*dmon_test_create.txt$' + assert_line --regexp '^DELETE:.*dmon_test_create.txt$' +} diff --git a/test/dmon.c b/test/dmon.c new file mode 100644 index 0000000..99de583 --- /dev/null +++ b/test/dmon.c @@ -0,0 +1,40 @@ +#include + +#include + + +static void watch_callback(dmon_watch_id watch_id, dmon_action action, const char* rootdir, + const char* filepath, const char* oldfilepath, void* user) +{ + (void)(user); + (void)(watch_id); + + switch (action) { + case DMON_ACTION_CREATE: + fprintf(stderr,"CREATE: [%s]%s\n", rootdir, filepath); + break; + case DMON_ACTION_DELETE: + fprintf(stderr,"DELETE: [%s]%s\n", rootdir, filepath); + break; + case DMON_ACTION_MODIFY: + fprintf(stderr,"MODIFY: [%s]%s\n", rootdir, filepath); + break; + case DMON_ACTION_MOVE: + fprintf(stderr,"MOVE: [%s]%s -> [%s]%s\n", rootdir, oldfilepath, rootdir, filepath); + break; + } +} + +int main(int argc, char* argv[]) +{ + if (argc > 1) { + dmon_init(); + puts("waiting for changes .."); + dmon_watch(argv[1], watch_callback, DMON_WATCHFLAGS_RECURSIVE, NULL); + getchar(); + dmon_deinit(); + } else { + puts("usage: test dirname"); + } + return 0; +} From a99fabb3a8716e80a9fba04b93a5099057d3f203 Mon Sep 17 00:00:00 2001 From: Jaromil Date: Mon, 9 Dec 2024 13:12:17 +0100 Subject: [PATCH 4/9] debug: support for gdb build ASAN switches back on compiler optimizations in gdb --- GNUmakefile | 3 +++ build/linux.mk | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/GNUmakefile b/GNUmakefile index 5451cde..d152f72 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -40,6 +40,9 @@ apple-osx: ## ๐ŸŽ Build cjit.command for Apple/OSX using clang static _: ## ------: ## __ Debugging targets +debug-gdb: ## ๐Ÿ”ฌ Build using the address sanitizer to detect memory leaks + $(MAKE) -f build/linux.mk GDB=1 + debug-asan: ## ๐Ÿ”ฌ Build using the address sanitizer to detect memory leaks $(MAKE) -f build/linux.mk ASAN=1 diff --git a/build/linux.mk b/build/linux.mk index 359264f..86d26dd 100644 --- a/build/linux.mk +++ b/build/linux.mk @@ -9,10 +9,18 @@ SOURCES += src/kilo.o ifdef ASAN cflags := -Og -ggdb -DDEBUG=1 -fno-omit-frame-pointer -fsanitize=address cflags += ${cflags_includes} ${cflags_gnu} -DKILO_SUPPORTED + cflags += -DCJIT_BUILD_LINUX ldflags := -fsanitize=address -static-libasan # tinycc_config += --extra-ldflags="${ldflags}" endif +ifdef GDB + cflags := -Og -ggdb -DDEBUG=1 -fno-omit-frame-pointer + cflags += ${cflags_includes} ${cflags_gnu} -DKILO_SUPPORTED + cflags += -DCJIT_BUILD_LINUX +# tinycc_config += --extra-ldflags="${ldflags}" +endif + tinycc_config += --with-libgcc ifeq ($(shell sestatus | awk -F': *' '/SELinux status:/ {print $2}'), enabled) From 1160bf73ccf765d9fbd5e4ff154a819bef54338f Mon Sep 17 00:00:00 2001 From: Jaromil Date: Mon, 9 Dec 2024 13:13:35 +0100 Subject: [PATCH 5/9] feat: new cjit internal header, write pid to file etc. A small refactoring of the code includes the new internal struct CJITState passing through functions some switches that are inherent to CJIT. In the close future this may also include TCCState. This facilitates propagation of cli options like pid file creation as well dmon switch to other parts of the code called by cjit.c main function. Yet to be tested on all platforms. Skip dmon bats test on musl and win. --- build/musl.mk | 2 +- lib/dmon/dmon.h | 8 ++-- src/cjit.c | 100 ++++++++++++++++++------------------------------ src/cjit.h | 55 ++++++++++++++++++++++++++ src/file.c | 72 +++++++++++++++++++++++----------- src/repl.c | 34 +++++++++++++--- test/dmon.bats | 13 +++++++ 7 files changed, 188 insertions(+), 96 deletions(-) create mode 100644 src/cjit.h diff --git a/build/musl.mk b/build/musl.mk index 3acfb8b..8c13db8 100644 --- a/build/musl.mk +++ b/build/musl.mk @@ -4,7 +4,7 @@ include build/init.mk cflags := -Wall -static -O2 ${cflags_stack_protect} cflags += -Isrc -Ilib/tinycc -DLIBC_MUSL -nostdlib -cflags += -DKILO_SUPPORTED +cflags += -DKILO_SUPPORTED -DCJIT_BUILD_MUSL ldadd := lib/tinycc/libtcc.a /usr/lib/x86_64-linux-musl/crt1.o /usr/lib/x86_64-linux-musl/libc.a diff --git a/lib/dmon/dmon.h b/lib/dmon/dmon.h index 18dbe5d..d0fe080 100644 --- a/lib/dmon/dmon.h +++ b/lib/dmon/dmon.h @@ -156,10 +156,10 @@ DMON_API_DECL void dmon_unwatch(dmon_watch_id id); # define NOMINMAX # endif # include -# include -# ifdef _MSC_VER -# pragma intrinsic(_InterlockedExchange) -# endif +// # include +// # ifdef _MSC_VER +// # pragma intrinsic(_InterlockedExchange) +// # endif #elif DMON_OS_LINUX # ifndef __USE_MISC # define __USE_MISC diff --git a/src/cjit.c b/src/cjit.c index 3280a00..511f862 100644 --- a/src/cjit.c +++ b/src/cjit.c @@ -32,46 +32,9 @@ #include #endif -#include #include +#include -///////////// -// from file.c -extern int detect_bom(const char *filename); -extern long file_size(const char *filename); -extern char* file_load(const char *filename); -extern char *load_stdin(); -extern char* dir_load(const char *path); -extern bool write_to_file(char *path, char *filename, - char *buf, unsigned int len); - -#ifdef LIBC_MINGW32 -extern char *win32_mkdtemp(); -// from win-compat.c -extern void win_compat_usleep(unsigned int microseconds); -extern ssize_t win_compat_getline(char **lineptr, size_t *n, FILE *stream); -#else -extern char *posix_mkdtemp(); -#endif -// from io.c -extern void _out(const char *fmt, ...); -extern void _err(const char *fmt, ...); -// from repl.c -#ifdef LIBC_MINGW32 -extern int cjit_exec_win(TCCState *TCC, const char *ep, int argc, char **argv); -#else -extern int cjit_exec_fork(TCCState *TCC, const char *ep, int argc, char **argv); -#endif -extern int cjit_cli_tty(TCCState *TCC); -#ifdef KILO_SUPPORTED -extern int cjit_cli_kilo(TCCState *TCC); -#endif -// from embed-dmon.c -extern char *lib_dmon_dmon_extra_h; -extern unsigned int lib_dmon_dmon_extra_h_len; -extern char *lib_dmon_dmon_h; -extern unsigned int lib_dmon_dmon_h_len; -///////////// void handle_error(void *n, const char *m) { (void)n; @@ -120,20 +83,23 @@ const char cli_help[] = " -l lib\t search the library named 'lib' when linking\n" " -L dir\t also search inside folder 'dir' for -l libs\n" " -e fun\t entry point function (default 'main')\n" + " -p pid\t write pid of executed program to file\n" " --live\t run interactive editor for live coding\n" - " --tgen\t create the runtime temporary dir and exit\n"; + " --tgen\t create the runtime temporary dir and exit\n" + " --dmon\t activate the filesystem monitoring extension\n"; int main(int argc, char **argv) { TCCState *TCC = NULL; + CJITState CJIT; + memset(&CJIT,0x0,sizeof(CJITState)); + // const char *progname = "cjit"; - char *tmpdir = NULL; const char *default_main = "main"; char *entry = (char*)default_main; bool live_mode = false; // quiet is by default on when cjit's output is redirected // errors will still be printed on stderr bool quiet = isatty(fileno(stdout))?false:true; - bool dmon = false; // activate dmon headers for fs monitoring int arg_separator = 0; int res = 1; int i, c; @@ -158,7 +124,7 @@ int main(int argc, char **argv) { { NULL, 0, 0 } }; ketopt_t opt = KETOPT_INIT; - while ((c = ketopt(&opt, argc, argv, 1, "qhvD:L:l:C:I:e:", longopts)) >= 0) { + while ((c = ketopt(&opt, argc, argv, 1, "qhvD:L:l:C:I:e:p:", longopts)) >= 0) { if(c == 'q') { quiet = true; } @@ -207,6 +173,10 @@ int main(int argc, char **argv) { if(entry!=default_main) free(entry); entry = malloc(strlen(opt.arg)+1); strcpy(entry,opt.arg); + } else if (c == 'p') { // write pid to file + if(!quiet)_err("pid file: %s",opt.arg); + CJIT.write_pid = malloc(strlen(opt.arg)+1); + strcpy(CJIT.write_pid,opt.arg); } else if (c == 301) { #ifdef LIBC_MINGW32 _err("Live mode not supported in Windows"); @@ -214,17 +184,18 @@ int main(int argc, char **argv) { if(!quiet)_err("Live mode activated"); live_mode = true; #endif + } else if (c == 501) { + CJIT.dmon = true; } else if (c == 401) { #ifndef LIBC_MINGW32 - tmpdir = posix_mkdtemp(); + posix_mkdtemp(&CJIT); #else - tmpdir = win32_mkdtemp(); + win32_mkdtemp(&CJIT); #endif - if(!quiet)_err("Temporary exec dir: %s",tmpdir); + fprintf(stdout,"%s\n",CJIT.tmpdir); + // TODO: free CJIT too here (not reaching endgame) tcc_delete(TCC); exit(0); - } else if (c == 501) { - dmon = true; } else if (c == '?') _err("unknown opt: -%c\n", opt.opt? opt.opt : ':'); else if (c == ':') _err("missing arg: -%c\n", opt.opt? opt.opt : ':'); @@ -239,26 +210,26 @@ int main(int argc, char **argv) { // from here onwards use goto endgame // as the main and only exit #ifndef LIBC_MINGW32 - tmpdir = posix_mkdtemp(); + posix_mkdtemp(&CJIT); #else - tmpdir = win32_mkdtemp(); + win32_mkdtemp(&CJIT); #endif - if(!tmpdir) { + if(!CJIT.tmpdir) { _err("Error creating temp dir: %s",strerror(errno)); goto endgame; } // finally set paths - tcc_add_include_path(TCC, tmpdir); + tcc_add_include_path(TCC, CJIT.tmpdir); #ifdef LIBC_MINGW32 { char tmp_winapi[512]; - snprintf(tmp_winapi,511,"%s/winapi",tmpdir); + snprintf(tmp_winapi,511,"%s/winapi",CJIT.tmpdir); tcc_add_include_path(TCC, tmp_winapi); tcc_add_library_path(TCC, "C:\\Windows\\System32"); } #endif - tcc_add_library_path(TCC, tmpdir); + tcc_add_library_path(TCC, CJIT.tmpdir); // tcc_set_lib_path(TCC,tmpdir); // this overrides all? // set output in memory for just in time execution @@ -268,20 +239,19 @@ int main(int argc, char **argv) { tcc_add_libc_symbols(TCC); #endif - if(dmon) { + if(CJIT.dmon) { _err("Activated DMON extension for filesystem monitoring"); - if(!write_to_file(tmpdir,"dmon.h", - (char*)&lib_dmon_dmon_h, - lib_dmon_dmon_h_len)) goto endgame; tcc_define_symbol(TCC,"DMON_IMPL",NULL); #if defined(CJIT_BUILD_LINUX) tcc_define_symbol(TCC,"DMON_OS_LINUX",NULL); -#endif -#if defined(CJIT_BUILD_OSX) + +#elif defined(CJIT_BUILD_OSX) tcc_define_symbol(TCC,"DMON_OS_MACOS",NULL); -#endif -#if defined(CJIT_BUILD_WIN) +#elif defined(CJIT_BUILD_WIN) tcc_define_symbol(TCC,"DMON_OS_WINDOWS",NULL); +#else + _err("Unsupported platform for DMON extension"); + tcc_undefine_symbol(TCC,"DMON_OS"); #endif } @@ -373,14 +343,18 @@ int main(int argc, char **argv) { int right_args = argc-left_args+1;//arg_separator? argc-arg_separator : 0; char **right_argv = &argv[left_args-1];//arg_separator?&argv[arg_separator]:0 #ifndef LIBC_MINGW32 - res = cjit_exec_fork(TCC, entry, right_args, right_argv); + res = cjit_exec_fork(TCC, &CJIT, entry, right_args, right_argv); #else - res = cjit_exec_win(TCC, entry, right_args, right_argv); + res = cjit_exec_win(TCC, &CJIT, entry, right_args, right_argv); #endif endgame: // free TCC if(TCC) tcc_delete(TCC); + // free CJITState + if(CJIT.tmpdir) free(CJIT.tmpdir); + if(CJIT.write_pid) free(CJIT.write_pid); + if(entry!=default_main) free(entry); exit(res); } diff --git a/src/cjit.h b/src/cjit.h new file mode 100644 index 0000000..c938353 --- /dev/null +++ b/src/cjit.h @@ -0,0 +1,55 @@ +#ifndef _CJIT_H_ +#define _CJIT_H_ + +#include +#include + +// passed to cjit_exec_fork with CJIT execution parameters +struct CJITState { + char *tmpdir; + char *write_pid; // filename to write the pid of execution + bool dmon; +}; +typedef struct CJITState CJITState; + +///////////// +// from file.c +extern int detect_bom(const char *filename); +extern long file_size(const char *filename); +extern char* file_load(const char *filename); +extern char *load_stdin(); +extern char* dir_load(const char *path); +extern bool write_to_file(const char *path, const char *filename, + const char *buf, unsigned int len); + +#ifdef LIBC_MINGW32 +bool win32_mkdtemp(CJITState *CJIT); +// from win-compat.c +extern void win_compat_usleep(unsigned int microseconds); +extern ssize_t win_compat_getline(char **lineptr, size_t *n, FILE *stream); +#else +bool posix_mkdtemp(CJITState *CJIT); +#endif +// from io.c +extern void _out(const char *fmt, ...); +extern void _err(const char *fmt, ...); +// from repl.c +#ifdef LIBC_MINGW32 +extern int cjit_exec_win(TCCState *TCC, CJITState *CJIT, + const char *ep, int argc, char **argv); +#else +extern int cjit_exec_fork(TCCState *TCC, CJITState *CJIT, + const char *ep, int argc, char **argv); +#endif +extern int cjit_cli_tty(TCCState *TCC); +#ifdef KILO_SUPPORTED +extern int cjit_cli_kilo(TCCState *TCC); +#endif +// from embed-dmon.c +extern char *lib_dmon_dmon_extra_h; +extern unsigned int lib_dmon_dmon_extra_h_len; +extern char *lib_dmon_dmon_h; +extern unsigned int lib_dmon_dmon_h_len; +///////////// + +#endif diff --git a/src/file.c b/src/file.c index 4af9060..0c4adc3 100644 --- a/src/file.c +++ b/src/file.c @@ -43,6 +43,8 @@ # define O_BINARY 0 #endif #endif + +#include extern void _err(const char *fmt, ...); // from exec-headers.c @@ -193,7 +195,7 @@ char *load_stdin() { #endif } -bool write_to_file(char *path, char *filename, char *buf, unsigned int len) { +bool write_to_file(const char *path, const char *filename, const char *buf, unsigned int len) { FILE *fd; size_t written; char fullpath[256]; @@ -323,25 +325,41 @@ char *dir_load(const char *path) #endif +// cross-platform creation of any CJIT extension requested +// also fills CJIT->tmpdir on success +bool cjit_extensions_mkdtemp(CJITState *CJIT, const char *tmpdir) { + // dmon.h + if(CJIT->dmon) { + if(!write_to_file(tmpdir,"dmon.h", + (char*)&lib_dmon_dmon_h, + lib_dmon_dmon_h_len)) return(false); + } + CJIT->tmpdir = malloc(strlen(tmpdir)+1); + strcpy(CJIT->tmpdir, tmpdir); + return(true); +} + #ifdef LIBC_MINGW32 /////////////// // WINDOWS SHIT -int dir_exists(const char *path) { +bool dir_exists(CJITState *CJIT, const char *path) { DWORD attributes = GetFileAttributes(path); if (attributes == INVALID_FILE_ATTRIBUTES) { // The path does not exist - return 0; + return false; } else if (attributes & FILE_ATTRIBUTE_DIRECTORY) { // The path exists and is a directory - return 1; + CJIT->tmpdir = malloc(strlen(path)+1); + strcpy(CJIT->tmpdir, path); + return true; } else { _err("Temp dir is a file, cannot overwrite: %s",path); // The path exists but is not a directory - return 0; + return false; } } -char *win32_mkdtemp() { +bool win32_mkdtemp(CJITState *CJIT) { static char tempDir[MAX_PATH]; char sysTempDir[MAX_PATH]; char secTempDir[MAX_PATH]; @@ -354,52 +372,55 @@ char *win32_mkdtemp() { // Get the temporary path if (GetTempPath(MAX_PATH, tempPath) == 0) { _err("Failed to get temporary path"); - return NULL; + return false; } PathCombine(tempDir, tempPath, filename); // return already if found existing - if(dir_exists(tempDir)) return(tempDir); + if(dir_exists(CJIT, tempDir)) return(true); // Create the temporary directory if (CreateDirectory(tempDir, NULL) == 0) { _err("Failed to create temporary dir: %s",tempDir); - return NULL; + return false; } PathCombine(sysTempDir, tempDir, "sys"); if (CreateDirectory(sysTempDir, NULL) == 0) { _err("Failed to create sys dir in temporary dir: %s",sysTempDir); - return NULL; + return false; } PathCombine(secTempDir, tempDir, "sec_api"); if (CreateDirectory(secTempDir, NULL) == 0) { _err("Failed to create sec_api dir in temporary dir: %s",secTempDir); - return NULL; + return false; } PathCombine(sysSecTempDir, secTempDir, "sys"); if (CreateDirectory(sysSecTempDir, NULL) == 0) { _err("Failed to create sec_api/sys dir in temporary dir: %s",sysSecTempDir); - return NULL; + return false; } PathCombine(tccTempDir, tempDir, "tcc"); if (CreateDirectory(tccTempDir, NULL) == 0) { _err("Failed to create tcc dir in temporary dir: %s",tccTempDir); - return NULL; + return false; } PathCombine(winTempDir, tempDir, "winapi"); if (CreateDirectory(winTempDir, NULL) == 0) { _err("Failed to create winapi dir in temporary dir: %s",winTempDir); - return NULL; + return false; } - if(!gen_exec_headers(tempDir)) return(NULL); - return(tempDir); + if(!gen_exec_headers(tempDir)) return(false); + if(!cjit_extensions_mkdtemp(CJIT, tempDir)) return(false); + return(true); } #else // POSIX -bool dir_exists(const char *path) { +bool dir_exists(CJITState *CJIT, const char *path) { struct stat info; if (stat(path, &info) != 0) { // stat() failed; the path does not exist return false; } else if (info.st_mode & S_IFDIR) { // The path exists and is a directory + CJIT->tmpdir = malloc(strlen(path)+1); + strcpy(CJIT->tmpdir, path); return true; } else { _err("Temp dir is a file, cannot overwrite: %s",path); @@ -408,12 +429,19 @@ bool dir_exists(const char *path) { } } -char *posix_mkdtemp() { - static char tpath[260]; +bool posix_mkdtemp(CJITState *CJIT) { + char tpath[260]; snprintf(tpath,259,"/tmp/cjit-%s",VERSION); - if(dir_exists(tpath)) return(tpath); + if(dir_exists(CJIT, tpath)) return(tpath); mkdir(tpath,0755); - if(! gen_exec_headers(tpath) ) return(NULL); - return(&tpath[0]); + if(!gen_exec_headers(tpath) ) { + _err("Cannot create temporary TCC headers"); + return(false); + } + if(!cjit_extensions_mkdtemp(CJIT, tpath)) { + _err("Cannot create temporary CJIT headers"); + return(false); + } + return(true); } #endif diff --git a/src/repl.c b/src/repl.c index 6217e46..91123a4 100644 --- a/src/repl.c +++ b/src/repl.c @@ -21,8 +21,9 @@ #include #include #include +#include -#include +#include #ifndef LIBC_MINGW32 #include @@ -36,7 +37,7 @@ extern void _out(const char *fmt, ...); extern void _err(const char *fmt, ...); #ifdef LIBC_MINGW32 -int cjit_exec_win(TCCState *TCC, const char *ep, int argc, char **argv) { +int cjit_exec_win(TCCState *TCC, CJITState *CJIT, const char *ep, int argc, char **argv) { int res = 1; int (*_ep)(int, char**); _ep = tcc_get_symbol(TCC, ep); @@ -44,13 +45,24 @@ int cjit_exec_win(TCCState *TCC, const char *ep, int argc, char **argv) { _err("Symbol not found in source: %s",ep); return -1; } + if(CJIT->write_pid) { + pid_t pid = getpid(); + FILE *fd = fopen(CJIT->write_pid, "w"); + if(!fd) { + _err("Cannot create pid file %s: %s", + CJIT->write_pid, strerror(errno)); + return -1; + } + fprintf(fd,"%d\n",pid); + fclose(fd); + } // _err("Execution start\n---"); res = _ep(argc, argv); return(res); } #else // LIBC_MINGW32 -int cjit_exec_fork(TCCState *TCC, const char *ep, int argc, char **argv) { +int cjit_exec_fork(TCCState *TCC, CJITState *CJIT, const char *ep, int argc, char **argv) { pid_t pid; int res = 1; int (*_ep)(int, char**); @@ -59,12 +71,22 @@ int cjit_exec_fork(TCCState *TCC, const char *ep, int argc, char **argv) { _err("Symbol not found in source: %s",ep); return -1; } - // _err("Start execution\n---------------"); pid = fork(); if (pid == 0) { res = _ep(argc, argv); exit(res); } else { + if(CJIT->write_pid) { + // pid_t pid = getpid(); + FILE *fd = fopen(CJIT->write_pid, "w"); + if(!fd) { + _err("Cannot create pid file %s: %s", + CJIT->write_pid, strerror(errno)); + return -1; + } + fprintf(fd,"%d\n",pid); + fclose(fd); + } int status; int ret; ret = waitpid(pid, &status, WUNTRACED | WCONTINUED); @@ -374,9 +396,9 @@ int cjit_cli_tty(TCCState *TCC) { _err("-----------------------------------\n"); #endif // VERBOSE_CLI #ifndef LIBC_MINGW32 - res = cjit_exec_fork(TCC, "main", 0, NULL); + res = cjit_exec_fork(TCC, NULL, "main", 0, NULL); #else // LIBC_MINGW32 - res = cjit_exec_win(TCC, "main", 0, NULL); + res = cjit_exec_win(TCC, NULL, "main", 0, NULL); #endif // LIBC_MINGW32 free(code); code = NULL; diff --git a/test/dmon.bats b/test/dmon.bats index 76cd139..039e9c8 100644 --- a/test/dmon.bats +++ b/test/dmon.bats @@ -1,6 +1,19 @@ load bats_setup @test "DMON monitoring of filesystem" { + + # Check if the script is being run by PowerShell + [ -n "$PSVersionTable" ] && { + >&3 echo ">> Skipping DMON test on Windows PowerShell" + >&3 echo ">> TODO: run in background, test monitor and quit in PS" + return 0 + } + ${CJIT} -v 2>&1| grep '^Build: MUSL' && { + >&3 echo ">> Skipping DMON test on MUSL libc build" + return 0 + } + + (sleep .5; rm -f ${TMP}/dmon_test_create.txt; touch ${TMP}/dmon_test_create.txt; From 3acd9ff7201a328381285722310002df6714795c Mon Sep 17 00:00:00 2001 From: Jaromil Date: Mon, 9 Dec 2024 13:43:37 +0100 Subject: [PATCH 6/9] improve version output including build and debug settings --- src/cjit.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/cjit.c b/src/cjit.c index 511f862..ae489a3 100644 --- a/src/cjit.c +++ b/src/cjit.c @@ -132,11 +132,23 @@ int main(int argc, char **argv) { _err("CJIT %s by Dyne.org",VERSION); // _err("Running version: %s\n",VERSION); // version is always shown -#ifdef LIBC_MINGW32 - _err("Built with MINGW32 libc"); +#if defined(CJIT_BUILD_WIN) + _err("Build: WINDOWS"); +#elif defined(CJIT_BUILD_MUSL) + _err("Build: MUSL"); +#elif defined(CJIT_BUILD_OSX) + _err("Build: OSX"); +#elif defined(CJIT_BUILD_LINUX) + _err("Build: LINUX"); +#else + _err("Build: UNKNOWN"); #endif -#ifdef LIBC_MUSL - _err("Built with Musl libc"); +#if defined(CJIT_DEBUG_ASAN) + _err("Debug: ASAN"); +#elif defined(CJIT_DEBUG_GDB) + _err("Debug: GDB"); +#else + _err("Debug: NONE"); #endif tcc_delete(TCC); exit(0); // print and exit From 2c07bc0a9c466a5aada6de03b974158fed23ef92 Mon Sep 17 00:00:00 2001 From: Jaromil Date: Mon, 9 Dec 2024 13:48:44 +0100 Subject: [PATCH 7/9] fix: bats setup delete cjit temp dir --- test/bats_setup | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/bats_setup b/test/bats_setup index 39b64a3..e35fce5 100644 --- a/test/bats_setup +++ b/test/bats_setup @@ -8,13 +8,13 @@ setup() { load "$T"/test_helper/bats_support/load load "$T"/test_helper/bats_assert/load CJIT="${R}/cjit" - rm -rf `${CJIT} --tgen` [ -r "$CJIT" ] || CJIT="${R}/cjit.exe" [ -r "$CJIT" ] || CJIT="${R}/cjit.command" [ -r "$CJIT" ] || { >&2 echo "CJIT is not built, cannot run test suite" exit 1 } + rm -rf `${CJIT} --tgen` TCC="${R}/lib/tinycc/tcc" [ -r "$TCC" ] || TCC="${R}/lib/tinycc/tcc.exe" } From 2664427e245eff4c1337fb6a6d4432fe06a474d6 Mon Sep 17 00:00:00 2001 From: Jaromil Date: Mon, 9 Dec 2024 17:18:11 +0100 Subject: [PATCH 8/9] fix: make check runs only relevant tests for each platform new check-ci target only for tests working in CI --- .github/workflows/main.yml | 18 +++++++++--------- GNUmakefile | 22 ++++++++++++++++++++-- test/dmon.bats | 7 +++---- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 16161e4..95c98fb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -58,21 +58,21 @@ jobs: # run: | # echo "๐Ÿ˜ค Some files failed the C linting checks!" - musl-test: - name: ๐Ÿง Musl Linux test + linux-test: + name: ๐Ÿง Linux x86 test needs: [reuse, c-lint] runs-on: "ubuntu-latest" steps: - uses: actions/checkout@v4 - name: install dependencies run: | - sudo apt install -yq musl-tools musl-dev - - name: Build x86_64 with musl-system + sudo apt install -yq make gcc xxd + - name: Build x86_64 with Linux run: | - make musl-linux + make linux-x86 - name: Run tests run: | - make check + make check-ci win-native-test: name: ๐ŸชŸ Windows native test @@ -85,7 +85,7 @@ jobs: make win-native - name: Run tests run: | - make check + make check-ci osx-native-test: name: ๐ŸŽ OSX native test @@ -98,11 +98,11 @@ jobs: make apple-osx - name: Run tests run: | - make check + make check-ci semantic-release: name: ๐Ÿค– Semantic release - needs: [musl-test, osx-native-test, win-native-test] + needs: [linux-test, osx-native-test, win-native-test] runs-on: ubuntu-latest outputs: new_release_published: ${{ steps.semantic-release.outputs.new_release_published }} diff --git a/GNUmakefile b/GNUmakefile index d152f72..7682d03 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -23,35 +23,53 @@ _: ## musl-linux: ## ๐Ÿ—ฟ Build a fully static cjit using musl-libc on Linux $(MAKE) -f build/musl.mk + @rm -f .build_done* + date | tee .build_done_musl linux-x86: ## ๐Ÿง Build a dynamically linked cjit using libs found on Linux x86 $(MAKE) -f build/linux.mk + @rm -f .build_done* + date | tee .build_done_linux win-wsl: ## ๐ŸชŸ Build cjit.exe for WIN64 on an Ubuntu WSL VM using gcc-mingw-w64 $(MAKE) -f build/win-wsl.mk + @rm -f .build_done* + date | tee .build_done_win win-native: ## ๐ŸชŸ Build cjit.exe for WIN64 on Windows Server cd ./lib/tinycc; bash configure --targetos=WIN32 --config-backtrace=no; make libtcc.a libtcc1.a $(MAKE) -f build/win-native.mk + @rm -f .build_done* + date | tee .build_done_win apple-osx: ## ๐ŸŽ Build cjit.command for Apple/OSX using clang static $(MAKE) -f build/osx.mk + @rm -f .build_done* + date | tee .build_done_osx _: ## ------: ## __ Debugging targets debug-gdb: ## ๐Ÿ”ฌ Build using the address sanitizer to detect memory leaks $(MAKE) -f build/linux.mk GDB=1 + date | tee .build_done_linux debug-asan: ## ๐Ÿ”ฌ Build using the address sanitizer to detect memory leaks $(MAKE) -f build/linux.mk ASAN=1 + date | tee .build_done_linux _: ## ------: ## __ Testing targets -check: CJIT ?= ./cjit check: ## ๐Ÿงช Run all tests using the currently built binary ./cjit - ./test/bats/bin/bats test + @./test/bats/bin/bats test/cli.bats + @./test/bats/bin/bats test/windows.bats + @if [ -r .build_done_linux ]; then ./test/bats/bin/bats test/dmon.bats; fi + + +check-ci: ## ๐Ÿงช Run all tests using the currently built binary ./cjit + @./test/bats/bin/bats test/cli.bats + @./test/bats/bin/bats test/windows.bats _: ## clean: ## ๐Ÿงน Clean the source from all built objects diff --git a/test/dmon.bats b/test/dmon.bats index 039e9c8..5b7a134 100644 --- a/test/dmon.bats +++ b/test/dmon.bats @@ -3,7 +3,7 @@ load bats_setup @test "DMON monitoring of filesystem" { # Check if the script is being run by PowerShell - [ -n "$PSVersionTable" ] && { + [ -r ${R}/.build_done_win ] && { >&3 echo ">> Skipping DMON test on Windows PowerShell" >&3 echo ">> TODO: run in background, test monitor and quit in PS" return 0 @@ -13,8 +13,7 @@ load bats_setup return 0 } - - (sleep .5; + (sleep 1; rm -f ${TMP}/dmon_test_create.txt; touch ${TMP}/dmon_test_create.txt; rm -f ${TMP}/dmon_test_create.txt; @@ -22,7 +21,7 @@ load bats_setup kill -HUP `cat ${TMP}/test_dmon.pid`) & run ${CJIT} -p ${TMP}/test_dmon.pid --dmon ${T}/dmon.c -- ${TMP} - assert_failure # TODO: cleaner way than kill -HUP + # assert_failure # TODO: cleaner way than kill -HUP assert_line --regexp '^CREATE:.*dmon_test_create.txt$' assert_line --regexp '^DELETE:.*dmon_test_create.txt$' } From 32d5abdea5be13b14e1ecfb295733e17c7d1c5dd Mon Sep 17 00:00:00 2001 From: Jaromil Date: Mon, 9 Dec 2024 22:43:43 +0100 Subject: [PATCH 9/9] fix: activate dmon by default on all supported platforms removes the need for the cli option --dmon --- build/win-wsl.mk | 1 + src/cjit.c | 38 +++++++++++++++++--------------------- src/file.c | 1 + test/dmon.bats | 2 +- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/build/win-wsl.mk b/build/win-wsl.mk index 8b08e28..a6ea0d1 100644 --- a/build/win-wsl.mk +++ b/build/win-wsl.mk @@ -36,6 +36,7 @@ deps: @bash build/embed-headers.sh win @sed -i 's/unsigned char/const char/' src/embed-headers.c @sed -i 's/unsigned int/const unsigned int/' src/embed-headers.c + @bash build/embed-dmon.sh .c.o: $(cc) \ diff --git a/src/cjit.c b/src/cjit.c index ae489a3..0bbbe42 100644 --- a/src/cjit.c +++ b/src/cjit.c @@ -85,8 +85,7 @@ const char cli_help[] = " -e fun\t entry point function (default 'main')\n" " -p pid\t write pid of executed program to file\n" " --live\t run interactive editor for live coding\n" - " --tgen\t create the runtime temporary dir and exit\n" - " --dmon\t activate the filesystem monitoring extension\n"; + " --tgen\t create the runtime temporary dir and exit\n"; int main(int argc, char **argv) { TCCState *TCC = NULL; @@ -120,7 +119,6 @@ int main(int argc, char **argv) { { "help", ko_no_argument, 100 }, { "live", ko_no_argument, 301 }, { "tgen", ko_no_argument, 401 }, - { "dmon", ko_no_argument, 501 }, { NULL, 0, 0 } }; ketopt_t opt = KETOPT_INIT; @@ -196,8 +194,6 @@ int main(int argc, char **argv) { if(!quiet)_err("Live mode activated"); live_mode = true; #endif - } else if (c == 501) { - CJIT.dmon = true; } else if (c == 401) { #ifndef LIBC_MINGW32 posix_mkdtemp(&CJIT); @@ -217,6 +213,22 @@ int main(int argc, char **argv) { } if(!quiet)_err("CJIT %s by Dyne.org",VERSION); + // DMON is activated on all supported platforms by default + CJIT.dmon = true; + tcc_define_symbol(TCC,"DMON_IMPL",NULL); +#if defined(CJIT_BUILD_LINUX) + tcc_define_symbol(TCC,"DMON_OS_LINUX",NULL); +// TODO: test dmon on OSX (missing library frameworks) +// #elif defined(CJIT_BUILD_OSX) +// tcc_define_symbol(TCC,"DMON_OS_MACOS",NULL); +#elif defined(CJIT_BUILD_WIN) + tcc_define_symbol(TCC,"DMON_OS_WINDOWS",NULL); +#else + tcc_undefine_symbol(TCC,"DMON_OS"); + tcc_undefine_symbol(TCC,"DMON_IMPL"); + CJIT.dmon = false; +#endif + ////////////////////////////////////// // initialize the tmpdir for execution // from here onwards use goto endgame @@ -251,22 +263,6 @@ int main(int argc, char **argv) { tcc_add_libc_symbols(TCC); #endif - if(CJIT.dmon) { - _err("Activated DMON extension for filesystem monitoring"); - tcc_define_symbol(TCC,"DMON_IMPL",NULL); -#if defined(CJIT_BUILD_LINUX) - tcc_define_symbol(TCC,"DMON_OS_LINUX",NULL); - -#elif defined(CJIT_BUILD_OSX) - tcc_define_symbol(TCC,"DMON_OS_MACOS",NULL); -#elif defined(CJIT_BUILD_WIN) - tcc_define_symbol(TCC,"DMON_OS_WINDOWS",NULL); -#else - _err("Unsupported platform for DMON extension"); - tcc_undefine_symbol(TCC,"DMON_OS"); -#endif - } - if (argc == 0 ) { _err("No input file: live mode!"); live_mode = true; diff --git a/src/file.c b/src/file.c index 0c4adc3..9ca6c11 100644 --- a/src/file.c +++ b/src/file.c @@ -334,6 +334,7 @@ bool cjit_extensions_mkdtemp(CJITState *CJIT, const char *tmpdir) { (char*)&lib_dmon_dmon_h, lib_dmon_dmon_h_len)) return(false); } + // setup the tmpdir path in CJIT CJIT->tmpdir = malloc(strlen(tmpdir)+1); strcpy(CJIT->tmpdir, tmpdir); return(true); diff --git a/test/dmon.bats b/test/dmon.bats index 5b7a134..1f8a846 100644 --- a/test/dmon.bats +++ b/test/dmon.bats @@ -20,7 +20,7 @@ load bats_setup sleep 2; kill -HUP `cat ${TMP}/test_dmon.pid`) & - run ${CJIT} -p ${TMP}/test_dmon.pid --dmon ${T}/dmon.c -- ${TMP} + run ${CJIT} -p ${TMP}/test_dmon.pid ${T}/dmon.c -- ${TMP} # assert_failure # TODO: cleaner way than kill -HUP assert_line --regexp '^CREATE:.*dmon_test_create.txt$' assert_line --regexp '^DELETE:.*dmon_test_create.txt$'