mcap/modules/File_Watcher/module.jai

1018 lines
44 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Our copy of the stdlib module, for File_Index. Changes: pending changes are deduplicated through a
// table; OVERFLOW reports lost events and IS_FOLDER a known folder; on Linux, enumerate_on_add can
// skip the recursive walk (the caller adds every folder itself), process_changes drains the whole
// inotify queue, IN_IGNORED drops its watch, and running out of watches (ENOSPC) is recorded in
// unwatched instead of failing; on macOS, the last event ID is kept, a stream can replay from a
// saved one (since_event_id), and dropped events or an unusable history arrive as OVERFLOW.
//
// At startup, call init() to create one or more watchers.
// Start watching directories by calling add_directories(). This will watch this directory and (and all subdirectories).
// Call deinit() to stop watching.
// Periodically call process_changes to get notified of any changes that have happened since the last call.
// Your callback will be called for every change.
File_Change :: struct {
full_path: string; // The fully-pathed filename.
events: File_Change_Event;
// @ToDo: We should probably replace this with Apollo_Time
time_of_last_change := 0.0; // What time it last changed. It's a bad idea to use floating-point numbers for time, but, we did that here. This time is computed by Basic.seconds_since_init(). Maybe we should let you override this.
time_of_first_change := 0.0; // When this pending change was first seen; see MAX_SETTLE_SECONDS.
}
// A path that keeps changing (a log being appended to, a download, a browser database) never goes
// quiet for merge_window_seconds, so it is emitted anyway once it has been pending this long.
MAX_SETTLE_SECONDS :: 1.0;
// The pool holding pending changes is reset when nothing is pending. While changes keep arriving it
// never empties, so it is rebuilt with only the pending changes at most this often.
POOL_REBUILD_SECONDS :: 10.0;
File_Change_Event :: enum_flags {
ADDED;
MODIFIED;
// You might get two MOVED events if a file gets renamed, one for the old path and one for the new path.
// File_Watcher will add the modifiers MOVED_FROM or MOVED_TO to a MOVED event if it knows that this event is for the old path or new path, respectively.
// But for example on macOS you might get just two MOVED events and have to check the full_path and see if that file still exists to know which part of the move the event belongs to.
// Also, you might only get events for part of the move if the file gets moved from an unwatched folder into a watched folder or vice versa.
MOVED;
MOVED_FROM; // Will be combined with MOVED if we know that the MOVED event is for the old path.
MOVED_TO; // Will be combined with MOVED if we know that the MOVED event is for the new path.
ATTRIBUTES_CHANGED;
REMOVED;
SCAN_CHILDREN; // Indicates that you might need to check all children of this directory for changes. If a directory gets added/moved we don't automatically get change notifications for all of its children.
OVERFLOW; // Events were lost (Linux IN_Q_OVERFLOW); anything watched may have changed. full_path is empty.
IS_FOLDER; // The platform said the path is a folder (Linux IN_ISDIR, macOS ItemIsDir).
ALL_EVENTS :: ADDED | MODIFIED | MOVED | ATTRIBUTES_CHANGED | REMOVED;
}
DEFAULT_BUFFER_SIZE :: 8000;
File_Watcher :: struct (User_Data: Type = void) {
File_Watcher_Callback :: #type (watcher: *File_Watcher(User_Data), change: *File_Change, user_data: *User_Data);
callback: File_Watcher_Callback;
user_data: *User_Data;
events_to_watch: File_Change_Event;
merge_window_seconds: float;
watch_recursively: bool;
enumerate_on_add: bool; // Linux: add_directories also watches every subfolder it finds
verbose: bool;
buffer_size: s32;
pending_index: Table(string, s64); // full_path to its index in changes
// @Stability: pool needs to be dynamically allocated since we don't control the lifetime of File_Watcher.
// If pool were a direct member of File_Watcher and that gets copied and discarded (eg by adding it to an array), then arrays like "changes" could still point to the old copy of Pool.
// -rluba, 2022-06-06
pool: *Pool;
changes: [..] File_Change;
pool_reset_at: float64; // seconds_since_init()
pool_allocator: Allocator; // what pool was made with (the caller's at init); rebuild_pool makes and frees with it too
#if OS == .WINDOWS {
directories: [..] *Directory_Info;
external_probe: bool; // the caller checks each directory's path and reports it with report_directory
} else #if OS_HAS_INOTIFY {
inotify_instance: s32 = -1;
inotify_data: *void;
inotify_bytes_read: s64;
inotify_cursor: s32;
path_pool: *Pool; // A second pool to avoid one allocation per directory in the tree we watch
watch_paths: Table(s32, string);
watched_paths: Table(string, s32); // the reverse of watch_paths, sharing its strings
// Watches count against fs.inotify.max_user_watches, which every program of the user shares.
// Past max_watches (0: no cap) a new folder is reported in unwatched instead.
max_watches: s64;
watch_limit_reached: bool; // max_watches or fs.inotify.max_user_watches was reached
unwatched: [..] string; // the folders that could not be watched since the caller last cleared it
} else #if OS == .MACOS {
event_streams: [..] FSEventStreamRef;
last_event_id: u64;
since_event_id: u64 = kFSEventStreamEventIdSinceNow; // set before add_directories to replay from a saved last_event_id
history_done: bool; // the replay since since_event_id has been delivered
// Set before add_directories: folders below the ones added whose changes FSEvents should not
// deliver. FSEvents takes at most FSEVENTS_MAX_EXCLUSIONS per stream; the rest are ignored.
exclusion_paths: [] string;
} else #if OS == .PS5 || OS == .NN_SWITCH2 {
} else {
#assert(false);
}
}
// Use events_to_watch to specify what kinds of events you're interested in. (But you might still get events from categories you didn't ask for, depending on the platform.)
// event_buffer_size is allocated _once_ on Linux but _once per directory_ on Windows. (It’s not used at all on macOS.) Therefore, using the same value on Linux and Windows might result in very different total memory usage.
init :: (watcher: *$T/File_Watcher, callback: T.File_Watcher_Callback, user_data: *T.User_Data = null, events_to_watch: File_Change_Event = .ALL_EVENTS, merge_window_seconds: float = 0.1, watch_recursively := true, verbose := false, event_buffer_size: s32 = DEFAULT_BUFFER_SIZE, enumerate_on_add := true) -> success: bool {
assert(merge_window_seconds >= 0);
watcher.callback = callback;
watcher.user_data = user_data;
watcher.events_to_watch = events_to_watch;
watcher.merge_window_seconds = merge_window_seconds;
watcher.watch_recursively = watch_recursively;
watcher.enumerate_on_add = enumerate_on_add;
watcher.verbose = verbose;
watcher.buffer_size = event_buffer_size;
watcher.pool_allocator = context.allocator;
watcher.pool = New(Pool);
set_allocators(watcher.pool);
// watcher.pool.overwrite_memory = true; // Enable this for basic memory debugging.
watcher.changes.allocator.proc = pool_allocator_proc;
watcher.changes.allocator.data = watcher.pool;
// Not the pool: add_change pushes it, and the pool is reset while the table lives on.
watcher.pending_index.allocator = context.default_allocator;
#if OS_HAS_INOTIFY {
watcher.inotify_instance = inotify_init(IN_NONBLOCK);
if watcher.inotify_instance == -1 {
log_error("Could not init inotify: %", errno());
free(watcher.pool);
return false;
}
watcher.inotify_data = alloc(event_buffer_size);
watcher.path_pool = New(Pool);
set_allocators(watcher.path_pool);
watcher.unwatched.allocator = context.default_allocator;
}
#if OS == .MACOS {
if !watch_recursively {
log_error("Ignoring \"watch_recursively = false\" flag since macOS always watches directories recursively.");
}
}
return true;
}
add_directories :: (using watcher: *$T/File_Watcher, dir_paths: ..string) -> bool {
assert(callback != null, "You must call init() on the File_Watcher before add_directories()!");
#if OS == .WINDOWS {
return add_directories_windows(watcher, ..dir_paths);
} else #if OS_HAS_INOTIFY {
return add_directories_linux(watcher, ..dir_paths);
} else #if OS == .MACOS {
return add_directories_macos(watcher, ..dir_paths);
} else #if OS == .PS5|| OS == .NN_SWITCH2 {
return false;
} else {
assert(false);
return false;
}
}
deinit :: (using watcher: $T/File_Watcher) {
#if OS == .WINDOWS {
for directories {
if it.lost {
release(it.*);
free(it);
continue;
}
// CancelIoEx, not CancelIo: deinit can run on another thread than the one that issued the
// read (the app's shutdown thread), and CancelIo cancels only the calling thread's I/O, which
// left the wait below blocked until the directory next changed.
success := CancelIoEx(it.handle, *it.overlapped);
if success {
// The cancelled read completes asynchronously, and the kernel writes its buffer and
// OVERLAPPED until it does, so wait before freeing them.
bytes: s32;
GetOverlappedResult(it.handle, *it.overlapped, *bytes, TRUE);
} else if GetLastError() != ERROR_NOT_FOUND { // not found: no read was pending
System :: #import "System";
error, error_string := System.get_error_value_and_string();
log_error("Couldn't cancel the async io on %: error % (%)", it.name, error, error_string);
}
release(it.*);
free(it);
}
array_free(directories);
} else #if OS_HAS_INOTIFY {
close(inotify_instance);
free(inotify_data);
table_deinit(*watch_paths);
table_deinit(*watched_paths);
release(path_pool);
free(path_pool);
} else #if OS == .MACOS {
for event_streams {
FSEventStreamStop(it);
FSEventStreamInvalidate(it); // Remove stream from all run loops
FSEventStreamRelease(it);
}
array_free(event_streams);
} else {
assert(false);
}
table_deinit(*pending_index);
release(pool);
free(pool);
}
// Process changes of any of the watched files.
// If you call this periodically, you can ignore the needs_wait and wait_seconds return values.
// But you can use POSIX poll(…) on the watcher’s inotify_instance to wait until changes are pending.
// In that case, you should check needs_wait. If it’s true you should call this function again after
// wait_seconds have elapsed, even if no further poll changes have happened.
process_changes :: (using watcher: *File_Watcher) -> changed: bool, needs_wait: bool, wait_seconds: float64 {
assert(callback != null, "You must call init() on the File_Watcher before process_changes()!");
#if OS == .WINDOWS {
changed, needs_wait, wait_seconds := process_changes_windows(watcher);
} else #if OS_HAS_INOTIFY {
changed, needs_wait, wait_seconds := process_changes_linux(watcher);
} else #if OS == .MACOS {
changed, needs_wait, wait_seconds := process_changes_macos(watcher);
} else #if OS == .PS5 || OS == .NN_SWITCH2 {
changed := false;
needs_wait := false;
wait_seconds := 0.0;
assert(!watcher.changes);
} else {
changed := false;
needs_wait := false;
wait_seconds := 0.0;
assert(false);
}
return changed, needs_wait, wait_seconds;
}
#if OS == .WINDOWS {
Directory_Identity :: struct {
volume_serial: u32;
index_high: u32;
index_low: u32;
}
// Opens the path, so on an unreachable network share it can block for a long time.
directory_identity :: (path: string) -> Directory_Identity, bool {
identity, ok := path_identity(path);
return identity, ok;
}
// A bare drive ("C:") names the process's current directory on that drive, not its root, so a
// watched drive is always opened and named as its root ("C:/").
watched_name :: (path: string) -> string {
name := ifx path.count == 2 && path[1] == #char ":" then tprint("%/", path) else copy_string(path,, temp);
path_overwrite_separators(name, #char "/");
return name;
}
// For a watcher with external_probe set, which never opens a watched directory's path by itself:
// the caller checks each path off the thread that processes changes (with directory_identity)
// and reports the result here. A watched directory whose path is gone or now names another one
// is lost; a lost one is opened again once it is present.
report_directory :: (using watcher: *File_Watcher, path: string, present: bool, identity: Directory_Identity) {
name := watched_name(path);
for info: directories {
if info.name != name continue;
if info.lost {
if present {
info.retry_at = 0;
retry_lost_directory(watcher, info);
}
} else if !present || !same_identity(identity, info.identity) {
CancelIoEx(info.handle, *info.overlapped);
bytes: s32;
GetOverlappedResult(info.handle, *info.overlapped, *bytes, TRUE);
lose_directory(watcher, info, ifx present then cast(u32) 0 else ERROR_FILE_NOT_FOUND);
}
}
}
}
FSEVENTS_MAX_EXCLUSIONS :: 8; // macOS: the most File_Watcher.exclusion_paths FSEvents takes
#scope_file
add_change ::(using watcher: *File_Watcher, full_path: string, events: File_Change_Event) {
push_allocator(pool_allocator_proc, pool);
now := seconds_since_init();
found, index := table_find(*pending_index, full_path);
if found {
change := *changes[index];
if watcher.verbose log("Deduplicating changes for %: % + %", full_path, change.events, events);
change.events |= events;
change.time_of_last_change = xx now;
return;
}
change: File_Change;
change.full_path = copy_string(full_path);
change.events = events;
change.time_of_last_change = xx now;
change.time_of_first_change = xx now;
array_add(*changes, change);
table_add(*pending_index, change.full_path, changes.count - 1);
if watcher.verbose log("Change for '%' issued at time %.", full_path, now);
}
// The pending changes, copied into a fresh pool; the old one also held every change emitted since it
// was last reset.
rebuild_pool :: (using watcher: *$T/File_Watcher) {
// Not the heap: init made the pool with whatever allocator was current, an arena on the app's test threads.
push_allocator(pool_allocator);
fresh := New(Pool);
set_allocators(fresh);
rebuilt: [..] File_Change;
rebuilt.allocator.proc = pool_allocator_proc;
rebuilt.allocator.data = fresh;
{
push_allocator(pool_allocator_proc, fresh);
array_reserve(*rebuilt, changes.count);
for changes {
change := it;
change.full_path = copy_string(it.full_path);
array_add(*rebuilt, change);
}
}
release(pool);
free(pool);
pool = fresh;
changes = rebuilt;
table_reset(*pending_index);
for changes table_add(*pending_index, it.full_path, it_index);
}
emit_changes :: (using watcher: *$T/File_Watcher) -> changed: bool, needs_wait: bool, wait_seconds: float64 {
assert(watcher.merge_window_seconds >= 0);
if !changes.count return false, false, 0;
now := seconds_since_init();
notified := false;
earliest_wait_time: float64 = merge_window_seconds;
for * change: changes {
#if OS == .MACOS {
wait_time : float64 = 0; // The FSEventStream already batches up events for us
} else {
delta := now - change.time_of_last_change;
wait_time := merge_window_seconds - delta;
wait_time = min(wait_time, MAX_SETTLE_SECONDS - (now - change.time_of_first_change));
}
// if watcher.verbose log("Wait time for %: %", change.*, wait_time);
if wait_time <= 0 {
if callback callback(watcher, change, user_data);
remove change;
notified = true;
} else if wait_time < earliest_wait_time {
earliest_wait_time = wait_time;
}
}
// remove moved the last change into each emitted one's slot, so the indices are rebuilt.
table_reset(*pending_index);
for changes table_add(*pending_index, it.full_path, it_index);
if changes.count == 0 {
// Nothing to handle, so, we can reset our memory. Note that right now we could in theory grow without bounds if someone keeps throwing
// changes at us such that there is always something < RELOAD_DELAY. We could fix this
// by having two pools and swapping between them every once in a while, but it doesn't
// seem worth it right now as this is meant for friendly production environments. -jblow, 1 March 2021.
array_reset(*changes);
reset(pool);
pool_reset_at = now;
} else if now - pool_reset_at >= POOL_REBUILD_SECONDS {
rebuild_pool(watcher);
pool_reset_at = now;
}
return notified, (changes.count > 0), earliest_wait_time;
}
#if OS == .WINDOWS {
TRUE :: 1;
FALSE :: 0;
Directory_Info :: struct {
name: string;
bytes_returned: s32;
overlapped: OVERLAPPED;
handle: HANDLE;
notify_information: *void;
// The read could not be issued or completed, usually because the directory was deleted.
// The handle is closed, so the directory can actually go away, and reopening is retried.
lost: bool;
retry_at: float64; // seconds_since_init(); also when a watched directory is next checked
// The handle follows a directory that is renamed or moved, and its reads keep succeeding, so
// the path is checked every WATCH_RETRY_SECONDS to still name the same directory.
identity: Directory_Identity;
}
WATCH_RETRY_SECONDS :: 5.0;
ERROR_NOTIFY_ENUM_DIR :: 1022;
// BY_HANDLE_FILE_INFORMATION, with its FILETIMEs as DWORD pairs to keep the C layout.
Watch_File_Information :: struct {
attributes: u32;
times: [6] u32;
volume_serial: u32;
size_high: u32;
size_low: u32;
link_count: u32;
index_high: u32;
index_low: u32;
}
#assert size_of(Watch_File_Information) == 52;
GetFileInformationByHandle :: (hFile: HANDLE, lpFileInformation: *Watch_File_Information) -> BOOL #foreign watcher_kernel32;
CancelIoEx :: (hFile: HANDLE, lpOverlapped: *OVERLAPPED) -> BOOL #foreign watcher_kernel32;
ERROR_NOT_FOUND :: 1168;
watcher_kernel32 :: #library,system "kernel32";
same_identity :: (a: Directory_Identity, b: Directory_Identity) -> bool {
return a.volume_serial == b.volume_serial && a.index_high == b.index_high && a.index_low == b.index_low;
}
handle_identity :: (handle: HANDLE) -> Directory_Identity, bool {
info: Watch_File_Information;
if !GetFileInformationByHandle(handle, *info) return .{}, false;
return .{info.volume_serial, info.index_high, info.index_low}, true;
}
path_identity :: (path: string) -> Directory_Identity, bool {
handle := CreateFileW(utf8_to_wide(long_path(watched_name(path)),, temp), FILE_READ_ATTRIBUTES,
FILE_SHARE_READ|FILE_SHARE_DELETE|FILE_SHARE_WRITE,
null, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, null);
if handle == INVALID_HANDLE_VALUE return .{}, false;
defer CloseHandle(handle);
identity, ok := handle_identity(handle);
return identity, ok;
}
release :: (info: Directory_Info) {
CloseHandle(info.overlapped.hEvent);
if info.handle != INVALID_HANDLE_VALUE CloseHandle(info.handle);
free(info.name);
free(info.notify_information);
}
add_directories_windows :: (using watcher: *File_Watcher, dir_paths: ..string) -> bool {
for dir_path: dir_paths {
// Init and allocate. We allocate one buffer of File_Watcher.buffer_size size for each directory,
// which might become big if there are many many directories. This may be something you'd want to tune.
event := CreateEventW(null, FALSE, FALSE, null);
if event == null return false;
name := watched_name(dir_path);
handle := open_directory(name);
if handle == INVALID_HANDLE_VALUE {
System :: #import "System";
error_code, description := System.get_error_value_and_string();
log_error("os_create_directory_notification encountered an error in CreateFileW: %, %\n", error_code, description);
CloseHandle(event);
return false;
}
info := New(Directory_Info);
info.overlapped.hEvent = event;
info.overlapped.Offset = 0;
info.handle = handle;
info.identity = handle_identity(handle);
info.retry_at = seconds_since_init() + WATCH_RETRY_SECONDS;
info.name = copy_string(name); // Allocated on the global heap.
info.notify_information = alloc(watcher.buffer_size);
// Start the first async read right away
issued, error := issue_one_read(watcher, info);
if !issued lose_directory(watcher, info, error);
array_add(*directories, info);
}
return true;
}
// As File_Index's long_path: \\?\ lifts MAX_PATH but skips normalization, so only long absolute
// paths get it.
long_path :: (path: string) -> string {
if path.count < MAX_PATH - 12 || begins_with(path, "\\\\?\\") return path;
result := copy_string(path,, temp);
path_overwrite_separators(result, #char "\\");
if result.count >= 3 && result[1] == #char ":" && result[2] == #char "\\" return tprint("\\\\?\\%", result);
if begins_with(result, "\\\\") return tprint("\\\\?\\UNC\\%", advance(result, 2));
return path;
}
open_directory :: (path: string) -> HANDLE {
return CreateFileW(utf8_to_wide(long_path(path),, temp), FILE_LIST_DIRECTORY,
FILE_SHARE_READ|FILE_SHARE_DELETE|FILE_SHARE_WRITE,
null,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OVERLAPPED,
null);
}
// Logs once per loss. The directory is reported as changed, so its contents are checked
// against the disk, and again when it is watched again.
lose_directory :: (using watcher: *File_Watcher, info: *Directory_Info, error: u32) {
if !info.lost {
retry := ifx external_probe then "watching it again once it is back" else tprint("retrying every % s", WATCH_RETRY_SECONDS);
if error log_error("Stopped watching % (error %); %.\n", info.name, error, retry);
else log("Stopped watching %, which was moved or replaced; %.\n", info.name, retry);
}
info.lost = true;
info.retry_at = seconds_since_init() + WATCH_RETRY_SECONDS;
if info.handle != INVALID_HANDLE_VALUE {
CloseHandle(info.handle);
info.handle = INVALID_HANDLE_VALUE;
}
add_change(watcher, info.name, File_Change_Event.MODIFIED | .SCAN_CHILDREN);
}
retry_lost_directory :: (using watcher: *File_Watcher, info: *Directory_Info) {
if seconds_since_init() < info.retry_at return;
info.retry_at = seconds_since_init() + WATCH_RETRY_SECONDS;
handle := open_directory(info.name);
if handle == INVALID_HANDLE_VALUE return;
info.handle = handle;
info.identity = handle_identity(handle);
issued := issue_one_read(watcher, info);
if !issued {
CloseHandle(handle);
info.handle = INVALID_HANDLE_VALUE;
return;
}
info.lost = false;
log("Watching % again.\n", info.name);
add_change(watcher, info.name, File_Change_Event.MODIFIED | .SCAN_CHILDREN);
}
issue_one_read :: (using watcher: *File_Watcher, info: *Directory_Info) -> issued: bool, error: u32 {
assert(info.handle != INVALID_HANDLE_VALUE);
notify_filter: s32;
if (events_to_watch & (.ADDED | .MOVED | .REMOVED)) {
notify_filter |= FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_CREATION;
}
if (events_to_watch & .MODIFIED) {
notify_filter |= FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE;
}
if (events_to_watch & .ATTRIBUTES_CHANGED) {
notify_filter |= FILE_NOTIFY_CHANGE_SECURITY | FILE_NOTIFY_CHANGE_ATTRIBUTES;
}
info.bytes_returned = 0;
success := ReadDirectoryChangesW(info.handle, info.notify_information, watcher.buffer_size, cast(s32) watcher.watch_recursively, notify_filter, null, *info.overlapped, null);
if !success return false, cast(u32) GetLastError();
return true, 0;
}
process_changes_windows :: (using watcher: *File_Watcher) -> changed: bool, needs_wait: bool, wait_seconds: float64 {
for info: directories {
if info.lost {
if !external_probe retry_lost_directory(watcher, info);
continue;
}
if !external_probe && seconds_since_init() >= info.retry_at {
info.retry_at = seconds_since_init() + WATCH_RETRY_SECONDS;
identity, found := path_identity(info.name);
error := ifx found then 0 else cast(u32) GetLastError();
if !found || !same_identity(identity, info.identity) {
// Pending I/O must finish before the buffer and OVERLAPPED are reused.
CancelIoEx(info.handle, *info.overlapped);
bytes: s32;
GetOverlappedResult(info.handle, *info.overlapped, *bytes, TRUE);
lose_directory(watcher, info, error);
continue;
}
}
handle := info.handle;
assert(handle != INVALID_HANDLE_VALUE);
if !HasOverlappedIoCompleted(info.overlapped) continue;
// Handle the change....
bytes_transferred: s32;
success := GetOverlappedResult(handle, *info.overlapped, *bytes_transferred, FALSE);
if success && verbose log("%: bytes_transferred == % (struct size %)\n", info.name, bytes_transferred, size_of(FILE_NOTIFY_INFORMATION));
if !success {
error := cast(u32) GetLastError();
if error != ERROR_NOTIFY_ENUM_DIR {
lose_directory(watcher, info, error);
continue;
}
bytes_transferred = 0; // too many changes to report: handled as an overflow
}
// Issue the next read once we're done processing this one
defer {
issued, error := issue_one_read(watcher, info);
if !issued lose_directory(watcher, info, error);
}
if bytes_transferred == 0 {
// Buffer has overflowed and we lost all the events. Add event for the whole directory and set SCAN_CHILDREN.
add_change(watcher, info.name, File_Change_Event.MODIFIED | .SCAN_CHILDREN);
continue;
}
notify := cast(*FILE_NOTIFY_INFORMATION) info.notify_information; // This is actually a list of events...
while notify != null {
defer notify = move_info_forward(notify);
events: File_Change_Event;
if notify.Action == {
case FILE_ACTION_ADDED; events = .ADDED;
case FILE_ACTION_MODIFIED; events = .MODIFIED;
case FILE_ACTION_RENAMED_OLD_NAME; events = File_Change_Event.MOVED | .MOVED_FROM;
case FILE_ACTION_RENAMED_NEW_NAME; events = File_Change_Event.MOVED | .MOVED_TO;
case FILE_ACTION_REMOVED; events = .REMOVED;
case;
if verbose log("discarded case %\n", notify.Action);
// Ignore RENAMED_OLD_NAME file actions.
continue;
}
name, success := wide_to_utf8(cast(*u16) notify.FileName.data, notify.FileNameLength / 2,, temp);
if !name {
if verbose log_error("WideCharToMultiByte returned empty string.\n");
continue;
}
path_overwrite_separators(name, #char "/");
full_path := tprint("%/%", trim_right(info.name, "/"), name);
// A folder is MODIFIED whenever its entries change, which is not a sign of lost events,
// so only the overflow paths above mark SCAN_CHILDREN.
add_change(watcher, full_path, events);
}
}
changed, needs_wait, wait_seconds := emit_changes(watcher);
return changed, needs_wait, wait_seconds;
}
move_info_forward :: (info: *FILE_NOTIFY_INFORMATION) -> *FILE_NOTIFY_INFORMATION {
if info.NextEntryOffset == 0 return null;
return cast(*FILE_NOTIFY_INFORMATION)((cast(*u8) info) + info.NextEntryOffset);
}
}
#if OS_HAS_INOTIFY {
add_directories_linux :: (using watcher: *$T/File_Watcher, dir_paths: .. string) -> bool {
for dir_paths {
if !add_single_directory_linux(watcher, it) return false;
}
if watcher.watch_recursively && watcher.enumerate_on_add {
Data :: struct {
watcher: *T;
success: bool;
}
data: Data;
data.watcher = watcher;
data.success = true;
for path: dir_paths {
is_dir, success := is_directory(path);
if !success {
if watcher.verbose log("Couldn’t check if \"%\" is a directory", path);
continue;
}
if is_dir {
success := visit_files(path, true, *data, (info: *File_Visit_Info, data: *Data) {
if !add_single_directory_linux(data.watcher, info.full_name) {
data.success = false;
}
}, visit_files = false, visit_directories = true, visit_symlinks = false, follow_directory_symlinks = false);
if !success || !data.success {
log_error("Couldn’t enumerate dirs in \"%\"", path);
return false;
}
}
}
}
return true;
}
add_single_directory_linux :: (using watcher: *File_Watcher, dir_path: string) -> bool {
push_allocator(pool_allocator_proc, path_pool);
c_path := temp_c_string(dir_path);
flags: u32;
if (events_to_watch & .ADDED) {
flags |= IN_CREATE;
}
if (events_to_watch & .MODIFIED) {
flags |= IN_MODIFY | IN_CLOSE_WRITE;
}
if (events_to_watch & .MOVED) {
flags |= IN_MOVED_TO | IN_MOVE_SELF;
}
if (events_to_watch & .ATTRIBUTES_CHANGED) {
flags |= IN_ATTRIB;
}
if (events_to_watch & .REMOVED) {
flags |= IN_DELETE | IN_DELETE_SELF | IN_MOVED_FROM;
}
// Out of watches: the caller rescans these instead, so this is not a failure.
if max_watches && watch_paths.count >= max_watches && !table_contains(*watched_paths, dir_path) {
watch_limit_reached = true;
array_add(*unwatched, copy_string(dir_path));
return true;
}
watch := inotify_add_watch(inotify_instance, c_path, flags);
if watch == -1 {
error := errno();
if error == ENOSPC {
watch_limit_reached = true;
array_add(*unwatched, copy_string(dir_path));
return true;
}
log_error("Could not watch path \"%\": %", dir_path, error);
return false;
}
previous_watch_path := table_find_pointer(*watch_paths, watch);
if previous_watch_path {
if watcher.verbose log("Watch % for \"%\" already exists", watch, previous_watch_path.*);
if compare(previous_watch_path.*, dir_path) != 0 {
if watcher.verbose log("Replacing path for watch %: \"%\" -> \"%\"", watch, previous_watch_path.*, dir_path);
table_remove(*watched_paths, previous_watch_path.*);
free(previous_watch_path.*);
previous_watch_path.* = copy_string(dir_path);
table_add(*watched_paths, previous_watch_path.*, watch);
}
} else {
if watcher.verbose log("Adding watch % for \"%\"", watch, dir_path);
path := copy_string(dir_path);
table_add(*watch_paths, watch, path);
table_add(*watched_paths, path, watch);
}
return true;
}
process_changes_linux :: (using watcher: *File_Watcher) -> changed: bool, needs_wait: bool, wait_seconds: float64 {
assert(inotify_instance != -1);
// One read returns at most buffer_size bytes, so read until the queue is empty.
while true {
inotify_bytes_read = read(inotify_instance, inotify_data, cast(u64) watcher.buffer_size);
if inotify_bytes_read == -1 {
if errno() != EAGAIN {
log_error("Could not read inotify events: %", errno());
}
break;
}
if inotify_bytes_read == 0 break;
inotify_cursor = 0;
notify := cast(*inotify_event) inotify_data; // This is actually a list of events...
while notify != null {
defer notify = move_data_forward(watcher, notify);
if watcher.verbose log("Received notify: % (mask: 0x%)", notify.*, formatInt(notify.mask, base = 16));
if notify.mask & IN_Q_OVERFLOW {
add_change(watcher, "", .OVERFLOW);
continue;
}
if notify.mask & IN_IGNORED {
// The watch is gone: its folder was deleted or unmounted, or the watch removed.
gone := table_find_pointer(*watch_paths, notify.wd);
if gone table_remove(*watched_paths, gone.*);
table_remove(*watch_paths, notify.wd);
continue;
}
c_name := notify.name.data;
// we cant just do this because notify.name can be null padded arbitrarily
// and notify.len includes these pads. Thanks Linux... -josh Feb 22, 2017
// length_in_bytes := notify.len;
name: string;
name.data = c_name;
if notify.len {
name.count = c_style_strlen(c_name);
} else {
name.count = 0;
}
events: File_Change_Event;
if notify.mask & IN_CREATE events |= .ADDED;
if notify.mask & (IN_MODIFY | IN_CLOSE_WRITE) events |= .MODIFIED;
if notify.mask & (IN_MOVED_FROM) events |= File_Change_Event.MOVED | .MOVED_FROM;
if notify.mask & (IN_MOVED_TO | IN_MOVE_SELF) events |= File_Change_Event.MOVED | .MOVED_TO;
if notify.mask & IN_ATTRIB events |= .ATTRIBUTES_CHANGED;
if notify.mask & (IN_DELETE | IN_DELETE_SELF) events |= .REMOVED;
if !events {
if watcher.verbose log("discarded mask % for \"%\"", notify.mask, name);
continue;
}
is_folder := (notify.mask & IN_ISDIR) != 0;
if is_folder events |= .IS_FOLDER;
if watcher.verbose log("Action '%' on name '%':", events, name);
full_path, success := get_full_name(watcher, notify.wd, name);
if !success {
log_error("Could not assemble full name for \"%\" from event %", name, notify.*);
continue;
}
was_created_or_moved := events & (File_Change_Event.ADDED | File_Change_Event.MOVED_TO);
if watch_recursively && was_created_or_moved && is_folder {
add_directories(watcher, full_path);
// Tell the callback that he has missed events for the children of this directory.
events |= .SCAN_CHILDREN;
}
add_change(watcher, full_path, events);
}
}
changed, needs_wait, wait_seconds := emit_changes(watcher);
return changed, needs_wait, wait_seconds;
}
move_data_forward :: (using watcher: *File_Watcher, event: *inotify_event) -> *inotify_event {
// Our inotify_event struct is too large because the "name" array has non-zero size.
INOTIFY_EVENT_OVERSIZE :: 4;
step: s32 = cast(s32) (size_of(inotify_event) - INOTIFY_EVENT_OVERSIZE + event.len);
if (inotify_cursor + step) >= inotify_bytes_read return null;
inotify_cursor += step;
return cast(*inotify_event) ((cast(*u8) inotify_data) + inotify_cursor);
}
get_full_name :: (using watcher: *File_Watcher, watch: s32, name: string) -> string, success: bool {
dir_path := table_find_pointer(*watch_paths, watch);
if !dir_path return "", false;
if name {
return tprint("%/%", dir_path.*, name), true;
} else {
return dir_path.*, true;
}
}
}
#if OS == .MACOS {
add_directories_macos :: (using watcher: *File_Watcher, dir_paths: .. string) -> bool {
assert(watcher.merge_window_seconds >= 0);
paths_to_watch := create_cfstring_array(dir_paths);
defer CFRelease(paths_to_watch);
fsinfo: FSEventStreamContext;
fsinfo.info = watcher;
// So a session without a single change still leaves a point to replay from.
start_id := ifx watcher.since_event_id == kFSEventStreamEventIdSinceNow then FSEventsGetCurrentEventId() else watcher.since_event_id;
if start_id > watcher.last_event_id watcher.last_event_id = start_id;
event_stream := FSEventStreamCreate(null, event_stream_callback, _context=*fsinfo, paths_to_watch, watcher.since_event_id,
latency=watcher.merge_window_seconds, kFSEventStreamCreateFlagFileEvents | FS_CREATE_FLAG_WATCH_ROOT);
if exclusion_paths.count {
excluded := array_view(exclusion_paths, 0, min(exclusion_paths.count, FSEVENTS_MAX_EXCLUSIONS));
excluded_array := create_cfstring_array(excluded);
defer CFRelease(excluded_array);
if FSEventStreamSetExclusionPaths(event_stream, excluded_array) == 0 {
log_error("[File_Watcher] FSEvents did not accept the excluded folders %; their changes still arrive and are dropped.", excluded);
}
}
FSEventStreamScheduleWithRunLoop(event_stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
FSEventStreamStart(event_stream);
array_add(*event_streams, event_stream);
return true;
}
FS_ATTRIBUTE_CHANGES :: kFSEventStreamEventFlagChangeOwner | kFSEventStreamEventFlagItemInodeMetaMod | kFSEventStreamEventFlagItemFinderInfoMod;
// From FSEvents.h; the macOS module does not declare these.
FS_EVENT_FLAG_USER_DROPPED :: 0x00000002;
FS_EVENT_FLAG_KERNEL_DROPPED :: 0x00000004;
FS_EVENT_FLAG_EVENT_IDS_WRAPPED :: 0x00000008;
FS_EVENT_FLAG_HISTORY_DONE :: 0x00000010;
FS_EVENT_FLAG_ROOT_CHANGED :: 0x00000020;
FS_CREATE_FLAG_WATCH_ROOT :: 0x00000004;
// The macOS module keeps its CoreServices library to itself.
CoreServices_For_Watcher :: #library,system "CoreServices";
FSEventsGetCurrentEventId :: () -> u64 #foreign CoreServices_For_Watcher;
FSEventStreamSetExclusionPaths :: (streamRef: FSEventStreamRef, pathsToExclude: CFArrayRef) -> u8 #foreign CoreServices_For_Watcher; // Boolean
// Events were lost, or a replay's history cannot be trusted: the caller must rescan.
FS_EVENTS_LOST :: FS_EVENT_FLAG_USER_DROPPED | FS_EVENT_FLAG_KERNEL_DROPPED | FS_EVENT_FLAG_EVENT_IDS_WRAPPED | FS_EVENT_FLAG_ROOT_CHANGED;
event_stream_callback :: (streamRef: FSEventStreamRef, clientCallBackInfo: *void, numEvents: u64, eventPaths: *void, eventFlags: *FSEventStreamEventFlags, eventIds: *FSEventStreamEventId) #c_call {
using watcher := cast(*File_Watcher) clientCallBackInfo;
push_context {
paths := cast(**u8)eventPaths;
for 0..numEvents-1 {
flags := eventFlags[it];
event_id := eventIds[it];
full_path := to_string(paths[it]);
if verbose log("Received event id % with flags 0x%", event_id, formatInt(flags, base=16));
if flags & FS_EVENT_FLAG_HISTORY_DONE {
history_done = true;
continue;
}
if flags & FS_EVENTS_LOST {
add_change(watcher, "", .OVERFLOW);
continue;
}
events: File_Change_Event;
if flags & kFSEventStreamEventFlagItemCreated events |= .ADDED;
if flags & kFSEventStreamEventFlagItemModified events |= .MODIFIED;
if flags & kFSEventStreamEventFlagItemRenamed events |= .MOVED;
if flags & kFSEventStreamEventFlagItemRemoved events |= .REMOVED;
if flags & kFSEventStreamEventFlagMustScanSubDirs events |= .SCAN_CHILDREN;
if flags & FS_ATTRIBUTE_CHANGES events |= .ATTRIBUTES_CHANGED;
if !events {
if verbose log("Discarding events % for %", flags, full_path);
continue;
}
if flags & kFSEventStreamEventFlagItemIsDir {
events |= .IS_FOLDER;
if events & (File_Change_Event.ADDED | File_Change_Event.MOVED) {
// Tell the callback that he has missed events for the children of this directory.
events |= .SCAN_CHILDREN;
}
}
if event_id > last_event_id last_event_id = event_id;
add_change(watcher, full_path, events);
}
}
}
process_changes_macos :: (using watcher: *File_Watcher) -> changed: bool, needs_wait: bool, wait_seconds: float64 {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false);
changed, needs_wait, wait_seconds := emit_changes(watcher);
return changed, needs_wait, wait_seconds;
}
create_cfstring_array :: (strings: [] string) -> CFArrayRef {
temp := NewArray(strings.count, CFStringRef);
defer {
for temp CFRelease(it);
array_free(temp);
}
for strings {
cfstring := CFStringCreateWithBytes(null, it.data, it.count, .UTF8, false);
temp[it_index] = cfstring;
}
cfarray := CFArrayCreate(null, cast(**void) temp.data, temp.count, *kCFTypeArrayCallBacks);
return cfarray;
}
}
#import "Basic";
#import "File_Utilities";
#import "Hash_Table";
#import "Pool";
#import "String";
OS_HAS_INOTIFY :: OS == .LINUX || OS == .ANDROID;
#if OS == .WINDOWS {
#import "Windows";
#import "Windows_Utf8";
}
#if OS_HAS_INOTIFY {
#import "Linux";
#import "POSIX";
}
#if OS == .MACOS {
#import "macOS";
#import "POSIX";
}