1018 lines
44 KiB
Plaintext
1018 lines
44 KiB
Plaintext
// 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";
|
||
}
|