From be03d1c7051e88fda97eec946c98ab118b888c0a Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 6 Oct 2024 18:20:15 -0700 Subject: [PATCH 001/168] Rewrite the native binding in N-API; add context safety in the process. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is an ongoing nightmare! It took me a while to realize that the original binding was a relic of how early it had been written in the life of Node. It used a lot of `libuv` functions directly for things that have had easier-to-use abstractions for a while now — both in `nan` and in N-API. It was also a good opportunity to decaffeinate the JavaScript files. Furthermore, we don't need the `HandleMap` stuff, nor the scarily-named `UnsafePersistent`, since we have proper `Map`s in JavaScript now. (I'm not sure we _ever_ needed `HandleMap`, but I digress.) This isn't over! It works on macOS, but it needs platform-specific adjustments for Windows and Linux. Both of those will be painful. I'm sure I can also improve upon the macOS file-watching somehow. --- package.json | 12 +- src/addon-data.h | 11 + src/common.cc | 243 +++++++++++----------- src/common.h | 100 +++++++-- src/directory.coffee | 334 ------------------------------ src/directory.js | 374 ++++++++++++++++++++++++++++++++++ src/file.coffee | 406 ------------------------------------- src/file.js | 437 ++++++++++++++++++++++++++++++++++++++++ src/handle_map.cc | 143 ------------- src/handle_map.h | 36 ---- src/main.cc | 25 ++- src/main.coffee | 137 ------------- src/main.js | 219 ++++++++++++++++++++ src/pathwatcher_unix.cc | 25 ++- src/unsafe_persistent.h | 52 ----- 15 files changed, 1273 insertions(+), 1281 deletions(-) create mode 100644 src/addon-data.h delete mode 100644 src/directory.coffee create mode 100644 src/directory.js delete mode 100644 src/file.coffee create mode 100644 src/file.js delete mode 100644 src/handle_map.cc delete mode 100644 src/handle_map.h delete mode 100644 src/main.coffee create mode 100644 src/main.js delete mode 100644 src/unsafe_persistent.h diff --git a/package.json b/package.json index 650864a..2961d22 100644 --- a/package.json +++ b/package.json @@ -23,15 +23,16 @@ }, "devDependencies": { "grunt": "~0.4.1", - "grunt-contrib-coffee": "~0.9.0", + "grunt-atomdoc": "^1.0", "grunt-cli": "~0.1.7", + "grunt-coffeelint": "git+https://github.com/atom/grunt-coffeelint.git", + "grunt-contrib-coffee": "~0.9.0", "grunt-shell": "~0.2.2", "jasmine-tagged": "^1.1", - "rimraf": "~2.2.0", + "node-addon-api": "^8.1.0", "node-cpplint": "~0.1.5", - "grunt-coffeelint": "git+https://github.com/atom/grunt-coffeelint.git", - "temp": "~0.9.0", - "grunt-atomdoc": "^1.0" + "rimraf": "~2.2.0", + "temp": "~0.9.0" }, "dependencies": { "async": "~0.2.10", @@ -40,7 +41,6 @@ "fs-plus": "^3.0.0", "grim": "^2.0.1", "iconv-lite": "~0.4.4", - "nan": "^2.10.0", "underscore-plus": "~1.x" } } diff --git a/src/addon-data.h b/src/addon-data.h new file mode 100644 index 0000000..2a57032 --- /dev/null +++ b/src/addon-data.h @@ -0,0 +1,11 @@ +#include "common.h" +#pragma once + +class AddonData final { +public: + explicit AddonData(Napi::Env env) {} + + Napi::FunctionReference callback; + PathWatcherWorker* worker; + int watch_count; +}; diff --git a/src/common.cc b/src/common.cc index b37c70c..f3af39b 100644 --- a/src/common.cc +++ b/src/common.cc @@ -1,169 +1,152 @@ #include "common.h" +#include "addon-data.h" -static uv_async_t g_async; -static int g_watch_count; -static uv_sem_t g_semaphore; -static uv_thread_t g_thread; +using namespace Napi; -static EVENT_TYPE g_type; -static WatcherHandle g_handle; -static std::vector g_new_path; -static std::vector g_old_path; -static Nan::Persistent g_callback; - -static void CommonThread(void* handle) { - WaitForMainThread(); - PlatformThread(); +void CommonInit(Napi::Env env) { + auto addonData = env.GetInstanceData(); + addonData->watch_count = 0; } -#if NODE_VERSION_AT_LEAST(0, 11, 13) -static void MakeCallbackInMainThread(uv_async_t* handle) { -#else -static void MakeCallbackInMainThread(uv_async_t* handle, int status) { -#endif - Nan::HandleScope scope; - - if (!g_callback.IsEmpty()) { - Local type; - switch (g_type) { - case EVENT_CHANGE: - type = Nan::New("change").ToLocalChecked(); - break; - case EVENT_DELETE: - type = Nan::New("delete").ToLocalChecked(); - break; - case EVENT_RENAME: - type = Nan::New("rename").ToLocalChecked(); - break; - case EVENT_CHILD_CREATE: - type = Nan::New("child-create").ToLocalChecked(); - break; - case EVENT_CHILD_CHANGE: - type = Nan::New("child-change").ToLocalChecked(); - break; - case EVENT_CHILD_DELETE: - type = Nan::New("child-delete").ToLocalChecked(); - break; - case EVENT_CHILD_RENAME: - type = Nan::New("child-rename").ToLocalChecked(); - break; - default: - type = Nan::New("unknown").ToLocalChecked(); - return; - } +PathWatcherWorker::PathWatcherWorker(Napi::Env env, Function &progressCallback) : + AsyncProgressQueueWorker(env) { + shouldStop = false; + this->progressCallback.Reset(progressCallback); +} - Local argv[] = { - type, - WatcherHandleToV8Value(g_handle), - Nan::New(g_new_path.data(), g_new_path.size()).ToLocalChecked(), - Nan::New(g_old_path.data(), g_old_path.size()).ToLocalChecked(), - }; - Local context = Nan::GetCurrentContext(); - Nan::New(g_callback)->Call(context, context->Global(), 4, argv).ToLocalChecked(); - } +void PathWatcherWorker::Execute( + const PathWatcherWorker::ExecutionProgress& progress +) { + PlatformThread(progress, shouldStop); +} - WakeupNewThread(); +void PathWatcherWorker::Stop() { + shouldStop = true; } -static void SetRef(bool value) { - uv_handle_t* h = reinterpret_cast(&g_async); - if (value) { - uv_ref(h); - } else { - uv_unref(h); +const char* PathWatcherWorker::GetEventTypeString(EVENT_TYPE type) { + switch (type) { + case EVENT_CHANGE: return "change"; + case EVENT_DELETE: return "delete"; + case EVENT_RENAME: return "rename"; + case EVENT_CHILD_CREATE: return "child-create"; + case EVENT_CHILD_CHANGE: return "child-change"; + case EVENT_CHILD_DELETE: return "child-delete"; + case EVENT_CHILD_RENAME: return "child-rename"; + default: return "unknown"; } } -void CommonInit() { - uv_sem_init(&g_semaphore, 0); - uv_async_init(uv_default_loop(), &g_async, MakeCallbackInMainThread); - // As long as any uv_ref'd uv_async_t handle remains active, the node - // process will never exit, so we must call uv_unref here (#47). - SetRef(false); - g_watch_count = 0; - uv_thread_create(&g_thread, &CommonThread, NULL); +void PathWatcherWorker::OnProgress(const PathWatcherEvent* data, size_t) { + HandleScope scope(Env()); + if (this->progressCallback.IsEmpty()) return; + std::string newPath(data->new_path.begin(), data->new_path.end()); + std::string oldPath(data->old_path.begin(), data->old_path.end()); + + this->progressCallback.Call({ + Napi::String::New(Env(), GetEventTypeString(data->type)), + WatcherHandleToV8Value(data->handle, Env()), + Napi::String::New(Env(), newPath), + Napi::String::New(Env(), oldPath) + }); } -void WaitForMainThread() { - uv_sem_wait(&g_semaphore); -} +void PathWatcherWorker::OnOK() {} + +// Called when the first watcher is created. +void Start(Napi::Env env) { + Napi::HandleScope scope(env); + auto addonData = env.GetInstanceData(); + if (!addonData->callback) { + return; + } + + Napi::Function fn = addonData->callback.Value(); -void WakeupNewThread() { - uv_sem_post(&g_semaphore); + addonData->worker = new PathWatcherWorker(env, fn); + addonData->worker->Queue(); } -void PostEventAndWait(EVENT_TYPE type, - WatcherHandle handle, - const std::vector& new_path, - const std::vector& old_path) { - // FIXME should not pass args by settings globals. - g_type = type; - g_handle = handle; - g_new_path = new_path; - g_old_path = old_path; - - uv_async_send(&g_async); - WaitForMainThread(); +// Called when the last watcher is stopped. +void Stop(Napi::Env env) { + auto addonData = env.GetInstanceData(); + if (addonData->worker) { + addonData->worker->Stop(); + } } -NAN_METHOD(SetCallback) { - Nan::HandleScope scope; +Napi::Value SetCallback(const Napi::CallbackInfo& info) { + auto env = info.Env(); + Napi::HandleScope scope(env); + + if (!info[0].IsFunction()) { + Napi::TypeError::New(env, "Function required").ThrowAsJavaScriptException(); + return env.Null(); + } - if (!info[0]->IsFunction()) - return Nan::ThrowTypeError("Function required"); + auto addonData = env.GetInstanceData(); + if (addonData->worker) { + addonData->worker->Stop(); + } + addonData->callback.Reset(info[0].As(), 1); - g_callback.Reset(Local::Cast(info[0])); - return; + return env.Undefined(); } -NAN_METHOD(Watch) { - Nan::HandleScope scope; +Napi::Value Watch(const Napi::CallbackInfo& info) { + auto env = info.Env(); + auto addonData = env.GetInstanceData(); + Napi::HandleScope scope(env); - if (!info[0]->IsString()) - return Nan::ThrowTypeError("String required"); + if (!info[0].IsString()) { + Napi::TypeError::New(env, "String required").ThrowAsJavaScriptException(); + return env.Null(); + } + + Napi::String path = info[0].ToString(); + std::string cppPath(path); + WatcherHandle handle = PlatformWatch(cppPath.c_str()); - Local context = Nan::GetCurrentContext(); - Local path = info[0]->ToString(context).ToLocalChecked(); - WatcherHandle handle = PlatformWatch(*String::Utf8Value(v8::Isolate::GetCurrent(), path)); if (!PlatformIsHandleValid(handle)) { int error_number = PlatformInvalidHandleToErrorNumber(handle); - v8::Local err = - v8::Exception::Error(Nan::New("Unable to watch path").ToLocalChecked()); - v8::Local err_obj = err.As(); + Napi::Error err = Napi::Error::New(env, "Unable to watch path"); + if (error_number != 0) { - err_obj->Set(context, - Nan::New("errno").ToLocalChecked(), - Nan::New(error_number)).FromJust(); -#if NODE_VERSION_AT_LEAST(0, 11, 5) - // Node 0.11.5 is the first version to contain libuv v0.11.6, which - // contains https://github.com/libuv/libuv/commit/3ee4d3f183 which changes - // uv_err_name from taking a struct uv_err_t (whose uv_err_code `code` is - // a difficult-to-produce uv-specific errno) to just take an int which is - // a negative errno. - err_obj->Set(context, - Nan::New("code").ToLocalChecked(), - Nan::New(uv_err_name(-error_number)).ToLocalChecked()).FromJust(); -#endif + err.Set("errno", Napi::Number::New(env, error_number)); + err.Set( + "code", + Napi::String::New(env, uv_err_name(-error_number)) + ); } - return Nan::ThrowError(err); + err.ThrowAsJavaScriptException(); + return env.Undefined(); } - if (g_watch_count++ == 0) - SetRef(true); + if (addonData->watch_count++ == 0) + Start(env); - info.GetReturnValue().Set(WatcherHandleToV8Value(handle)); + return WatcherHandleToV8Value(handle, info.Env()); } -NAN_METHOD(Unwatch) { - Nan::HandleScope scope; +Napi::Value Unwatch(const Napi::CallbackInfo& info) { + auto env = info.Env(); + auto addonData = env.GetInstanceData(); + Napi::HandleScope scope(env); + + if (!IsV8ValueWatcherHandle(info[0])) { + Napi::TypeError::New( + env, + "Local type required" + ).ThrowAsJavaScriptException(); + return env.Null(); + } - if (!IsV8ValueWatcherHandle(info[0])) - return Nan::ThrowTypeError("Local type required"); + Napi::Number num = info[0].ToNumber(); - PlatformUnwatch(V8ValueToWatcherHandle(info[0])); + PlatformUnwatch(V8ValueToWatcherHandle(num)); - if (--g_watch_count == 0) - SetRef(false); + if (--addonData->watch_count == 0) + Stop(env); - return; + return env.Undefined(); } diff --git a/src/common.h b/src/common.h index 8f0d85f..2b55a9b 100644 --- a/src/common.h +++ b/src/common.h @@ -3,27 +3,26 @@ #include -#include "nan.h" -using namespace v8; +#include "napi.h" +using namespace Napi; #ifdef _WIN32 // Platform-dependent definetion of handle. typedef HANDLE WatcherHandle; // Conversion between V8 value and WatcherHandle. -Local WatcherHandleToV8Value(WatcherHandle handle); -WatcherHandle V8ValueToWatcherHandle(Local value); -bool IsV8ValueWatcherHandle(Local value); +Napi::Value WatcherHandleToV8Value(WatcherHandle handle); +WatcherHandle V8ValueToWatcherHandle(Napi::Value value); +bool IsV8ValueWatcherHandle(Napi::Value value); #else // Correspoding definetions on OS X and Linux. typedef int32_t WatcherHandle; -#define WatcherHandleToV8Value(h) Nan::New(h) -#define V8ValueToWatcherHandle(v) v->Int32Value(Nan::GetCurrentContext()).FromJust() -#define IsV8ValueWatcherHandle(v) v->IsInt32() +#define WatcherHandleToV8Value(h, e) Napi::Number::New(e, h) +#define V8ValueToWatcherHandle(v) v.Int32Value() +#define IsV8ValueWatcherHandle(v) v.IsNumber() #endif void PlatformInit(); -void PlatformThread(); WatcherHandle PlatformWatch(const char* path); void PlatformUnwatch(WatcherHandle handle); bool PlatformIsHandleValid(WatcherHandle handle); @@ -40,17 +39,80 @@ enum EVENT_TYPE { EVENT_CHILD_CREATE, }; -void WaitForMainThread(); -void WakeupNewThread(); -void PostEventAndWait(EVENT_TYPE type, - WatcherHandle handle, - const std::vector& new_path, - const std::vector& old_path = std::vector()); +struct PathWatcherEvent { + EVENT_TYPE type; + WatcherHandle handle; + std::vector new_path; + std::vector old_path; -void CommonInit(); + // Default constructor + PathWatcherEvent() = default; -NAN_METHOD(SetCallback); -NAN_METHOD(Watch); -NAN_METHOD(Unwatch); + // Constructor + PathWatcherEvent(EVENT_TYPE t, WatcherHandle h, const std::vector& np, const std::vector& op = std::vector()) + : type(t), handle(h), new_path(np), old_path(op) {} + + // Copy constructor + PathWatcherEvent(const PathWatcherEvent& other) + : type(other.type), handle(other.handle), new_path(other.new_path), old_path(other.old_path) {} + + // Copy assignment operator + PathWatcherEvent& operator=(const PathWatcherEvent& other) { + if (this != &other) { + type = other.type; + handle = other.handle; + new_path = other.new_path; + old_path = other.old_path; + } + return *this; + } + + // Move constructor + PathWatcherEvent(PathWatcherEvent&& other) noexcept + : type(other.type), handle(other.handle), + new_path(std::move(other.new_path)), old_path(std::move(other.old_path)) {} + + // Move assignment operator + PathWatcherEvent& operator=(PathWatcherEvent&& other) noexcept { + if (this != &other) { + type = other.type; + handle = other.handle; + new_path = std::move(other.new_path); + old_path = std::move(other.old_path); + } + return *this; + } +}; + +using namespace Napi; + +inline Function EMPTY_OK = *(new Napi::Function()); + +class PathWatcherWorker: public AsyncProgressQueueWorker { + public: + PathWatcherWorker(Napi::Env env, Function &progressCallback); + + ~PathWatcherWorker() {} + + void Execute(const PathWatcherWorker::ExecutionProgress& progress) override; + void OnOK() override; + + void OnProgress(const PathWatcherEvent* data, size_t) override; + void Stop(); + + private: + bool shouldStop = false; + FunctionReference progressCallback; + + const char* GetEventTypeString(EVENT_TYPE type); +}; + +void PlatformThread(const PathWatcherWorker::ExecutionProgress& progress, bool& shouldStop); + +void CommonInit(Napi::Env env); + +Napi::Value SetCallback(const Napi::CallbackInfo& info); +Napi::Value Watch(const Napi::CallbackInfo& info); +Napi::Value Unwatch(const Napi::CallbackInfo& info); #endif // SRC_COMMON_H_ diff --git a/src/directory.coffee b/src/directory.coffee deleted file mode 100644 index 513835e..0000000 --- a/src/directory.coffee +++ /dev/null @@ -1,334 +0,0 @@ -path = require 'path' - -async = require 'async' -{Emitter, Disposable} = require 'event-kit' -fs = require 'fs-plus' -Grim = require 'grim' - -File = require './file' -PathWatcher = require './main' - -# Extended: Represents a directory on disk that can be watched for changes. -module.exports = -class Directory - realPath: null - subscriptionCount: 0 - - ### - Section: Construction - ### - - # Public: Configures a new Directory instance, no files are accessed. - # - # * `directoryPath` A {String} containing the absolute path to the directory - # * `symlink` (optional) A {Boolean} indicating if the path is a symlink. - # (default: false) - constructor: (directoryPath, @symlink=false, includeDeprecatedAPIs=Grim.includeDeprecatedAPIs) -> - @emitter = new Emitter - - if includeDeprecatedAPIs - @on 'contents-changed-subscription-will-be-added', @willAddSubscription - @on 'contents-changed-subscription-removed', @didRemoveSubscription - - if directoryPath - directoryPath = path.normalize(directoryPath) - # Remove a trailing slash - if directoryPath.length > 1 and directoryPath[directoryPath.length - 1] is path.sep - directoryPath = directoryPath.substring(0, directoryPath.length - 1) - @path = directoryPath - - @lowerCasePath = @path.toLowerCase() if fs.isCaseInsensitive() - @reportOnDeprecations = true if Grim.includeDeprecatedAPIs - - # Public: Creates the directory on disk that corresponds to `::getPath()` if - # no such directory already exists. - # - # * `mode` (optional) {Number} that defaults to `0777`. - # - # Returns a {Promise} that resolves once the directory is created on disk. It - # resolves to a boolean value that is true if the directory was created or - # false if it already existed. - create: (mode = 0o0777) -> - @exists().then (isExistingDirectory) => - return false if isExistingDirectory - - throw Error("Root directory does not exist: #{@getPath()}") if @isRoot() - - @getParent().create().then => - new Promise (resolve, reject) => - fs.mkdir @getPath(), mode, (error) -> - if error - reject error - else - resolve true - ### - Section: Event Subscription - ### - - # Public: Invoke the given callback when the directory's contents change. - # - # * `callback` {Function} to be called when the directory's contents change. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChange: (callback) -> - @willAddSubscription() - @trackUnsubscription(@emitter.on('did-change', callback)) - - willAddSubscription: => - @subscribeToNativeChangeEvents() if @subscriptionCount is 0 - @subscriptionCount++ - - didRemoveSubscription: => - @subscriptionCount-- - @unsubscribeFromNativeChangeEvents() if @subscriptionCount is 0 - - trackUnsubscription: (subscription) -> - new Disposable => - subscription.dispose() - @didRemoveSubscription() - - ### - Section: Directory Metadata - ### - - # Public: Returns a {Boolean}, always false. - isFile: -> false - - # Public: Returns a {Boolean}, always true. - isDirectory: -> true - - # Public: Returns a {Boolean} indicating whether or not this is a symbolic link - isSymbolicLink: -> - @symlink - - # Public: Returns a promise that resolves to a {Boolean}, true if the - # directory exists, false otherwise. - exists: -> - new Promise (resolve) => fs.exists(@getPath(), resolve) - - # Public: Returns a {Boolean}, true if the directory exists, false otherwise. - existsSync: -> - fs.existsSync(@getPath()) - - # Public: Return a {Boolean}, true if this {Directory} is the root directory - # of the filesystem, or false if it isn't. - isRoot: -> - @getParent().getRealPathSync() is @getRealPathSync() - - ### - Section: Managing Paths - ### - - # Public: Returns the directory's {String} path. - # - # This may include unfollowed symlinks or relative directory entries. Or it - # may be fully resolved, it depends on what you give it. - getPath: -> @path - - # Public: Returns this directory's completely resolved {String} path. - # - # All relative directory entries are removed and symlinks are resolved to - # their final destination. - getRealPathSync: -> - unless @realPath? - try - @realPath = fs.realpathSync(@path) - @lowerCaseRealPath = @realPath.toLowerCase() if fs.isCaseInsensitive() - catch e - @realPath = @path - @lowerCaseRealPath = @lowerCasePath if fs.isCaseInsensitive() - @realPath - - # Public: Returns the {String} basename of the directory. - getBaseName: -> - path.basename(@path) - - # Public: Returns the relative {String} path to the given path from this - # directory. - relativize: (fullPath) -> - return fullPath unless fullPath - - # Normalize forward slashes to back slashes on windows - fullPath = fullPath.replace(/\//g, '\\') if process.platform is 'win32' - - if fs.isCaseInsensitive() - pathToCheck = fullPath.toLowerCase() - directoryPath = @lowerCasePath - else - pathToCheck = fullPath - directoryPath = @path - - if pathToCheck is directoryPath - return '' - else if @isPathPrefixOf(directoryPath, pathToCheck) - return fullPath.substring(directoryPath.length + 1) - - # Check real path - @getRealPathSync() - if fs.isCaseInsensitive() - directoryPath = @lowerCaseRealPath - else - directoryPath = @realPath - - if pathToCheck is directoryPath - '' - else if @isPathPrefixOf(directoryPath, pathToCheck) - fullPath.substring(directoryPath.length + 1) - else - fullPath - - # Given a relative path, this resolves it to an absolute path relative to this - # directory. If the path is already absolute or prefixed with a URI scheme, it - # is returned unchanged. - # - # * `uri` A {String} containing the path to resolve. - # - # Returns a {String} containing an absolute path or `undefined` if the given - # URI is falsy. - resolve: (relativePath) -> - return unless relativePath - - if relativePath?.match(/[A-Za-z0-9+-.]+:\/\//) # leave path alone if it has a scheme - relativePath - else if fs.isAbsolute(relativePath) - path.normalize(fs.resolveHome(relativePath)) - else - path.normalize(fs.resolveHome(path.join(@getPath(), relativePath))) - - ### - Section: Traversing - ### - - # Public: Traverse to the parent directory. - # - # Returns a {Directory}. - getParent: -> - new Directory(path.join @path, '..') - - # Public: Traverse within this Directory to a child File. This method doesn't - # actually check to see if the File exists, it just creates the File object. - # - # * `filename` The {String} name of a File within this Directory. - # - # Returns a {File}. - getFile: (filename...) -> - new File(path.join @getPath(), filename...) - - # Public: Traverse within this a Directory to a child Directory. This method - # doesn't actually check to see if the Directory exists, it just creates the - # Directory object. - # - # * `dirname` The {String} name of the child Directory. - # - # Returns a {Directory}. - getSubdirectory: (dirname...) -> - new Directory(path.join @path, dirname...) - - # Public: Reads file entries in this directory from disk synchronously. - # - # Returns an {Array} of {File} and {Directory} objects. - getEntriesSync: -> - directories = [] - files = [] - for entryPath in fs.listSync(@path) - try - stat = fs.lstatSync(entryPath) - symlink = stat.isSymbolicLink() - stat = fs.statSync(entryPath) if symlink - - if stat?.isDirectory() - directories.push(new Directory(entryPath, symlink)) - else if stat?.isFile() - files.push(new File(entryPath, symlink)) - - directories.concat(files) - - # Public: Reads file entries in this directory from disk asynchronously. - # - # * `callback` A {Function} to call with the following arguments: - # * `error` An {Error}, may be null. - # * `entries` An {Array} of {File} and {Directory} objects. - getEntries: (callback) -> - fs.list @path, (error, entries) -> - return callback(error) if error? - - directories = [] - files = [] - addEntry = (entryPath, stat, symlink, callback) -> - if stat?.isDirectory() - directories.push(new Directory(entryPath, symlink)) - else if stat?.isFile() - files.push(new File(entryPath, symlink)) - callback() - - statEntry = (entryPath, callback) -> - fs.lstat entryPath, (error, stat) -> - if stat?.isSymbolicLink() - fs.stat entryPath, (error, stat) -> - addEntry(entryPath, stat, true, callback) - else - addEntry(entryPath, stat, false, callback) - - async.eachLimit entries, 1, statEntry, -> - callback(null, directories.concat(files)) - - # Public: Determines if the given path (real or symbolic) is inside this - # directory. This method does not actually check if the path exists, it just - # checks if the path is under this directory. - # - # * `pathToCheck` The {String} path to check. - # - # Returns a {Boolean} whether the given path is inside this directory. - contains: (pathToCheck) -> - return false unless pathToCheck - - # Normalize forward slashes to back slashes on windows - pathToCheck = pathToCheck.replace(/\//g, '\\') if process.platform is 'win32' - - if fs.isCaseInsensitive() - directoryPath = @lowerCasePath - pathToCheck = pathToCheck.toLowerCase() - else - directoryPath = @path - - return true if @isPathPrefixOf(directoryPath, pathToCheck) - - # Check real path - @getRealPathSync() - if fs.isCaseInsensitive() - directoryPath = @lowerCaseRealPath - else - directoryPath = @realPath - - @isPathPrefixOf(directoryPath, pathToCheck) - - ### - Section: Private - ### - - subscribeToNativeChangeEvents: -> - @watchSubscription ?= PathWatcher.watch @path, (eventType) => - if eventType is 'change' - @emit 'contents-changed' if Grim.includeDeprecatedAPIs - @emitter.emit 'did-change' - - unsubscribeFromNativeChangeEvents: -> - if @watchSubscription? - @watchSubscription.close() - @watchSubscription = null - - # Does given full path start with the given prefix? - isPathPrefixOf: (prefix, fullPath) -> - fullPath.indexOf(prefix) is 0 and fullPath[prefix.length] is path.sep - -if Grim.includeDeprecatedAPIs - EmitterMixin = require('emissary').Emitter - EmitterMixin.includeInto(Directory) - - Directory::on = (eventName) -> - if eventName is 'contents-changed' - Grim.deprecate("Use Directory::onDidChange instead") - else if @reportOnDeprecations - Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.") - - EmitterMixin::on.apply(this, arguments) diff --git a/src/directory.js b/src/directory.js new file mode 100644 index 0000000..7acdc0c --- /dev/null +++ b/src/directory.js @@ -0,0 +1,374 @@ +const Path = require('path'); +const FS = require('fs-plus'); +const Grim = require('grim'); +const async = require('async'); +const { Emitter, Disposable } = require('event-kit'); + +const File = require('./file'); +const PathWatcher = require('./main'); + +class Directory { + realPath = null; + subscriptionCount = 0; + + constructor(directoryPath, symlink = false, includeDeprecatedAPIs = Grim.includeDeprecatedAPIs) { + this.emitter = new Emitter(); + this.symlink = symlink; + + if (includeDeprecatedAPIs) { + this.on('contents-changed-subscription-will-be-added', this.willAddSubscription.bind(this)); + this.on('contents-changed-subscription-removed', this.didRemoveSubscription.bind(this)); + } + + if (directoryPath) { + directoryPath = Path.normalize(directoryPath); + if (directoryPath.length > 1 && directoryPath.endsWith(Path.sep)) { + directoryPath = directoryPath.substring(0, directoryPath.length - 1); + } + } + this.path = directoryPath; + if (FS.isCaseInsensitive()) { + this.lowerCasePath = this.path.toLowerCase(); + } + if (Grim.includeDeprecatedAPIs) { + this.reportOnDeprecations = true; + } + } + + async create (mode = 0o0777) { + let isExistingDirectory = await this.exists(); + if (isExistingDirectory) return false; + if (this.isRoot()) { + throw new Error(`Root directory does not exist: ${this.getPath()}`); + } + await this.getParent().create(); + return new Promise((resolve, reject) => { + FS.mkdir(this.getPath(), mode, (error) => { + if (error) { + reject(error); + } else { + resolve(true); + } + }); + }); + } + + onDidChange (callback) { + this.willAddSubscription(); + return this.trackUnsubscription( + this.emitter.on('did-change', callback) + ); + } + + willAddSubscription () { + if (this.subscriptionCount === 0) { + this.subscribeToNativeChangeEvents(); + } + this.subscriptionCount++; + } + + didRemoveSubscription () { + this.subscriptionCount--; + if (this.subscriptionCount === 0) { + this.unsubscribeFromNativeChangeEvents(); + } + } + + trackUnsubscription (subscription) { + return new Disposable(() => { + subscription.dispose(); + this.didRemoveSubscription(); + }); + } + + isFile () { + return false; + } + + isDirectory() { + return true; + } + + isSymbolicLink () { + return this.symlink; + } + + exists () { + return new Promise((resolve) => { + FS.exists(this.getPath(), resolve) + }); + } + + existsSync () { + return FS.existsSync(this.getPath()); + } + + isRoot () { + let realPath = this.getRealPathSync(); + return realPath === this.getParent().getRealPathSync(); + } + + getPath () { + return this.path; + } + + getRealPathSync () { + if (!this.realPath) { + try { + this.realPath = FS.realpathSync(this.path); + if (FS.isCaseInsensitive()) { + this.lowerCaseRealPath = this.realPath.toLowerCase(); + } + } catch (err) { + this.realPath = this.path; + if (FS.isCaseInsensitive()) { + this.lowerCaseRealPath = this.lowerCasePath; + } + } + } + return this.realPath; + } + + getBaseName () { + return Path.basename(this.path); + } + + relativize (fullPath) { + if (!fullPath) return fullPath; + + if (process.platform === 'win32') { + fullPath = fullPath.replace(/\//g, '\\'); + } + + let pathToCheck; + let directoryPath; + if (FS.isCaseInsensitive()) { + pathToCheck = fullPath.toLowerCase(); + directoryPath = this.lowerCasePath; + } else { + pathToCheck = fullPath; + directoryPath = this.path; + } + + if (pathToCheck === directoryPath) { + return ''; + } else if (this.isPathPrefixOf(directoryPath, pathToCheck)) { + return fullPath.substring(directoryPath.length + 1); + } + + // Check the real path. + this.getRealPathSync(); + if (FS.isCaseInsensitive()) { + directoryPath = this.lowerCaseRealPath; + } else { + directoryPath = this.realPath; + } + + if (pathToCheck === directoryPath) { + return ''; + } else if (this.isPathPrefixOf(directoryPath, pathToCheck)) { + return fullPath.substring(directoryPath.length + 1); + } else { + return fullPath; + } + } + + resolve (relativePath) { + if (!relativePath) return; + + if (relativePath?.match(/[A-Za-z0-9+-.]+:\/\//)) { + // Leave the path alone if it has a scheme. + return relativePath; + } else if (FS.isAbsolute(relativePath)) { + return Path.normalize(FS.resolveHome(relativePath)); + } else { + return Path.normalize( + FS.resolveHome(Path.join(this.getPath(), relativePath)) + ); + } + } + + // TRAVERSING + + getParent () { + return new Directory(Path.join(this.path, '..')); + } + + // Public: Traverse within this {Directory} to a child {File}. This method + // doesn't actually check to see if the {File} exists; it just creates the + // {File} object. + // + // You can also access descendant files by passing multiple arguments. In + // this usage, the final segment should be the name of a file; the others + // should be directories. + // + // * `filename` The {String} name of a File within this Directory. + // + // Returns a {File}. + getFile (...fileName) { + return new File(Path.join(this.getPath(), ...fileName)); + } + + // Public: Traverse within this a {Directory} to a child {Directory}. This + // method doesn't actually check to see if the {Directory} exists; it just + // creates the Directory object. + // + // You can also access descendant directories by passing multiple arguments. + // In this usage, all segments should be directory names. + // + // * `dirname` The {String} name of the child {Directory}. + // + // Returns a {Directory}. + getSubdirectory (...dirName) { + return new Directory(Path.join(this.getPath(), ...dirName)); + } + + // Public: Reads file entries in this directory from disk asynchronously. + // + // * `callback` A {Function} to call with the following arguments: + // * `error` An {Error}, may be null. + // * `entries` An {Array} of {File} and {Directory} objects. + getEntries (callback) { + FS.list(this.path, (error, entries) => { + if (error) return callback(error); + + let directories = []; + let files = []; + + let addEntry = (entryPath, stat, symlink, innerCallback) => { + if (stat?.isDirectory()) { + directories.push(new Directory(entryPath, symlink)); + } else if (stat?.isFile()) { + files.push(new File(entryPath, symlink)) + } + return innerCallback(); + }; + + let statEntry = (entryPath, innerCallback) => { + FS.lstat(entryPath, (_error, stat) => { + if (stat?.isSymbolicLink()) { + FS.stat(entryPath, (_error, stat) => { + addEntry(entryPath, stat, true, innerCallback) + }); + } else { + addEntry(entryPath, stat, false, innerCallback); + } + }); + }; + + return async.eachLimit( + entries, + 1, + statEntry, + function() { + return callback(null, directories.concat(files)); + } + ); + }); + } + + // Public: Reads file entries in this directory from disk synchronously. + // + // Returns an {Array} of {File} and {Directory} objects. + getEntriesSync () { + let directories = []; + let files = []; + for (let entryPath of FS.listSync(this.path)) { + let stat; + let symlink = false; + try { + stat = FS.lstatSync(entryPath); + symlink = stat.isSymbolicLink(); + if (symlink) { + stat = FS.statSync(entryPath); + } + } catch (_err) {} + if (stat?.isDirectory()) { + directories.push(new Directory(entryPath, symlink)); + } else if (stat?.isFile()) { + files.push(new File(entryPath, symlink)); + } + } + return directories.concat(files); + } + + + // Public: Determines if the given path (real or symbolic) is inside this + // directory. This method does not actually check if the path exists; it just + // checks if the path is under this directory. + // + // * `pathToCheck` The {String} path to check. + // + // Returns a {Boolean} whether the given path is inside this directory. + contains (pathToCheck) { + if (!pathToCheck) return false; + + // Normalize forward slashes to backslashes on Windows. + if (process.platform === 'win32') { + pathToCheck = pathToCheck.replace(/\//g, '\\'); + } + + let directoryPath; + if (FS.isCaseInsensitive()) { + directoryPath = this.lowerCasePath; + pathToCheck = pathToCheck.toLowerCase(); + } else { + directoryPath = this.path; + } + + if (this.isPathPrefixOf(directoryPath, pathToCheck)) { + return true; + } + + // Check the real path. + this.getRealPathSync(); + if (FS.isCaseInsensitive()) { + directoryPath = this.lowerCaseRealPath; + } else { + directoryPath = this.realPath; + } + + return this.isPathPrefixOf(directoryPath, pathToCheck); + } + + // PRIVATE + + subscribeToNativeChangeEvents () { + this.watchSubscription ??= PathWatcher.watch( + this.path, + (_eventType) => { + if (Grim.includeDeprecatedAPIs) { + this.emit('contents-changed'); + } + this.emitter.emit('did-change'); + } + ); + } + + unsubscribeFromNativeChangeEvents () { + this.watchSubscription?.close(); + this.watchSubscription &&= null; + } + + // Does the given full path start with the given prefix? + isPathPrefixOf (prefix, fullPath) { + return fullPath.startsWith(prefix) && fullPath[prefix.length] === Path.sep; + } +} + +let EmitterMixin; +if (Grim.includeDeprecatedAPIs) { + EmitterMixin = require('emissary').Emitter; + EmitterMixin.includeInto(Directory); + + Directory.prototype.on = function on(eventName, ...args) { + if (eventName === 'contents-changed') { + Grim.deprecate("Use Directory::onDidChange instead"); + } else if (this.reportOnDeprecations) { + Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead."); + } + EmitterMixin.prototype.on.call(this, eventName, ...args); + }; +} + +module.exports = Directory; diff --git a/src/file.coffee b/src/file.coffee deleted file mode 100644 index b9bf58f..0000000 --- a/src/file.coffee +++ /dev/null @@ -1,406 +0,0 @@ -crypto = require 'crypto' -path = require 'path' - -_ = require 'underscore-plus' -{Emitter, Disposable} = require 'event-kit' -fs = require 'fs-plus' -Grim = require 'grim' - -iconv = null # Defer until used - -Directory = null -PathWatcher = require './main' - -# Extended: Represents an individual file that can be watched, read from, and -# written to. -module.exports = -class File - encoding: 'utf8' - realPath: null - subscriptionCount: 0 - - ### - Section: Construction - ### - - # Public: Configures a new File instance, no files are accessed. - # - # * `filePath` A {String} containing the absolute path to the file - # * `symlink` (optional) A {Boolean} indicating if the path is a symlink (default: false). - constructor: (filePath, @symlink=false, includeDeprecatedAPIs=Grim.includeDeprecatedAPIs) -> - filePath = path.normalize(filePath) if filePath - @path = filePath - @emitter = new Emitter - - if includeDeprecatedAPIs - @on 'contents-changed-subscription-will-be-added', @willAddSubscription - @on 'moved-subscription-will-be-added', @willAddSubscription - @on 'removed-subscription-will-be-added', @willAddSubscription - @on 'contents-changed-subscription-removed', @didRemoveSubscription - @on 'moved-subscription-removed', @didRemoveSubscription - @on 'removed-subscription-removed', @didRemoveSubscription - - @cachedContents = null - @reportOnDeprecations = true - - # Public: Creates the file on disk that corresponds to `::getPath()` if no - # such file already exists. - # - # Returns a {Promise} that resolves once the file is created on disk. It - # resolves to a boolean value that is true if the file was created or false if - # it already existed. - create: -> - @exists().then (isExistingFile) => - unless isExistingFile - parent = @getParent() - parent.create().then => - @write('').then -> true - else - false - - ### - Section: Event Subscription - ### - - # Public: Invoke the given callback when the file's contents change. - # - # * `callback` {Function} to be called when the file's contents change. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChange: (callback) -> - @willAddSubscription() - @trackUnsubscription(@emitter.on('did-change', callback)) - - # Public: Invoke the given callback when the file's path changes. - # - # * `callback` {Function} to be called when the file's path changes. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidRename: (callback) -> - @willAddSubscription() - @trackUnsubscription(@emitter.on('did-rename', callback)) - - # Public: Invoke the given callback when the file is deleted. - # - # * `callback` {Function} to be called when the file is deleted. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDelete: (callback) -> - @willAddSubscription() - @trackUnsubscription(@emitter.on('did-delete', callback)) - - # Public: Invoke the given callback when there is an error with the watch. - # When your callback has been invoked, the file will have unsubscribed from - # the file watches. - # - # * `callback` {Function} callback - # * `errorObject` {Object} - # * `error` {Object} the error object - # * `handle` {Function} call this to indicate you have handled the error. - # The error will not be thrown if this function is called. - onWillThrowWatchError: (callback) -> - @emitter.on('will-throw-watch-error', callback) - - willAddSubscription: => - @subscriptionCount++ - try - @subscribeToNativeChangeEvents() - - didRemoveSubscription: => - @subscriptionCount-- - @unsubscribeFromNativeChangeEvents() if @subscriptionCount is 0 - - trackUnsubscription: (subscription) -> - new Disposable => - subscription.dispose() - @didRemoveSubscription() - - ### - Section: File Metadata - ### - - # Public: Returns a {Boolean}, always true. - isFile: -> true - - # Public: Returns a {Boolean}, always false. - isDirectory: -> false - - # Public: Returns a {Boolean} indicating whether or not this is a symbolic link - isSymbolicLink: -> - @symlink - - # Public: Returns a promise that resolves to a {Boolean}, true if the file - # exists, false otherwise. - exists: -> - new Promise (resolve) => - fs.exists @getPath(), resolve - - # Public: Returns a {Boolean}, true if the file exists, false otherwise. - existsSync: -> - fs.existsSync(@getPath()) - - # Public: Get the SHA-1 digest of this file - # - # Returns a promise that resolves to a {String}. - getDigest: -> - if @digest? - Promise.resolve(@digest) - else - @read().then => @digest # read assigns digest as a side-effect - - # Public: Get the SHA-1 digest of this file - # - # Returns a {String}. - getDigestSync: -> - @readSync() unless @digest - @digest - - setDigest: (contents) -> - @digest = crypto.createHash('sha1').update(contents ? '').digest('hex') - - # Public: Sets the file's character set encoding name. - # - # * `encoding` The {String} encoding to use (default: 'utf8') - setEncoding: (encoding='utf8') -> - # Throws if encoding doesn't exist. Better to throw an exception early - # instead of waiting until the file is saved. - - if encoding isnt 'utf8' - iconv ?= require 'iconv-lite' - iconv.getCodec(encoding) - - @encoding = encoding - - # Public: Returns the {String} encoding name for this file (default: 'utf8'). - getEncoding: -> @encoding - - ### - Section: Managing Paths - ### - - # Public: Returns the {String} path for the file. - getPath: -> @path - - # Sets the path for the file. - setPath: (@path) -> - @realPath = null - - # Public: Returns this file's completely resolved {String} path. - getRealPathSync: -> - unless @realPath? - try - @realPath = fs.realpathSync(@path) - catch error - @realPath = @path - @realPath - - # Public: Returns a promise that resolves to the file's completely resolved {String} path. - getRealPath: -> - if @realPath? - Promise.resolve(@realPath) - else - new Promise (resolve, reject) => - fs.realpath @path, (err, result) => - if err? - reject(err) - else - resolve(@realPath = result) - - # Public: Return the {String} filename without any directory information. - getBaseName: -> - path.basename(@path) - - ### - Section: Traversing - ### - - # Public: Return the {Directory} that contains this file. - getParent: -> - Directory ?= require './directory' - new Directory(path.dirname @path) - - ### - Section: Reading and Writing - ### - - readSync: (flushCache) -> - if not @existsSync() - @cachedContents = null - else if not @cachedContents? or flushCache - encoding = @getEncoding() - if encoding is 'utf8' - @cachedContents = fs.readFileSync(@getPath(), encoding) - else - iconv ?= require 'iconv-lite' - @cachedContents = iconv.decode(fs.readFileSync(@getPath()), encoding) - - @setDigest(@cachedContents) - @cachedContents - - writeFileSync: (filePath, contents) -> - encoding = @getEncoding() - if encoding is 'utf8' - fs.writeFileSync(filePath, contents, {encoding}) - else - iconv ?= require 'iconv-lite' - fs.writeFileSync(filePath, iconv.encode(contents, encoding)) - - # Public: Reads the contents of the file. - # - # * `flushCache` A {Boolean} indicating whether to require a direct read or if - # a cached copy is acceptable. - # - # Returns a promise that resolves to either a {String}, or null if the file does not exist. - read: (flushCache) -> - if @cachedContents? and not flushCache - promise = Promise.resolve(@cachedContents) - else - promise = new Promise (resolve, reject) => - content = [] - readStream = @createReadStream() - - readStream.on 'data', (chunk) -> - content.push(chunk) - - readStream.on 'end', -> - resolve(content.join('')) - - readStream.on 'error', (error) -> - if error.code == 'ENOENT' - resolve(null) - else - reject(error) - - promise.then (contents) => - @setDigest(contents) - @cachedContents = contents - - # Public: Returns a stream to read the content of the file. - # - # Returns a {ReadStream} object. - createReadStream: -> - encoding = @getEncoding() - if encoding is 'utf8' - fs.createReadStream(@getPath(), {encoding}) - else - iconv ?= require 'iconv-lite' - fs.createReadStream(@getPath()).pipe(iconv.decodeStream(encoding)) - - # Public: Overwrites the file with the given text. - # - # * `text` The {String} text to write to the underlying file. - # - # Returns a {Promise} that resolves when the file has been written. - write: (text) -> - @exists().then (previouslyExisted) => - @writeFile(@getPath(), text).then => - @cachedContents = text - @setDigest(text) - @subscribeToNativeChangeEvents() if not previouslyExisted and @hasSubscriptions() - undefined - - # Public: Returns a stream to write content to the file. - # - # Returns a {WriteStream} object. - createWriteStream: -> - encoding = @getEncoding() - if encoding is 'utf8' - fs.createWriteStream(@getPath(), {encoding}) - else - iconv ?= require 'iconv-lite' - stream = iconv.encodeStream(encoding) - stream.pipe(fs.createWriteStream(@getPath())) - stream - - # Public: Overwrites the file with the given text. - # - # * `text` The {String} text to write to the underlying file. - # - # Returns undefined. - writeSync: (text) -> - previouslyExisted = @existsSync() - @writeFileSync(@getPath(), text) - @cachedContents = text - @setDigest(text) - @emit 'contents-changed' if Grim.includeDeprecatedAPIs - @emitter.emit 'did-change' - @subscribeToNativeChangeEvents() if not previouslyExisted and @hasSubscriptions() - undefined - - writeFile: (filePath, contents) -> - encoding = @getEncoding() - if encoding is 'utf8' - new Promise (resolve, reject) -> - fs.writeFile filePath, contents, {encoding}, (err, result) -> - if err? - reject(err) - else - resolve(result) - else - iconv ?= require 'iconv-lite' - new Promise (resolve, reject) -> - fs.writeFile filePath, iconv.encode(contents, encoding), (err, result) -> - if err? - reject(err) - else - resolve(result) - - ### - Section: Private - ### - - handleNativeChangeEvent: (eventType, eventPath) -> - switch eventType - when 'delete' - @unsubscribeFromNativeChangeEvents() - @detectResurrectionAfterDelay() - when 'rename' - @setPath(eventPath) - @emit 'moved' if Grim.includeDeprecatedAPIs - @emitter.emit 'did-rename' - when 'change', 'resurrect' - @cachedContents = null - @emitter.emit 'did-change' - - detectResurrectionAfterDelay: -> - _.delay (=> @detectResurrection()), 50 - - detectResurrection: -> - @exists().then (exists) => - if exists - @subscribeToNativeChangeEvents() - @handleNativeChangeEvent('resurrect') - else - @cachedContents = null - @emit 'removed' if Grim.includeDeprecatedAPIs - @emitter.emit 'did-delete' - - subscribeToNativeChangeEvents: -> - @watchSubscription ?= PathWatcher.watch @path, (args...) => - @handleNativeChangeEvent(args...) - - unsubscribeFromNativeChangeEvents: -> - if @watchSubscription? - @watchSubscription.close() - @watchSubscription = null - -if Grim.includeDeprecatedAPIs - EmitterMixin = require('emissary').Emitter - EmitterMixin.includeInto(File) - - File::on = (eventName) -> - switch eventName - when 'contents-changed' - Grim.deprecate("Use File::onDidChange instead") - when 'moved' - Grim.deprecate("Use File::onDidRename instead") - when 'removed' - Grim.deprecate("Use File::onDidDelete instead") - else - if @reportOnDeprecations - Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.") - - EmitterMixin::on.apply(this, arguments) -else - File::hasSubscriptions = -> - @subscriptionCount > 0 diff --git a/src/file.js b/src/file.js new file mode 100644 index 0000000..1fd6a2f --- /dev/null +++ b/src/file.js @@ -0,0 +1,437 @@ +const crypto = require('crypto'); +const Path = require('path'); +const _ = require('underscore-plus'); +const { Emitter, Disposable } = require('event-kit'); +const FS = require('fs-plus'); +const Grim = require('grim'); + +let iconv; +let Directory; + +const PathWatcher = require('./main'); + +class File { + encoding = 'utf8'; + realPath = null; + subscriptionCount = 0; + + constructor( + filePath, + symlink = false, + includeDeprecatedAPIs = Grim.includeDeprecatedAPIs + ) { + this.didRemoveSubscription = this.didRemoveSubscription.bind(this); + this.willAddSubscription = this.willAddSubscription.bind(this); + this.symlink = symlink; + + filePath &&= Path.normalize(filePath); + this.path = filePath; + this.emitter = new Emitter(); + + if (includeDeprecatedAPIs) { + this.on('contents-changed-subscription-will-be-added', this.willAddSubscription); + this.on('moved-subscription-will-be-added', this.willAddSubscription); + this.on('removed-subscription-will-be-added', this.willAddSubscription); + this.on('contents-changed-subscription-removed', this.didRemoveSubscription); + this.on('moved-subscription-removed', this.didRemoveSubscription); + this.on('removed-subscription-removed', this.didRemoveSubscription); + } + + this.cachedContents = null; + this.reportOnDeprecations = true; + } + + async create () { + let isExistingFile = await this.exists(); + let parent; + if (!isExistingFile) { + parent = this.getParent(); + await parent.create(); + await this.write(''); + return true; + } else { + return false; + } + } + + /* + Section: Event Subscription + */ + + onDidChange (callback) { + this.willAddSubscription(); + return this.trackUnsubscription(this.emitter.on('did-change', callback)); + } + + onDidRename (callback) { + this.willAddSubscription(); + return this.trackUnsubscription(this.emitter.on('did-rename', callback)); + } + + onDidDelete (callback) { + this.willAddSubscription(); + return this.trackUnsubscription(this.emitter.on('did-delete', callback)); + } + + onWillThrowWatchError (_callback) { + // DEPRECATED + } + + willAddSubscription () { + this.subscriptionCount++; + try { + return this.subscribeToNativeChangeEvents(); + } catch (_err) {} + } + + didRemoveSubscription () { + this.subscriptionCount--; + if (this.subscriptionCount === 0) { + return this.unsubscribeFromNativeChangeEvents(); + } + } + + trackUnsubscription (subscription) { + return new Disposable(() => { + subscription.dispose(); + this.didRemoveSubscription(); + }); + } + + /* + Section: File Metadata + */ + + isFile () { + return true; + } + + isDirectory () { + return false; + } + + isSymbolicLink () { + return this.symlink; + } + + async exists () { + return new Promise((resolve) => FS.exists(this.getPath(), resolve)); + } + + existsSync () { + return FS.existsSync(this.getPath()); + } + + async getDigest () { + if (this.digest != null) { + return this.digest; + } + await this.read(); + return this.digest; + } + + getDigestSync () { + if (this.digest == null) { + this.readSync(); + } + return this.digest; + } + + setDigest (contents) { + this.digest = crypto + .createHash('sha1') + .update(contents ?? '') + .digest('hex'); + return this.digest; + } + + setEncoding (encoding = 'utf8') { + if (encoding !== 'utf8') { + iconv ??= require('iconv-lite'); + iconv.getCodec(encoding); + } + this.encoding = encoding; + return encoding; + } + + getEncoding () { + return this.encoding; + } + + /* + Section: Managing Paths + */ + + getPath () { + return this.path; + } + + setPath (path) { + this.path = path; + this.realPath = null; + } + + getRealPathSync () { + if (this.realPath == null) { + try { + this.realPath = FS.realpathSync(this.path); + } catch (_error) { + this.realPath = this.path; + } + } + return this.realPath; + } + + async getRealPath () { + if (this.realPath != null) { + return this.realPath; + } + return new Promise((resolve, reject) => { + FS.realpath(this.path, (err, result) => { + if (err != null) return reject(err); + this.realPath = result; + return resolve(this.realPath); + }); + }); + } + + getBaseName () { + return Path.basename(this.path); + } + + /* + Section: Traversing + */ + + getParent () { + Directory ??= require('./directory'); + return new Directory(Path.dirname(this.path)); + } + + + /* + Section: Reading and Writing + */ + + readSync (flushCache) { + if (!this.existsSync()) { + this.cachedContents = null; + } else if ((this.cachedContents == null) || flushCache) { + let encoding = this.getEncoding(); + if (encoding === 'utf8') { + this.cachedContents = FS.readFileSync(this.getPath(), encoding); + } else { + iconv ??= require('iconv-lite'); + this.cachedContents = iconv.decode( + FS.readFileSync(this.getPath()), + encoding + ); + } + } + this.setDigest(this.cachedContents); + return this.cachedContents; + } + + writeFileSync (filePath, contents) { + let encoding = this.getEncoding(); + if (encoding === 'utf8') { + return FS.writeFileSync(filePath, contents, { encoding }); + } else { + iconv ??= require('iconv-lite'); + return FS.writeFileSync(filePath, iconv.encode(contents, encoding)); + } + } + + async read (flushCache) { + let contents; + if (!flushCache && this.cachedContents != null) { + contents = this.cachedContents; + } else { + contents = await new Promise((resolve, reject) => { + let content = []; + let readStream = this.createReadStream(); + readStream.on('data', (chunk) => content.push(chunk)); + readStream.on('end', () => resolve(content.join(''))); + readStream.on('error', (error) => { + if (error.code === 'ENOENT') { + return resolve(null); + } else { + return reject(error); + } + }) + }); + } + this.setDigest(contents); + this.cachedContents = contents; + return contents; + } + + createReadStream () { + let encoding = this.getEncoding(); + if (encoding === 'utf8') { + return FS.createReadStream(this.getPath(), { encoding }); + } else { + iconv ??= require('iconv-lite'); + return FS.createReadStream(this.getPath()) + .pipe(iconv.decodeStream(encoding)); + } + } + + async write (text) { + let previouslyExisted = await this.exists(); + await this.writeFile(this.getPath(), text); + this.cachedContents = text; + this.setDigest(text); + if (!previouslyExisted && this.hasSubscriptions()) { + this.subscribeToNativeChangeEvents(); + } + } + + createWriteStream () { + let encoding = this.getEncoding(); + if (encoding === 'utf8') { + return FS.createWriteStream(this.getPath(), { encoding }); + } else { + iconv ??= require('iconv-lite'); + let stream = iconv.encodeStream(encoding); + stream.pipe(FS.createWriteStream(this.getPath())); + return stream; + } + } + + writeSync (text) { + let previouslyExisted = this.existsSync(); + this.writeFileSync(this.getPath(), text); + this.cachedContents = text; + this.setDigest(text); + if (Grim.includeDeprecatedAPIs) { + this.emit('contents-changed'); + } + this.emitter.emit('did-change'); + if (!previouslyExisted && this.hasSubscriptions()) { + this.subscribeToNativeChangeEvents(); + } + } + + async writeFile (filePath, contents) { + let encoding = this.getEncoding(); + if (encoding === 'utf8') { + return new Promise((resolve, reject) => { + FS.writeFile( + filePath, + contents, + { encoding }, + (err, result) => { + if (err != null) { + reject(err); + } else { + resolve(result); + } + } + ) + }); + } else { + iconv ??= require('iconv-lite'); + return new Promise((resolve, reject) => { + FS.writeFile( + filePath, + iconv.encode(contents, encoding), + (err, result) => { + if (err != null) { + reject(err); + } else { + resolve(result); + } + } + ) + }); + } + } + + /* + Section: Private + */ + + handleNativeChangeEvent (eventType, eventPath) { + switch (eventType) { + case 'delete': + this.unsubscribeFromNativeChangeEvents(); + this.detectResurrectionAfterDelay(); + return; + case 'rename': + this.setPath(eventPath); + if (Grim.includeDeprecatedAPIs) { + this.emit('moved'); + } + this.emitter.emit('did-rename'); + return; + case 'change': + case 'resurrect': + this.cachedContents = null; + this.emitter.emit('did-change'); + } + } + + detectResurrectionAfterDelay () { + return _.delay(() => this.detectResurrection(), 50); + } + + async detectResurrection () { + let exists = await this.exists(); + if (exists) { + this.subscribeToNativeChangeEvents(); + this.handleNativeChangeEvent('resurrect'); + } else { + this.cachedContents = null; + if (Grim.includeDeprecatedAPIs) { + this.emit('removed'); + } + this.emitter.emit('did-delete'); + } + } + + subscribeToNativeChangeEvents () { + this.watchSubscription ??= PathWatcher.watch( + this.path, + (...args) => { + return this.handleNativeChangeEvent(...args); + } + ); + return this.watchSubscription; + } + + unsubscribeFromNativeChangeEvents () { + this.watchSubscription?.close(); + this.watchSubscription &&= null; + } +} + +if (Grim.includeDeprecatedAPIs) { + EmitterMixin = require('emissary').Emitter; + EmitterMixin.includeInto(File); + File.prototype.on = function(eventName) { + switch (eventName) { + case 'contents-changed': + Grim.deprecate("Use File::onDidChange instead"); + break; + case 'moved': + Grim.deprecate("Use File::onDidRename instead"); + break; + case 'removed': + Grim.deprecate("Use File::onDidDelete instead"); + break; + default: + if (this.reportOnDeprecations) { + Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead."); + } + } + return EmitterMixin.prototype.on.apply(this, arguments); + }; +} else { + File.prototype.hasSubscriptions = function() { + return this.subscriptionCount > 0; + }; +} + + +module.exports = File; diff --git a/src/handle_map.cc b/src/handle_map.cc deleted file mode 100644 index c55a85f..0000000 --- a/src/handle_map.cc +++ /dev/null @@ -1,143 +0,0 @@ -#include "handle_map.h" - -#include - -HandleMap::HandleMap() { -} - -HandleMap::~HandleMap() { - Clear(); -} - -bool HandleMap::Has(WatcherHandle key) const { - return map_.find(key) != map_.end(); -} - -bool HandleMap::Erase(WatcherHandle key) { - Map::iterator iter = map_.find(key); - if (iter == map_.end()) - return false; - - NanDisposeUnsafePersistent(iter->second); - map_.erase(iter); - return true; -} - -void HandleMap::Clear() { - for (Map::iterator iter = map_.begin(); iter != map_.end(); ++iter) - NanDisposeUnsafePersistent(iter->second); - map_.clear(); -} - -// static -NAN_METHOD(HandleMap::New) { - Nan::HandleScope scope; - HandleMap* obj = new HandleMap(); - obj->Wrap(info.This()); - return; -} - -// static -NAN_METHOD(HandleMap::Add) { - Nan::HandleScope scope; - - if (!IsV8ValueWatcherHandle(info[0])) - return Nan::ThrowTypeError("Bad argument"); - - HandleMap* obj = Nan::ObjectWrap::Unwrap(info.This()); - WatcherHandle key = V8ValueToWatcherHandle(info[0]); - if (obj->Has(key)) - return Nan::ThrowError("Duplicate key"); - - NanAssignUnsafePersistent(obj->map_[key], info[1]); - return; -} - -// static -NAN_METHOD(HandleMap::Get) { - Nan::HandleScope scope; - - if (!IsV8ValueWatcherHandle(info[0])) - return Nan::ThrowTypeError("Bad argument"); - - HandleMap* obj = Nan::ObjectWrap::Unwrap(info.This()); - WatcherHandle key = V8ValueToWatcherHandle(info[0]); - if (!obj->Has(key)) - return Nan::ThrowError("Invalid key"); - - info.GetReturnValue().Set(NanUnsafePersistentToLocal(obj->map_[key])); -} - -// static -NAN_METHOD(HandleMap::Has) { - Nan::HandleScope scope; - - if (!IsV8ValueWatcherHandle(info[0])) - return Nan::ThrowTypeError("Bad argument"); - - HandleMap* obj = Nan::ObjectWrap::Unwrap(info.This()); - info.GetReturnValue().Set(Nan::New(obj->Has(V8ValueToWatcherHandle(info[0])))); -} - -// static -NAN_METHOD(HandleMap::Values) { - Nan::HandleScope scope; - - HandleMap* obj = Nan::ObjectWrap::Unwrap(info.This()); - - int i = 0; - v8::Local context = Nan::GetCurrentContext(); - v8::Local keys = Nan::New(obj->map_.size()); - for (Map::const_iterator iter = obj->map_.begin(); - iter != obj->map_.end(); - ++iter, ++i) { - keys->Set(context, i, NanUnsafePersistentToLocal(iter->second)).FromJust(); - } - - info.GetReturnValue().Set(keys); -} - -// static -NAN_METHOD(HandleMap::Remove) { - Nan::HandleScope scope; - - if (!IsV8ValueWatcherHandle(info[0])) - return Nan::ThrowTypeError("Bad argument"); - - HandleMap* obj = Nan::ObjectWrap::Unwrap(info.This()); - if (!obj->Erase(V8ValueToWatcherHandle(info[0]))) - return Nan::ThrowError("Invalid key"); - - return; -} - -// static -NAN_METHOD(HandleMap::Clear) { - Nan::HandleScope scope; - - HandleMap* obj = Nan::ObjectWrap::Unwrap(info.This()); - obj->Clear(); - - return; -} - -// static -void HandleMap::Initialize(Local target) { - Nan::HandleScope scope; - - Local t = Nan::New(HandleMap::New); - t->InstanceTemplate()->SetInternalFieldCount(1); - t->SetClassName(Nan::New("HandleMap").ToLocalChecked()); - - Nan::SetPrototypeMethod(t, "add", Add); - Nan::SetPrototypeMethod(t, "get", Get); - Nan::SetPrototypeMethod(t, "has", Has); - Nan::SetPrototypeMethod(t, "values", Values); - Nan::SetPrototypeMethod(t, "remove", Remove); - Nan::SetPrototypeMethod(t, "clear", Clear); - - Local context = Nan::GetCurrentContext(); - target->Set(context, - Nan::New("HandleMap").ToLocalChecked(), - t->GetFunction(context).ToLocalChecked()).FromJust(); -} diff --git a/src/handle_map.h b/src/handle_map.h deleted file mode 100644 index a9638b0..0000000 --- a/src/handle_map.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef SRC_HANDLE_MAP_H_ -#define SRC_HANDLE_MAP_H_ - -#include - -#include "common.h" -#include "unsafe_persistent.h" - -class HandleMap : public Nan::ObjectWrap { - public: - static void Initialize(Local target); - - private: - typedef std::map > Map; - - HandleMap(); - virtual ~HandleMap(); - - bool Has(WatcherHandle key) const; - bool Erase(WatcherHandle key); - void Clear(); - - static void DisposeHandle(NanUnsafePersistent& value); - - static NAN_METHOD(New); - static NAN_METHOD(Add); - static NAN_METHOD(Get); - static NAN_METHOD(Has); - static NAN_METHOD(Values); - static NAN_METHOD(Remove); - static NAN_METHOD(Clear); - - Map map_; -}; - -#endif // SRC_HANDLE_MAP_H_ diff --git a/src/main.cc b/src/main.cc index 898f45f..038889e 100644 --- a/src/main.cc +++ b/src/main.cc @@ -1,19 +1,22 @@ #include "common.h" -#include "handle_map.h" +#include "addon-data.h" namespace { -void Init(Local exports) { - CommonInit(); - PlatformInit(); + Napi::Object Init(Napi::Env env, Napi::Object exports) { + auto* data = new AddonData(env); + env.SetInstanceData(data); - Nan::SetMethod(exports, "setCallback", SetCallback); - Nan::SetMethod(exports, "watch", Watch); - Nan::SetMethod(exports, "unwatch", Unwatch); + CommonInit(env); + PlatformInit(); - HandleMap::Initialize(exports); -} + exports.Set("setCallback", Napi::Function::New(env, SetCallback)); + exports.Set("watch", Napi::Function::New(env, Watch)); + exports.Set("unwatch", Napi::Function::New(env, Unwatch)); -} // namespace + return exports; + } -NODE_MODULE(pathwatcher, Init) +} // namespace + +NODE_API_MODULE(pathwatcher, Init) diff --git a/src/main.coffee b/src/main.coffee deleted file mode 100644 index 81b6fb4..0000000 --- a/src/main.coffee +++ /dev/null @@ -1,137 +0,0 @@ -binding = require '../build/Release/pathwatcher.node' -{HandleMap} = binding -{Emitter} = require 'event-kit' -fs = require 'fs' -path = require 'path' - -handleWatchers = null - -class HandleWatcher - constructor: (@path) -> - @emitter = new Emitter() - @start() - - onEvent: (event, filePath, oldFilePath) -> - filePath = path.normalize(filePath) if filePath - oldFilePath = path.normalize(oldFilePath) if oldFilePath - - switch event - when 'rename' - # Detect atomic write. - @close() - detectRename = => - fs.stat @path, (err) => - if err # original file is gone it's a rename. - @path = filePath - # On OS X files moved to ~/.Trash should be handled as deleted. - if process.platform is 'darwin' and (/\/\.Trash\//).test(filePath) - @emitter.emit('did-change', {event: 'delete', newFilePath: null}) - @close() - else - @start() - @emitter.emit('did-change', {event: 'rename', newFilePath: filePath}) - else # atomic write. - @start() - @emitter.emit('did-change', {event: 'change', newFilePath: null}) - setTimeout(detectRename, 100) - when 'delete' - @emitter.emit('did-change', {event: 'delete', newFilePath: null}) - @close() - when 'unknown' - throw new Error("Received unknown event for path: #{@path}") - else - @emitter.emit('did-change', {event, newFilePath: filePath, oldFilePath: oldFilePath}) - - onDidChange: (callback) -> - @emitter.on('did-change', callback) - - start: -> - @handle = binding.watch(@path) - if handleWatchers.has(@handle) - troubleWatcher = handleWatchers.get(@handle) - troubleWatcher.close() - console.error("The handle(#{@handle}) returned by watching #{@path} is the same with an already watched path(#{troubleWatcher.path})") - handleWatchers.add(@handle, this) - - closeIfNoListener: -> - @close() if @emitter.getTotalListenerCount() is 0 - - close: -> - if handleWatchers.has(@handle) - binding.unwatch(@handle) - handleWatchers.remove(@handle) - -class PathWatcher - isWatchingParent: false - path: null - handleWatcher: null - - constructor: (filePath, callback) -> - @path = filePath - @emitter = new Emitter() - - # On Windows watching a file is emulated by watching its parent folder. - if process.platform is 'win32' - stats = fs.statSync(filePath) - @isWatchingParent = not stats.isDirectory() - - filePath = path.dirname(filePath) if @isWatchingParent - for watcher in handleWatchers.values() - if watcher.path is filePath - @handleWatcher = watcher - break - - @handleWatcher ?= new HandleWatcher(filePath) - - @onChange = ({event, newFilePath, oldFilePath}) => - switch event - when 'rename', 'change', 'delete' - @path = newFilePath if event is 'rename' - callback.call(this, event, newFilePath) if typeof callback is 'function' - @emitter.emit('did-change', {event, newFilePath}) - when 'child-rename' - if @isWatchingParent - @onChange({event: 'rename', newFilePath}) if @path is oldFilePath - else - @onChange({event: 'change', newFilePath: ''}) - when 'child-delete' - if @isWatchingParent - @onChange({event: 'delete', newFilePath: null}) if @path is newFilePath - else - @onChange({event: 'change', newFilePath: ''}) - when 'child-change' - @onChange({event: 'change', newFilePath: ''}) if @isWatchingParent and @path is newFilePath - when 'child-create' - @onChange({event: 'change', newFilePath: ''}) unless @isWatchingParent - - @disposable = @handleWatcher.onDidChange(@onChange) - - onDidChange: (callback) -> - @emitter.on('did-change', callback) - - close: -> - @emitter.dispose() - @disposable.dispose() - @handleWatcher.closeIfNoListener() - -exports.watch = (pathToWatch, callback) -> - unless handleWatchers? - handleWatchers = new HandleMap - binding.setCallback (event, handle, filePath, oldFilePath) -> - handleWatchers.get(handle).onEvent(event, filePath, oldFilePath) if handleWatchers.has(handle) - - new PathWatcher(path.resolve(pathToWatch), callback) - -exports.closeAllWatchers = -> - if handleWatchers? - watcher.close() for watcher in handleWatchers.values() - handleWatchers.clear() - -exports.getWatchedPaths = -> - paths = [] - if handleWatchers? - paths.push(watcher.path) for watcher in handleWatchers.values() - paths - -exports.File = require './file' -exports.Directory = require './directory' diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..9b018ba --- /dev/null +++ b/src/main.js @@ -0,0 +1,219 @@ + +const binding = require('../build/Release/pathwatcher.node'); +const { Emitter } = require('event-kit'); +const fs = require('fs'); +const path = require('path'); + +const HANDLE_WATCHERS = new Map(); +let initialized = false; + +class HandleWatcher { + + constructor(path) { + this.path = path; + this.emitter = new Emitter(); + this.start(); + } + + onEvent (event, filePath, oldFilePath) { + filePath &&= path.normalize(filePath); + oldFilePath &&= path.normalize(oldFilePath); + + switch (event) { + case 'rename': + this.close(); + let detectRename = () => { + return fs.stat( + this.path, + (err) => { + if (err) { + this.path = filePath; + if (process.platform === 'darwin' && /\/\.Trash\//.test(filePath)) { + this.emitter.emit( + 'did-change', + { event: 'delete', newFilePath: null } + ); + this.close(); + return; + } else { + this.start(); + this.emitter.emit( + 'did-change', + { event: 'rename', newFilePath: filePath } + ); + return; + } + } else { + this.start(); + this.emitter.emit( + 'did-change', + { event: 'change', newFilePath: null } + ); + return; + } + } + ); + }; + setTimeout(detectRename, 100); + return; + case 'delete': + this.emitter.emit( + 'did-change', + { event: 'delete', newFilePath: null } + ); + this.close(); + return; + case 'unknown': + throw new Error("Received unknown event for path: " + this.path); + default: + this.emitter.emit( + 'did-change', + { event, newFilePath: filePath, oldFilePath } + ); + } + } + + onDidChange (callback) { + return this.emitter.on('did-change', callback); + } + + start () { + let troubleWatcher; + this.handle = binding.watch(this.path); + if (HANDLE_WATCHERS.has(this.handle)) { + troubleWatcher = HANDLE_WATCHERS.get(this.handle); + troubleWatcher.close(); + console.error(`The handle (${this.handle}) returned by watching path ${this.path} is the same as an already-watched path: ${troubleWatcher.path}`); + } + return HANDLE_WATCHERS.set(this.handle, this); + } + + closeIfNoListener () { + if (this.emitter.getTotalListenerCount() === 0) { + this.close(); + } + } + + close () { + if (!HANDLE_WATCHERS.has(this.handle)) return; + binding.unwatch(this.handle); + HANDLE_WATCHERS.delete(this.handle); + } +} + +class PathWatcher { + isWatchingParent = false; + path = null; + handleWatcher = null; + constructor(filePath, callback) { + this.path = filePath; + this.emitter = new Emitter(); + if (process.platform === 'win32') { + stats = fs.statSync(filePath); + this.isWatchingParent = !stats.isDirectory(); + } + if (this.isWatchingParent) { + filePath = path.dirname(filePath); + } + for (let watcher of HANDLE_WATCHERS.values()) { + if (watcher.path === filePath) { + this.handleWatcher = watcher; + break; + } + } + this.handleWatcher ??= new HandleWatcher(filePath); + + this.onChange = ({ event, newFilePath, oldFilePath }) => { + switch (event) { + case 'rename': + case 'change': + case 'delete': + if (event === 'rename') { + this.path = newFilePath; + } + if (typeof callback === 'function') { + callback.call(this, event, newFilePath); + } + this.emitter.emit( + 'did-change', + { event, newFilePath } + ); + return; + case 'child-rename': + if (this.isWatchingParent) { + if (this.path === oldFilePath) { + return this.onChange({ event: 'rename', newFilePath }); + } + } else { + return this.onChange({ event: 'change', newFilePath: '' }); + } + break; + case 'child-delete': + if (this.isWatchingParent) { + if (this.path === newFilePath) { + return this.onChange({ event: 'delete', newFilePath: null }); + } + } else { + return this.onChange({ event: 'change', newFilePath: '' }); + } + break; + case 'child-change': + if (this.isWatchingParent && this.path === newFilePath) { + return this.onChange({ event: 'change', newFilePath: '' }); + } + break; + case 'child-create': + if (!this.isWatchingParent) { + return this.onChange({ event: 'change', newFilePath: '' }); + } + } + }; + + this.disposable = this.handleWatcher.onDidChange(this.onChange); + } + + onDidChange (callback) { + return this.emitter.on('did-change', callback); + } + + close () { + this.emitter?.dispose(); + this.disposable?.dispose(); + this.handleWatcher?.closeIfNoListener(); + } +} + +function watch (pathToWatch, callback) { + if (!initialized) { + initialized = true; + binding.setCallback((event, handle, filePath, oldFilePath) => { + if (!HANDLE_WATCHERS.has(handle)) return; + HANDLE_WATCHERS.get(handle).onEvent(event, filePath, oldFilePath); + }); + } + + return new PathWatcher(path.resolve(pathToWatch), callback); +} + +function closeAllWatchers () { + for (let watcher of HANDLE_WATCHERS.values()) { + watcher?.close(); + } + HANDLE_WATCHERS.clear(); +} + +function getWatchedPaths () { + let watchers = Array.from(HANDLE_WATCHERS.values()); + return watchers.map(w => w.path); +} + +const File = require('./file'); +const Directory = require('./directory'); + +module.exports = { + watch, + closeAllWatchers, + getWatchedPaths, + File, + Directory +}; diff --git a/src/pathwatcher_unix.cc b/src/pathwatcher_unix.cc index d88865e..160fe00 100644 --- a/src/pathwatcher_unix.cc +++ b/src/pathwatcher_unix.cc @@ -19,8 +19,12 @@ // see: http://fxr.watson.org/fxr/source/bsd/sys/fcntl.h?v=xnu-792.6.70 #ifndef F_GETPATH #define F_GETPATH 50 + #endif +// NOTE: You might see the globals and get nervous here. Our working theory is +// that this this is fine; this is thread-safe without having to be isolated +// between contexts. static int g_kqueue; static int g_init_errno; @@ -30,23 +34,28 @@ void PlatformInit() { g_init_errno = errno; return; } - - WakeupNewThread(); } -void PlatformThread() { +void PlatformThread( + const PathWatcherWorker::ExecutionProgress& progress, + bool& shouldStop +) { struct kevent event; + struct timespec timeout = { 0, 500000000 }; - while (true) { + while (!shouldStop) { int r; do { - r = kevent(g_kqueue, NULL, 0, &event, 1, NULL); + if (shouldStop) return; + r = kevent(g_kqueue, NULL, 0, &event, 1, &timeout); } while ((r == -1 && errno == EINTR) || r == 0); EVENT_TYPE type; int fd = static_cast(event.ident); std::vector path; + // std::cout << "EVENT delete: " << (event.fflags & NOTE_DELETE) << " write: " << (event.fflags & NOTE_WRITE) << " rename: " << (event.fflags & NOTE_RENAME) << "empty: " << (event.fflags & NOTE_ATTRIB && lseek(fd, 0, SEEK_END) == 0) << std::endl; + if (event.fflags & NOTE_WRITE) { type = EVENT_CHANGE; } else if (event.fflags & NOTE_DELETE) { @@ -68,7 +77,8 @@ void PlatformThread() { continue; } - PostEventAndWait(type, fd, path); + PathWatcherEvent event(type, fd, path); + progress.Send(&event, 1); } } @@ -82,7 +92,7 @@ WatcherHandle PlatformWatch(const char* path) { return -errno; } - struct timespec timeout = { 0, 0 }; + struct timespec timeout = { 0, 50000000 }; struct kevent event; int filter = EVFILT_VNODE; int flags = EV_ADD | EV_ENABLE | EV_CLEAR; @@ -96,6 +106,7 @@ WatcherHandle PlatformWatch(const char* path) { return fd; } + void PlatformUnwatch(WatcherHandle fd) { close(fd); } diff --git a/src/unsafe_persistent.h b/src/unsafe_persistent.h deleted file mode 100644 index 6b2fb8a..0000000 --- a/src/unsafe_persistent.h +++ /dev/null @@ -1,52 +0,0 @@ -#ifndef UNSAFE_PERSISTENT_H_ -#define UNSAFE_PERSISTENT_H_ - -#include "nan.h" - -#if NODE_VERSION_AT_LEAST(0, 11, 0) -template -struct NanUnsafePersistentTraits { - typedef v8::Persistent > HandleType; - static const bool kResetInDestructor = false; - template - static V8_INLINE void Copy(const Persistent& source, - HandleType* dest) { - // do nothing, just allow copy - } -}; -template -class NanUnsafePersistent : public NanUnsafePersistentTraits::HandleType { - public: - V8_INLINE NanUnsafePersistent() {} - - template - V8_INLINE NanUnsafePersistent(v8::Isolate* isolate, S that) - : NanUnsafePersistentTraits::HandleType(isolate, that) { - } -}; -template -NAN_INLINE void NanAssignUnsafePersistent( - NanUnsafePersistent& handle - , v8::Local obj) { - handle.Reset(); - handle = NanUnsafePersistent(v8::Isolate::GetCurrent(), obj); -} -template -NAN_INLINE v8::Local NanUnsafePersistentToLocal(const NanUnsafePersistent &arg1) { - return v8::Local::New(v8::Isolate::GetCurrent(), arg1); -} -#define NanDisposeUnsafePersistent(handle) handle.Reset() -#else -#define NanUnsafePersistent v8::Persistent -#define NanUnsafePersistentToLocal Nan::New -#define NanDisposeUnsafePersistent(handle) handle.Dispose() -template -NAN_INLINE void NanAssignUnsafePersistent( - v8::Persistent& handle - , v8::Local obj) { - handle.Dispose(); - handle = v8::Persistent::New(obj); -} -#endif - -#endif // UNSAFE_PERSISTENT_H_ From 3002a749d425be182c0be30336f6a8a3c3be994c Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 6 Oct 2024 18:23:01 -0700 Subject: [PATCH 002/168] Convert the specs to JavaScript. This comes with the painful revelation that one spec has been spuriously passing for years, and the functionality it claims to support doesn't seem to work (at least on macOS). That spec is skipped until that bug can be fixed. This required a more modern version of Jasmine that could grok `async`/`await` syntax. The magical `jasmine-tagged` stuff is currently being done manually instead. --- package.json | 2 +- spec/directory-spec.coffee | 387 -------------------------- spec/directory-spec.js | 473 +++++++++++++++++++++++++++++++ spec/file-spec.coffee | 515 ---------------------------------- spec/file-spec.js | 522 +++++++++++++++++++++++++++++++++++ spec/pathwatcher-spec.coffee | 167 ----------- spec/pathwatcher-spec.js | 225 +++++++++++++++ spec/spec-helper.coffee | 28 -- spec/spec-helper.js | 37 +++ 9 files changed, 1258 insertions(+), 1098 deletions(-) delete mode 100644 spec/directory-spec.coffee create mode 100644 spec/directory-spec.js delete mode 100644 spec/file-spec.coffee create mode 100644 spec/file-spec.js delete mode 100644 spec/pathwatcher-spec.coffee create mode 100644 spec/pathwatcher-spec.js delete mode 100644 spec/spec-helper.coffee create mode 100644 spec/spec-helper.js diff --git a/package.json b/package.json index 2961d22..2eadf1c 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "grunt-coffeelint": "git+https://github.com/atom/grunt-coffeelint.git", "grunt-contrib-coffee": "~0.9.0", "grunt-shell": "~0.2.2", - "jasmine-tagged": "^1.1", + "jasmine": "^5.3.1", "node-addon-api": "^8.1.0", "node-cpplint": "~0.1.5", "rimraf": "~2.2.0", diff --git a/spec/directory-spec.coffee b/spec/directory-spec.coffee deleted file mode 100644 index 9931b13..0000000 --- a/spec/directory-spec.coffee +++ /dev/null @@ -1,387 +0,0 @@ -path = require 'path' -fs = require 'fs-plus' -temp = require 'temp' -Directory = require '../lib/directory' -PathWatcher = require '../lib/main' - -describe "Directory", -> - directory = null - - beforeEach -> - directory = new Directory(path.join(__dirname, 'fixtures')) - - afterEach -> - PathWatcher.closeAllWatchers() - - it "normalizes the specified path", -> - expect(new Directory(directory.path + path.sep + 'abc' + path.sep + '..').getBaseName()).toBe 'fixtures' - expect(new Directory(directory.path + path.sep + 'abc' + path.sep + '..').path.toLowerCase()).toBe directory.path.toLowerCase() - - expect(new Directory(directory.path + path.sep).getBaseName()).toBe 'fixtures' - expect(new Directory(directory.path + path.sep).path.toLowerCase()).toBe directory.path.toLowerCase() - - expect(new Directory(directory.path + path.sep + path.sep).getBaseName()).toBe 'fixtures' - expect(new Directory(directory.path + path.sep + path.sep).path.toLowerCase()).toBe directory.path.toLowerCase() - - expect(new Directory(path.sep).getBaseName()).toBe '' - expect(new Directory(path.sep).path).toBe path.sep - - it 'returns false from isFile()', -> - expect(directory.isFile()).toBe false - - it 'returns true from isDirectory()', -> - expect(directory.isDirectory()).toBe true - - describe '::isSymbolicLink()', -> - it 'returns false for regular directories', -> - expect(directory.isSymbolicLink()).toBe false - - it 'returns true for symlinked directories', -> - symbolicDirectory = new Directory(path.join(__dirname, 'fixtures'), true) - expect(symbolicDirectory.isSymbolicLink()).toBe true - - describe '::exists()', -> - [callback, tempDir] = [] - - beforeEach -> - tempDir = temp.mkdirSync('node-pathwatcher-directory') - callback = jasmine.createSpy('promiseCallback') - - it 'returns a Promise that resolves to true for an existing directory', -> - directory = new Directory(tempDir) - - waitsForPromise -> - directory.exists().then(callback) - - runs -> - expect(callback.argsForCall[0][0]).toBe true - - it 'returns a Promise that resolves to false for a non-existent directory', -> - directory = new Directory(path.join(tempDir, 'foo')) - - waitsForPromise -> - directory.exists().then(callback) - - runs -> - expect(callback.argsForCall[0][0]).toBe false - - describe '::existsSync()', -> - [tempDir] = [] - - beforeEach -> - tempDir = temp.mkdirSync('node-pathwatcher-directory') - - it 'returns true for an existing directory', -> - directory = new Directory(tempDir) - expect(directory.existsSync()).toBe true - - it 'returns false for a non-existent directory', -> - directory = new Directory(path.join(tempDir, 'foo')) - expect(directory.existsSync()).toBe false - - describe '::create()', -> - [callback, tempDir] = [] - - beforeEach -> - tempDir = temp.mkdirSync('node-pathwatcher-directory') - callback = jasmine.createSpy('promiseCallback') - - it 'creates directory if directory does not exist', -> - directoryName = path.join(tempDir, 'subdir') - expect(fs.existsSync(directoryName)).toBe false - nonExistentDirectory = new Directory(directoryName) - - waitsForPromise -> - nonExistentDirectory.create(0o0600).then(callback) - - runs -> - expect(callback.argsForCall[0][0]).toBe true - expect(fs.existsSync(directoryName)).toBe true - expect(fs.isDirectorySync(directoryName)).toBe true - - return if process.platform is 'win32' # No mode on Windows - - rawMode = fs.statSync(directoryName).mode - mode = rawMode & 0o07777 - expect(mode.toString(8)).toBe (0o0600).toString(8) - - it 'leaves existing directory alone if it exists', -> - directoryName = path.join(tempDir, 'subdir') - fs.mkdirSync(directoryName) - existingDirectory = new Directory(directoryName) - - waitsForPromise -> - existingDirectory.create().then(callback) - - runs -> - expect(callback.argsForCall[0][0]).toBe false - expect(fs.existsSync(directoryName)).toBe true - expect(fs.isDirectorySync(directoryName)).toBe true - - it 'creates parent directories if they do not exist', -> - directoryName = path.join(tempDir, 'foo', 'bar', 'baz') - expect(fs.existsSync(directoryName)).toBe false - nonExistentDirectory = new Directory(directoryName) - - waitsForPromise -> - nonExistentDirectory.create().then(callback) - - runs -> - expect(callback.argsForCall[0][0]).toBe true - - expect(fs.existsSync(directoryName)).toBe true - expect(fs.isDirectorySync(directoryName)).toBe true - - parentName = path.join(tempDir, 'foo', 'bar') - expect(fs.existsSync(parentName)).toBe true - expect(fs.isDirectorySync(parentName)).toBe true - - it "throws an error when called on a root directory that does not exist", -> - spyOn(Directory::, 'isRoot').andReturn(true) - directory = new Directory(path.join(tempDir, 'subdir')) - - waitsForPromise shouldReject: true, -> - directory.create() - - runs -> - expect(fs.existsSync(path.join(tempDir, 'subdir'))).toBe false - - describe "when the contents of the directory change on disk", -> - temporaryFilePath = null - - beforeEach -> - temporaryFilePath = path.join(__dirname, 'fixtures', 'temporary') - fs.removeSync(temporaryFilePath) - - afterEach -> - fs.removeSync(temporaryFilePath) - - it "notifies ::onDidChange observers", -> - changeHandler = null - - runs -> - directory.onDidChange changeHandler = jasmine.createSpy('changeHandler') - fs.writeFileSync(temporaryFilePath, '') - - waitsFor "first change", -> changeHandler.callCount > 0 - - runs -> - changeHandler.reset() - fs.removeSync(temporaryFilePath) - - waitsFor "second change", -> changeHandler.callCount > 0 - - describe "when the directory unsubscribes from events", -> - temporaryFilePath = null - - beforeEach -> - temporaryFilePath = path.join(directory.path, 'temporary') - fs.removeSync(temporaryFilePath) if fs.existsSync(temporaryFilePath) - - afterEach -> - fs.removeSync(temporaryFilePath) if fs.existsSync(temporaryFilePath) - - it "no longer triggers events", -> - [subscription, changeHandler] = [] - - runs -> - subscription = directory.onDidChange changeHandler = jasmine.createSpy('changeHandler') - fs.writeFileSync(temporaryFilePath, '') - - waitsFor "change event", -> changeHandler.callCount > 0 - - runs -> - changeHandler.reset() - subscription.dispose() - waits 20 - - runs -> fs.removeSync(temporaryFilePath) - waits 20 - runs -> expect(changeHandler.callCount).toBe 0 - - describe "on #darwin or #linux", -> - it "includes symlink information about entries", -> - entries = directory.getEntriesSync() - for entry in entries - name = entry.getBaseName() - if name is 'symlink-to-dir' or name is 'symlink-to-file' - expect(entry.symlink).toBeTruthy() - else - expect(entry.symlink).toBeFalsy() - - callback = jasmine.createSpy('getEntries') - directory.getEntries(callback) - - waitsFor -> callback.callCount is 1 - - runs -> - entries = callback.mostRecentCall.args[1] - for entry in entries - name = entry.getBaseName() - if name is 'symlink-to-dir' or name is 'symlink-to-file' - expect(entry.isSymbolicLink()).toBe true - else - expect(entry.isSymbolicLink()).toBe false - - describe ".relativize(path)", -> - describe "on #darwin or #linux", -> - it "returns a relative path based on the directory's path", -> - absolutePath = directory.getPath() - expect(directory.relativize(absolutePath)).toBe '' - expect(directory.relativize(path.join(absolutePath, "b"))).toBe "b" - expect(directory.relativize(path.join(absolutePath, "b/file.coffee"))).toBe "b/file.coffee" - expect(directory.relativize(path.join(absolutePath, "file.coffee"))).toBe "file.coffee" - - it "returns a relative path based on the directory's symlinked source path", -> - symlinkPath = path.join(__dirname, 'fixtures', 'symlink-to-dir') - symlinkDirectory = new Directory(symlinkPath) - realFilePath = require.resolve('./fixtures/dir/a') - expect(symlinkDirectory.relativize(symlinkPath)).toBe '' - expect(symlinkDirectory.relativize(realFilePath)).toBe 'a' - - it "returns the full path if the directory's path is not a prefix of the path", -> - expect(directory.relativize('/not/relative')).toBe '/not/relative' - - it "handled case insensitive filesystems", -> - spyOn(fs, 'isCaseInsensitive').andReturn true - directoryPath = temp.mkdirSync('Mixed-case-directory-') - directory = new Directory(directoryPath) - - expect(directory.relativize(directoryPath.toUpperCase())).toBe "" - expect(directory.relativize(path.join(directoryPath.toUpperCase(), "b"))).toBe "b" - expect(directory.relativize(path.join(directoryPath.toUpperCase(), "B"))).toBe "B" - expect(directory.relativize(path.join(directoryPath.toUpperCase(), "b/file.coffee"))).toBe "b/file.coffee" - expect(directory.relativize(path.join(directoryPath.toUpperCase(), "file.coffee"))).toBe "file.coffee" - - expect(directory.relativize(directoryPath.toLowerCase())).toBe "" - expect(directory.relativize(path.join(directoryPath.toLowerCase(), "b"))).toBe "b" - expect(directory.relativize(path.join(directoryPath.toLowerCase(), "B"))).toBe "B" - expect(directory.relativize(path.join(directoryPath.toLowerCase(), "b/file.coffee"))).toBe "b/file.coffee" - expect(directory.relativize(path.join(directoryPath.toLowerCase(), "file.coffee"))).toBe "file.coffee" - - expect(directory.relativize(directoryPath)).toBe "" - expect(directory.relativize(path.join(directoryPath, "b"))).toBe "b" - expect(directory.relativize(path.join(directoryPath, "B"))).toBe "B" - expect(directory.relativize(path.join(directoryPath, "b/file.coffee"))).toBe "b/file.coffee" - expect(directory.relativize(path.join(directoryPath, "file.coffee"))).toBe "file.coffee" - - describe "on #win32", -> - it "returns a relative path based on the directory's path", -> - absolutePath = directory.getPath() - expect(directory.relativize(absolutePath)).toBe '' - expect(directory.relativize(path.join(absolutePath, "b"))).toBe "b" - expect(directory.relativize(path.join(absolutePath, "b/file.coffee"))).toBe "b\\file.coffee" - expect(directory.relativize(path.join(absolutePath, "file.coffee"))).toBe "file.coffee" - - it "returns the full path if the directory's path is not a prefix of the path", -> - expect(directory.relativize('/not/relative')).toBe "\\not\\relative" - - describe ".resolve(uri)", -> - describe "when passed an absolute or relative path", -> - it "returns an absolute path based on the directory's path", -> - absolutePath = require.resolve('./fixtures/dir/a') - expect(directory.resolve('dir/a')).toBe absolutePath - expect(directory.resolve(absolutePath + '/../a')).toBe absolutePath - expect(directory.resolve('dir/a/../a')).toBe absolutePath - expect(directory.resolve()).toBeUndefined() - - describe "when passed a uri with a scheme", -> - it "does not modify uris that begin with a scheme", -> - expect(directory.resolve('http://zombo.com')).toBe 'http://zombo.com' - - describe ".contains(path)", -> - it "returns true if the path is a child of the directory's path", -> - absolutePath = directory.getPath() - expect(directory.contains(path.join(absolutePath))).toBe false - expect(directory.contains(path.join(absolutePath, "b"))).toBe true - expect(directory.contains(path.join(absolutePath, "b", "file.coffee"))).toBe true - expect(directory.contains(path.join(absolutePath, "file.coffee"))).toBe true - - it "returns false if the directory's path is not a prefix of the path", -> - expect(directory.contains('/not/relative')).toBe false - - it "handles case insensitive filesystems", -> - spyOn(fs, 'isCaseInsensitive').andReturn true - directoryPath = temp.mkdirSync('Mixed-case-directory-') - directory = new Directory(directoryPath) - - expect(directory.contains(directoryPath.toUpperCase())).toBe false - expect(directory.contains(path.join(directoryPath.toUpperCase(), "b"))).toBe true - expect(directory.contains(path.join(directoryPath.toUpperCase(), "B"))).toBe true - expect(directory.contains(path.join(directoryPath.toUpperCase(), "b", "file.coffee"))).toBe true - expect(directory.contains(path.join(directoryPath.toUpperCase(), "file.coffee"))).toBe true - - expect(directory.contains(directoryPath.toLowerCase())).toBe false - expect(directory.contains(path.join(directoryPath.toLowerCase(), "b"))).toBe true - expect(directory.contains(path.join(directoryPath.toLowerCase(), "B"))).toBe true - expect(directory.contains(path.join(directoryPath.toLowerCase(), "b", "file.coffee"))).toBe true - expect(directory.contains(path.join(directoryPath.toLowerCase(), "file.coffee"))).toBe true - - expect(directory.contains(directoryPath)).toBe false - expect(directory.contains(path.join(directoryPath, "b"))).toBe true - expect(directory.contains(path.join(directoryPath, "B"))).toBe true - expect(directory.contains(path.join(directoryPath, "b", "file.coffee"))).toBe true - expect(directory.contains(path.join(directoryPath, "file.coffee"))).toBe true - - describe "on #darwin or #linux", -> - it "returns true if the path is a child of the directory's symlinked source path", -> - symlinkPath = path.join(__dirname, 'fixtures', 'symlink-to-dir') - symlinkDirectory = new Directory(symlinkPath) - realFilePath = require.resolve('./fixtures/dir/a') - expect(symlinkDirectory.contains(realFilePath)).toBe true - - describe "traversal", -> - beforeEach -> - directory = new Directory(path.join __dirname, 'fixtures', 'dir') - - fixturePath = (parts...) -> - path.join __dirname, 'fixtures', parts... - - describe "getFile(filename)", -> - it "returns a File within this directory", -> - f = directory.getFile("a") - expect(f.isFile()).toBe(true) - expect(f.getRealPathSync()).toBe(fixturePath 'dir', 'a') - - it "can descend more than one directory at a time", -> - f = directory.getFile("subdir", "b") - expect(f.isFile()).toBe(true) - expect(f.getRealPathSync()).toBe(fixturePath 'dir', 'subdir', 'b') - - it "doesn't have to actually exist", -> - f = directory.getFile("the-silver-bullet") - expect(f.isFile()).toBe(true) - expect(f.existsSync()).toBe(false) - - describe "getSubdir(dirname)", -> - it "returns a subdirectory within this directory", -> - d = directory.getSubdirectory("subdir") - expect(d.isDirectory()).toBe(true) - expect(d.getRealPathSync()).toBe(fixturePath 'dir', 'subdir') - - it "can descend more than one directory at a time", -> - d = directory.getSubdirectory("subdir", "subsubdir") - expect(d.isDirectory()).toBe(true) - expect(d.getRealPathSync()).toBe(fixturePath 'dir', 'subdir', 'subsubdir') - - it "doesn't have to exist", -> - d = directory.getSubdirectory("why-would-you-call-a-directory-this-come-on-now") - expect(d.isDirectory()).toBe(true) - - describe "getParent()", -> - it "returns the parent Directory", -> - d = directory.getParent() - expect(d.isDirectory()).toBe(true) - expect(d.getRealPathSync()).toBe(fixturePath()) - - describe "isRoot()", -> - it "returns false if the Directory isn't the root", -> - expect(directory.isRoot()).toBe(false) - - it "returns true if the Directory is the root", -> - [current, previous] = [directory, null] - while current.getPath() isnt previous?.getPath() - previous = current - current = current.getParent() - - expect(current.isRoot()).toBe(true) diff --git a/spec/directory-spec.js b/spec/directory-spec.js new file mode 100644 index 0000000..913769a --- /dev/null +++ b/spec/directory-spec.js @@ -0,0 +1,473 @@ +const path = require('path'); +const fs = require('fs-plus'); +const temp = require('temp'); +const Directory = require('../src/directory'); +const PathWatcher = require('../src/main'); +require('./spec-helper.js'); + +describe('Directory', () => { + let directory; + let isCaseInsensitiveSpy; + let didSpy = false; + + beforeEach(() => { + // TODO: There's got to be a better way to do this. + if (!didSpy) { + isCaseInsensitiveSpy = spyOn(fs, 'isCaseInsensitive'); + didSpy = true; + } + directory = new Directory(path.join(__dirname, 'fixtures')); + }); + + afterEach(() => { + PathWatcher.closeAllWatchers(); + isCaseInsensitiveSpy.and.callThrough(); + }); + + it('normalizes the specified path', () => { + let filePath = path.join(directory.path, 'abc', '..'); + let otherDirectory = new Directory(filePath); + expect(otherDirectory.getBaseName()).toBe('fixtures'); + expect(otherDirectory.path.toLowerCase()).toBe(directory.path.toLowerCase()); + + otherDirectory = new Directory(`${directory.path}${path.sep}`); + expect(otherDirectory.getBaseName()).toBe('fixtures'); + expect(otherDirectory.path.toLowerCase()).toBe(directory.path.toLowerCase()); + + otherDirectory = new Directory(path.sep); + expect(otherDirectory.getBaseName()).toBe(''); + expect(otherDirectory.path).toBe(path.sep); + }); + + it('returns false from ::isFile', () => { + expect(directory.isFile()).toBe(false); + }); + + it('returns true from ::isDirectory', () => { + expect(directory.isDirectory()).toBe(true); + }); + + describe('::isSymbolicLink', () => { + it('returns false for regular directories', () => { + expect(directory.isSymbolicLink()).toBe(false); + }); + + it('returns true for symlinked directories', () => { + let symbolicDirectory = new Directory(path.join(__dirname, 'fixtures'), true); + expect(symbolicDirectory.isSymbolicLink()).toBe(true); + }); + }); + + describe('::exists', () => { + let tempDir; + + beforeEach(() => { + tempDir = temp.mkdirSync('node-pathwatcher-directory'); + }); + + it('resolves to true for a directory that exists', async () => { + directory = new Directory(tempDir); + expect( + await directory.exists() + ).toBe(true); + }); + + it('resolves to false for a directory that doesn’t exist', async () => { + directory = new Directory(path.join(tempDir, 'foo')); + expect( + await directory.exists() + ).toBe(false); + }); + }); + + describe('::existsSync', () => { + let tempDir; + + beforeEach(() => { + tempDir = temp.mkdirSync('node-pathwatcher-directory'); + }); + + it('returns true for a directory that exists', () => { + directory = new Directory(tempDir); + expect( + directory.existsSync() + ).toBe(true); + }); + + it('returns false for a directory that doesn’t exist', () => { + directory = new Directory(path.join(tempDir, 'foo')); + expect( + directory.existsSync() + ).toBe(false); + }); + }); + + describe('::create', () => { + let tempDir; + + beforeEach(() => { + tempDir = temp.mkdirSync('node-pathwatcher-directory'); + }); + + it('creates the directory if the directory doesn’t exist', async () => { + let directoryName = path.join(tempDir, 'subdir'); + expect(fs.existsSync(directoryName)).toBe(false); + let nonExistentDirectory = new Directory(directoryName); + + let didCreate = await nonExistentDirectory.create(0o0600); + + expect(didCreate).toBe(true); + expect(fs.existsSync(directoryName)).toBe(true); + expect(fs.isDirectorySync(directoryName)).toBe(true); + + if (process.platform === 'win32') return; + + let rawMode = fs.statSync(directoryName).mode; + mode = rawMode & 0o07777; + expect(mode.toString(8)).toBe((0o0600).toString(8)); + }); + + it('leaves an existing directory alone', async () => { + let directoryName = path.join(tempDir, 'subdir'); + fs.mkdirSync(directoryName); + let existingDirectory = new Directory(directoryName); + + let didCreate = await existingDirectory.create(); + + expect(didCreate).toBe(false); + expect(fs.existsSync(directoryName)).toBe(true); + expect(fs.isDirectorySync(directoryName)).toBe(true); + }); + + it('creates parent directories if they don’t exist', async () => { + let directoryName = path.join(tempDir, 'foo', 'bar', 'baz'); + expect(fs.existsSync(directoryName)).toBe(false); + let nonExistentDirectory = new Directory(directoryName); + + let didCreate = await nonExistentDirectory.create(0o0600); + + expect(didCreate).toBe(true); + expect(fs.existsSync(directoryName)).toBe(true); + expect(fs.isDirectorySync(directoryName)).toBe(true); + + let parentName = path.join(tempDir, 'foo', 'bar'); + expect(fs.existsSync(parentName)).toBe(true); + expect(fs.isDirectorySync(parentName)).toBe(true); + }); + + it('throws an error when called on a root directory that does not exist', async () => { + spyOn(Directory.prototype, 'isRoot').and.returnValue(true); + let directory = new Directory(path.join(tempDir, 'subdir')); + + expect(directory.isRoot()).toBe(true); + + try { + await directory.create(); + expect(false).toBe(true); + } catch (err) { + expect(true).toBe(true); + } + + expect(fs.existsSync(path.join(tempDir, 'subdir'))).toBe(false); + }); + }); + + describe('when the contents of the directory change on disk', () => { + let temporaryFilePath; + + beforeEach(() => { + temporaryFilePath = path.join(__dirname, 'fixtures', 'temporary'); + fs.removeSync(temporaryFilePath); + }); + + afterEach(() => { + fs.removeSync(temporaryFilePath); + }); + + it('notifies ::onDidChange observers', async () => { + let handler = jasmine.createSpy('changeHandler'); + directory.onDidChange(handler); + fs.writeFileSync(temporaryFilePath, ''); + + await condition(() => handler.calls.count() > 0); + + handler.calls.reset(); + fs.removeSync(temporaryFilePath); + + await condition(() => handler.calls.count() > 0); + }); + }); + + describe('when the directory unsubscribes from events', () => { + let temporaryFilePath; + + beforeEach(() => { + temporaryFilePath = path.join(__dirname, 'fixtures', 'temporary'); + if (fs.existsSync(temporaryFilePath)) { + fs.removeSync(temporaryFilePath); + } + }); + + afterEach(() => { + if (fs.existsSync(temporaryFilePath)) { + fs.removeSync(temporaryFilePath); + } + }); + + it('no longer triggers events', async () => { + let changeHandler = jasmine.createSpy('changeHandler'); + let subscription = directory.onDidChange(changeHandler); + + fs.writeFileSync(temporaryFilePath, ''); + + await condition(() => changeHandler.calls.count() > 0); + + changeHandler.calls.reset(); + subscription.dispose(); + + await wait(20); + + fs.removeSync(temporaryFilePath); + await wait(20); + expect(changeHandler.calls.count()).toBe(0); + }); + }); + + if (process.platform !== 'win32') { + describe('on #darwin or #linux', () => { + it('includes symlink information about entries', async () => { + let entries = directory.getEntriesSync(); + for (let entry of entries) { + let name = entry.getBaseName(); + if (name === 'symlink-to-dir' || name === 'symlink-to-file') { + expect(entry.symlink).toBeTruthy(); + } else { + expect(entry.symlink).toBeFalsy(); + } + } + + let callback = jasmine.createSpy('getEntries'); + directory.getEntries(callback); + + await condition(() => callback.calls.count() === 1); + + entries = callback.calls.mostRecent().args[1]; + for (let entry of entries) { + let name = entry.getBaseName(); + if (name === 'symlink-to-dir' || name === 'symlink-to-file') { + expect(entry.symlink).toBeTruthy(); + } else { + expect(entry.symlink).toBeFalsy(); + } + } + }); + }); + } + + describe('::relativize', () => { + if (process.platform !== 'win32') { + describe('on #darwin or #linux', () => { + it('returns a relative path based on the directory’s path', () => { + let absolutePath = directory.getPath(); + expect(directory.relativize(absolutePath)).toBe(''); + expect(directory.relativize(path.join(absolutePath, 'b'))).toBe('b') + expect(directory.relativize(path.join(absolutePath, 'b/file.coffee'))).toBe('b/file.coffee'); + expect(directory.relativize(path.join(absolutePath, "file.coffee"))).toBe('file.coffee'); + }); + + it('returns a relative path based on the directory’s symlinked source path', () => { + let symlinkPath = path.join(__dirname, 'fixtures', 'symlink-to-dir'); + let symlinkDirectory = new Directory(symlinkPath); + let realFilePath = require.resolve('./fixtures/dir/a'); + expect(symlinkDirectory.relativize(symlinkPath)).toBe(''); + expect(symlinkDirectory.relativize(realFilePath)).toBe('a'); + }); + + it('returns the full path if the directory’s path is not a prefix of the path', () => { + expect(directory.relativize('/not/relative')).toBe('/not/relative'); + }); + + it('handles case-insensitive filesystems', () => { + isCaseInsensitiveSpy.and.returnValue(true); + let directoryPath = temp.mkdirSync('Mixed-case-directory-') + let directory = new Directory(directoryPath) + + expect(directory.relativize(directoryPath.toUpperCase())).toBe(""); + expect(directory.relativize(path.join(directoryPath.toUpperCase(), "b"))).toBe("b"); + expect(directory.relativize(path.join(directoryPath.toUpperCase(), "B"))).toBe("B"); + expect(directory.relativize(path.join(directoryPath.toUpperCase(), "b/file.coffee"))).toBe("b/file.coffee"); + expect(directory.relativize(path.join(directoryPath.toUpperCase(), "file.coffee"))).toBe("file.coffee"); + + expect(directory.relativize(directoryPath.toLowerCase())).toBe(""); + expect(directory.relativize(path.join(directoryPath.toLowerCase(), "b"))).toBe("b"); + expect(directory.relativize(path.join(directoryPath.toLowerCase(), "B"))).toBe("B"); + expect(directory.relativize(path.join(directoryPath.toLowerCase(), "b/file.coffee"))).toBe("b/file.coffee"); + expect(directory.relativize(path.join(directoryPath.toLowerCase(), "file.coffee"))).toBe("file.coffee"); + + expect(directory.relativize(directoryPath)).toBe(""); + expect(directory.relativize(path.join(directoryPath, "b"))).toBe("b"); + expect(directory.relativize(path.join(directoryPath, "B"))).toBe("B"); + expect(directory.relativize(path.join(directoryPath, "b/file.coffee"))).toBe("b/file.coffee"); + expect(directory.relativize(path.join(directoryPath, "file.coffee"))).toBe("file.coffee"); + }); + }); + } // end #darwin/#linux + + if (process.platform === 'win32') { + describe('on #win32', () => { + it('returns a relative path based on the directory’s path', () => { + let absolutePath = directory.getPath(); + expect(directory.relativize(absolutePath)).toBe(''); + expect(directory.relativize(path.join(absolutePath, 'b'))).toBe('b') + expect(directory.relativize(path.join(absolutePath, 'b/file.coffee'))).toBe('b\\file.coffee'); + expect(directory.relativize(path.join(absolutePath, "file.coffee"))).toBe('file.coffee'); + }); + + it('returns the full path if the directory’s path is not a prefix of the path', () => { + expect(directory.relativize('/not/relative')).toBe("\\not\\relative"); + }); + }); + } + }); + + describe('::resolve', () => { + describe('when passed an absolute or relative path', () => { + it('returns an absolute path based on the directory’s path', () => { + let absolutePath = require.resolve('./fixtures/dir/a'); + expect(directory.resolve('dir/a')).toBe(absolutePath); + expect(directory.resolve(absolutePath + '/../a')).toBe(absolutePath) + expect(directory.resolve('dir/a/../a')).toBe(absolutePath) + expect(directory.resolve()).toBeUndefined() + }); + }); + + describe('when passed a URI with a scheme', () => { + it('does not modify URIs that begin with a scheme', () => { + expect(directory.resolve('http://zombo.com')).toBe('http://zombo.com'); + }); + }); + }); + + describe('::contains', () => { + it('returns true if the path is a child of the directory’s path', () => { + let absolutePath = directory.getPath(); + + expect(directory.contains(path.join(absolutePath))).toBe(false); + expect(directory.contains(path.join(absolutePath, "b"))).toBe(true); + expect(directory.contains(path.join(absolutePath, "b", "file.coffee"))).toBe(true); + expect(directory.contains(path.join(absolutePath, "file.coffee"))).toBe(true); + }); + + it('returns false if the directory’s path is not a prefix of the path', () => { + expect(directory.contains('/not/relative')).toBe(false); + }); + + it('handles case-insensitive filesystems', () => { + isCaseInsensitiveSpy.and.returnValue(true); + let directoryPath = temp.mkdirSync('Mixed-case-directory-') + let directory = new Directory(directoryPath) + + expect(directory.contains(directoryPath.toUpperCase())).toBe(false); + expect(directory.contains(path.join(directoryPath.toUpperCase(), "b"))).toBe(true); + expect(directory.contains(path.join(directoryPath.toUpperCase(), "B"))).toBe(true); + expect(directory.contains(path.join(directoryPath.toUpperCase(), "b", "file.coffee"))).toBe(true); + expect(directory.contains(path.join(directoryPath.toUpperCase(), "file.coffee"))).toBe(true); + + expect(directory.contains(directoryPath.toLowerCase())).toBe(false); + expect(directory.contains(path.join(directoryPath.toLowerCase(), "b"))).toBe(true); + expect(directory.contains(path.join(directoryPath.toLowerCase(), "B"))).toBe(true); + expect(directory.contains(path.join(directoryPath.toLowerCase(), "b", "file.coffee"))).toBe(true); + expect(directory.contains(path.join(directoryPath.toLowerCase(), "file.coffee"))).toBe(true); + + expect(directory.contains(directoryPath)).toBe(false); + expect(directory.contains(path.join(directoryPath, "b"))).toBe(true); + expect(directory.contains(path.join(directoryPath, "B"))).toBe(true); + expect(directory.contains(path.join(directoryPath, "b", "file.coffee"))).toBe(true); + expect(directory.contains(path.join(directoryPath, "file.coffee"))).toBe(true); + }); + + if (process.platform !== 'win32') { + describe('on #darwin or #linux', () => { + it('returns true if the path is a child of the directory’s symlinked source path', () => { + let symlinkPath = path.join(__dirname, 'fixtures', 'symlink-to-dir'); + let symlinkDirectory = new Directory(symlinkPath); + let realFilePath = require.resolve('./fixtures/dir/a'); + expect(symlinkDirectory.contains(realFilePath)).toBe(true); + }); + }); + } + + describe('traversal', () => { + beforeEach(() => { + directory = new Directory(path.join(__dirname, 'fixtures', 'dir')); + }); + + function fixturePath (...parts) { + return path.join(__dirname, 'fixtures', ...parts); + } + + describe('::getFile', () => { + it('returns a File within this directory', () => { + let f = directory.getFile('a'); + expect(f.isFile()).toBe(true); + expect(f.getRealPathSync()).toBe(fixturePath('dir', 'a')); + }); + + it('can descend more than one directory at a time', () => { + let f = directory.getFile('subdir', 'b'); + expect(f.isFile()).toBe(true); + expect(f.getRealPathSync()).toBe(fixturePath('dir', 'subdir', 'b')); + }); + + it('doesn’t have to exist', () => { + let f = directory.getFile('the-silver-bullet'); + expect(f.isFile()).toBe(true); + expect(f.existsSync()).toBe(false); + }); + }); + + describe('::getSubdirectory', () => { + it('returns a subdirectory within this directory', () => { + let d = directory.getSubdirectory('subdir'); + expect(d.isDirectory()).toBe(true); + expect(d.getRealPathSync()).toBe(fixturePath('dir', 'subdir')); + }); + + it('can descend more than one directory at a time', () => { + let d = directory.getSubdirectory('subdir', 'subsubdir'); + expect(d.isDirectory()).toBe(true); + expect(d.getRealPathSync()).toBe(fixturePath('dir', 'subdir', 'subsubdir')); + }); + + it('doesn’t have to exist', () => { + let d = directory.getSubdirectory("why-would-you-call-a-directory-this-come-on-now"); + expect(d.isDirectory()).toBe(true); + }); + }); + + describe('::getParent', () => { + it('returns the parent Directory', () => { + let d = directory.getParent(); + expect(d.isDirectory()).toBe(true); + expect(d.getRealPathSync()).toBe(fixturePath()); + }); + }); + + describe('::isRoot', () => { + it('returns false if the Directory isn’t the root', () => { + expect(directory.isRoot()).toBe(false); + }); + + it('returns true if the Directory is the root', () => { + let current = directory; + let previous = null; + while (current.getPath() !== previous?.getPath()) { + previous = current; + current = current.getParent(); + } + expect(current.isRoot()).toBe(true); + }); + }); + }); + }); +}); diff --git a/spec/file-spec.coffee b/spec/file-spec.coffee deleted file mode 100644 index bad6b09..0000000 --- a/spec/file-spec.coffee +++ /dev/null @@ -1,515 +0,0 @@ -path = require 'path' -fs = require 'fs-plus' -temp = require 'temp' -File = require '../lib/file' -PathWatcher = require '../lib/main' - -describe 'File', -> - [filePath, file] = [] - - beforeEach -> - filePath = path.join(__dirname, 'fixtures', 'file-test.txt') # Don't put in /tmp because /tmp symlinks to /private/tmp and screws up the rename test - fs.removeSync(filePath) - fs.writeFileSync(filePath, "this is old!") - file = new File(filePath) - - afterEach -> - file.unsubscribeFromNativeChangeEvents() - fs.removeSync(filePath) - PathWatcher.closeAllWatchers() - - it "normalizes the specified path", -> - expect(new File(__dirname + path.sep + 'fixtures' + path.sep + 'abc' + path.sep + '..' + path.sep + 'file-test.txt').getBaseName()).toBe 'file-test.txt' - expect(new File(__dirname + path.sep + 'fixtures' + path.sep + 'abc' + path.sep + '..' + path.sep + 'file-test.txt').path.toLowerCase()).toBe file.path.toLowerCase() - - it 'returns true from isFile()', -> - expect(file.isFile()).toBe true - - it 'returns false from isDirectory()', -> - expect(file.isDirectory()).toBe false - - describe '::isSymbolicLink', -> - it 'returns false for regular files', -> - expect(file.isSymbolicLink()).toBe false - - it 'returns true for symlinked files', -> - symbolicFile = new File(filePath, true) - expect(symbolicFile.isSymbolicLink()).toBe true - - describe "::getDigestSync", -> - it "computes and returns the SHA-1 digest and caches it", -> - filePath = path.join(temp.mkdirSync('node-pathwatcher-directory'), 'file.txt') - fs.writeFileSync(filePath, '') - - file = new File(filePath) - spyOn(file, 'readSync').andCallThrough() - - expect(file.getDigestSync()).toBe 'da39a3ee5e6b4b0d3255bfef95601890afd80709' - expect(file.readSync.callCount).toBe 1 - expect(file.getDigestSync()).toBe 'da39a3ee5e6b4b0d3255bfef95601890afd80709' - expect(file.readSync.callCount).toBe 1 - - file.writeSync('x') - - expect(file.getDigestSync()).toBe '11f6ad8ec52a2984abaafd7c3b516503785c2072' - expect(file.readSync.callCount).toBe 1 - expect(file.getDigestSync()).toBe '11f6ad8ec52a2984abaafd7c3b516503785c2072' - expect(file.readSync.callCount).toBe 1 - - describe '::create()', -> - [callback, nonExistentFile, tempDir] = [] - - beforeEach -> - tempDir = temp.mkdirSync('node-pathwatcher-directory') - callback = jasmine.createSpy('promiseCallback') - - afterEach -> - nonExistentFile.unsubscribeFromNativeChangeEvents() - fs.removeSync(nonExistentFile.getPath()) - - it 'creates file in directory if file does not exist', -> - fileName = path.join(tempDir, 'file.txt') - expect(fs.existsSync(fileName)).toBe false - nonExistentFile = new File(fileName) - - waitsForPromise -> - nonExistentFile.create().then(callback) - - runs -> - expect(callback.argsForCall[0][0]).toBe true - expect(fs.existsSync(fileName)).toBe true - expect(fs.isFileSync(fileName)).toBe true - expect(fs.readFileSync(fileName).toString()).toBe '' - - it 'leaves existing file alone if it exists', -> - fileName = path.join(tempDir, 'file.txt') - fs.writeFileSync(fileName, 'foo') - existingFile = new File(fileName) - - waitsForPromise -> - existingFile.create().then(callback) - - runs -> - expect(callback.argsForCall[0][0]).toBe false - expect(fs.existsSync(fileName)).toBe true - expect(fs.isFileSync(fileName)).toBe true - expect(fs.readFileSync(fileName).toString()).toBe 'foo' - - it 'creates parent directories and file if they do not exist', -> - fileName = path.join(tempDir, 'foo', 'bar', 'file.txt') - expect(fs.existsSync(fileName)).toBe false - nonExistentFile = new File(fileName) - - waitsForPromise -> - nonExistentFile.create().then(callback) - - runs -> - expect(callback.argsForCall[0][0]).toBe true - expect(fs.existsSync(fileName)).toBe true - expect(fs.isFileSync(fileName)).toBe true - - parentName = path.join(tempDir, 'foo' ,'bar') - expect(fs.existsSync(parentName)).toBe true - expect(fs.isDirectorySync(parentName)).toBe true - - describe "when the file has not been read", -> - describe "when the contents of the file change", -> - it "notifies ::onDidChange observers", -> - file.onDidChange changeHandler = jasmine.createSpy('changeHandler') - fs.writeFileSync(file.getPath(), "this is new!") - - waitsFor "change event", -> - changeHandler.callCount > 0 - - describe "when the contents of the file are deleted", -> - it "notifies ::onDidChange observers", -> - file.onDidChange changeHandler = jasmine.createSpy('changeHandler') - fs.writeFileSync(file.getPath(), "") - - waitsFor "change event", -> - changeHandler.callCount > 0 - - describe "when the file has already been read #darwin", -> - beforeEach -> - file.readSync() - - describe "when the contents of the file change", -> - it "notifies ::onDidChange observers", -> - lastText = null - - file.onDidChange -> - file.read().then (text) -> - lastText = text - - runs -> fs.writeFileSync(file.getPath(), 'this is new!') - waitsFor 'read after first change event', -> lastText is 'this is new!' - runs -> expect(file.readSync()).toBe('this is new!') - - runs -> fs.writeFileSync(file.getPath(), 'this is newer!') - waitsFor 'read after second change event', -> lastText is 'this is newer!' - runs -> expect(file.readSync()).toBe('this is newer!') - - describe "when the file is deleted", -> - it "notifies ::onDidDelete observers", -> - deleteHandler = jasmine.createSpy('deleteHandler') - file.onDidDelete(deleteHandler) - fs.removeSync(file.getPath()) - - waitsFor "remove event", -> - deleteHandler.callCount > 0 - - describe "when a file is moved (via the filesystem)", -> - newPath = null - - beforeEach -> - newPath = path.join(path.dirname(filePath), "file-was-moved-test.txt") - - afterEach -> - if fs.existsSync(newPath) - fs.removeSync(newPath) - deleteHandler = jasmine.createSpy('deleteHandler') - file.onDidDelete(deleteHandler) - waitsFor "remove event", 30000, -> deleteHandler.callCount > 0 - - it "it updates its path", -> - moveHandler = jasmine.createSpy('moveHandler') - file.onDidRename moveHandler - - fs.moveSync(filePath, newPath) - - waitsFor "move event", 30000, -> - moveHandler.callCount > 0 - - runs -> - expect(file.getPath()).toBe newPath - - it "maintains ::onDidChange observers that were subscribed on the previous path", -> - moveHandler = null - moveHandler = jasmine.createSpy('moveHandler') - file.onDidRename moveHandler - changeHandler = null - changeHandler = jasmine.createSpy('changeHandler') - file.onDidChange changeHandler - - fs.moveSync(filePath, newPath) - - waitsFor "move event", -> - moveHandler.callCount > 0 - - runs -> - expect(changeHandler).not.toHaveBeenCalled() - fs.writeFileSync(file.getPath(), "this is new!") - - waitsFor "change event", -> - changeHandler.callCount > 0 - - describe "when a file is deleted and the recreated within a small amount of time (git sometimes does this)", -> - it "triggers a contents change event if the contents change", -> - changeHandler = jasmine.createSpy("file changed") - deleteHandler = jasmine.createSpy("file deleted") - file.onDidChange changeHandler - file.onDidDelete deleteHandler - - expect(changeHandler).not.toHaveBeenCalled() - - fs.removeSync(filePath) - - expect(changeHandler).not.toHaveBeenCalled() - waits 20 - runs -> - fs.writeFileSync(filePath, "HE HAS RISEN!") - expect(changeHandler).not.toHaveBeenCalled() - - waitsFor "resurrection change event", -> - changeHandler.callCount == 1 - - runs -> - expect(deleteHandler).not.toHaveBeenCalled() - fs.writeFileSync(filePath, "Hallelujah!") - changeHandler.reset() - - waitsFor "post-resurrection change event", -> - changeHandler.callCount > 0 - - describe "when a file is moved to the trash", -> - osxTrashDir = process.env.HOME + "/.Trash" - osxTrashPath = path.join(osxTrashDir, "file-was-moved-to-trash.txt") - it "triggers a delete event", -> - deleteHandler = null - deleteHandler = jasmine.createSpy("deleteHandler") - file.onDidDelete(deleteHandler) - - fs.moveSync(filePath, osxTrashPath) - - waitsFor "remove event", -> - deleteHandler.callCount > 0 - - # Clean up - if fs.existsSync(osxTrashPath) - fs.removeSync(osxTrashPath) - - describe "when a file cannot be opened after the watch has been applied", -> - errorSpy = null - beforeEach -> - errorSpy = jasmine.createSpy() - errorSpy.andCallFake ({error, handle})-> - handle() - file.onWillThrowWatchError errorSpy - - describe "when the error happens in the promise callback chain", -> - beforeEach -> - spyOn(file, 'setDigest').andCallFake -> - error = new Error('ENOENT open "FUUU"') - error.code = 'ENOENT' - throw error - - it "emits an event with the error", -> - changeHandler = jasmine.createSpy('changeHandler') - file.onDidChange changeHandler - fs.writeFileSync(file.getPath(), "this is new!!") - - waitsFor "change event", -> - errorSpy.callCount > 0 - - runs -> - args = errorSpy.mostRecentCall.args[0] - expect(args.error.code).toBe 'ENOENT' - expect(args.error.eventType).toBe 'change' - expect(args.handle).toBeTruthy() - - describe "when the error happens in the read method", -> - beforeEach -> - spyOn(file, 'read').andCallFake -> - error = new Error('ENOENT open "FUUU"') - error.code = 'ENOENT' - throw error - - it "emits an event with the error", -> - changeHandler = jasmine.createSpy('changeHandler') - file.onDidChange changeHandler - fs.writeFileSync(file.getPath(), "this is new!!") - - waitsFor "change event", -> - errorSpy.callCount > 0 - - runs -> - args = errorSpy.mostRecentCall.args[0] - expect(args.error.code).toBe 'ENOENT' - expect(args.error.eventType).toBe 'change' - expect(args.handle).toBeTruthy() - - describe "getRealPathSync()", -> - tempDir = null - - beforeEach -> - tempDir = temp.mkdirSync('node-pathwatcher-directory') - fs.writeFileSync(path.join(tempDir, 'file'), '') - fs.writeFileSync(path.join(tempDir, 'file2'), '') - - it "returns the resolved path to the file", -> - tempFile = new File(path.join(tempDir, 'file')) - expect(tempFile.getRealPathSync()).toBe fs.realpathSync(path.join(tempDir, 'file')) - tempFile.setPath(path.join(tempDir, 'file2')) - expect(tempFile.getRealPathSync()).toBe fs.realpathSync(path.join(tempDir, 'file2')) - - describe "on #darwin and #linux", -> - it "returns the target path for symlinks", -> - fs.symlinkSync(path.join(tempDir, 'file2'), path.join(tempDir, 'file3')) - tempFile = new File(path.join(tempDir, 'file3')) - expect(tempFile.getRealPathSync()).toBe fs.realpathSync(path.join(tempDir, 'file2')) - - describe "exists()", -> - tempDir = null - - beforeEach -> - tempDir = temp.mkdirSync('node-pathwatcher-directory') - fs.writeFileSync(path.join(tempDir, 'file'), '') - - it "does actually exist", -> - existingFile = new File(path.join(tempDir, 'file')) - existsHandler = jasmine.createSpy('exists handler') - existingFile.exists().then(existsHandler) - waitsFor 'exists handler', -> - existsHandler.callCount > 0 - runs -> - expect(existsHandler.argsForCall[0][0]).toBe(true) - - it "doesn't exist", -> - nonExistingFile = new File(path.join(tempDir, 'not_file')) - existsHandler = jasmine.createSpy('exists handler') - nonExistingFile.exists().then(existsHandler) - waitsFor 'exists handler', -> - existsHandler.callCount > 0 - runs -> - expect(existsHandler.argsForCall[0][0]).toBe(false) - - describe "getRealPath()", -> - tempDir = null - - beforeEach -> - tempDir = temp.mkdirSync('node-pathwatcher-directory') - fs.writeFileSync(path.join(tempDir, 'file'), '') - fs.writeFileSync(path.join(tempDir, 'file2'), '') - - it "returns the resolved path to the file", -> - tempFile = new File(path.join(tempDir, 'file')) - realpathHandler = jasmine.createSpy('realpath handler') - tempFile.getRealPath().then(realpathHandler) - waitsFor 'realpath handler', -> - realpathHandler.callCount > 0 - runs -> - expect(realpathHandler.argsForCall[0][0]).toBe fs.realpathSync(path.join(tempDir, 'file')) - - it "returns the resolved path to the file after setPath", -> - tempFile = new File(path.join(tempDir, 'file')) - tempFile.setPath(path.join(tempDir, 'file2')) - realpathHandler = jasmine.createSpy('realpath handler') - tempFile.getRealPath().then(realpathHandler) - waitsFor 'realpath handler', -> - realpathHandler.callCount > 0 - runs -> - expect(realpathHandler.argsForCall[0][0]).toBe fs.realpathSync(path.join(tempDir, 'file2')) - - describe "on #darwin and #linux", -> - it "returns the target path for symlinks", -> - fs.symlinkSync(path.join(tempDir, 'file2'), path.join(tempDir, 'file3')) - tempFile = new File(path.join(tempDir, 'file3')) - realpathHandler = jasmine.createSpy('realpath handler') - tempFile.getRealPath().then(realpathHandler) - waitsFor 'realpath handler', -> - realpathHandler.callCount > 0 - runs -> - expect(realpathHandler.argsForCall[0][0]).toBe fs.realpathSync(path.join(tempDir, 'file2')) - - describe "getParent()", -> - it "gets the parent Directory", -> - d = file.getParent() - expected = path.join __dirname, 'fixtures' - expect(d.getRealPathSync()).toBe(expected) - - describe 'encoding', -> - it "should be 'utf8' by default", -> - expect(file.getEncoding()).toBe('utf8') - - it "should be settable", -> - file.setEncoding('cp1252') - expect(file.getEncoding()).toBe('cp1252') - - it "throws an exception when assigning an invalid encoding", -> - expect(-> - file.setEncoding('utf-8-bom') - ).toThrow() - - describe 'createReadStream()', -> - it 'returns a stream to read the file', -> - stream = file.createReadStream() - ended = false - content = [] - - stream.on 'data', (chunk) -> content.push(chunk) - stream.on 'end', -> ended = true - - waitsFor 'stream ended', -> ended - - runs -> - expect(content.join('')).toEqual('this is old!') - - it 'honors the specified encoding', -> - unicodeText = 'ё' - unicodeBytes = Buffer.from('\x51\x04') # 'ё' - - fs.writeFileSync(file.getPath(), unicodeBytes) - - file.setEncoding('utf16le') - - stream = file.createReadStream() - ended = false - content = [] - - stream.on 'data', (chunk) -> content.push(chunk) - stream.on 'end', -> ended = true - - waitsFor 'stream ended', -> ended - - runs -> - expect(content.join('')).toEqual(unicodeText) - - describe 'createWriteStream()', -> - it 'returns a stream to read the file', -> - unicodeText = 'ё' - unicodeBytes = Buffer.from('\x51\x04') # 'ё' - - file.setEncoding('utf16le') - stream = file.createWriteStream() - ended = false - - stream.on 'finish', -> ended = true - - stream.end(unicodeText) - - waitsFor 'stream finished', -> ended - - runs -> - expect(fs.statSync(file.getPath()).size).toBe(2) - content = fs.readFileSync(file.getPath()).toString('ascii') - expect(content).toBe(unicodeBytes.toString('ascii')) - - describe 'encoding support', -> - [unicodeText, unicodeBytes] = [] - - beforeEach -> - unicodeText = 'ё' - unicodeBytes = Buffer.from('\x51\x04') # 'ё' - - it 'should read a file in UTF-16', -> - fs.writeFileSync(file.getPath(), unicodeBytes) - file.setEncoding('utf16le') - - readHandler = jasmine.createSpy('read handler') - file.read().then(readHandler) - - waitsFor 'read handler', -> - readHandler.callCount > 0 - - runs -> - expect(readHandler.argsForCall[0][0]).toBe(unicodeText) - - it 'should readSync a file in UTF-16', -> - fs.writeFileSync(file.getPath(), unicodeBytes) - file.setEncoding('utf16le') - expect(file.readSync()).toBe(unicodeText) - - it 'should write a file in UTF-16', -> - file.setEncoding('utf16le') - writeHandler = jasmine.createSpy('write handler') - file.write(unicodeText).then(writeHandler) - waitsFor 'write handler', -> - writeHandler.callCount > 0 - runs -> - expect(fs.statSync(file.getPath()).size).toBe(2) - content = fs.readFileSync(file.getPath()).toString('ascii') - expect(content).toBe(unicodeBytes.toString('ascii')) - - it 'should write a file in UTF-16 synchronously', -> - file.setEncoding('utf16le') - file.writeSync(unicodeText) - expect(fs.statSync(file.getPath()).size).toBe(2) - content = fs.readFileSync(file.getPath()).toString('ascii') - expect(content).toBe(unicodeBytes.toString('ascii')) - - describe 'reading a non-existing file', -> - it 'should return null', -> - file = new File('not_existing.txt') - readHandler = jasmine.createSpy('read handler') - file.read().then(readHandler) - waitsFor 'read handler', -> - readHandler.callCount > 0 - runs -> - expect(readHandler.argsForCall[0][0]).toBe(null) - - describe 'writeSync()', -> - it 'emits did-change event', -> - file.onDidChange writeHandler = jasmine.createSpy('write handler') - file.writeSync('ok') - waitsFor 'write handler', -> - writeHandler.callCount > 0 diff --git a/spec/file-spec.js b/spec/file-spec.js new file mode 100644 index 0000000..e019cde --- /dev/null +++ b/spec/file-spec.js @@ -0,0 +1,522 @@ +const path = require('path'); +const fs = require('fs-plus'); +const temp = require('temp'); +const File = require('../src/file'); +const PathWatcher = require('../src/main'); +require('./spec-helper.js'); + +describe('File', () => { + let filePath; + let file; + + beforeEach(() => { + // Don't put in /tmp because /tmp symlinks to /private/tmp and screws up + // the rename test + filePath = path.join(__dirname, 'fixtures', 'file-test.txt'); + fs.removeSync(filePath); + fs.writeFileSync(filePath, 'this is old!'); + file = new File(filePath); + }); + + afterEach(() => { + file.unsubscribeFromNativeChangeEvents(); + fs.removeSync(filePath); + PathWatcher.closeAllWatchers(); + }); + + it('normalizes the specified path', () => { + let fileName = path.join(__dirname, 'fixtures', 'abc', '..', 'file-test.txt'); + let f = new File(fileName); + + expect(f.getBaseName()).toBe('file-test.txt'); + expect(f.path.toLowerCase()).toBe(file.path.toLowerCase()); + }); + + it('returns true from isFile()', () => { + expect(file.isFile()).toBe(true); + }); + + it('returns false from isDirectory()', () => { + expect(file.isDirectory()).toBe(false); + }); + + describe('::isSymbolicLink', () => { + it('returns false for regular files', () => { + expect(file.isSymbolicLink()).toBe(false); + }); + + it('returns true for symlinked files', () => { + let symbolicFile = new File(filePath, true); + expect(symbolicFile.isSymbolicLink()).toBe(true); + }); + }); + + describe('::getDigestSync', () => { + it('computes and returns the SHA-1 digest and caches it', () => { + filePath = path.join(temp.mkdirSync('node-pathwatcher-directory'), 'file.txt'); + fs.writeFileSync(filePath, ''); + file = new File(filePath); + spyOn(file, 'readSync').and.callThrough(); + + expect( + file.getDigestSync() + ).toBe('da39a3ee5e6b4b0d3255bfef95601890afd80709'); + expect(file.readSync.calls.count()).toBe(1); + expect( + file.getDigestSync() + ).toBe('da39a3ee5e6b4b0d3255bfef95601890afd80709'); + expect(file.readSync.calls.count()).toBe(1); + + file.writeSync('x'); + + expect( + file.getDigestSync() + ).toBe('11f6ad8ec52a2984abaafd7c3b516503785c2072'); + expect(file.readSync.calls.count()).toBe(1); + expect( + file.getDigestSync() + ).toBe('11f6ad8ec52a2984abaafd7c3b516503785c2072'); + expect(file.readSync.calls.count()).toBe(1); + }); + }); + + describe('::create()', () => { + let nonExistentFile; + let tempDir; + + beforeEach(() => { + tempDir = temp.mkdirSync('node-pathwatcher-directory'); + }); + + afterEach(() => { + nonExistentFile?.unsubscribeFromNativeChangeEvents(); + if (nonExistentFile?.getPath()) { + fs.removeSync(nonExistentFile?.getPath()); + } + }); + + it('creates file in directory if file does not exist', async () => { + fileName = path.join(tempDir, 'file.txt'); + expect(fs.existsSync(fileName)).toBe(false); + nonExistentFile = new File(fileName); + + let didCreate = await nonExistentFile.create(); + expect(didCreate).toBe(true); + expect(fs.existsSync(fileName)).toBe(true); + expect(fs.isFileSync(fileName)).toBe(true); + expect(fs.readFileSync(fileName).toString()).toBe(''); + }); + + it('leaves existing file alone if it exists', async () => { + fileName = path.join(tempDir, 'file.txt'); + fs.writeFileSync(fileName, 'foo'); + let existingFile = new File(fileName); + + let didCreate = await existingFile.create(); + expect(didCreate).toBe(false); + expect(fs.existsSync(fileName)).toBe(true); + expect(fs.isFileSync(fileName)).toBe(true); + expect(fs.readFileSync(fileName).toString()).toBe('foo'); + }); + + it('creates parent directories and files if they do not exist', async () => { + fileName = path.join(tempDir, 'foo', 'bar', 'file.txt'); + expect(fs.existsSync(fileName)).toBe(false); + nonExistentFile = new File(fileName); + + let didCreate = await nonExistentFile.create(); + expect(didCreate).toBe(true); + expect(fs.existsSync(fileName)).toBe(true); + expect(fs.isFileSync(fileName)).toBe(true); + + let parentName = path.join(tempDir, 'foo', 'bar'); + expect(fs.existsSync(parentName)).toBe(true); + expect(fs.isDirectorySync(parentName)).toBe(true); + }); + }); + + describe('when the file has not been read', () => { + describe('when the contents of the file change', () => { + it('notifies ::onDidChange observers', async () => { + let spy = jasmine.createSpy('changeHandler'); + let [promise, changeHandler] = makePromiseCallback(spy) + file.onDidChange(changeHandler); + fs.writeFileSync(file.getPath(), 'this is new!'); + await promise; + expect(spy.calls.count()).toBe(1); + }); + }); + + describe('when the contents of the file are deleted', () => { + it('notifies ::onDidChange observers', async () => { + let spy = jasmine.createSpy('changeHandler'); + let [promise, changeHandler] = makePromiseCallback(spy) + file.onDidChange(changeHandler); + fs.writeFileSync(file.getPath(), ''); + await promise; + expect(spy.calls.count()).toBe(1); + }); + }); + }); + + if (process.platform === 'darwin') { + describe('when the file has already been read', () => { + beforeEach(() => file.readSync()); + + describe('when the contents of the file change', () => { + it('notifies ::onDidChange observers', async () => { + let lastText = null; + + file.onDidChange(async () => { + lastText = await file.read(); + }); + + fs.writeFileSync(file.getPath(), 'this is new!'); + await condition(() => lastText === 'this is new!'); + expect(file.readSync()).toBe('this is new!'); + + fs.writeFileSync(file.getPath(), 'this is newer!'); + await condition(() => lastText === 'this is newer!'); + expect(file.readSync()).toBe('this is newer!'); + }); + }) + }); + + describe('when the file is deleted', () => { + it('notifies ::onDidDelete observers', async () => { + let deleteHandler = jasmine.createSpy('deleteHandler'); + file.onDidDelete(deleteHandler); + fs.removeSync(file.getPath()); + + await condition(() => deleteHandler.calls.count() > 0); + }); + }); + + describe('when a file is moved (via the filesystem)', () => { + let newPath = null; + + beforeEach(() => { + newPath = path.join(path.dirname(filePath), 'file-was-moved-test.txt'); + }); + + afterEach(async () => { + if (!fs.existsSync(newPath)) return; + fs.removeSync(newPath); + let deleteHandler = jasmine.createSpy('deleteHandler'); + file.onDidDelete(deleteHandler); + await condition(() => deleteHandler.calls.count() > 0, 30000); + }); + + it('updates its path', async () => { + let moveHandler = jasmine.createSpy('moveHandler'); + file.onDidRename(moveHandler); + + fs.moveSync(filePath, newPath); + + await condition(() => moveHandler.calls.count() > 0, 30000); + + expect(file.getPath()).toBe(newPath); + }); + + it('maintains ::onDidChange observers that were subscribed on the previous path', async () => { + let moveHandler = jasmine.createSpy('moveHandler'); + file.onDidRename(moveHandler); + + let changeHandler = jasmine.createSpy('changeHandler'); + file.onDidChange(changeHandler); + + fs.moveSync(filePath, newPath); + + await condition(() => moveHandler.calls.count() > 0); + + expect(changeHandler).not.toHaveBeenCalled(); + fs.writeFileSync(file.getPath(), 'this is new!'); + + await condition(() => changeHandler.calls.count() > 0); + }); + }); + + describe('when a file is deleted and then recreated within a small amount of time (git sometimes does this)', () => { + it('triggers a contents-changed event if the contents change', async () => { + let changeHandler = jasmine.createSpy('file changed'); + let deleteHandler = jasmine.createSpy('file deleted'); + file.onDidChange(changeHandler); + file.onDidDelete(deleteHandler); + + expect(changeHandler).not.toHaveBeenCalled(); + fs.removeSync(filePath); + expect(changeHandler).not.toHaveBeenCalled(); + + await wait(20); + + fs.writeFileSync(filePath, 'HE HAS RISEN!'); + expect(changeHandler).not.toHaveBeenCalled(); + + await condition(() => changeHandler.calls.count() === 1); + + expect(deleteHandler).not.toHaveBeenCalled(); + fs.writeFileSync(filePath, 'Hallelujah!'); + changeHandler.calls.reset(); + + await condition(() => changeHandler.calls.count() > 0); + }); + }); + + describe('when a file is moved to the trash', () => { + const MACOS_TRASH_DIR = path.join(process.env.HOME, '.Trash'); + let expectedTrashPath = path.join(MACOS_TRASH_DIR, 'file-was-moved-to-trash.txt'); + + it('triggers a delete event', async () => { + let deleteHandler = jasmine.createSpy('deleteHandler'); + file.onDidDelete(deleteHandler); + + fs.moveSync(filePath, expectedTrashPath); + + await condition(() => deleteHandler.calls.count() > 0); + + if (fs.existsSync(expectedTrashPath)) { + fs.removeSync(expectedTrashPath); + } + }); + }); + + // NOTE: We used to have tests for the ` onWillThrowWatchError` callback, + // but that callback was made a no-op many years ago. This seems to have + // been done for performance reasons, since there is no practical way to + // detect errors of the sort that were thrown via `onWillThrowWatchError` + // without re-reading the entire file whenever a change is detected. + + + } // end darwin-only tests + + describe('::getRealPathSync', () => { + let tempDir; + + beforeEach(() => { + tempDir = temp.mkdirSync('node-pathwatcher-directory'); + fs.writeFileSync(path.join(tempDir, 'file'), ''); + fs.writeFileSync(path.join(tempDir, 'file2'), ''); + }); + + it('returns the resolved path to the file', () => { + let tempFile = new File(path.join(tempDir, 'file')); + expect( + tempFile.getRealPathSync() + ).toBe( + fs.realpathSync(path.join(tempDir, 'file')) + ); + + tempFile.setPath(path.join(tempDir, 'file2')); + expect( + tempFile.getRealPathSync() + ).toBe( + fs.realpathSync(path.join(tempDir, 'file2')) + ); + }); + }); + + describe('::exists', () => { + let tempDir; + + beforeEach(() => { + tempDir = temp.mkdirSync('node-pathwatcher-directory'); + fs.writeFileSync(path.join(tempDir, 'file'), ''); + }); + + it('does actually exist', async () => { + let existingFile = new File(path.join(tempDir, 'file')); + let exists = await existingFile.exists(); + expect(exists).toBe(true); + }); + + it('doesn’t exist', async () => { + let nonExistentFile = new File(path.join(tempDir, 'not_file')); + let exists = await nonExistentFile.exists(); + expect(exists).toBe(false); + }); + }); + + describe('::getRealPath', () => { + let tempDir; + + beforeEach(() => { + tempDir = temp.mkdirSync('node-pathwatcher-directory'); + fs.writeFileSync(path.join(tempDir, 'file'), ''); + fs.writeFileSync(path.join(tempDir, 'file2'), ''); + }); + + it('returns the resolved path to the file', async () => { + let tempFile = new File(path.join(tempDir, 'file')); + expect( + await tempFile.getRealPath() + ).toBe( + fs.realpathSync(path.join(tempDir, 'file')) + ); + }); + + it('returns the resolved path to the file after a call to setPath', async () => { + let tempFile = new File(path.join(tempDir, 'file')); + tempFile.setPath(path.join(tempDir, 'file2')); + expect( + await tempFile.getRealPath() + ).toBe( + fs.realpathSync(path.join(tempDir, 'file2')) + ); + }); + + if (process.platform !== 'win32') { + describe('on #darwin and #linux', () => { + it('returns the target path for symlinks', async () => { + fs.symlinkSync( + path.join(tempDir, 'file2'), + path.join(tempDir, 'file3') + ); + let tempFile = new File(path.join(tempDir, 'file3')); + expect( + await tempFile.getRealPath() + ).toBe( + fs.realpathSync(path.join(tempDir, 'file2')) + ); + }); + }); + } + }); + + describe('::getParent', () => { + it('gets the parent directory', () => { + expect( + file.getParent().getRealPathSync() + ).toBe( + path.join(__dirname, 'fixtures') + ); + }); + }); + + describe('encoding', () => { + it('should be utf8 by default', () => { + expect(file.getEncoding()).toBe('utf8'); + }); + + it('should be settable', () => { + file.setEncoding('cp1252'); + expect(file.getEncoding()).toBe('cp1252'); + }); + + it('throws an exception when assigning an invalid encoding', () => { + expect(() => { + file.setEncoding('utf-8-bom'); + }).toThrow(); + }); + }) + + describe('::createReadStream', () => { + it('returns a stream to read the file', async () => { + let stream = file.createReadStream(); + let ended = false; + let content = []; + + stream.on('data', (chunk) => content.push(chunk)); + stream.on('end', () => ended = true); + + await condition(() => ended); + + expect(content.join('')).toEqual('this is old!'); + }); + + it('honors the specified encoding', async () => { + let unicodeText = 'ё'; + let unicodeBytes = Buffer.from('\x51\x04') // 'ё' + + fs.writeFileSync(file.getPath(), unicodeBytes); + + file.setEncoding('utf16le'); + + let stream = file.createReadStream(); + let ended = false; + let content = []; + + stream.on('data', (chunk) => content.push(chunk)); + stream.on('end', () => ended = true); + + await condition(() => ended); + expect(content.join('')).toEqual(unicodeText); + }); + }); + + describe('::createWriteStream', () => { + it('returns a stream to read the file', async () => { + let unicodeText = 'ё'; + let unicodeBytes = Buffer.from('\x51\x04') // 'ё' + + file.setEncoding('utf16le'); + + let stream = file.createWriteStream(); + let ended = false; + stream.on('finish', () => ended = true); + + stream.end(unicodeText); + await condition(() => ended); + expect(fs.statSync(file.getPath()).size).toBe(2); + let content = fs.readFileSync(file.getPath()).toString('ascii'); + expect(content).toBe(unicodeBytes.toString('ascii')); + }); + }); + + describe('encoding support', () => { + let unicodeText; + let unicodeBytes; + + beforeEach(() => { + unicodeText = 'ё'; + unicodeBytes = Buffer.from('\x51\x04') // 'ё' + }); + + it('should read a file in UTF-16', async () => { + fs.writeFileSync(file.getPath(), unicodeBytes); + file.setEncoding('utf16le'); + + let contents = await file.read(); + expect(contents).toBe(unicodeText); + }); + + it('should readSync a file in UTF-16', () => { + fs.writeFileSync(file.getPath(), unicodeBytes); + file.setEncoding('utf16le'); + expect(file.readSync()).toBe(unicodeText); + }); + + it('should write a file in UTF-16', async () => { + file.setEncoding('utf16le'); + await file.write(unicodeText); + expect(fs.statSync(file.getPath()).size).toBe(2); + let content = fs.readFileSync(file.getPath()).toString('ascii'); + expect(content).toBe(unicodeBytes.toString('ascii')); + }); + + it('should writeSync a file in UTF-16', () => { + file.setEncoding('utf16le'); + file.writeSync(unicodeText); + expect(fs.statSync(file.getPath()).size).toBe(2); + let content = fs.readFileSync(file.getPath()).toString('ascii'); + expect(content).toBe(unicodeBytes.toString('ascii')); + }); + }); + + describe('reading a nonexistent file', () => { + it('should return null', async () => { + file = new File('not_existing.txt'); + expect( + await file.read() + ).toBe(null); + }); + }); + + describe('::writeSync', () => { + it('emits did-change event', async () => { + let handler = jasmine.createSpy('write handler'); + file.onDidChange(handler); + file.writeSync('ok'); + await condition(() => handler.calls.count() > 0); + }); + }); +}); diff --git a/spec/pathwatcher-spec.coffee b/spec/pathwatcher-spec.coffee deleted file mode 100644 index f57c758..0000000 --- a/spec/pathwatcher-spec.coffee +++ /dev/null @@ -1,167 +0,0 @@ -pathWatcher = require '../lib/main' -fs = require 'fs' -path = require 'path' -temp = require 'temp' - -temp.track() - -describe 'PathWatcher', -> - tempDir = temp.mkdirSync('node-pathwatcher-directory') - tempFile = path.join(tempDir, 'file') - - beforeEach -> - fs.writeFileSync(tempFile, '') - - afterEach -> - pathWatcher.closeAllWatchers() - - describe '.getWatchedPaths()', -> - it 'returns an array of all watched paths', -> - expect(pathWatcher.getWatchedPaths()).toEqual [] - watcher1 = pathWatcher.watch tempFile, -> - expect(pathWatcher.getWatchedPaths()).toEqual [watcher1.handleWatcher.path] - watcher2 = pathWatcher.watch tempFile, -> - expect(pathWatcher.getWatchedPaths()).toEqual [watcher1.handleWatcher.path] - watcher1.close() - expect(pathWatcher.getWatchedPaths()).toEqual [watcher1.handleWatcher.path] - watcher2.close() - expect(pathWatcher.getWatchedPaths()).toEqual [] - - describe '.closeAllWatchers()', -> - it 'closes all watched paths', -> - expect(pathWatcher.getWatchedPaths()).toEqual [] - watcher = pathWatcher.watch tempFile, -> - expect(pathWatcher.getWatchedPaths()).toEqual [watcher.handleWatcher.path] - pathWatcher.closeAllWatchers() - expect(pathWatcher.getWatchedPaths()).toEqual [] - - describe 'when a watched path is changed', -> - it 'fires the callback with the event type and empty path', -> - eventType = null - eventPath = null - watcher = pathWatcher.watch tempFile, (type, path) -> - eventType = type - eventPath = path - - fs.writeFileSync(tempFile, 'changed') - waitsFor -> eventType? - runs -> - expect(eventType).toBe 'change' - expect(eventPath).toBe '' - - describe 'when a watched path is renamed #darwin #win32', -> - it 'fires the callback with the event type and new path and watches the new path', -> - eventType = null - eventPath = null - watcher = pathWatcher.watch tempFile, (type, path) -> - eventType = type - eventPath = path - - tempRenamed = path.join(tempDir, 'renamed') - fs.renameSync(tempFile, tempRenamed) - waitsFor -> eventType? - runs -> - expect(eventType).toBe 'rename' - expect(fs.realpathSync(eventPath)).toBe fs.realpathSync(tempRenamed) - expect(pathWatcher.getWatchedPaths()).toEqual [watcher.handleWatcher.path] - - describe 'when a watched path is deleted #win32 #darwin', -> - it 'fires the callback with the event type and null path', -> - deleted = false - watcher = pathWatcher.watch tempFile, (type, path) -> - deleted = true if type is 'delete' and path is null - - fs.unlinkSync(tempFile) - waitsFor -> deleted - - describe 'when a file under watched directory is deleted', -> - it 'fires the callback with the change event and empty path', (done) -> - fileUnderDir = path.join(tempDir, 'file') - fs.writeFileSync(fileUnderDir, '') - watcher = pathWatcher.watch tempDir, (type, path) -> - expect(type).toBe 'change' - expect(path).toBe '' - done() - fs.unlinkSync(fileUnderDir) - - describe 'when a new file is created under watched directory', -> - it 'fires the callback with the change event and empty path', -> - newFile = path.join(tempDir, 'file') - watcher = pathWatcher.watch tempDir, (type, path) -> - fs.unlinkSync(newFile) - - expect(type).toBe 'change' - expect(path).toBe '' - done() - fs.writeFileSync(newFile, '') - - describe 'when a file under watched directory is moved', -> - it 'fires the callback with the change event and empty path', (done) -> - newName = path.join(tempDir, 'file2') - watcher = pathWatcher.watch tempDir, (type, path) -> - expect(type).toBe 'change' - expect(path).toBe '' - done() - fs.renameSync(tempFile, newName) - - describe 'when en exception is thrown in the closed watcher\'s callback', -> - it 'does not crash', (done) -> - watcher = pathWatcher.watch tempFile, (type, path) -> - watcher.close() - try - throw new Error('test') - catch e - done() - fs.writeFileSync(tempFile, 'changed') - - describe 'when watching a file that does not exist', -> - it 'throws an error with a code #darwin #linux', -> - doesNotExist = path.join(tempDir, 'does-not-exist') - watcher = null - try - watcher = pathWatcher.watch doesNotExist, -> null - catch error - expect(error.message).toBe 'Unable to watch path' - expect(error.code).toBe 'ENOENT' - expect(watcher).toBe null # ensure it threw - - describe 'when watching multiple files under the same directory', -> - it 'fires the callbacks when both of the files are modifiled', -> - called = 0 - tempFile2 = path.join(tempDir, 'file2') - fs.writeFileSync(tempFile2, '') - pathWatcher.watch tempFile, (type, path) -> - called |= 1 - pathWatcher.watch tempFile2, (type, path) -> - called |= 2 - fs.writeFileSync(tempFile, 'changed') - fs.writeFileSync(tempFile2, 'changed') - waitsFor -> called == 3 - - it 'shares the same handle watcher between the two files on #win32', -> - tempFile2 = path.join(tempDir, 'file2') - fs.writeFileSync(tempFile2, '') - watcher1 = pathWatcher.watch tempFile, (type, path) -> - watcher2 = pathWatcher.watch tempFile2, (type, path) -> - expect(watcher1.handleWatcher).toBe(watcher2.handleWatcher) - - describe 'when a file is unwatched', -> - it 'it does not lock the filesystem tree', -> - nested1 = path.join(tempDir, 'nested1') - nested2 = path.join(nested1, 'nested2') - nested3 = path.join(nested2, 'nested3') - fs.mkdirSync(nested1) - fs.mkdirSync(nested2) - fs.writeFileSync(nested3) - - subscription1 = pathWatcher.watch nested1, -> - subscription2 = pathWatcher.watch nested2, -> - subscription3 = pathWatcher.watch nested3, -> - - subscription1.close() - subscription2.close() - subscription3.close() - - fs.unlinkSync(nested3) - fs.rmdirSync(nested2) - fs.rmdirSync(nested1) diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js new file mode 100644 index 0000000..2d02626 --- /dev/null +++ b/spec/pathwatcher-spec.js @@ -0,0 +1,225 @@ +const PathWatcher = require('../src/main'); +const fs = require('fs-plus'); +const path = require('path'); +const temp = require('temp'); +require('./spec-helper.js'); + +temp.track(); + +function EMPTY() {} + +describe('PathWatcher', () => { + let tempDir = temp.mkdirSync('node-pathwatcher-directory'); + let tempFile = path.join(tempDir, 'file'); + + beforeEach(() => fs.writeFileSync(tempFile, '')); + afterEach(() => PathWatcher.closeAllWatchers()); + + describe('getWatchedPaths', () => { + it('returns an array of all watched paths', () => { + expect(PathWatcher.getWatchedPaths()).toEqual([]); + let watcher1 = PathWatcher.watch(tempFile, EMPTY); + expect(PathWatcher.getWatchedPaths()).toEqual([watcher1.handleWatcher.path]); + let watcher2 = PathWatcher.watch(tempFile, EMPTY); + expect(PathWatcher.getWatchedPaths()).toEqual([watcher1.handleWatcher.path]); + watcher1.close(); + expect(PathWatcher.getWatchedPaths()).toEqual([watcher1.handleWatcher.path]); + watcher2.close(); + expect(PathWatcher.getWatchedPaths()).toEqual([]); + }); + }); + + describe('closeAllWatchers', () => { + it('closes all watched paths', () => { + expect(PathWatcher.getWatchedPaths()).toEqual([]); + let watcher1 = PathWatcher.watch(tempFile, EMPTY); + expect(PathWatcher.getWatchedPaths()).toEqual([watcher1.handleWatcher.path]); + PathWatcher.closeAllWatchers(); + expect(PathWatcher.getWatchedPaths()).toEqual([]); + }); + }); + + describe('when a watched path is changed', () => { + it('fires the callback with the event type and empty path', async () => { + let eventType; + let eventPath; + + PathWatcher.watch(tempFile, (type, path) => { + eventType = type; + eventPath = path; + }); + + fs.writeFileSync(tempFile, 'changed'); + + await condition(() => !!eventType); + + expect(eventType).toBe('change'); + expect(eventPath).toBe(''); + }); + }); + + if (process.platform !== 'linux') { + describe('when a watched path is renamed #darwin #win32', () => { + it('fires the callback with the event type and new path and watches the new path', async () => { + let eventType; + let eventPath; + + let watcher = PathWatcher.watch(tempFile, (type, path) => { + eventType = type; + eventPath = path; + }); + + let tempRenamed = path.join(tempDir, 'renamed'); + fs.renameSync(tempFile, tempRenamed); + + await condition(() => !!eventType); + + expect(eventType).toBe('rename'); + expect(fs.realpathSync(eventPath)).toBe(fs.realpathSync(tempRenamed)); + expect(PathWatcher.getWatchedPaths()).toEqual([watcher.handleWatcher.path]); + }); + }); + + describe('when a watched path is deleted #darwin #win32', () => { + it('fires the callback with the event type and null path', async () => { + let deleted = false; + PathWatcher.watch(tempFile, (type, path) => { + if (type === 'delete' && path === null) { + deleted = true; + } + }); + + fs.unlinkSync(tempFile); + + await condition(() => deleted); + }); + }); + } + + describe('when a file under a watched directory is deleted', () => { + it('fires the callback with the change event and empty path', async () => { + let fileUnderDir = path.join(tempDir, 'file'); + fs.writeFileSync(fileUnderDir, ''); + let done = false; + PathWatcher.watch(tempDir, (type, path) => { + expect(type).toBe('change'); + expect(path).toBe(''); + done = true; + }); + + fs.writeFileSync(fileUnderDir, 'what'); + await wait(2000); + fs.unlinkSync(fileUnderDir); + await condition(() => done); + + }); + }); + + // TODO: This test has never worked. It incorrectly passes on `master` + // because of a typo. This needs to be fixed somehow. + xdescribe('when a new file is created under a watched directory', () => { + it('fires the callback with the change event and empty path', (done) => { + + let newFile = path.join(tempDir, 'file'); + PathWatcher.watch(tempDir, (type, path) => { + fs.unlinkSync(newFile); + expect(type).toBe('change'); + expect(path).toBe(''); + done(); + }); + + fs.writeFileSync(newFile, ''); + }); + }); + + describe('when a file under a watched directory is moved', () => { + it('fires the callback with the change event and empty path', (done) => { + + let newName = path.join(tempDir, 'file2'); + PathWatcher.watch(tempDir, (type, path) => { + expect(type).toBe('change'); + expect(path).toBe(''); + done(); + }); + + fs.renameSync(tempFile, newName); + }); + }); + + describe('when an exception is thrown in the closed watcher’s callback', () => { + it('does not crash', (done) => { + let watcher = PathWatcher.watch(tempFile, (_type, _path) => { + watcher.close(); + try { + throw new Error('test'); + } catch (e) { + done(); + } + }); + + fs.writeFileSync(tempFile, 'changed'); + }); + }); + + describe('when watching a file that does not exist', () => { + if (process.platform !== 'win32') { + it('throws an error with a code #darwin #linux', () => { + let doesNotExist = path.join(tempDir, 'does-not-exist'); + let watcher; + try { + watcher = PathWatcher.watch(doesNotExist, EMPTY); + } catch (error) { + expect(error.message).toBe('Unable to watch path'); + expect(error.code).toBe('ENOENT'); + } + expect(watcher).toBe(undefined); // (ensure it threw) + }); + } + }); + + describe('when watching multiple files under the same directory', () => { + it('fires the callbacks when both of the files are modified', async () => { + let called = 0; + let tempFile2 = path.join(tempDir, 'file2'); + fs.writeFileSync(tempFile2, ''); + PathWatcher.watch(tempFile, () => called |= 1); + PathWatcher.watch(tempFile2, () => called |= 2); + fs.writeFileSync(tempFile, 'changed'); + fs.writeFileSync(tempFile2, 'changed'); + await condition(() => called === 3); + }); + + if (process.platform === 'win32') { + it('shares the same handle watcher between the two files on #win32', () => { + let tempFile2 = path.join(tempDir, 'file2'); + fs.writeFileSync(tempFile2, ''); + let watcher1 = PathWatcher.watch(tempFile, EMPTY); + let watcher2 = PathWatcher.watch(tempFile2, EMPTY); + expect(watcher1.handleWatcher).toBe(watcher2.handleWatcher); + }); + } + }); + + describe('when a file is unwatched', () => { + it('does not lock the file system tree', () => { + let nested1 = path.join(tempDir, 'nested1'); + let nested2 = path.join(nested1, 'nested2'); + let nested3 = path.join(nested2, 'nested3'); + fs.mkdirSync(nested1); + fs.mkdirSync(nested2); + fs.writeFileSync(nested3, ''); + + let subscription1 = PathWatcher.watch(nested1, EMPTY); + let subscription2 = PathWatcher.watch(nested2, EMPTY); + let subscription3 = PathWatcher.watch(nested3, EMPTY); + + subscription1.close(); + subscription2.close(); + subscription3.close(); + + fs.unlinkSync(nested3); + fs.rmdirSync(nested2); + fs.rmdirSync(nested1); + }); + }); +}); diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee deleted file mode 100644 index fccfadc..0000000 --- a/spec/spec-helper.coffee +++ /dev/null @@ -1,28 +0,0 @@ -jasmine.getEnv().setIncludedTags([process.platform]) - -global.waitsForPromise = (args...) -> - if args.length > 1 - {shouldReject} = args[0] - else - shouldReject = false - fn = args[args.length - 1] - - promiseFinished = false - - process.nextTick -> - promise = fn() - if shouldReject - promise.catch -> - promiseFinished = true - promise.then -> - jasmine.getEnv().currentSpec.fail("Expected promise to be rejected, but it was resolved") - promiseFinished = true - else - promise.then -> promiseFinished = true - promise.catch (error) -> - jasmine.getEnv().currentSpec.fail("Expected promise to be resolved, but it was rejected with #{jasmine.pp(error)}") - promiseFinished = true - - global.waitsFor "promise to complete", -> promiseFinished - -require('grim').includeDeprecatedAPIs = false diff --git a/spec/spec-helper.js b/spec/spec-helper.js new file mode 100644 index 0000000..f312e3b --- /dev/null +++ b/spec/spec-helper.js @@ -0,0 +1,37 @@ +// jasmine.getEnv().setIncludedTags([process.platform]); + +global.makePromiseCallback = function makePromiseCallback(fn = () => {}) { + let outerResolve; + let promise = new Promise((resolve) => { + outerResolve = resolve; + }); + + let callback = (...args) => { + fn(...args); + outerResolve(); + }; + + return [promise, callback]; +} + +function timeoutPromise (ms) { + return new Promise((_, reject) => setTimeout(reject, ms)); +} + +global.condition = function condition(fn, timeoutMs = 5000) { + let promise = new Promise((resolve) => { + let poll = () => { + let outcome = fn(); + if (outcome) resolve(); + setTimeout(poll, 50); + }; + poll(); + }); + + return Promise.race([promise, timeoutPromise(timeoutMs)]); +}; + + +global.wait = function wait(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +}; From 847e2febb700abc20e1ade26b0c8e78bd8acc403 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 6 Oct 2024 18:24:24 -0700 Subject: [PATCH 003/168] Update `binding.gyp` --- binding.gyp | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/binding.gyp b/binding.gyp index 9bf2936..502e3ac 100644 --- a/binding.gyp +++ b/binding.gyp @@ -2,17 +2,24 @@ "targets": [ { "target_name": "pathwatcher", + "cflags!": ["-fno-exceptions"], + "cflags_cc!": ["-fno-exceptions"], + "xcode_settings": { + "GCC_ENABLE_CPP_EXCEPTIONS": "YES", + "CLANG_CXX_LIBRARY": "libc++", + "MACOSX_DEPLOYMENT_TARGET": "10.7", + }, + "msvs_settings": { + "VCCLCompilerTool": {"ExceptionHandling": 1}, + }, "sources": [ "src/main.cc", "src/common.cc", - "src/common.h", - "src/handle_map.cc", - "src/handle_map.h", - "src/unsafe_persistent.h", + "src/common.h" ], "include_dirs": [ + " Date: Mon, 7 Oct 2024 13:20:45 -0700 Subject: [PATCH 004/168] Fix import --- src/common.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common.cc b/src/common.cc index f3af39b..52cdc01 100644 --- a/src/common.cc +++ b/src/common.cc @@ -1,5 +1,6 @@ #include "common.h" #include "addon-data.h" +#include "uv.h" using namespace Napi; From aad8e4d677694e9311a48441624417a3d31ab05a Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 7 Oct 2024 13:21:03 -0700 Subject: [PATCH 005/168] Rewrite Linux approach to allow for graceful stopping --- src/pathwatcher_linux.cc | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/pathwatcher_linux.cc b/src/pathwatcher_linux.cc index 13e6a0b..e29d096 100644 --- a/src/pathwatcher_linux.cc +++ b/src/pathwatcher_linux.cc @@ -19,26 +19,38 @@ void PlatformInit() { g_init_errno = errno; return; } - - WakeupNewThread(); } -void PlatformThread() { +void PlatformThread( + const PathWatcherWorker::ExecutionProgress& progress, + bool& shouldStop +) { // Needs to be large enough for sizeof(inotify_event) + strlen(filename). char buf[4096]; - while (true) { - int size; - do { - size = read(g_inotify, buf, sizeof(buf)); - } while (size == -1 && errno == EINTR); + while (!shouldStop) { + fd_set read_fds; + FD_ZERO(&read_fds); + FD_SET(g_inotify, &read_fds); - if (size == -1) { - break; - } else if (size == 0) { + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 100000; // 100ms timeout + + int ret = select(g_inotify + 1, &read_fds, NULL, NULL, &tv); + + if (ret == -1 && errno != EINTR) { break; } + if (ret == 0) { + // Timeout. + continue; + } + + int size = read(g_inotify, buf, sizeof(buf)); + if (size <= 0) break; + inotify_event* e; for (char* p = buf; p < buf + size; p += sizeof(*e) + e->len) { e = reinterpret_cast(p); @@ -57,7 +69,8 @@ void PlatformThread() { continue; } - PostEventAndWait(type, fd, path); + PathWatcherEvent event(type, fd, path); + progress.Send(&event, 1); } } } From 92ad3c7e5caec37c649808c43113da5b082899b6 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 7 Oct 2024 14:00:27 -0700 Subject: [PATCH 006/168] Fix failing test (man, that was silly) --- spec/pathwatcher-spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index 2d02626..e91fd31 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -115,12 +115,12 @@ describe('PathWatcher', () => { }); }); - // TODO: This test has never worked. It incorrectly passes on `master` - // because of a typo. This needs to be fixed somehow. - xdescribe('when a new file is created under a watched directory', () => { + describe('when a new file is created under a watched directory', () => { it('fires the callback with the change event and empty path', (done) => { - let newFile = path.join(tempDir, 'file'); + if (fs.existsSync(newFile)) { + fs.unlinkSync(newFile); + } PathWatcher.watch(tempDir, (type, path) => { fs.unlinkSync(newFile); expect(type).toBe('change'); From 751f98763356708e1e267ee63fb028a698617cd0 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 7 Oct 2024 14:00:59 -0700 Subject: [PATCH 007/168] Redefine the `test` script in `package.json` --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 2eadf1c..2859eaf 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "homepage": "http://atom.github.io/node-pathwatcher", "scripts": { "prepublish": "grunt prepublish", - "test": "grunt test" + "test": "jasmine spec/*-spec.js" }, "devDependencies": { "grunt": "~0.4.1", @@ -32,6 +32,7 @@ "node-addon-api": "^8.1.0", "node-cpplint": "~0.1.5", "rimraf": "~2.2.0", + "segfault-handler": "^1.3.0", "temp": "~0.9.0" }, "dependencies": { From b88a91fa7e6ae56c79f4dd6f7569615104d493ce Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 7 Oct 2024 14:01:16 -0700 Subject: [PATCH 008/168] Fix import --- src/pathwatcher_unix.cc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pathwatcher_unix.cc b/src/pathwatcher_unix.cc index 160fe00..abe6f22 100644 --- a/src/pathwatcher_unix.cc +++ b/src/pathwatcher_unix.cc @@ -6,7 +6,7 @@ #include #include - +#include #include "common.h" // test for descriptor event notification, if not available set to O_RDONLY @@ -54,8 +54,6 @@ void PlatformThread( int fd = static_cast(event.ident); std::vector path; - // std::cout << "EVENT delete: " << (event.fflags & NOTE_DELETE) << " write: " << (event.fflags & NOTE_WRITE) << " rename: " << (event.fflags & NOTE_RENAME) << "empty: " << (event.fflags & NOTE_ATTRIB && lseek(fd, 0, SEEK_END) == 0) << std::endl; - if (event.fflags & NOTE_WRITE) { type = EVENT_CHANGE; } else if (event.fflags & NOTE_DELETE) { From 48dfc4afe85da82e92756a3811f9e55bd410e08c Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 7 Oct 2024 16:43:24 -0700 Subject: [PATCH 009/168] Attempt to modernize GitHub actions workflow --- .github/workflows/ci.yml | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfb4876..e2f5847 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,28 +1,28 @@ -name: ci +name: CI on: - - pull_request - - push + push: + pull_request: jobs: Test: - if: "!contains(github.event.head_commit.message, '[skip ci]')" + name: "Test" runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: - ubuntu-latest - # - macos-latest + - macos-latest - windows-latest node_version: - - 10 - - 12 - # - 14 + - 14 + - 18 + - 20 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - - name: Install Node - uses: actions/setup-node@v1 + - name: Install Node ${{ matrix.node }} + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node_version }} @@ -30,11 +30,4 @@ jobs: run: npm install - name: Run tests - run: npm run test - - Skip: - if: contains(github.event.head_commit.message, '[skip ci]') - runs-on: ubuntu-latest - steps: - - name: Skip CI 🚫 - run: echo skip ci + run: npm test From 5c6a79f2ecb9a3e8fd4ce244286ceb6ec6c9981b Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 7 Oct 2024 16:45:03 -0700 Subject: [PATCH 010/168] Fix README type (testing CI) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7efdbd0..ff4e73e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Path Watcher Node module ![ci](https://github.com/atom/node-pathwatcher/workflows/ci/badge.svg) -[![Depenency Status](https://david-dm.org/atom/node-pathwatcher/status.svg)](https://david-dm.org/atom/node-pathwatcher) +[![Dependency Status](https://david-dm.org/atom/node-pathwatcher/status.svg)](https://david-dm.org/atom/node-pathwatcher) ## Installing From 53b2ef8ac30a2d70a6ad865128630c81a09232b6 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 7 Oct 2024 16:57:07 -0700 Subject: [PATCH 011/168] Testing a possible Windows fix --- src/addon-data.h | 9 +-------- src/common.h | 5 +++-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/addon-data.h b/src/addon-data.h index 2a57032..b39cbd7 100644 --- a/src/addon-data.h +++ b/src/addon-data.h @@ -1,11 +1,4 @@ #include "common.h" #pragma once -class AddonData final { -public: - explicit AddonData(Napi::Env env) {} - - Napi::FunctionReference callback; - PathWatcherWorker* worker; - int watch_count; -}; +// TODO: Remove diff --git a/src/common.h b/src/common.h index 2b55a9b..7f8ca64 100644 --- a/src/common.h +++ b/src/common.h @@ -7,7 +7,8 @@ using namespace Napi; #ifdef _WIN32 -// Platform-dependent definetion of handle. +#include +// Platform-dependent definition of HANDLE. typedef HANDLE WatcherHandle; // Conversion between V8 value and WatcherHandle. @@ -15,7 +16,7 @@ Napi::Value WatcherHandleToV8Value(WatcherHandle handle); WatcherHandle V8ValueToWatcherHandle(Napi::Value value); bool IsV8ValueWatcherHandle(Napi::Value value); #else -// Correspoding definetions on OS X and Linux. +// Correspoding definitions on OS X and Linux. typedef int32_t WatcherHandle; #define WatcherHandleToV8Value(h, e) Napi::Number::New(e, h) #define V8ValueToWatcherHandle(v) v.Int32Value() From 2945228b7c6e08c510d333190aa76df72a78f1cb Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 7 Oct 2024 16:57:23 -0700 Subject: [PATCH 012/168] Skip Node 14 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2f5847..ceecd49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: - macos-latest - windows-latest node_version: - - 14 + # - 14 - 18 - 20 steps: From b66d0c3a50ec4c3407e120c05121a049326dd7d1 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 7 Oct 2024 16:59:37 -0700 Subject: [PATCH 013/168] (oops) --- src/addon-data.h | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/addon-data.h b/src/addon-data.h index b39cbd7..2a57032 100644 --- a/src/addon-data.h +++ b/src/addon-data.h @@ -1,4 +1,11 @@ #include "common.h" #pragma once -// TODO: Remove +class AddonData final { +public: + explicit AddonData(Napi::Env env) {} + + Napi::FunctionReference callback; + PathWatcherWorker* worker; + int watch_count; +}; From 07ea89768cc4cd198a3ad3676c791c9cfa2a4e1e Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 7 Oct 2024 17:03:30 -0700 Subject: [PATCH 014/168] Trying a different fix --- src/common.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common.h b/src/common.h index 7f8ca64..d411642 100644 --- a/src/common.h +++ b/src/common.h @@ -7,7 +7,7 @@ using namespace Napi; #ifdef _WIN32 -#include +#include // Platform-dependent definition of HANDLE. typedef HANDLE WatcherHandle; From 0df653c3eab94818bef867fd2634498af5feb696 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 7 Oct 2024 19:32:59 -0700 Subject: [PATCH 015/168] Attempt to load debug binding first --- src/main.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 9b018ba..85aeec1 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,10 @@ -const binding = require('../build/Release/pathwatcher.node'); +let binding; +try { + binding = require('../build/Debug/pathwatcher.node'); +} catch (err) { + binding = require('../build/Release/pathwatcher.node'); +} const { Emitter } = require('event-kit'); const fs = require('fs'); const path = require('path'); From e72daa37359589578f54d00808843f54c5504f27 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 7 Oct 2024 19:33:50 -0700 Subject: [PATCH 016/168] Don't try to stop a worker we're about to replace --- src/common.cc | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/common.cc b/src/common.cc index 52cdc01..5537eaf 100644 --- a/src/common.cc +++ b/src/common.cc @@ -86,9 +86,6 @@ Napi::Value SetCallback(const Napi::CallbackInfo& info) { } auto addonData = env.GetInstanceData(); - if (addonData->worker) { - addonData->worker->Stop(); - } addonData->callback.Reset(info[0].As(), 1); return env.Undefined(); From 0756a650d6b2f768b9880acdbc65f909575ad318 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 7 Oct 2024 19:38:28 -0700 Subject: [PATCH 017/168] =?UTF-8?q?Maybe=20I=20don=E2=80=99t=20actually=20?= =?UTF-8?q?need=20this=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common.h b/src/common.h index d411642..8fb5f11 100644 --- a/src/common.h +++ b/src/common.h @@ -7,7 +7,7 @@ using namespace Napi; #ifdef _WIN32 -#include +// #include // Platform-dependent definition of HANDLE. typedef HANDLE WatcherHandle; From 670f97a249d2bc84338934b73698460fca77893d Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 12:06:00 -0700 Subject: [PATCH 018/168] Attempt to rewrite the Nan stuff in the Windows implementation --- src/common.h | 2 +- src/main.cc | 2 +- src/pathwatcher_linux.cc | 2 +- src/pathwatcher_unix.cc | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common.h b/src/common.h index 8fb5f11..737695d 100644 --- a/src/common.h +++ b/src/common.h @@ -23,7 +23,7 @@ typedef int32_t WatcherHandle; #define IsV8ValueWatcherHandle(v) v.IsNumber() #endif -void PlatformInit(); +void PlatformInit(Napi::Env env); WatcherHandle PlatformWatch(const char* path); void PlatformUnwatch(WatcherHandle handle); bool PlatformIsHandleValid(WatcherHandle handle); diff --git a/src/main.cc b/src/main.cc index 038889e..3acbc1c 100644 --- a/src/main.cc +++ b/src/main.cc @@ -8,7 +8,7 @@ namespace { env.SetInstanceData(data); CommonInit(env); - PlatformInit(); + PlatformInit(env); exports.Set("setCallback", Napi::Function::New(env, SetCallback)); exports.Set("watch", Napi::Function::New(env, Watch)); diff --git a/src/pathwatcher_linux.cc b/src/pathwatcher_linux.cc index e29d096..f72a6ff 100644 --- a/src/pathwatcher_linux.cc +++ b/src/pathwatcher_linux.cc @@ -13,7 +13,7 @@ static int g_inotify; static int g_init_errno; -void PlatformInit() { +void PlatformInit(Napi::Env _env) { g_inotify = inotify_init(); if (g_inotify == -1) { g_init_errno = errno; diff --git a/src/pathwatcher_unix.cc b/src/pathwatcher_unix.cc index abe6f22..089e48c 100644 --- a/src/pathwatcher_unix.cc +++ b/src/pathwatcher_unix.cc @@ -28,7 +28,7 @@ static int g_kqueue; static int g_init_errno; -void PlatformInit() { +void PlatformInit(Napi::Env _env) { g_kqueue = kqueue(); if (g_kqueue == -1) { g_init_errno = errno; From 9093d7a9bede3e95639210c4c9247d65a29356bf Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 12:10:30 -0700 Subject: [PATCH 019/168] (oops) --- src/common.h | 2 +- src/pathwatcher_win.cc | 40 +++++++++++++++++++++------------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/common.h b/src/common.h index 737695d..291c6cc 100644 --- a/src/common.h +++ b/src/common.h @@ -7,7 +7,7 @@ using namespace Napi; #ifdef _WIN32 -// #include +#include // Platform-dependent definition of HANDLE. typedef HANDLE WatcherHandle; diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 1a99bd4..011c485 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -1,15 +1,15 @@ #include #include #include +#include #include "common.h" +#include "js_native_api_types.h" +#include "napi.h" // Size of the buffer to store result of ReadDirectoryChangesW. static const unsigned int kDirectoryWatcherBufferSize = 4096; -// Object template to create representation of WatcherHandle. -static Nan::Persistent g_object_template; - // Mutex for the HandleWrapper map. static uv_mutex_t g_handle_wrap_map_mutex; @@ -98,37 +98,39 @@ static bool QueueReaddirchanges(HandleWrapper* handle) { NULL) == TRUE; } -Local WatcherHandleToV8Value(WatcherHandle handle) { - Local context = Nan::GetCurrentContext(); - Local value = Nan::New(g_object_template)->NewInstance(context).ToLocalChecked(); - Nan::SetInternalFieldPointer(value->ToObject(context).ToLocalChecked(), 0, handle); - return value; +Napi::Value WatcherHandleToV8Value(WatcherHandle handle, Napi::Env env) { + uint64_t HandleInt = reinterpret_cast(handle); + return Napi::BigInt::New(env, handleInt); } WatcherHandle V8ValueToWatcherHandle(Local value) { - return reinterpret_cast(Nan::GetInternalFieldPointer( - value->ToObject(Nan::GetCurrentContext()).ToLocalChecked(), 0)); + if (!value.IsBigInt()) { + return NULL; + } + bool lossless; + uint64_t handleInt = value.As().Uint64Value(&lossless); + if (!lossless) { + return NULL; + } + return reinterpret_cast(handleInt); } bool IsV8ValueWatcherHandle(Local value) { - return value->IsObject() && - value->ToObject(Nan::GetCurrentContext()).ToLocalChecked()->InternalFieldCount() == 1; + return value->IsBigInt(); } -void PlatformInit() { +void PlatformInit(Napi::Env _env) { uv_mutex_init(&g_handle_wrap_map_mutex); g_file_handles_free_event = CreateEvent(NULL, TRUE, TRUE, NULL); g_wake_up_event = CreateEvent(NULL, FALSE, FALSE, NULL); g_events.push_back(g_wake_up_event); - - g_object_template.Reset(Nan::New()); - Nan::New(g_object_template)->SetInternalFieldCount(1); - - WakeupNewThread(); } -void PlatformThread() { +void PlatformThread( + const PathWatcherWorker::ExecutionProgress& progress, + bool& shouldStop +) { while (true) { // Do not use g_events directly, since reallocation could happen when there // are new watchers adding to g_events when WaitForMultipleObjects is still From 912a6582dce932129b7e5b1de78e2a8ce913ae1b Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 12:15:40 -0700 Subject: [PATCH 020/168] (Claude thinks this might help) --- src/common.h | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/common.h b/src/common.h index 291c6cc..551bcbc 100644 --- a/src/common.h +++ b/src/common.h @@ -1,13 +1,24 @@ #ifndef SRC_COMMON_H_ #define SRC_COMMON_H_ +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#endif + #include #include "napi.h" using namespace Napi; #ifdef _WIN32 -#include // Platform-dependent definition of HANDLE. typedef HANDLE WatcherHandle; From 3ed47b664d51fcfdc44d1090f7538f01e4816d8f Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 12:18:56 -0700 Subject: [PATCH 021/168] Fix signature --- src/common.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common.h b/src/common.h index 551bcbc..55c4463 100644 --- a/src/common.h +++ b/src/common.h @@ -23,7 +23,7 @@ using namespace Napi; typedef HANDLE WatcherHandle; // Conversion between V8 value and WatcherHandle. -Napi::Value WatcherHandleToV8Value(WatcherHandle handle); +Napi::Value WatcherHandleToV8Value(WatcherHandle handle, Napi::Env env); WatcherHandle V8ValueToWatcherHandle(Napi::Value value); bool IsV8ValueWatcherHandle(Napi::Value value); #else From 1ca28b794586e739f112cb5138a14b1ed1b6876f Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 12:23:51 -0700 Subject: [PATCH 022/168] Fix typos --- src/pathwatcher_win.cc | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 011c485..29aa910 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -6,6 +6,7 @@ #include "common.h" #include "js_native_api_types.h" #include "napi.h" +#include "uv.h" // Size of the buffer to store result of ReadDirectoryChangesW. static const unsigned int kDirectoryWatcherBufferSize = 4096; @@ -99,11 +100,11 @@ static bool QueueReaddirchanges(HandleWrapper* handle) { } Napi::Value WatcherHandleToV8Value(WatcherHandle handle, Napi::Env env) { - uint64_t HandleInt = reinterpret_cast(handle); - return Napi::BigInt::New(env, handleInt); + uint64_t handleInt = reinterpret_cast(handle); + return Napi::BigInt::New(env, handleInt); } -WatcherHandle V8ValueToWatcherHandle(Local value) { +WatcherHandle V8ValueToWatcherHandle(Napi::Value value) { if (!value.IsBigInt()) { return NULL; } @@ -115,7 +116,7 @@ WatcherHandle V8ValueToWatcherHandle(Local value) { return reinterpret_cast(handleInt); } -bool IsV8ValueWatcherHandle(Local value) { +bool IsV8ValueWatcherHandle(Napi::Value value) { return value->IsBigInt(); } From ad618888d3e9e55fdff4508523c7a78e2b977972 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 12:27:05 -0700 Subject: [PATCH 023/168] Typo --- src/pathwatcher_win.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 29aa910..7822ca3 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -117,7 +117,7 @@ WatcherHandle V8ValueToWatcherHandle(Napi::Value value) { } bool IsV8ValueWatcherHandle(Napi::Value value) { - return value->IsBigInt(); + return value.IsBigInt(); } void PlatformInit(Napi::Env _env) { From ed7e13a27de351cc3ae5737241f468d9dc882315 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 12:30:20 -0700 Subject: [PATCH 024/168] Fix old API usage --- src/pathwatcher_win.cc | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 7822ca3..61141e8 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -241,11 +241,15 @@ void PlatformThread( locker.Unlock(); - for (size_t i = 0; i < events.size(); ++i) - PostEventAndWait(events[i].type, - events[i].handle, - events[i].new_path, - events[i].old_path); + for (size_t i = 0; i < events.size(); ++i) { + PathWatcherEvent event( + events[i].type, + events[i].handle, + events[i].new_path, + events[i].old_path + ); + progress.Send(&event, 1); + } } } } From cb8df272e70ff374c7c40bddf028b3acefae5d24 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 12:33:01 -0700 Subject: [PATCH 025/168] Fix typo on Windows --- src/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 85aeec1..56a26a0 100644 --- a/src/main.js +++ b/src/main.js @@ -114,7 +114,7 @@ class PathWatcher { this.path = filePath; this.emitter = new Emitter(); if (process.platform === 'win32') { - stats = fs.statSync(filePath); + let stats = fs.statSync(filePath); this.isWatchingParent = !stats.isDirectory(); } if (this.isWatchingParent) { From 9fdabd8335e4011ee14a3909cd8a2c539eae6137 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 12:42:45 -0700 Subject: [PATCH 026/168] Fix ordering of conditional test --- spec/pathwatcher-spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index e91fd31..4a5f760 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -161,8 +161,8 @@ describe('PathWatcher', () => { }); }); - describe('when watching a file that does not exist', () => { - if (process.platform !== 'win32') { + if (process.platform !== 'win32') { + describe('when watching a file that does not exist', () => { it('throws an error with a code #darwin #linux', () => { let doesNotExist = path.join(tempDir, 'does-not-exist'); let watcher; @@ -174,8 +174,8 @@ describe('PathWatcher', () => { } expect(watcher).toBe(undefined); // (ensure it threw) }); - } - }); + }); + } describe('when watching multiple files under the same directory', () => { it('fires the callbacks when both of the files are modified', async () => { From 062001a53527b672abce8136b3fbdacf89c2c3ee Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 12:45:30 -0700 Subject: [PATCH 027/168] Fix expectation of number --- src/common.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/common.cc b/src/common.cc index 5537eaf..bdd9e4d 100644 --- a/src/common.cc +++ b/src/common.cc @@ -139,7 +139,13 @@ Napi::Value Unwatch(const Napi::CallbackInfo& info) { return env.Null(); } + auto thing = info[0]; + +#ifdef _WIN32 + Napi::BigInt num = info[0]; +#else Napi::Number num = info[0].ToNumber(); +#endif PlatformUnwatch(V8ValueToWatcherHandle(num)); From 7faae35d52514056b5611bb5026ba99496ddf2f0 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 12:49:01 -0700 Subject: [PATCH 028/168] Whoops --- src/common.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common.cc b/src/common.cc index bdd9e4d..e077c63 100644 --- a/src/common.cc +++ b/src/common.cc @@ -142,7 +142,7 @@ Napi::Value Unwatch(const Napi::CallbackInfo& info) { auto thing = info[0]; #ifdef _WIN32 - Napi::BigInt num = info[0]; + Napi::Value num = info[0]; #else Napi::Number num = info[0].ToNumber(); #endif From 6f92a52dbb40796935985cd30f591f672480b1fa Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 12:55:57 -0700 Subject: [PATCH 029/168] Logging --- src/common.cc | 10 ++++++++++ src/pathwatcher_win.cc | 13 +++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/common.cc b/src/common.cc index e077c63..3f800ef 100644 --- a/src/common.cc +++ b/src/common.cc @@ -22,6 +22,7 @@ void PathWatcherWorker::Execute( } void PathWatcherWorker::Stop() { + std::cout << "PathWatcherWorker::Stop" << std::endl; shouldStop = true; } @@ -56,6 +57,7 @@ void PathWatcherWorker::OnOK() {} // Called when the first watcher is created. void Start(Napi::Env env) { + std::cout << "Start" << std::endl; Napi::HandleScope scope(env); auto addonData = env.GetInstanceData(); if (!addonData->callback) { @@ -70,6 +72,7 @@ void Start(Napi::Env env) { // Called when the last watcher is stopped. void Stop(Napi::Env env) { + std::cout << "Stop" << std::endl; auto addonData = env.GetInstanceData(); if (addonData->worker) { addonData->worker->Stop(); @@ -77,6 +80,7 @@ void Stop(Napi::Env env) { } Napi::Value SetCallback(const Napi::CallbackInfo& info) { + std::cout << "SetCallback" << std::endl; auto env = info.Env(); Napi::HandleScope scope(env); @@ -86,12 +90,17 @@ Napi::Value SetCallback(const Napi::CallbackInfo& info) { } auto addonData = env.GetInstanceData(); + if (addonData->worker) { + std::cout << "Worker already exists" << std::endl; + + } addonData->callback.Reset(info[0].As(), 1); return env.Undefined(); } Napi::Value Watch(const Napi::CallbackInfo& info) { + std::cout << "Watch" << std::endl; auto env = info.Env(); auto addonData = env.GetInstanceData(); Napi::HandleScope scope(env); @@ -127,6 +136,7 @@ Napi::Value Watch(const Napi::CallbackInfo& info) { } Napi::Value Unwatch(const Napi::CallbackInfo& info) { + std::cout << "Unwatch" << std::endl; auto env = info.Env(); auto addonData = env.GetInstanceData(); Napi::HandleScope scope(env); diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 61141e8..65234ff 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -132,7 +132,8 @@ void PlatformThread( const PathWatcherWorker::ExecutionProgress& progress, bool& shouldStop ) { - while (true) { + std::cout << "PlatformThread" << std::endl; + while (!shouldStop) { // Do not use g_events directly, since reallocation could happen when there // are new watchers adding to g_events when WaitForMultipleObjects is still // polling. @@ -144,8 +145,14 @@ void PlatformThread( DWORD r = WaitForMultipleObjects(copied_events.size(), copied_events.data(), FALSE, - INFINITE); + 100); SetEvent(g_file_handles_free_event); + + if (r == WAIT_TIMEOUT) { + // Timeout occurred, check shouldStop flag + continue; + } + int i = r - WAIT_OBJECT_0; if (i >= 0 && i < copied_events.size()) { // It's a wake up event, there is no fs events. @@ -255,6 +262,7 @@ void PlatformThread( } WatcherHandle PlatformWatch(const char* path) { + std::cout << "PlatformWatch" << std::endl; wchar_t wpath[MAX_PATH] = { 0 }; MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, MAX_PATH); @@ -295,6 +303,7 @@ WatcherHandle PlatformWatch(const char* path) { } void PlatformUnwatch(WatcherHandle key) { + std::cout << "PlatformUnwatch" << std::endl; if (PlatformIsHandleValid(key)) { HandleWrapper* handle; { From 8460f45f45b3b0d6f757ff87df452bf7723b49e4 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 12:57:19 -0700 Subject: [PATCH 030/168] Ugh --- src/common.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common.cc b/src/common.cc index 3f800ef..3285be3 100644 --- a/src/common.cc +++ b/src/common.cc @@ -2,6 +2,8 @@ #include "addon-data.h" #include "uv.h" +#include + using namespace Napi; void CommonInit(Napi::Env env) { From 390755f30f45fadcab34d164ba2ad9b8d0b27a25 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 13:05:50 -0700 Subject: [PATCH 031/168] Add Linux logging --- src/pathwatcher_linux.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pathwatcher_linux.cc b/src/pathwatcher_linux.cc index f72a6ff..133351e 100644 --- a/src/pathwatcher_linux.cc +++ b/src/pathwatcher_linux.cc @@ -7,6 +7,7 @@ #include #include +#include #include "common.h" @@ -25,6 +26,7 @@ void PlatformThread( const PathWatcherWorker::ExecutionProgress& progress, bool& shouldStop ) { + std::cout << "PlatformThread START" << std::endl; // Needs to be large enough for sizeof(inotify_event) + strlen(filename). char buf[4096]; @@ -73,6 +75,8 @@ void PlatformThread( progress.Send(&event, 1); } } + + std::cout << "PlatformThread END" << std::endl; } WatcherHandle PlatformWatch(const char* path) { From da58f0d35f29886734b0498eca455c3838e05285 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 15:28:37 -0700 Subject: [PATCH 032/168] OK, now once again without the logging --- src/common.cc | 17 +++++++---------- src/pathwatcher_linux.cc | 4 ++-- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/common.cc b/src/common.cc index 3285be3..8ed740e 100644 --- a/src/common.cc +++ b/src/common.cc @@ -24,7 +24,7 @@ void PathWatcherWorker::Execute( } void PathWatcherWorker::Stop() { - std::cout << "PathWatcherWorker::Stop" << std::endl; + // std::cout << "PathWatcherWorker::Stop" << std::endl; shouldStop = true; } @@ -59,7 +59,7 @@ void PathWatcherWorker::OnOK() {} // Called when the first watcher is created. void Start(Napi::Env env) { - std::cout << "Start" << std::endl; + // std::cout << "Start" << std::endl; Napi::HandleScope scope(env); auto addonData = env.GetInstanceData(); if (!addonData->callback) { @@ -74,7 +74,7 @@ void Start(Napi::Env env) { // Called when the last watcher is stopped. void Stop(Napi::Env env) { - std::cout << "Stop" << std::endl; + // std::cout << "Stop" << std::endl; auto addonData = env.GetInstanceData(); if (addonData->worker) { addonData->worker->Stop(); @@ -82,7 +82,7 @@ void Stop(Napi::Env env) { } Napi::Value SetCallback(const Napi::CallbackInfo& info) { - std::cout << "SetCallback" << std::endl; + // std::cout << "SetCallback" << std::endl; auto env = info.Env(); Napi::HandleScope scope(env); @@ -93,8 +93,7 @@ Napi::Value SetCallback(const Napi::CallbackInfo& info) { auto addonData = env.GetInstanceData(); if (addonData->worker) { - std::cout << "Worker already exists" << std::endl; - + // std::cout << "Worker already exists" << std::endl; } addonData->callback.Reset(info[0].As(), 1); @@ -102,7 +101,7 @@ Napi::Value SetCallback(const Napi::CallbackInfo& info) { } Napi::Value Watch(const Napi::CallbackInfo& info) { - std::cout << "Watch" << std::endl; + // std::cout << "Watch" << std::endl; auto env = info.Env(); auto addonData = env.GetInstanceData(); Napi::HandleScope scope(env); @@ -138,7 +137,7 @@ Napi::Value Watch(const Napi::CallbackInfo& info) { } Napi::Value Unwatch(const Napi::CallbackInfo& info) { - std::cout << "Unwatch" << std::endl; + // std::cout << "Unwatch" << std::endl; auto env = info.Env(); auto addonData = env.GetInstanceData(); Napi::HandleScope scope(env); @@ -151,8 +150,6 @@ Napi::Value Unwatch(const Napi::CallbackInfo& info) { return env.Null(); } - auto thing = info[0]; - #ifdef _WIN32 Napi::Value num = info[0]; #else diff --git a/src/pathwatcher_linux.cc b/src/pathwatcher_linux.cc index 133351e..ff89f88 100644 --- a/src/pathwatcher_linux.cc +++ b/src/pathwatcher_linux.cc @@ -26,7 +26,7 @@ void PlatformThread( const PathWatcherWorker::ExecutionProgress& progress, bool& shouldStop ) { - std::cout << "PlatformThread START" << std::endl; + // std::cout << "PlatformThread START" << std::endl; // Needs to be large enough for sizeof(inotify_event) + strlen(filename). char buf[4096]; @@ -76,7 +76,7 @@ void PlatformThread( } } - std::cout << "PlatformThread END" << std::endl; + // std::cout << "PlatformThread END" << std::endl; } WatcherHandle PlatformWatch(const char* path) { From 5bad1538904fc0ebcb9c3a0a2fd03e56448beb5f Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 16:03:00 -0700 Subject: [PATCH 033/168] Add context-safety test --- package.json | 3 +- spec/context-safety.js | 13 ++++++ spec/worker.js | 97 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 spec/context-safety.js create mode 100644 spec/worker.js diff --git a/package.json b/package.json index 2859eaf..afc6f91 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "homepage": "http://atom.github.io/node-pathwatcher", "scripts": { "prepublish": "grunt prepublish", - "test": "jasmine spec/*-spec.js" + "test": "jasmine spec/*-spec.js", + "test-context-safety": "node spec/context-safety.js" }, "devDependencies": { "grunt": "~0.4.1", diff --git a/spec/context-safety.js b/spec/context-safety.js new file mode 100644 index 0000000..6512abf --- /dev/null +++ b/spec/context-safety.js @@ -0,0 +1,13 @@ + +// This script tests the library for context safety by creating several +// instances on separate threads. +// +// This test is successful when the script exits gracefully. It fails when the +// script segfaults or runs indefinitely. +const spawnThread = require('./worker'); + +const NUM_WORKERS = 3; + +for (let i = 0; i < NUM_WORKERS; i++) { + spawnThread(i); +} diff --git a/spec/worker.js b/spec/worker.js new file mode 100644 index 0000000..5cd3c54 --- /dev/null +++ b/spec/worker.js @@ -0,0 +1,97 @@ +const { + Worker, isMainThread, parentPort, workerData +} = require('node:worker_threads'); +const { + performance +} = require('node:perf_hooks'); + +const temp = require('temp'); +const fs = require('fs'); +const path = require('path'); +const util = require('util'); + +const wait = util.promisify(setTimeout); + +const EXPECTED_CALL_COUNT = 3; + +if (isMainThread) { + module.exports = function spawnThread(id) { + return new Promise(async (resolve, reject) => { + console.log('Spawning worker:', id); + const worker = new Worker(__filename, { + workerData: id, + }); + worker.on('message', async (msg) => { + console.log('[parent] Worker', id, 'reported call count:', msg); + await wait(500); + if (msg >= EXPECTED_CALL_COUNT) { + resolve(); + } else { + reject(`Not enough calls! Expected: ${EXPECTED_CALL_COUNT} Actual: ${msg}`); + } + }); + worker.on('error', reject); + worker.on('exit', (code) => { + if (code !== 0) { + console.log(`Worker stopped with exit code ${code}`); + reject(); + } + }); + }); + }; +} else { + const tempDir = temp.mkdirSync('node-pathwatcher-directory'); + const tempFile = path.join(tempDir, 'file'); + + const { watch, closeAllWatchers } = require('../src/main'); + + class Scheduler { + constructor(id, pathToWatch) { + this.id = id; + this.path = pathToWatch; + this.callCount = 0; + } + + async start () { + console.log('Scheduler', this.id, 'starting at', performance.now(), 'watching path:', this.path); + this.watcher = watch(this.path, (event) => { + this.callCount++; + console.log('PathWatcher event for worker', this.id, event) + console.log('callCount is now:', this.callCount); + }); + console.log('Scheduler', this.id, 'ready at:', performance.now()); + } + + stop () { + this.watcher?.close(); + } + } + + (async () => { + console.log('Worker', workerData, 'creating file:', tempFile); + fs.writeFileSync(tempFile, ''); + await wait(500); + const scheduler = new Scheduler(workerData, tempFile); + scheduler.start(); + await wait(2000); + + console.log('Worker', workerData, 'changing file:', tempFile); + // Should generate one or two events: + fs.writeFileSync(tempFile, 'changed'); + await wait(1000); + console.log('Worker', workerData, 'changing file again:', tempFile); + // Should generate another event: + fs.writeFileSync(tempFile, 'changed again'); + await wait(1000); + // Should generate a final event (total count 3 or 4): + console.log('Worker', workerData, 'deleting file:', tempFile); + fs.rmSync(tempFile); + await wait(1000); + + parentPort.postMessage(scheduler.callCount); + + closeAllWatchers(); + console.log('Worker', workerData, 'closing'); + process.exit(0); + })(); +} From 514fcb8ab5a0f5c8af5b793038697665b175ec2e Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 16:04:34 -0700 Subject: [PATCH 034/168] =?UTF-8?q?Get=20macOS=20context=20safety=20workin?= =?UTF-8?q?g=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …by moving away from global definitions. I thought that it was OK for the `pathwatcher_*.cc` implementations to share global state, but apparently not! macOS doesn't crash, but it does drop callbacks. So the context-safety test now enforces that the callback was invoked the expected number of times. --- src/addon-data.h | 5 +++++ src/common.cc | 8 ++++---- src/common.h | 7 ++++--- src/pathwatcher_linux.cc | 7 ++++--- src/pathwatcher_unix.cc | 42 ++++++++++++++++++++++++---------------- src/pathwatcher_win.cc | 10 +++++----- 6 files changed, 47 insertions(+), 32 deletions(-) diff --git a/src/addon-data.h b/src/addon-data.h index 2a57032..d6b7081 100644 --- a/src/addon-data.h +++ b/src/addon-data.h @@ -8,4 +8,9 @@ class AddonData final { Napi::FunctionReference callback; PathWatcherWorker* worker; int watch_count; + +#ifdef __APPLE__ + int kqueue; + int init_errno; +#endif }; diff --git a/src/common.cc b/src/common.cc index 8ed740e..166b042 100644 --- a/src/common.cc +++ b/src/common.cc @@ -12,7 +12,7 @@ void CommonInit(Napi::Env env) { } PathWatcherWorker::PathWatcherWorker(Napi::Env env, Function &progressCallback) : - AsyncProgressQueueWorker(env) { + AsyncProgressQueueWorker(env), _env(env) { shouldStop = false; this->progressCallback.Reset(progressCallback); } @@ -20,7 +20,7 @@ PathWatcherWorker::PathWatcherWorker(Napi::Env env, Function &progressCallback) void PathWatcherWorker::Execute( const PathWatcherWorker::ExecutionProgress& progress ) { - PlatformThread(progress, shouldStop); + PlatformThread(progress, shouldStop, _env); } void PathWatcherWorker::Stop() { @@ -113,7 +113,7 @@ Napi::Value Watch(const Napi::CallbackInfo& info) { Napi::String path = info[0].ToString(); std::string cppPath(path); - WatcherHandle handle = PlatformWatch(cppPath.c_str()); + WatcherHandle handle = PlatformWatch(cppPath.c_str(), env); if (!PlatformIsHandleValid(handle)) { int error_number = PlatformInvalidHandleToErrorNumber(handle); @@ -156,7 +156,7 @@ Napi::Value Unwatch(const Napi::CallbackInfo& info) { Napi::Number num = info[0].ToNumber(); #endif - PlatformUnwatch(V8ValueToWatcherHandle(num)); + PlatformUnwatch(V8ValueToWatcherHandle(num), env); if (--addonData->watch_count == 0) Stop(env); diff --git a/src/common.h b/src/common.h index 55c4463..9e59b0d 100644 --- a/src/common.h +++ b/src/common.h @@ -35,8 +35,8 @@ typedef int32_t WatcherHandle; #endif void PlatformInit(Napi::Env env); -WatcherHandle PlatformWatch(const char* path); -void PlatformUnwatch(WatcherHandle handle); +WatcherHandle PlatformWatch(const char* path, Napi::Env env); +void PlatformUnwatch(WatcherHandle handle, Napi::Env env); bool PlatformIsHandleValid(WatcherHandle handle); int PlatformInvalidHandleToErrorNumber(WatcherHandle handle); @@ -113,13 +113,14 @@ class PathWatcherWorker: public AsyncProgressQueueWorker { void Stop(); private: + Napi::Env _env; bool shouldStop = false; FunctionReference progressCallback; const char* GetEventTypeString(EVENT_TYPE type); }; -void PlatformThread(const PathWatcherWorker::ExecutionProgress& progress, bool& shouldStop); +void PlatformThread(const PathWatcherWorker::ExecutionProgress& progress, bool& shouldStop, Napi::Env env); void CommonInit(Napi::Env env); diff --git a/src/pathwatcher_linux.cc b/src/pathwatcher_linux.cc index ff89f88..3d6c2d1 100644 --- a/src/pathwatcher_linux.cc +++ b/src/pathwatcher_linux.cc @@ -24,7 +24,8 @@ void PlatformInit(Napi::Env _env) { void PlatformThread( const PathWatcherWorker::ExecutionProgress& progress, - bool& shouldStop + bool& shouldStop, + Napi::Env env ) { // std::cout << "PlatformThread START" << std::endl; // Needs to be large enough for sizeof(inotify_event) + strlen(filename). @@ -79,7 +80,7 @@ void PlatformThread( // std::cout << "PlatformThread END" << std::endl; } -WatcherHandle PlatformWatch(const char* path) { +WatcherHandle PlatformWatch(const char* path, Napi::Env env) { if (g_inotify == -1) { return -g_init_errno; } @@ -92,7 +93,7 @@ WatcherHandle PlatformWatch(const char* path) { return fd; } -void PlatformUnwatch(WatcherHandle fd) { +void PlatformUnwatch(WatcherHandle fd, Napi::Env env) { inotify_rm_watch(g_inotify, fd); } diff --git a/src/pathwatcher_unix.cc b/src/pathwatcher_unix.cc index 089e48c..1c00ef3 100644 --- a/src/pathwatcher_unix.cc +++ b/src/pathwatcher_unix.cc @@ -4,10 +4,11 @@ #include #include #include - +#include #include #include #include "common.h" +#include "addon-data.h" // test for descriptor event notification, if not available set to O_RDONLY #ifndef O_EVTONLY @@ -25,21 +26,26 @@ // NOTE: You might see the globals and get nervous here. Our working theory is // that this this is fine; this is thread-safe without having to be isolated // between contexts. -static int g_kqueue; -static int g_init_errno; - -void PlatformInit(Napi::Env _env) { - g_kqueue = kqueue(); - if (g_kqueue == -1) { - g_init_errno = errno; +// static int g_kqueue; +// static int g_init_errno; + +void PlatformInit(Napi::Env env) { + auto addonData = env.GetInstanceData(); + addonData->kqueue = kqueue(); + if (addonData->kqueue == -1) { + addonData->init_errno = errno; return; } } void PlatformThread( const PathWatcherWorker::ExecutionProgress& progress, - bool& shouldStop + bool& shouldStop, + Napi::Env env ) { + auto addonData = env.GetInstanceData(); + int l_kqueue = addonData->kqueue; + std::cout << "PlatformThread " << std::this_thread::get_id() << std::endl; struct kevent event; struct timespec timeout = { 0, 500000000 }; @@ -47,7 +53,7 @@ void PlatformThread( int r; do { if (shouldStop) return; - r = kevent(g_kqueue, NULL, 0, &event, 1, &timeout); + r = kevent(l_kqueue, NULL, 0, &event, 1, &timeout); } while ((r == -1 && errno == EINTR) || r == 0); EVENT_TYPE type; @@ -75,19 +81,21 @@ void PlatformThread( continue; } + std::cout << "PlatformThread EVENT " << std::this_thread::get_id() << std::endl; PathWatcherEvent event(type, fd, path); progress.Send(&event, 1); } } -WatcherHandle PlatformWatch(const char* path) { - if (g_kqueue == -1) { - return -g_init_errno; +WatcherHandle PlatformWatch(const char* path, Napi::Env env) { + auto addonData = env.GetInstanceData(); + if (addonData->kqueue == -1) { + return -addonData->init_errno; } int fd = open(path, O_EVTONLY, 0); if (fd < 0) { - return -errno; + return -addonData->init_errno; } struct timespec timeout = { 0, 50000000 }; @@ -96,16 +104,16 @@ WatcherHandle PlatformWatch(const char* path) { int flags = EV_ADD | EV_ENABLE | EV_CLEAR; int fflags = NOTE_WRITE | NOTE_DELETE | NOTE_RENAME | NOTE_ATTRIB; EV_SET(&event, fd, filter, flags, fflags, 0, reinterpret_cast(const_cast(path))); - int r = kevent(g_kqueue, &event, 1, NULL, 0, &timeout); + int r = kevent(addonData->kqueue, &event, 1, NULL, 0, &timeout); if (r == -1) { - return -errno; + return -addonData->init_errno; } return fd; } -void PlatformUnwatch(WatcherHandle fd) { +void PlatformUnwatch(WatcherHandle fd, Napi::Env _env) { close(fd); } diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 65234ff..9d8c1c6 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -132,7 +132,7 @@ void PlatformThread( const PathWatcherWorker::ExecutionProgress& progress, bool& shouldStop ) { - std::cout << "PlatformThread" << std::endl; + // std::cout << "PlatformThread" << std::endl; while (!shouldStop) { // Do not use g_events directly, since reallocation could happen when there // are new watchers adding to g_events when WaitForMultipleObjects is still @@ -261,8 +261,8 @@ void PlatformThread( } } -WatcherHandle PlatformWatch(const char* path) { - std::cout << "PlatformWatch" << std::endl; +WatcherHandle PlatformWatch(const char* path, Napi::Env env) { + // std::cout << "PlatformWatch" << std::endl; wchar_t wpath[MAX_PATH] = { 0 }; MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, MAX_PATH); @@ -302,8 +302,8 @@ WatcherHandle PlatformWatch(const char* path) { return handle.release()->overlapped.hEvent; } -void PlatformUnwatch(WatcherHandle key) { - std::cout << "PlatformUnwatch" << std::endl; +void PlatformUnwatch(WatcherHandle key, Napi::Env env) { + // std::cout << "PlatformUnwatch" << std::endl; if (PlatformIsHandleValid(key)) { HandleWrapper* handle; { From 6c7700db4d89fbd179c06a857a0672feba4164a7 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 16:05:09 -0700 Subject: [PATCH 035/168] Add the context safety test to CI runs --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ceecd49..e591447 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,3 +31,6 @@ jobs: - name: Run tests run: npm test + + - name: Run context safety test + run: npm run test-context-safety From 20b987b9879f9d614ce3855ad38b1b93182753dd Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 8 Oct 2024 16:14:02 -0700 Subject: [PATCH 036/168] Attempt to make Linux context-safe --- src/addon-data.h | 18 ++++++++++++++++++ src/pathwatcher_linux.cc | 30 +++++++++++++++++------------- src/pathwatcher_unix.cc | 8 ++++---- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/addon-data.h b/src/addon-data.h index d6b7081..926924e 100644 --- a/src/addon-data.h +++ b/src/addon-data.h @@ -10,7 +10,25 @@ class AddonData final { int watch_count; #ifdef __APPLE__ + // macOS. int kqueue; int init_errno; +#endif + // Not macOS. +#ifdef _WIN32 + // Mutex for the HandleWrapper map. + uv_mutex_t handle_wrap_map_mutex; + // The events to be waited on. + std::vector events; + // The dummy event to wakeup the thread. + HANDLE wake_up_event; + // The dummy event to ensure we are not waiting on a file handle when + // destroying it. + HANDLE file_handles_free_event; +#endif +#ifdef __linux__ + // Linux. + int inotify; + int init_errno; #endif }; diff --git a/src/pathwatcher_linux.cc b/src/pathwatcher_linux.cc index 3d6c2d1..ddbd64a 100644 --- a/src/pathwatcher_linux.cc +++ b/src/pathwatcher_linux.cc @@ -10,14 +10,16 @@ #include #include "common.h" +#include "addon-data.h" -static int g_inotify; -static int g_init_errno; +// static int g_inotify; +// static int g_init_errno; -void PlatformInit(Napi::Env _env) { - g_inotify = inotify_init(); - if (g_inotify == -1) { - g_init_errno = errno; +void PlatformInit(Napi::Env env) { + auto addonData = env.GetInstanceData(); + addonData->inotify = inotify_init(); + if (addonData->inotify == -1) { + addonData->init_errno = errno; return; } } @@ -27,6 +29,7 @@ void PlatformThread( bool& shouldStop, Napi::Env env ) { + auto addonData = env.GetInstanceData(); // std::cout << "PlatformThread START" << std::endl; // Needs to be large enough for sizeof(inotify_event) + strlen(filename). char buf[4096]; @@ -34,13 +37,13 @@ void PlatformThread( while (!shouldStop) { fd_set read_fds; FD_ZERO(&read_fds); - FD_SET(g_inotify, &read_fds); + FD_SET(addonData->inotify, &read_fds); struct timeval tv; tv.tv_sec = 0; tv.tv_usec = 100000; // 100ms timeout - int ret = select(g_inotify + 1, &read_fds, NULL, NULL, &tv); + int ret = select(addonData->inotify + 1, &read_fds, NULL, NULL, &tv); if (ret == -1 && errno != EINTR) { break; @@ -51,7 +54,7 @@ void PlatformThread( continue; } - int size = read(g_inotify, buf, sizeof(buf)); + int size = read(addonData->inotify, buf, sizeof(buf)); if (size <= 0) break; inotify_event* e; @@ -81,11 +84,11 @@ void PlatformThread( } WatcherHandle PlatformWatch(const char* path, Napi::Env env) { - if (g_inotify == -1) { - return -g_init_errno; + if (addonData->inotify == -1) { + return -addonData->init_errno; } - int fd = inotify_add_watch(g_inotify, path, IN_ATTRIB | IN_CREATE | + int fd = inotify_add_watch(addonData->inotify, path, IN_ATTRIB | IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVE | IN_MOVE_SELF | IN_DELETE_SELF); if (fd == -1) { return -errno; @@ -94,7 +97,8 @@ WatcherHandle PlatformWatch(const char* path, Napi::Env env) { } void PlatformUnwatch(WatcherHandle fd, Napi::Env env) { - inotify_rm_watch(g_inotify, fd); + auto addonData = env.GetInstanceData(); + inotify_rm_watch(addonData->inotify, fd); } bool PlatformIsHandleValid(WatcherHandle handle) { diff --git a/src/pathwatcher_unix.cc b/src/pathwatcher_unix.cc index 1c00ef3..6bddeb8 100644 --- a/src/pathwatcher_unix.cc +++ b/src/pathwatcher_unix.cc @@ -45,7 +45,7 @@ void PlatformThread( ) { auto addonData = env.GetInstanceData(); int l_kqueue = addonData->kqueue; - std::cout << "PlatformThread " << std::this_thread::get_id() << std::endl; + // std::cout << "PlatformThread " << std::this_thread::get_id() << std::endl; struct kevent event; struct timespec timeout = { 0, 500000000 }; @@ -81,7 +81,7 @@ void PlatformThread( continue; } - std::cout << "PlatformThread EVENT " << std::this_thread::get_id() << std::endl; + // std::cout << "PlatformThread EVENT " << std::this_thread::get_id() << std::endl; PathWatcherEvent event(type, fd, path); progress.Send(&event, 1); } @@ -95,7 +95,7 @@ WatcherHandle PlatformWatch(const char* path, Napi::Env env) { int fd = open(path, O_EVTONLY, 0); if (fd < 0) { - return -addonData->init_errno; + return -errno; } struct timespec timeout = { 0, 50000000 }; @@ -106,7 +106,7 @@ WatcherHandle PlatformWatch(const char* path, Napi::Env env) { EV_SET(&event, fd, filter, flags, fflags, 0, reinterpret_cast(const_cast(path))); int r = kevent(addonData->kqueue, &event, 1, NULL, 0, &timeout); if (r == -1) { - return -addonData->init_errno; + return -errno; } return fd; From 8f1cf55be14151bdeea20db669e954e649a3fa86 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 11:00:40 -0700 Subject: [PATCH 037/168] Remove Windows `AddonData` stuff --- src/addon-data.h | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/addon-data.h b/src/addon-data.h index 926924e..b01f4c3 100644 --- a/src/addon-data.h +++ b/src/addon-data.h @@ -14,18 +14,7 @@ class AddonData final { int kqueue; int init_errno; #endif - // Not macOS. -#ifdef _WIN32 - // Mutex for the HandleWrapper map. - uv_mutex_t handle_wrap_map_mutex; - // The events to be waited on. - std::vector events; - // The dummy event to wakeup the thread. - HANDLE wake_up_event; - // The dummy event to ensure we are not waiting on a file handle when - // destroying it. - HANDLE file_handles_free_event; -#endif + #ifdef __linux__ // Linux. int inotify; From bc5e854e94adf18df0de6dce1f2a2b9cf86895da Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 11:05:36 -0700 Subject: [PATCH 038/168] Missing argument --- src/pathwatcher_win.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 9d8c1c6..b68a9ec 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -130,7 +130,8 @@ void PlatformInit(Napi::Env _env) { void PlatformThread( const PathWatcherWorker::ExecutionProgress& progress, - bool& shouldStop + bool& shouldStop, + Napi::Env _env ) { // std::cout << "PlatformThread" << std::endl; while (!shouldStop) { From b532ef83013208bc2997f8f81f48a93c9ac36fb7 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 11:10:18 -0700 Subject: [PATCH 039/168] Fix Linux omission --- src/pathwatcher_linux.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pathwatcher_linux.cc b/src/pathwatcher_linux.cc index ddbd64a..9c82888 100644 --- a/src/pathwatcher_linux.cc +++ b/src/pathwatcher_linux.cc @@ -84,6 +84,7 @@ void PlatformThread( } WatcherHandle PlatformWatch(const char* path, Napi::Env env) { + auto addonData = env.GetInstanceData(); if (addonData->inotify == -1) { return -addonData->init_errno; } From b42dec1436eb818d54e80b0240695555ca490043 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 11:23:02 -0700 Subject: [PATCH 040/168] Add some logging --- src/addon-data.h | 7 +++- src/pathwatcher_win.cc | 87 +++++++++++++++++++++++------------------- 2 files changed, 54 insertions(+), 40 deletions(-) diff --git a/src/addon-data.h b/src/addon-data.h index b01f4c3..df0dc5c 100644 --- a/src/addon-data.h +++ b/src/addon-data.h @@ -1,13 +1,18 @@ #include "common.h" #pragma once +static int g_next_addon_data_id = 1; + class AddonData final { public: - explicit AddonData(Napi::Env env) {} + explicit AddonData(Napi::Env env) { + id = g_next_addon_data_id++; + } Napi::FunctionReference callback; PathWatcherWorker* worker; int watch_count; + int id; #ifdef __APPLE__ // macOS. diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index b68a9ec..a74d7ad 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -82,21 +82,18 @@ struct WatcherEvent { }; static bool QueueReaddirchanges(HandleWrapper* handle) { - return ReadDirectoryChangesW(handle->dir_handle, - handle->buffer, - kDirectoryWatcherBufferSize, - FALSE, - FILE_NOTIFY_CHANGE_FILE_NAME | - FILE_NOTIFY_CHANGE_DIR_NAME | - FILE_NOTIFY_CHANGE_ATTRIBUTES | - FILE_NOTIFY_CHANGE_SIZE | - FILE_NOTIFY_CHANGE_LAST_WRITE | - FILE_NOTIFY_CHANGE_LAST_ACCESS | - FILE_NOTIFY_CHANGE_CREATION | - FILE_NOTIFY_CHANGE_SECURITY, - NULL, - &handle->overlapped, - NULL) == TRUE; + return ReadDirectoryChangesW( + handle->dir_handle, + handle->buffer, + kDirectoryWatcherBufferSize, + FALSE, + FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | + FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_LAST_ACCESS | + FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_SECURITY, + NULL, + &handle->overlapped, + NULL + ) == TRUE; } Napi::Value WatcherHandleToV8Value(WatcherHandle handle, Napi::Env env) { @@ -131,8 +128,11 @@ void PlatformInit(Napi::Env _env) { void PlatformThread( const PathWatcherWorker::ExecutionProgress& progress, bool& shouldStop, - Napi::Env _env + Napi::Env env ) { + auto addonData = env.GetInstanceData(); + std::cout "PlatformThread ID: " << addonData->id << std::endl; + // std::cout << "PlatformThread" << std::endl; while (!shouldStop) { // Do not use g_events directly, since reallocation could happen when there @@ -143,10 +143,12 @@ void PlatformThread( locker.Unlock(); ResetEvent(g_file_handles_free_event); - DWORD r = WaitForMultipleObjects(copied_events.size(), - copied_events.data(), - FALSE, - 100); + DWORD r = WaitForMultipleObjects( + copied_events.size(), + copied_events.data(), + FALSE, + 100 + ); SetEvent(g_file_handles_free_event); if (r == WAIT_TIMEOUT) { @@ -207,14 +209,16 @@ void PlatformThread( file_info->FileNameLength / sizeof(wchar_t); char filename[MAX_PATH] = { 0 }; - int size = WideCharToMultiByte(CP_UTF8, - 0, - file_info->FileName, - file_name_length_in_characters, - filename, - MAX_PATH, - NULL, - NULL); + int size = WideCharToMultiByte( + CP_UTF8, + 0, + file_info->FileName, + file_name_length_in_characters, + filename, + MAX_PATH, + NULL, + NULL + ); // Convert file name to file path, same with: // path = handle->path + '\\' + filename @@ -249,6 +253,8 @@ void PlatformThread( locker.Unlock(); + std::cout "Total events processed on thread " << addonData->id << ": " << events.size() << std::endl; + for (size_t i = 0; i < events.size(); ++i) { PathWatcherEvent event( events[i].type, @@ -263,7 +269,8 @@ void PlatformThread( } WatcherHandle PlatformWatch(const char* path, Napi::Env env) { - // std::cout << "PlatformWatch" << std::endl; + auto addonData = env.GetInstanceData(); + std::cout << "PlatformWatch ID: " << addonData->id << " Path: " << path << std::endl; wchar_t wpath[MAX_PATH] = { 0 }; MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, MAX_PATH); @@ -273,15 +280,16 @@ WatcherHandle PlatformWatch(const char* path, Napi::Env env) { return INVALID_HANDLE_VALUE; } - WatcherHandle dir_handle = CreateFileW(wpath, - FILE_LIST_DIRECTORY, - FILE_SHARE_READ | FILE_SHARE_DELETE | - FILE_SHARE_WRITE, - NULL, - OPEN_EXISTING, - FILE_FLAG_BACKUP_SEMANTICS | - FILE_FLAG_OVERLAPPED, - NULL); + WatcherHandle dir_handle = CreateFileW( + wpath, + FILE_LIST_DIRECTORY, + FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, + NULL + ); + if (!PlatformIsHandleValid(dir_handle)) { return INVALID_HANDLE_VALUE; } @@ -304,7 +312,8 @@ WatcherHandle PlatformWatch(const char* path, Napi::Env env) { } void PlatformUnwatch(WatcherHandle key, Napi::Env env) { - // std::cout << "PlatformUnwatch" << std::endl; + auto addonData = env.GetInstanceData(); + std::cout << "PlatformUnwatch ID: " << addonData->id << std::endl; if (PlatformIsHandleValid(key)) { HandleWrapper* handle; { From 99c1595a6e31d0e4e27f6ea42b5786362d5e54b5 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 11:25:49 -0700 Subject: [PATCH 041/168] Ugh --- src/pathwatcher_win.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index a74d7ad..53257e6 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -4,6 +4,7 @@ #include #include "common.h" +#include "addon-data.h" #include "js_native_api_types.h" #include "napi.h" #include "uv.h" From 92750d8133bfb06aa49b345b0ad812438014927e Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 11:30:10 -0700 Subject: [PATCH 042/168] I am good at this --- src/pathwatcher_win.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 53257e6..233d71e 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -132,7 +132,7 @@ void PlatformThread( Napi::Env env ) { auto addonData = env.GetInstanceData(); - std::cout "PlatformThread ID: " << addonData->id << std::endl; + std::cout << "PlatformThread ID: " << addonData->id << std::endl; // std::cout << "PlatformThread" << std::endl; while (!shouldStop) { @@ -254,7 +254,7 @@ void PlatformThread( locker.Unlock(); - std::cout "Total events processed on thread " << addonData->id << ": " << events.size() << std::endl; + std::cout << "Total events processed on thread " << addonData->id << ": " << events.size() << std::endl; for (size_t i = 0; i < events.size(); ++i) { PathWatcherEvent event( From dd085470b2079a206d3ff5a7416cd18069764653 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 11:39:54 -0700 Subject: [PATCH 043/168] More logging, plus harmonize thread/worker IDs --- spec/worker.js | 3 ++- src/pathwatcher_win.cc | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/worker.js b/spec/worker.js index 5cd3c54..5eea421 100644 --- a/spec/worker.js +++ b/spec/worker.js @@ -15,7 +15,8 @@ const wait = util.promisify(setTimeout); const EXPECTED_CALL_COUNT = 3; if (isMainThread) { - module.exports = function spawnThread(id) { + module.exports = function spawnThread(index) { + let id = index + 1; return new Promise(async (resolve, reject) => { console.log('Spawning worker:', id); const worker = new Worker(__filename, { diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 233d71e..f74011e 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -257,6 +257,7 @@ void PlatformThread( std::cout << "Total events processed on thread " << addonData->id << ": " << events.size() << std::endl; for (size_t i = 0; i < events.size(); ++i) { + std::cout << "Emitting " << events[i].type << " event on thread " << addonData->id << " for path: " << events[i].new_path.data() << std::endl; PathWatcherEvent event( events[i].type, events[i].handle, From c1ec4f0fcc8624afcdd33215d4d91cad3f2037dc Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 12:12:46 -0700 Subject: [PATCH 044/168] =?UTF-8?q?More=20logging;=20try=20to=20skip=20han?= =?UTF-8?q?dles=20that=20we=20didn=E2=80=99t=20create?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pathwatcher_win.cc | 58 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index f74011e..f3dba1c 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -9,6 +9,29 @@ #include "napi.h" #include "uv.h" +// class VectorMap { +// public: +// using Vector = std::vector; +// +// std::shared_ptr get_or_create(int addonDataId) { +// auto it = vectors_.find(addonDataId); +// if (it == vectors_.end()) { +// it = vectors_.emplace(addonDataId, std::make_shared()).first; +// } +// return it->second; +// } +// +// void remove(int addonDataId) { +// vectors_.erase(addonDataId); +// } +// +// private: +// std::map> vectors_; +// }; +// +// // Global instance of VectorMap +// VectorMap g_vector_map; + // Size of the buffer to store result of ReadDirectoryChangesW. static const unsigned int kDirectoryWatcherBufferSize = 4096; @@ -34,8 +57,9 @@ struct ScopedLocker { }; struct HandleWrapper { - HandleWrapper(WatcherHandle handle, const char* path_str) - : dir_handle(handle), + HandleWrapper(WatcherHandle handle, const char* path_str, int addon_data_id) + : addonDataId(addon_data_id), + dir_handle(handle), path(strlen(path_str)), canceled(false) { memset(&overlapped, 0, sizeof(overlapped)); @@ -62,6 +86,7 @@ struct HandleWrapper { map_.erase(overlapped.hEvent); } + int addonDataId; WatcherHandle dir_handle; std::vector path; bool canceled; @@ -144,6 +169,7 @@ void PlatformThread( locker.Unlock(); ResetEvent(g_file_handles_free_event); + std::cout << "Thread with ID: " << addonData->id << " is waiting..." << std::endl; DWORD r = WaitForMultipleObjects( copied_events.size(), copied_events.data(), @@ -156,24 +182,37 @@ void PlatformThread( // Timeout occurred, check shouldStop flag continue; } + std::cout << "Thread with ID: " << addonData->id << " is done waiting." << std::endl; int i = r - WAIT_OBJECT_0; if (i >= 0 && i < copied_events.size()) { // It's a wake up event, there is no fs events. - if (copied_events[i] == g_wake_up_event) + if (copied_events[i] == g_wake_up_event) { + std::cout << "Thread with ID: " << addonData->id << " received wake-up event. Continuing." << std::endl; continue; + } ScopedLocker locker(g_handle_wrap_map_mutex); HandleWrapper* handle = HandleWrapper::Get(copied_events[i]); - if (!handle || handle->canceled) + if (!handle || handle->canceled) { continue; + } + + if (handle->addonDataId != addonData->id) { + std::cout << "Thread with ID: " << addonData->id << " ignoring handle from different context." << std::endl; + continue; + } DWORD bytes_transferred; - if (!GetOverlappedResult(handle->dir_handle, &handle->overlapped, &bytes_transferred, FALSE)) + if (!GetOverlappedResult(handle->dir_handle, &handle->overlapped, &bytes_transferred, FALSE)) { + std::cout << "Nothing for thread: " << addonData->id << std::endl; continue; - if (bytes_transferred == 0) + } + if (bytes_transferred == 0) { + std::cout << "Nothing for thread: " << addonData->id << std::endl; continue; + } std::vector old_path; std::vector events; @@ -270,6 +309,11 @@ void PlatformThread( } } +// // Function to get the vector for a given AddonData +// std::shared_ptr GetVectorForAddonData(AddonData* addonData) { +// return g_vector_map.get_or_create(addonData->id); +// } + WatcherHandle PlatformWatch(const char* path, Napi::Env env) { auto addonData = env.GetInstanceData(); std::cout << "PlatformWatch ID: " << addonData->id << " Path: " << path << std::endl; @@ -299,7 +343,7 @@ WatcherHandle PlatformWatch(const char* path, Napi::Env env) { std::unique_ptr handle; { ScopedLocker locker(g_handle_wrap_map_mutex); - handle.reset(new HandleWrapper(dir_handle, path)); + handle.reset(new HandleWrapper(dir_handle, path, addonData->id)); } if (!QueueReaddirchanges(handle.get())) { From e432b9816b7a2ca02529c3767c188a8b1c2f564d Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 13:52:31 -0700 Subject: [PATCH 045/168] What if we have one thread that takes care of all handles? --- src/pathwatcher_win.cc | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index f3dba1c..97178eb 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -47,6 +47,8 @@ static HANDLE g_wake_up_event; // The dummy event to ensure we are not waiting on a file handle when destroying it. static HANDLE g_file_handles_free_event; +static bool g_is_running = false; + struct ScopedLocker { explicit ScopedLocker(uv_mutex_t& mutex) : mutex_(&mutex) { uv_mutex_lock(mutex_); } ~ScopedLocker() { Unlock(); } @@ -156,6 +158,8 @@ void PlatformThread( bool& shouldStop, Napi::Env env ) { + if (g_is_running) return; + g_is_running = true; auto addonData = env.GetInstanceData(); std::cout << "PlatformThread ID: " << addonData->id << std::endl; @@ -199,10 +203,10 @@ void PlatformThread( continue; } - if (handle->addonDataId != addonData->id) { - std::cout << "Thread with ID: " << addonData->id << " ignoring handle from different context." << std::endl; - continue; - } + // if (handle->addonDataId != addonData->id) { + // std::cout << "Thread with ID: " << addonData->id << " ignoring handle from different context." << std::endl; + // continue; + // } DWORD bytes_transferred; if (!GetOverlappedResult(handle->dir_handle, &handle->overlapped, &bytes_transferred, FALSE)) { @@ -307,6 +311,8 @@ void PlatformThread( } } } + + g_is_running = false; } // // Function to get the vector for a given AddonData From dace0715d2588ee5907332fd8050ba27c3a49aa6 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 14:00:10 -0700 Subject: [PATCH 046/168] Match up handles to workers in the single loop --- src/pathwatcher_win.cc | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 97178eb..e2ec397 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -31,6 +31,33 @@ // // // Global instance of VectorMap // VectorMap g_vector_map; +// +// class ProgressMap { +// public: +// using Progess = PathWatcherWorker::ExecutionProgress; +// void set(const Progress& progress) { +// +// } +// std::shared_ptr get_or_create(int addonDataId) { +// auto it = vectors_.find(addonDataId); +// if (it == vectors_.end()) { +// it = vectors_.emplace(addonDataId, std::make_shared()).first; +// } +// return it->second; +// } +// +// void remove(int addonDataId) { +// vectors_.erase(addonDataId); +// } +// +// private: +// std::map> vectors_; +// }; + +static std::map g_progress_map; + +// Global instance of VectorMap +// ProgressMap g_progress_map; // Size of the buffer to store result of ReadDirectoryChangesW. static const unsigned int kDirectoryWatcherBufferSize = 4096; @@ -158,9 +185,10 @@ void PlatformThread( bool& shouldStop, Napi::Env env ) { + auto addonData = env.GetInstanceData(); + g_progress_map.insert(addonData->id, progress); if (g_is_running) return; g_is_running = true; - auto addonData = env.GetInstanceData(); std::cout << "PlatformThread ID: " << addonData->id << std::endl; // std::cout << "PlatformThread" << std::endl; @@ -208,6 +236,12 @@ void PlatformThread( // continue; // } + PathWatcherWorker::ExecutionProgress progressForEvent = g_progress_map.find(handle->addonDataId); + if (!progressForEvent) { + std::cout << "Could not match up ID: " << addonData->id << " with a PathWatcherWorker." << std::endl; + continue; + } + DWORD bytes_transferred; if (!GetOverlappedResult(handle->dir_handle, &handle->overlapped, &bytes_transferred, FALSE)) { std::cout << "Nothing for thread: " << addonData->id << std::endl; @@ -307,7 +341,7 @@ void PlatformThread( events[i].new_path, events[i].old_path ); - progress.Send(&event, 1); + progressForEvent.Send(&event, 1); } } } From e8f4645ea215d336ee76822fddcc9d174721f8e9 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 15:04:20 -0700 Subject: [PATCH 047/168] Fixed, I think --- src/pathwatcher_win.cc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index e2ec397..68fc74a 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -236,12 +236,14 @@ void PlatformThread( // continue; // } - PathWatcherWorker::ExecutionProgress progressForEvent = g_progress_map.find(handle->addonDataId); - if (!progressForEvent) { + auto progressIterator = g_progress_map.find(handle->addonDataId); + if (!progressIterator == g_progress_map.end()) { std::cout << "Could not match up ID: " << addonData->id << " with a PathWatcherWorker." << std::endl; continue; } + PathWatcherWorker::ExecutionProgress& progressForEvent = progressIterator->second; + DWORD bytes_transferred; if (!GetOverlappedResult(handle->dir_handle, &handle->overlapped, &bytes_transferred, FALSE)) { std::cout << "Nothing for thread: " << addonData->id << std::endl; From 7da0cfa260dde5e7502ba52ee24dd4a3f338a9a3 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 15:08:42 -0700 Subject: [PATCH 048/168] Try `emplace` --- src/pathwatcher_win.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 68fc74a..9e75179 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -186,7 +186,7 @@ void PlatformThread( Napi::Env env ) { auto addonData = env.GetInstanceData(); - g_progress_map.insert(addonData->id, progress); + g_progress_map.emplace(addonData->id, progress); if (g_is_running) return; g_is_running = true; std::cout << "PlatformThread ID: " << addonData->id << std::endl; From 91d6c072f7340ac94d764f6d0e7b2e1ff9a45a50 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 15:14:37 -0700 Subject: [PATCH 049/168] Fixes --- src/common.cc | 1 + src/common.h | 1 + src/pathwatcher_linux.cc | 2 ++ src/pathwatcher_unix.cc | 2 ++ src/pathwatcher_win.cc | 14 ++++++++++++-- 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/common.cc b/src/common.cc index 166b042..bd9336f 100644 --- a/src/common.cc +++ b/src/common.cc @@ -75,6 +75,7 @@ void Start(Napi::Env env) { // Called when the last watcher is stopped. void Stop(Napi::Env env) { // std::cout << "Stop" << std::endl; + PlatformStop(env); auto addonData = env.GetInstanceData(); if (addonData->worker) { addonData->worker->Stop(); diff --git a/src/common.h b/src/common.h index 9e59b0d..8fadc84 100644 --- a/src/common.h +++ b/src/common.h @@ -121,6 +121,7 @@ class PathWatcherWorker: public AsyncProgressQueueWorker { }; void PlatformThread(const PathWatcherWorker::ExecutionProgress& progress, bool& shouldStop, Napi::Env env); +void PlatformStop(Napi::Env env); void CommonInit(Napi::Env env); diff --git a/src/pathwatcher_linux.cc b/src/pathwatcher_linux.cc index 9c82888..a0ce992 100644 --- a/src/pathwatcher_linux.cc +++ b/src/pathwatcher_linux.cc @@ -109,3 +109,5 @@ bool PlatformIsHandleValid(WatcherHandle handle) { int PlatformInvalidHandleToErrorNumber(WatcherHandle handle) { return -handle; } + +void PlatformStop(Napi::Env env) {} diff --git a/src/pathwatcher_unix.cc b/src/pathwatcher_unix.cc index 6bddeb8..1e0a47c 100644 --- a/src/pathwatcher_unix.cc +++ b/src/pathwatcher_unix.cc @@ -124,3 +124,5 @@ bool PlatformIsHandleValid(WatcherHandle handle) { int PlatformInvalidHandleToErrorNumber(WatcherHandle handle) { return -handle; } + +void PlatformStop(Napi::Env env) {} diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 9e75179..79ee28c 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -75,6 +75,7 @@ static HANDLE g_wake_up_event; static HANDLE g_file_handles_free_event; static bool g_is_running = false; +static bool g_env_count = 0; struct ScopedLocker { explicit ScopedLocker(uv_mutex_t& mutex) : mutex_(&mutex) { uv_mutex_lock(mutex_); } @@ -185,6 +186,7 @@ void PlatformThread( bool& shouldStop, Napi::Env env ) { + g_env_count++; auto addonData = env.GetInstanceData(); g_progress_map.emplace(addonData->id, progress); if (g_is_running) return; @@ -192,7 +194,10 @@ void PlatformThread( std::cout << "PlatformThread ID: " << addonData->id << std::endl; // std::cout << "PlatformThread" << std::endl; - while (!shouldStop) { + while (true) { + if (shouldStop && g_env_count == 0) { + return; + } // Do not use g_events directly, since reallocation could happen when there // are new watchers adding to g_events when WaitForMultipleObjects is still // polling. @@ -237,7 +242,7 @@ void PlatformThread( // } auto progressIterator = g_progress_map.find(handle->addonDataId); - if (!progressIterator == g_progress_map.end()) { + if (progressIterator == g_progress_map.end()) { std::cout << "Could not match up ID: " << addonData->id << " with a PathWatcherWorker." << std::endl; continue; } @@ -425,3 +430,8 @@ bool PlatformIsHandleValid(WatcherHandle handle) { int PlatformInvalidHandleToErrorNumber(WatcherHandle handle) { return 0; } + +void PlatformStop(Napi::Env env) { + auto addonData = env.GetInstanceData(); + g_env_count--; +} From e62ed4e2e8ab41fcd2f2c87f57c8eca459668020 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 15:18:00 -0700 Subject: [PATCH 050/168] sigh --- src/pathwatcher_win.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 79ee28c..945622f 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -75,7 +75,7 @@ static HANDLE g_wake_up_event; static HANDLE g_file_handles_free_event; static bool g_is_running = false; -static bool g_env_count = 0; +static int g_env_count = 0; struct ScopedLocker { explicit ScopedLocker(uv_mutex_t& mutex) : mutex_(&mutex) { uv_mutex_lock(mutex_); } From 8ddae5077a641dec478bc5f6e9ff7b02b192d8e8 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 15:30:00 -0700 Subject: [PATCH 051/168] Add a delay when closing watchers --- spec/pathwatcher-spec.js | 4 +++- src/main.js | 14 +++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index 4a5f760..c7cb693 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -13,7 +13,9 @@ describe('PathWatcher', () => { let tempFile = path.join(tempDir, 'file'); beforeEach(() => fs.writeFileSync(tempFile, '')); - afterEach(() => PathWatcher.closeAllWatchers()); + afterEach(async () => { + await PathWatcher.closeAllWatchers(); + }); describe('getWatchedPaths', () => { it('returns an array of all watched paths', () => { diff --git a/src/main.js b/src/main.js index 56a26a0..c21716c 100644 --- a/src/main.js +++ b/src/main.js @@ -12,6 +12,10 @@ const path = require('path'); const HANDLE_WATCHERS = new Map(); let initialized = false; +function wait (ms) { + return new Promise(r => setTimeout(r, ms)); +} + class HandleWatcher { constructor(path) { @@ -99,10 +103,12 @@ class HandleWatcher { } } - close () { + async close () { if (!HANDLE_WATCHERS.has(this.handle)) return; binding.unwatch(this.handle); HANDLE_WATCHERS.delete(this.handle); + // Watchers take 100ms to realize they're closed. + await wait(100); } } @@ -200,11 +206,13 @@ function watch (pathToWatch, callback) { return new PathWatcher(path.resolve(pathToWatch), callback); } -function closeAllWatchers () { +async function closeAllWatchers () { + let promises = []; for (let watcher of HANDLE_WATCHERS.values()) { - watcher?.close(); + promises.push(watcher?.close()); } HANDLE_WATCHERS.clear(); + await Promise.allSettled(promises); } function getWatchedPaths () { From 7de16fe4f6fef609c653333ba52c9d44f1dc4e9d Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 15:30:09 -0700 Subject: [PATCH 052/168] Even more logging --- src/pathwatcher_win.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 945622f..6fcc637 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -341,7 +341,7 @@ void PlatformThread( std::cout << "Total events processed on thread " << addonData->id << ": " << events.size() << std::endl; for (size_t i = 0; i < events.size(); ++i) { - std::cout << "Emitting " << events[i].type << " event on thread " << addonData->id << " for path: " << events[i].new_path.data() << std::endl; + std::cout << "Emitting " << events[i].type << " event(s) on thread " << addonData->id << " for path: " << events[i].new_path.data() << " for worker with ID: " << handle->addonDataId << std::endl; PathWatcherEvent event( events[i].type, events[i].handle, @@ -353,6 +353,7 @@ void PlatformThread( } } + std::cout << "PlatformThread with ID: " << addonData->id << " is exiting! " << std::endl; g_is_running = false; } From cd0b328b452db39882b1ae82694aa9c1a29300b4 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 15:35:33 -0700 Subject: [PATCH 053/168] `break`, not `return` --- src/pathwatcher_win.cc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 6fcc637..fd5d721 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -196,7 +196,13 @@ void PlatformThread( // std::cout << "PlatformThread" << std::endl; while (true) { if (shouldStop && g_env_count == 0) { - return; + std::cout << "Thread with ID: " << addonData->id << " wants to stop and is the last worker. Stopping PlatformThread" << std::endl; + break; + } else if (shouldStop) { + std::cout << "Thread with ID: " << addonData->id << " wants to stop, but it is not the main worker." << std::endl; + } else if (g_env_count == 0) { + std::cout << "WARNING: g_env_count is 0!" << std::endl; + break; } // Do not use g_events directly, since reallocation could happen when there // are new watchers adding to g_events when WaitForMultipleObjects is still From d849465fb3fd47d47980d19c92f24cce9124fcae Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 16:26:08 -0700 Subject: [PATCH 054/168] A different approach --- src/pathwatcher_win.cc | 193 +++++++++++++++++++++++++---------------- 1 file changed, 117 insertions(+), 76 deletions(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index fd5d721..72fd415 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -1,60 +1,105 @@ +#include #include +#include #include #include +#include #include - +#include +#include #include "common.h" #include "addon-data.h" #include "js_native_api_types.h" #include "napi.h" #include "uv.h" -// class VectorMap { -// public: -// using Vector = std::vector; -// -// std::shared_ptr get_or_create(int addonDataId) { -// auto it = vectors_.find(addonDataId); -// if (it == vectors_.end()) { -// it = vectors_.emplace(addonDataId, std::make_shared()).first; -// } -// return it->second; -// } -// -// void remove(int addonDataId) { -// vectors_.erase(addonDataId); -// } -// -// private: -// std::map> vectors_; -// }; -// -// // Global instance of VectorMap -// VectorMap g_vector_map; -// -// class ProgressMap { -// public: -// using Progess = PathWatcherWorker::ExecutionProgress; -// void set(const Progress& progress) { -// -// } -// std::shared_ptr get_or_create(int addonDataId) { -// auto it = vectors_.find(addonDataId); -// if (it == vectors_.end()) { -// it = vectors_.emplace(addonDataId, std::make_shared()).first; -// } -// return it->second; -// } -// -// void remove(int addonDataId) { -// vectors_.erase(addonDataId); -// } -// -// private: -// std::map> vectors_; -// }; - -static std::map g_progress_map; +struct ThreadData { + std::queue event_queue; + std::mutex mutex; + std::condition_variable cv; + PathWatcherWorker::ExecutionProgress progress; + bool should_stop = false; +}; + +class ThreadManager { +public: + void register_thread(int id, PathWatcherWorker::ExecutionProgress progress) { + std::lock_guard lock(mutex_); + threads_[id] = std::make_unique(); + threads_[id]->progress = std::move(progress); + } + + void unregister_thread(int id) { + std::lock_guard lock(mutex_); + threads_.erase(id); + } + + void queue_event(int id, PathWatcherEvent event) { + std::lock_guard lock(mutex_); + if (threads_.count(id) > 0) { + std::lock_guard thread_lock(threads_[id]->mutex); + threads_[id]->event_queue.push(std::move(event)); + threads_[id]->cv.notify_one(); + } + } + + void stop_all() { + std::lock_guard lock(mutex_); + for (auto& pair : threads_) { + std::lock_guard thread_lock(pair.second->mutex); + pair.second->should_stop = true; + pair.second->cv.notify_one(); + } + } + + bool is_empty() const { + std::lock_guard lock(mutex_); + return threads_.empty(); + } + + bool has_thread(int id) const { + std::lock_guard lock(mutex_); + return threads_.find(id) != threads_.end(); + } + + std::unordered_map> threads_; + +private: + mutable std::mutex mutex_; +}; + +// Global instance +ThreadManager g_thread_manager; + +// Global atomic flag to ensure only one PlatformThread is running +std::atomic g_platform_thread_running(false); + +void ThreadWorker(int id) { + while (true) { + std::unique_lock lock(g_thread_manager.threads_[id]->mutex); + g_thread_manager.threads_[id]->cv.wait(lock, [id] { + return !( + g_thread_manager.threads_[id]->event_queue.empty() || + g_thread_manager.threads_[id]->should_stop + ); + }); + + if ( + g_thread_manager.threads_[id]->should_stop && + g_thread_manager.threads_[id]->event_queue.empty() + ) { + break; + } + + while (!g_thread_manager.threads_[id]->event_queue.empty()) { + auto event = g_thread_manager.threads_[id]->event_queue.front(); + g_thread_manager.threads_[id]->event_queue.pop(); + lock.unlock(); + g_thread_manager.threads_[id]->progress.Send(&event, 1); + lock.lock(); + } + } +} // Global instance of VectorMap // ProgressMap g_progress_map; @@ -74,8 +119,8 @@ static HANDLE g_wake_up_event; // The dummy event to ensure we are not waiting on a file handle when destroying it. static HANDLE g_file_handles_free_event; -static bool g_is_running = false; -static int g_env_count = 0; +// static bool g_is_running = false; +// static int g_env_count = 0; struct ScopedLocker { explicit ScopedLocker(uv_mutex_t& mutex) : mutex_(&mutex) { uv_mutex_lock(mutex_); } @@ -186,24 +231,26 @@ void PlatformThread( bool& shouldStop, Napi::Env env ) { - g_env_count++; auto addonData = env.GetInstanceData(); - g_progress_map.emplace(addonData->id, progress); - if (g_is_running) return; - g_is_running = true; + + bool expected = false; + if (!g_platform_thread_running.compare_exchange_strong(expected, true)) { + // Another PlatformThread is already running. + g_thread_manager.register_thread(addon_data->id, progress); + ThreadWorker(addon_data->id); + g_thread_manager.unregister_thread(addon_data->id); + return; + } + + // This is the primary thread + g_thread_manager.register_thread(addon_data->id, progress); + + // if (g_is_running) return; + // g_is_running = true; std::cout << "PlatformThread ID: " << addonData->id << std::endl; // std::cout << "PlatformThread" << std::endl; - while (true) { - if (shouldStop && g_env_count == 0) { - std::cout << "Thread with ID: " << addonData->id << " wants to stop and is the last worker. Stopping PlatformThread" << std::endl; - break; - } else if (shouldStop) { - std::cout << "Thread with ID: " << addonData->id << " wants to stop, but it is not the main worker." << std::endl; - } else if (g_env_count == 0) { - std::cout << "WARNING: g_env_count is 0!" << std::endl; - break; - } + while (!g_thread_manager.is_empty()) { // Do not use g_events directly, since reallocation could happen when there // are new watchers adding to g_events when WaitForMultipleObjects is still // polling. @@ -242,19 +289,11 @@ void PlatformThread( continue; } - // if (handle->addonDataId != addonData->id) { - // std::cout << "Thread with ID: " << addonData->id << " ignoring handle from different context." << std::endl; - // continue; - // } - - auto progressIterator = g_progress_map.find(handle->addonDataId); - if (progressIterator == g_progress_map.end()) { - std::cout << "Could not match up ID: " << addonData->id << " with a PathWatcherWorker." << std::endl; + if (!g_thread_manager.has_thread(handle->addonDataId)) { + std::cout << "Unrecognized environment: " << handle->addonDataId << std::endl; continue; } - PathWatcherWorker::ExecutionProgress& progressForEvent = progressIterator->second; - DWORD bytes_transferred; if (!GetOverlappedResult(handle->dir_handle, &handle->overlapped, &bytes_transferred, FALSE)) { std::cout << "Nothing for thread: " << addonData->id << std::endl; @@ -354,13 +393,15 @@ void PlatformThread( events[i].new_path, events[i].old_path ); - progressForEvent.Send(&event, 1); + g_thread_manager.queue_event(handle->addonDataId, event); } } } std::cout << "PlatformThread with ID: " << addonData->id << " is exiting! " << std::endl; - g_is_running = false; + g_thread_manager.stop_all(); + g_thread_manager.unregister_thread(addonData->id); + g_platform_thread_running = false; } // // Function to get the vector for a given AddonData @@ -440,5 +481,5 @@ int PlatformInvalidHandleToErrorNumber(WatcherHandle handle) { void PlatformStop(Napi::Env env) { auto addonData = env.GetInstanceData(); - g_env_count--; + g_thread_manager.unregister_thread(addon_data->id); } From 49ee1882e0f5705f9d508236ae9dcaf75074a57b Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 18:50:21 -0700 Subject: [PATCH 055/168] Should pass now? --- src/common.cc | 10 ++++- src/pathwatcher_win.cc | 85 +++++++++++++++++++++++++++++------------- 2 files changed, 67 insertions(+), 28 deletions(-) diff --git a/src/common.cc b/src/common.cc index bd9336f..308af52 100644 --- a/src/common.cc +++ b/src/common.cc @@ -24,7 +24,10 @@ void PathWatcherWorker::Execute( } void PathWatcherWorker::Stop() { - // std::cout << "PathWatcherWorker::Stop" << std::endl; + Napi::Env env = Env(); + auto addonData = env.GetInstanceData(); + std::cout << "PathWatcherWorker::Stop for ID: " << addonData->id << std::endl; + shouldStop = true; } @@ -42,7 +45,10 @@ const char* PathWatcherWorker::GetEventTypeString(EVENT_TYPE type) { } void PathWatcherWorker::OnProgress(const PathWatcherEvent* data, size_t) { - HandleScope scope(Env()); + Napi::Env env = Env(); + HandleScope scope(env); + auto addonData = env.GetInstanceData(); + std::cout << "OnProgress reporting event for environment with ID: " << addonData->id << std::endl; if (this->progressCallback.IsEmpty()) return; std::string newPath(data->new_path.begin(), data->new_path.end()); std::string oldPath(data->old_path.begin(), data->old_path.end()); diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 72fd415..9f78fab 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include "common.h" @@ -17,21 +18,25 @@ struct ThreadData { std::queue event_queue; std::mutex mutex; std::condition_variable cv; - PathWatcherWorker::ExecutionProgress progress; + const PathWatcherWorker::ExecutionProgress* progress; bool should_stop = false; + bool is_main = false; }; class ThreadManager { public: - void register_thread(int id, PathWatcherWorker::ExecutionProgress progress) { + void register_thread( + int id, + const PathWatcherWorker::ExecutionProgress* progress + ) { std::lock_guard lock(mutex_); threads_[id] = std::make_unique(); - threads_[id]->progress = std::move(progress); + threads_[id]->progress = progress; } - void unregister_thread(int id) { + bool unregister_thread(int id) { std::lock_guard lock(mutex_); - threads_.erase(id); + return threads_.erase(id) > 0; } void queue_event(int id, PathWatcherEvent event) { @@ -62,6 +67,12 @@ class ThreadManager { return threads_.find(id) != threads_.end(); } + ThreadData* get_thread_data(int id) { + std::lock_guard lock(mutex_); + auto it = threads_.find(id); + return it != threads_.end() ? it->second.get() : nullptr; + } + std::unordered_map> threads_; private: @@ -76,28 +87,35 @@ std::atomic g_platform_thread_running(false); void ThreadWorker(int id) { while (true) { - std::unique_lock lock(g_thread_manager.threads_[id]->mutex); - g_thread_manager.threads_[id]->cv.wait(lock, [id] { - return !( - g_thread_manager.threads_[id]->event_queue.empty() || - g_thread_manager.threads_[id]->should_stop - ); + ThreadData* thread_data = g_thread_manager.get_thread_data(id); + if (!thread_data) break; // (thread was unregistered) + + std::unique_lock lock(thread_data->mutex); + std::cout << "[WAIT WAIT WAIT] ThreadWorker with ID: " << id << " has should_stop of: " << thread_data->should_stop << std::endl; + thread_data->cv.wait(lock, [thread_data] { + if (thread_data->should_stop) return true; + if (!thread_data->event_queue.empty()) return true; + return false; }); - if ( - g_thread_manager.threads_[id]->should_stop && - g_thread_manager.threads_[id]->event_queue.empty() - ) { + // std::cout << "ThreadWorker with ID: " << id << "is unblocked. Why? " << "(internal_stop? " << thread_data->internal_stop << ") (should_stop? " << *(thread_data->should_stop) << ") (items in queue? " << !thread_data->event_queue.empty() << ")" << std::endl; + + if (thread_data->should_stop && thread_data->event_queue.empty()) { break; } - while (!g_thread_manager.threads_[id]->event_queue.empty()) { - auto event = g_thread_manager.threads_[id]->event_queue.front(); - g_thread_manager.threads_[id]->event_queue.pop(); + while (!thread_data->event_queue.empty()) { + auto event = thread_data->event_queue.front(); + thread_data->event_queue.pop(); lock.unlock(); - g_thread_manager.threads_[id]->progress.Send(&event, 1); + std::cout << "ThreadWorker with ID: " << id << " is sending event!" << std::endl; + thread_data->progress->Send(&event, 1); lock.lock(); + + if (thread_data->should_stop) break; } + + if (thread_data->should_stop) break; } } @@ -236,14 +254,14 @@ void PlatformThread( bool expected = false; if (!g_platform_thread_running.compare_exchange_strong(expected, true)) { // Another PlatformThread is already running. - g_thread_manager.register_thread(addon_data->id, progress); - ThreadWorker(addon_data->id); - g_thread_manager.unregister_thread(addon_data->id); + g_thread_manager.register_thread(addonData->id, &progress); + ThreadWorker(addonData->id); + g_thread_manager.unregister_thread(addonData->id); return; } // This is the primary thread - g_thread_manager.register_thread(addon_data->id, progress); + g_thread_manager.register_thread(addonData->id, &progress); // if (g_is_running) return; // g_is_running = true; @@ -294,6 +312,10 @@ void PlatformThread( continue; } + if (handle->addonDataId == addonData->id) { + std::cout << "OURS to handle! " << handle->addonDataId << std::endl; + } + DWORD bytes_transferred; if (!GetOverlappedResult(handle->dir_handle, &handle->overlapped, &bytes_transferred, FALSE)) { std::cout << "Nothing for thread: " << addonData->id << std::endl; @@ -386,14 +408,19 @@ void PlatformThread( std::cout << "Total events processed on thread " << addonData->id << ": " << events.size() << std::endl; for (size_t i = 0; i < events.size(); ++i) { - std::cout << "Emitting " << events[i].type << " event(s) on thread " << addonData->id << " for path: " << events[i].new_path.data() << " for worker with ID: " << handle->addonDataId << std::endl; + std::cout << "Emitting " << events[i].type << " event on thread " << addonData->id << " for path: " << events[i].new_path.data() << " for worker with ID: " << handle->addonDataId << std::endl; PathWatcherEvent event( events[i].type, events[i].handle, events[i].new_path, events[i].old_path ); - g_thread_manager.queue_event(handle->addonDataId, event); + if (handle->addonDataId == addonData->id) { + std::cout << "Invoking directly " << addonData->id << std::endl; + progress.Send(&event, 1); + } else { + g_thread_manager.queue_event(handle->addonDataId, event); + } } } } @@ -481,5 +508,11 @@ int PlatformInvalidHandleToErrorNumber(WatcherHandle handle) { void PlatformStop(Napi::Env env) { auto addonData = env.GetInstanceData(); - g_thread_manager.unregister_thread(addon_data->id); + auto thread_data = g_thread_manager.get_thread_data(addonData->id); + if (thread_data) { + std::lock_guard lock(thread_data->mutex); + thread_data->should_stop = true; + thread_data->cv.notify_one(); + g_thread_manager.unregister_thread(addonData->id); + } } From 1713d94dbc00e68d46bada180b8088359ca2cc59 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 20:10:56 -0700 Subject: [PATCH 056/168] =?UTF-8?q?Try=20to=20promote=20another=20thread?= =?UTF-8?q?=20if=20the=20=E2=80=9Cboss=E2=80=9D=20thread=20is=20stopped?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pathwatcher_win.cc | 92 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 79 insertions(+), 13 deletions(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 9f78fab..33a187d 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -27,18 +27,43 @@ class ThreadManager { public: void register_thread( int id, - const PathWatcherWorker::ExecutionProgress* progress + const PathWatcherWorker::ExecutionProgress* progress, + bool is_main ) { std::lock_guard lock(mutex_); threads_[id] = std::make_unique(); threads_[id]->progress = progress; + threads_[id]->is_main = is_main; + if (is_main) { + this->main_id = id; + } } bool unregister_thread(int id) { std::lock_guard lock(mutex_); + if (id == this->main_id) { + this->main_id = -1; + for (const auto& pair : threads_) { + if (pair.first != id) { + std::cout << "Unregistering the main thread. Promoting " << pair.first << " to be the new boss thread." << std::endl; + promote(pair.first); + break; + } + } + } return threads_.erase(id) > 0; } + int has_main () { + return this->main_id > -1; + } + + void promote(int id) { + auto data = this->get_thread_data(id); + data->is_main = true; + this->main_id = id; + } + void queue_event(int id, PathWatcherEvent event) { std::lock_guard lock(mutex_); if (threads_.count(id) > 0) { @@ -77,6 +102,7 @@ class ThreadManager { private: mutable std::mutex mutex_; + int main_id = -1; }; // Global instance @@ -251,24 +277,64 @@ void PlatformThread( ) { auto addonData = env.GetInstanceData(); - bool expected = false; - if (!g_platform_thread_running.compare_exchange_strong(expected, true)) { - // Another PlatformThread is already running. - g_thread_manager.register_thread(addonData->id, &progress); - ThreadWorker(addonData->id); + bool hasMainThread = !g_thread_manager.is_empty(); + // bool expected = false; + // bool hasMainThread = g_platform_thread_running.compare_exchange_strong(expected, true); + + if (!g_thread_manager.has_thread(addonData->id)) { + g_thread_manager.register_thread(addonData->id, &progress, !hasMainThread); + } + + ThreadData* thread_data = g_thread_manager.get_thread_data(id); + + if (!thread_data->is_main) { + while (!thread_data->is_main) { + // A holding-pattern loop for threads that aren't the “boss” thread. + ThreadData* thread_data = g_thread_manager.get_thread_data(id); + if (!thread_data) break; // (thread was unregistered) + if (thread_data->is_main) break; + + std::unique_lock lock(thread_data->mutex); + thread_data->cv.wait(lock, [thread_data] { + if (thread_data->should_stop) return true; + if (!thread_data->event_queue.empty()) return true; + return false; + }); + + if (thread_data->should_stop && thread_data->event_queue.empty()) { + break; + } + + while (!thread_data->event_queue.empty()) { + auto event = thread_data->event_queue.front(); + thread_data->event_queue.pop(); + lock.unlock(); + thread_data->progress->Send(&event, 1); + lock.lock(); + + if (thread_data->should_stop) break; + } + + if (thread_data->should_stop) break; + } + } + + if (!thread_data->is_main) { + // If we get to this point and this still isn't the “boss” thread, then + // we’ve broken out of the above loop but should not proceed. This thread + // hasn't been promoted; it should stop. g_thread_manager.unregister_thread(addonData->id); return; } - // This is the primary thread - g_thread_manager.register_thread(addonData->id, &progress); + // If we get this far, then this is the main thread — either because it was + // the first to be created or because it's been promoted after another thread + // was stopped. - // if (g_is_running) return; - // g_is_running = true; std::cout << "PlatformThread ID: " << addonData->id << std::endl; // std::cout << "PlatformThread" << std::endl; - while (!g_thread_manager.is_empty()) { + while (!thread_data->should_stop) { // Do not use g_events directly, since reallocation could happen when there // are new watchers adding to g_events when WaitForMultipleObjects is still // polling. @@ -426,9 +492,9 @@ void PlatformThread( } std::cout << "PlatformThread with ID: " << addonData->id << " is exiting! " << std::endl; - g_thread_manager.stop_all(); + // g_thread_manager.stop_all(); g_thread_manager.unregister_thread(addonData->id); - g_platform_thread_running = false; + // g_platform_thread_running = false; } // // Function to get the vector for a given AddonData From 529af98335144ca32c9d90d0361a69760da931a4 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 20:14:01 -0700 Subject: [PATCH 057/168] Typo --- src/pathwatcher_win.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 33a187d..bdb592e 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -285,7 +285,7 @@ void PlatformThread( g_thread_manager.register_thread(addonData->id, &progress, !hasMainThread); } - ThreadData* thread_data = g_thread_manager.get_thread_data(id); + ThreadData* thread_data = g_thread_manager.get_thread_data(addonData->id); if (!thread_data->is_main) { while (!thread_data->is_main) { From a0b3f7cda6f7216d54fba8938dc6ffc292ecb38f Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 20:16:56 -0700 Subject: [PATCH 058/168] Another oversight --- src/pathwatcher_win.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index bdb592e..0550c86 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -290,7 +290,7 @@ void PlatformThread( if (!thread_data->is_main) { while (!thread_data->is_main) { // A holding-pattern loop for threads that aren't the “boss” thread. - ThreadData* thread_data = g_thread_manager.get_thread_data(id); + ThreadData* thread_data = g_thread_manager.get_thread_data(addonData->id); if (!thread_data) break; // (thread was unregistered) if (thread_data->is_main) break; From 2c30fa0f96f5b173d0927b415392308762ae5f16 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 20:41:22 -0700 Subject: [PATCH 059/168] Changes --- src/pathwatcher_win.cc | 288 +++++++++++++++++++++-------------------- 1 file changed, 148 insertions(+), 140 deletions(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 0550c86..889746a 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -58,6 +58,10 @@ class ThreadManager { return this->main_id > -1; } + bool is_main (int id) { + return id == this->main_id; + } + void promote(int id) { auto data = this->get_thread_data(id); data->is_main = true; @@ -287,12 +291,13 @@ void PlatformThread( ThreadData* thread_data = g_thread_manager.get_thread_data(addonData->id); - if (!thread_data->is_main) { - while (!thread_data->is_main) { + if (!g_thread_manager->is_main(addonData->id)) { + while (!g_thread_manager->is_main(addonData->id)) { + std::cout << "Thread with ID: " << addonData->id " in holding pattern" << std::endl; // A holding-pattern loop for threads that aren't the “boss” thread. ThreadData* thread_data = g_thread_manager.get_thread_data(addonData->id); if (!thread_data) break; // (thread was unregistered) - if (thread_data->is_main) break; + if (g_thread_manager->is_main(addonData->id)) break; std::unique_lock lock(thread_data->mutex); thread_data->cv.wait(lock, [thread_data] { @@ -319,7 +324,7 @@ void PlatformThread( } } - if (!thread_data->is_main) { + if (!g_thread_manager->is_main(addonData->id)) { // If we get to this point and this still isn't the “boss” thread, then // we’ve broken out of the above loop but should not proceed. This thread // hasn't been promoted; it should stop. @@ -334,161 +339,164 @@ void PlatformThread( std::cout << "PlatformThread ID: " << addonData->id << std::endl; // std::cout << "PlatformThread" << std::endl; - while (!thread_data->should_stop) { - // Do not use g_events directly, since reallocation could happen when there - // are new watchers adding to g_events when WaitForMultipleObjects is still - // polling. - ScopedLocker locker(g_handle_wrap_map_mutex); - std::vector copied_events(g_events); - locker.Unlock(); - - ResetEvent(g_file_handles_free_event); - std::cout << "Thread with ID: " << addonData->id << " is waiting..." << std::endl; - DWORD r = WaitForMultipleObjects( - copied_events.size(), - copied_events.data(), - FALSE, - 100 - ); - SetEvent(g_file_handles_free_event); - - if (r == WAIT_TIMEOUT) { - // Timeout occurred, check shouldStop flag - continue; - } - std::cout << "Thread with ID: " << addonData->id << " is done waiting." << std::endl; - - int i = r - WAIT_OBJECT_0; - if (i >= 0 && i < copied_events.size()) { - // It's a wake up event, there is no fs events. - if (copied_events[i] == g_wake_up_event) { - std::cout << "Thread with ID: " << addonData->id << " received wake-up event. Continuing." << std::endl; - continue; - } - + if (g_thread_manager->is_main(addonData->id)) { + while (!thread_data->should_stop) { + // Do not use g_events directly, since reallocation could happen when there + // are new watchers adding to g_events when WaitForMultipleObjects is still + // polling. ScopedLocker locker(g_handle_wrap_map_mutex); + std::vector copied_events(g_events); + locker.Unlock(); - HandleWrapper* handle = HandleWrapper::Get(copied_events[i]); - if (!handle || handle->canceled) { + ResetEvent(g_file_handles_free_event); + std::cout << "Thread with ID: " << addonData->id << " is waiting..." << std::endl; + DWORD r = WaitForMultipleObjects( + copied_events.size(), + copied_events.data(), + FALSE, + 100 + ); + SetEvent(g_file_handles_free_event); + + if (r == WAIT_TIMEOUT) { + // Timeout occurred, check shouldStop flag continue; } + std::cout << "Thread with ID: " << addonData->id << " is done waiting." << std::endl; + + int i = r - WAIT_OBJECT_0; + if (i >= 0 && i < copied_events.size()) { + // It's a wake up event; there is no FS event. + if (copied_events[i] == g_wake_up_event) { + std::cout << "Thread with ID: " << addonData->id << " received wake-up event. Continuing." << std::endl; + continue; + } - if (!g_thread_manager.has_thread(handle->addonDataId)) { - std::cout << "Unrecognized environment: " << handle->addonDataId << std::endl; - continue; - } + ScopedLocker locker(g_handle_wrap_map_mutex); - if (handle->addonDataId == addonData->id) { - std::cout << "OURS to handle! " << handle->addonDataId << std::endl; - } + // Match up the filesystem event with the handle responsible for it. + HandleWrapper* handle = HandleWrapper::Get(copied_events[i]); + if (!handle || handle->canceled) { + continue; + } - DWORD bytes_transferred; - if (!GetOverlappedResult(handle->dir_handle, &handle->overlapped, &bytes_transferred, FALSE)) { - std::cout << "Nothing for thread: " << addonData->id << std::endl; - continue; - } - if (bytes_transferred == 0) { - std::cout << "Nothing for thread: " << addonData->id << std::endl; - continue; - } + if (!g_thread_manager.has_thread(handle->addonDataId)) { + // Ignore handles that belong to stale environments. + std::cout << "Unrecognized environment: " << handle->addonDataId << std::endl; + continue; + } - std::vector old_path; - std::vector events; - - DWORD offset = 0; - while (true) { - FILE_NOTIFY_INFORMATION* file_info = - reinterpret_cast(handle->buffer + offset); - - // Emit events for children. - EVENT_TYPE event = EVENT_NONE; - switch (file_info->Action) { - case FILE_ACTION_ADDED: - event = EVENT_CHILD_CREATE; - break; - case FILE_ACTION_REMOVED: - event = EVENT_CHILD_DELETE; - break; - case FILE_ACTION_RENAMED_OLD_NAME: - event = EVENT_CHILD_RENAME; - break; - case FILE_ACTION_RENAMED_NEW_NAME: - event = EVENT_CHILD_RENAME; - break; - case FILE_ACTION_MODIFIED: - event = EVENT_CHILD_CHANGE; - break; + DWORD bytes_transferred; + if (!GetOverlappedResult(handle->dir_handle, &handle->overlapped, &bytes_transferred, FALSE)) { + std::cout << "Nothing for thread: " << addonData->id << std::endl; + continue; + } + if (bytes_transferred == 0) { + std::cout << "Nothing for thread: " << addonData->id << std::endl; + continue; } - if (event != EVENT_NONE) { - // The FileNameLength is in "bytes", but the WideCharToMultiByte - // requires the length to be in "characters"! - int file_name_length_in_characters = - file_info->FileNameLength / sizeof(wchar_t); - - char filename[MAX_PATH] = { 0 }; - int size = WideCharToMultiByte( - CP_UTF8, - 0, - file_info->FileName, - file_name_length_in_characters, - filename, - MAX_PATH, - NULL, - NULL - ); + std::vector old_path; + std::vector events; + + DWORD offset = 0; + while (true) { + FILE_NOTIFY_INFORMATION* file_info = + reinterpret_cast(handle->buffer + offset); + + // Emit events for children. + EVENT_TYPE event = EVENT_NONE; + switch (file_info->Action) { + case FILE_ACTION_ADDED: + event = EVENT_CHILD_CREATE; + break; + case FILE_ACTION_REMOVED: + event = EVENT_CHILD_DELETE; + break; + case FILE_ACTION_RENAMED_OLD_NAME: + event = EVENT_CHILD_RENAME; + break; + case FILE_ACTION_RENAMED_NEW_NAME: + event = EVENT_CHILD_RENAME; + break; + case FILE_ACTION_MODIFIED: + event = EVENT_CHILD_CHANGE; + break; + } - // Convert file name to file path, same with: - // path = handle->path + '\\' + filename - std::vector path(handle->path.size() + 1 + size); - std::vector::iterator iter = path.begin(); - iter = std::copy(handle->path.begin(), handle->path.end(), iter); - *(iter++) = '\\'; - std::copy(filename, filename + size, iter); - - if (file_info->Action == FILE_ACTION_RENAMED_OLD_NAME) { - // Do not send rename event until the NEW_NAME event, but still keep - // a record of old name. - old_path.swap(path); - } else if (file_info->Action == FILE_ACTION_RENAMED_NEW_NAME) { - WatcherEvent e = { event, handle->overlapped.hEvent }; - e.new_path.swap(path); - e.old_path.swap(old_path); - events.push_back(e); - } else { - WatcherEvent e = { event, handle->overlapped.hEvent }; - e.new_path.swap(path); - events.push_back(e); + if (event != EVENT_NONE) { + // The FileNameLength is in "bytes", but the WideCharToMultiByte + // requires the length to be in "characters"! + int file_name_length_in_characters = + file_info->FileNameLength / sizeof(wchar_t); + + char filename[MAX_PATH] = { 0 }; + int size = WideCharToMultiByte( + CP_UTF8, + 0, + file_info->FileName, + file_name_length_in_characters, + filename, + MAX_PATH, + NULL, + NULL + ); + + // Convert file name to file path, same with: + // path = handle->path + '\\' + filename + std::vector path(handle->path.size() + 1 + size); + std::vector::iterator iter = path.begin(); + iter = std::copy(handle->path.begin(), handle->path.end(), iter); + *(iter++) = '\\'; + std::copy(filename, filename + size, iter); + + if (file_info->Action == FILE_ACTION_RENAMED_OLD_NAME) { + // Do not send rename event until the NEW_NAME event, but still keep + // a record of old name. + old_path.swap(path); + } else if (file_info->Action == FILE_ACTION_RENAMED_NEW_NAME) { + WatcherEvent e = { event, handle->overlapped.hEvent }; + e.new_path.swap(path); + e.old_path.swap(old_path); + events.push_back(e); + } else { + WatcherEvent e = { event, handle->overlapped.hEvent }; + e.new_path.swap(path); + events.push_back(e); + } } + + if (file_info->NextEntryOffset == 0) break; + offset += file_info->NextEntryOffset; } - if (file_info->NextEntryOffset == 0) break; - offset += file_info->NextEntryOffset; - } + // Restart the monitor, it was reset after each call. + QueueReaddirchanges(handle); - // Restart the monitor, it was reset after each call. - QueueReaddirchanges(handle); + locker.Unlock(); - locker.Unlock(); + std::cout << "Total events processed on thread " << addonData->id << ": " << events.size() << std::endl; - std::cout << "Total events processed on thread " << addonData->id << ": " << events.size() << std::endl; - - for (size_t i = 0; i < events.size(); ++i) { - std::cout << "Emitting " << events[i].type << " event on thread " << addonData->id << " for path: " << events[i].new_path.data() << " for worker with ID: " << handle->addonDataId << std::endl; - PathWatcherEvent event( - events[i].type, - events[i].handle, - events[i].new_path, - events[i].old_path - ); - if (handle->addonDataId == addonData->id) { - std::cout << "Invoking directly " << addonData->id << std::endl; - progress.Send(&event, 1); - } else { - g_thread_manager.queue_event(handle->addonDataId, event); + for (size_t i = 0; i < events.size(); ++i) { + std::cout << "Emitting " << events[i].type << " event on thread " << addonData->id << " for path: " << events[i].new_path.data() << " for worker with ID: " << handle->addonDataId << std::endl; + PathWatcherEvent event( + events[i].type, + events[i].handle, + events[i].new_path, + events[i].old_path + ); + if (handle->addonDataId == addonData->id) { + // This event belongs to our thread, so we can handle it directly. + std::cout << "Invoking directly " << addonData->id << std::endl; + progress.Send(&event, 1); + } else { + // Since it's not ours, we should enqueue it to be handled by the + // thread responsible for it. + g_thread_manager.queue_event(handle->addonDataId, event); + } } } - } + } // while } std::cout << "PlatformThread with ID: " << addonData->id << " is exiting! " << std::endl; From cf9acc294264c2a629d59b6ef9582808a37117bc Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 20:45:59 -0700 Subject: [PATCH 060/168] More typos --- src/pathwatcher_win.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 889746a..05aa76d 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -291,13 +291,13 @@ void PlatformThread( ThreadData* thread_data = g_thread_manager.get_thread_data(addonData->id); - if (!g_thread_manager->is_main(addonData->id)) { - while (!g_thread_manager->is_main(addonData->id)) { + if (!g_thread_manager.is_main(addonData->id)) { + while (!g_thread_manager.is_main(addonData->id)) { std::cout << "Thread with ID: " << addonData->id " in holding pattern" << std::endl; // A holding-pattern loop for threads that aren't the “boss” thread. ThreadData* thread_data = g_thread_manager.get_thread_data(addonData->id); if (!thread_data) break; // (thread was unregistered) - if (g_thread_manager->is_main(addonData->id)) break; + if (g_thread_manager.is_main(addonData->id)) break; std::unique_lock lock(thread_data->mutex); thread_data->cv.wait(lock, [thread_data] { @@ -324,7 +324,7 @@ void PlatformThread( } } - if (!g_thread_manager->is_main(addonData->id)) { + if (!g_thread_manager.is_main(addonData->id)) { // If we get to this point and this still isn't the “boss” thread, then // we’ve broken out of the above loop but should not proceed. This thread // hasn't been promoted; it should stop. @@ -339,7 +339,7 @@ void PlatformThread( std::cout << "PlatformThread ID: " << addonData->id << std::endl; // std::cout << "PlatformThread" << std::endl; - if (g_thread_manager->is_main(addonData->id)) { + if (g_thread_manager.is_main(addonData->id)) { while (!thread_data->should_stop) { // Do not use g_events directly, since reallocation could happen when there // are new watchers adding to g_events when WaitForMultipleObjects is still From 984e67255a307864b58e2a3d5927a997dd960674 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 20:49:22 -0700 Subject: [PATCH 061/168] Sigh --- src/pathwatcher_win.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 05aa76d..4f0a668 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -293,7 +293,7 @@ void PlatformThread( if (!g_thread_manager.is_main(addonData->id)) { while (!g_thread_manager.is_main(addonData->id)) { - std::cout << "Thread with ID: " << addonData->id " in holding pattern" << std::endl; + std::cout << "Thread with ID: " << addonData->id << " in holding pattern" << std::endl; // A holding-pattern loop for threads that aren't the “boss” thread. ThreadData* thread_data = g_thread_manager.get_thread_data(addonData->id); if (!thread_data) break; // (thread was unregistered) From 21da2c5945d0f53ee70c84fee4845851123a8759 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 20:55:57 -0700 Subject: [PATCH 062/168] Reintroduce `shouldStop` --- src/pathwatcher_win.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 4f0a668..9866054 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -340,7 +340,7 @@ void PlatformThread( // std::cout << "PlatformThread" << std::endl; if (g_thread_manager.is_main(addonData->id)) { - while (!thread_data->should_stop) { + while (!thread_data->should_stop && !shouldStop) { // Do not use g_events directly, since reallocation could happen when there // are new watchers adding to g_events when WaitForMultipleObjects is still // polling. @@ -359,7 +359,6 @@ void PlatformThread( SetEvent(g_file_handles_free_event); if (r == WAIT_TIMEOUT) { - // Timeout occurred, check shouldStop flag continue; } std::cout << "Thread with ID: " << addonData->id << " is done waiting." << std::endl; From 87d86887123521fb90352e2a1c808e0a9f319335 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 21:03:02 -0700 Subject: [PATCH 063/168] More verbose test --- spec/pathwatcher-spec.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index c7cb693..c3d5e9e 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -72,6 +72,7 @@ describe('PathWatcher', () => { }); let tempRenamed = path.join(tempDir, 'renamed'); + await wait(100); fs.renameSync(tempFile, tempRenamed); await condition(() => !!eventType); @@ -123,6 +124,8 @@ describe('PathWatcher', () => { if (fs.existsSync(newFile)) { fs.unlinkSync(newFile); } + console.log('ABOUT TO WATCH FAILING TEST'); + console.log('==========================='); PathWatcher.watch(tempDir, (type, path) => { fs.unlinkSync(newFile); expect(type).toBe('change'); @@ -130,7 +133,9 @@ describe('PathWatcher', () => { done(); }); - fs.writeFileSync(newFile, ''); + console.log('WRITING NEW FILE'); + console.log('================'); + fs.writeFileSync(newFile, 'x'); }); }); From 34efc12c71dd6ff40b2e792d481e973977112a83 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 21:45:07 -0700 Subject: [PATCH 064/168] =?UTF-8?q?It=20is=20failing=E2=80=A6=20but=20=5Fw?= =?UTF-8?q?hy=5F=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/directory-spec.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/spec/directory-spec.js b/spec/directory-spec.js index 913769a..5b3fef2 100644 --- a/spec/directory-spec.js +++ b/spec/directory-spec.js @@ -215,18 +215,25 @@ describe('Directory', () => { }); it('no longer triggers events', async () => { - let changeHandler = jasmine.createSpy('changeHandler'); + console.log('ABOUT TO WATCH FAILING FILE TEST'); + console.log('================================'); + + let changeHandler = jasmine.createSpy('changeHandler', () => { + console.log('[[[CHANGE HANDLER!]]]'); + }); let subscription = directory.onDidChange(changeHandler); fs.writeFileSync(temporaryFilePath, ''); + console.log('ABOUT TO WATCH FAILING FILE TEST'); + console.log('================================'); + await condition(() => changeHandler.calls.count() > 0); changeHandler.calls.reset(); subscription.dispose(); await wait(20); - fs.removeSync(temporaryFilePath); await wait(20); expect(changeHandler.calls.count()).toBe(0); From 8e8c25f0e4681aa05b05affb4c18b6cc579e486e Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 21:51:06 -0700 Subject: [PATCH 065/168] More logging --- spec/directory-spec.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/spec/directory-spec.js b/spec/directory-spec.js index 5b3fef2..30bce3d 100644 --- a/spec/directory-spec.js +++ b/spec/directory-spec.js @@ -215,7 +215,7 @@ describe('Directory', () => { }); it('no longer triggers events', async () => { - console.log('ABOUT TO WATCH FAILING FILE TEST'); + console.log('\nABOUT TO WATCH FAILING FILE TEST'); console.log('================================'); let changeHandler = jasmine.createSpy('changeHandler', () => { @@ -223,11 +223,14 @@ describe('Directory', () => { }); let subscription = directory.onDidChange(changeHandler); - fs.writeFileSync(temporaryFilePath, ''); + console.log('\nWAITING'); + console.log('======='); + await wait(1000); - console.log('ABOUT TO WATCH FAILING FILE TEST'); - console.log('================================'); + fs.writeFileSync(temporaryFilePath, ''); + console.log('\nWROTE FILE'); + console.log('=========='); await condition(() => changeHandler.calls.count() > 0); changeHandler.calls.reset(); From 9be75f946e5e384af2ef6d70285cf4d1ad982df0 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 9 Oct 2024 23:56:14 -0700 Subject: [PATCH 066/168] More Linux logging --- src/pathwatcher_linux.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pathwatcher_linux.cc b/src/pathwatcher_linux.cc index a0ce992..5d64951 100644 --- a/src/pathwatcher_linux.cc +++ b/src/pathwatcher_linux.cc @@ -35,6 +35,7 @@ void PlatformThread( char buf[4096]; while (!shouldStop) { + std::cout << "PlatformThread loop ID: " << addonData->id << std::endl; fd_set read_fds; FD_ZERO(&read_fds); FD_SET(addonData->inotify, &read_fds); From b4d0879a7814dc620e733b38e9edbf6419ff6e35 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 10 Oct 2024 11:45:40 -0700 Subject: [PATCH 067/168] Windows fixes --- spec/context-safety.js | 12 ++- spec/file-spec.js | 4 +- spec/worker.js | 54 ++++++++----- src/common.cc | 5 +- src/common.h | 2 - src/pathwatcher_win.cc | 169 ++++++++++++++++++++++++++++++++++++----- 6 files changed, 200 insertions(+), 46 deletions(-) diff --git a/spec/context-safety.js b/spec/context-safety.js index 6512abf..c7dd947 100644 --- a/spec/context-safety.js +++ b/spec/context-safety.js @@ -6,8 +6,16 @@ // script segfaults or runs indefinitely. const spawnThread = require('./worker'); -const NUM_WORKERS = 3; +const NUM_WORKERS = 2; + +const earlyReturn = Math.floor(Math.random() * NUM_WORKERS); for (let i = 0; i < NUM_WORKERS; i++) { - spawnThread(i); + spawnThread(i, earlyReturn); + // .catch((err) => { + // console.error(`Worker ${i + 1} threw error:`); + // console.error(err); + // }).finally(() => { + // console.log(`Worker ${i + 1} finished.`); + // }); } diff --git a/spec/file-spec.js b/spec/file-spec.js index e019cde..043915c 100644 --- a/spec/file-spec.js +++ b/spec/file-spec.js @@ -18,10 +18,10 @@ describe('File', () => { file = new File(filePath); }); - afterEach(() => { + afterEach(async () => { file.unsubscribeFromNativeChangeEvents(); fs.removeSync(filePath); - PathWatcher.closeAllWatchers(); + await PathWatcher.closeAllWatchers(); }); it('normalizes the specified path', () => { diff --git a/spec/worker.js b/spec/worker.js index 5eea421..6cec9e0 100644 --- a/spec/worker.js +++ b/spec/worker.js @@ -15,27 +15,37 @@ const wait = util.promisify(setTimeout); const EXPECTED_CALL_COUNT = 3; if (isMainThread) { - module.exports = function spawnThread(index) { + module.exports = function spawnThread(index, indexOfEarlyReturn) { let id = index + 1; return new Promise(async (resolve, reject) => { console.log('Spawning worker:', id); const worker = new Worker(__filename, { - workerData: id, + workerData: { id, earlyReturn: indexOfEarlyReturn === null ? false : id === (indexOfEarlyReturn + 1) }, }); worker.on('message', async (msg) => { console.log('[parent] Worker', id, 'reported call count:', msg); - await wait(500); - if (msg >= EXPECTED_CALL_COUNT) { + await wait(1000); + let expected = id === indexOfEarlyReturn + 1 ? (EXPECTED_CALL_COUNT - 1) : EXPECTED_CALL_COUNT; + let passes = msg >= expected; + if (passes) { + console.log(`Worker ${id} passed!`); resolve(); } else { - reject(`Not enough calls! Expected: ${EXPECTED_CALL_COUNT} Actual: ${msg}`); + reject(`Not enough calls on worker ${id}! Expected: ${expected} Actual: ${msg}`); } }); - worker.on('error', reject); + worker.on('error', (err) => { + console.error(`ERROR IN WORKER: ${id}`); + console.error(err); + reject(err); + }); worker.on('exit', (code) => { if (code !== 0) { - console.log(`Worker stopped with exit code ${code}`); + console.log(`Worker ${id} stopped with exit code ${code}`); reject(); + } else { + console.log(`Worker ${id} exited gracefully`); + // resolve(); } }); }); @@ -46,6 +56,8 @@ if (isMainThread) { const { watch, closeAllWatchers } = require('../src/main'); + console.log('NEW WORKER:', workerData); + class Scheduler { constructor(id, pathToWatch) { this.id = id; @@ -69,30 +81,36 @@ if (isMainThread) { } (async () => { - console.log('Worker', workerData, 'creating file:', tempFile); + console.log('Worker', workerData.id, 'creating file:', tempFile); fs.writeFileSync(tempFile, ''); await wait(500); - const scheduler = new Scheduler(workerData, tempFile); + const scheduler = new Scheduler(workerData.id, tempFile); scheduler.start(); await wait(2000); - console.log('Worker', workerData, 'changing file:', tempFile); + console.log('Worker', scheduler.id, 'changing file:', tempFile); // Should generate one or two events: fs.writeFileSync(tempFile, 'changed'); await wait(1000); - console.log('Worker', workerData, 'changing file again:', tempFile); + console.log('Worker', scheduler.id, 'changing file again:', tempFile); // Should generate another event: fs.writeFileSync(tempFile, 'changed again'); - await wait(1000); - // Should generate a final event (total count 3 or 4): - console.log('Worker', workerData, 'deleting file:', tempFile); - fs.rmSync(tempFile); - await wait(1000); + if (workerData.earlyReturn) { + console.log('Worker', scheduler.id, 'returning early!'); + } else { + await wait(1000); + // Should generate a final event (total count 3 or 4): + console.log('Worker', scheduler.id, 'deleting file:', tempFile); + fs.rmSync(tempFile); + await wait(1000); + + await wait(Math.random() * 2000); + } parentPort.postMessage(scheduler.callCount); closeAllWatchers(); - console.log('Worker', workerData, 'closing'); - process.exit(0); + console.log('Worker', scheduler.id, 'closing'); + // process.exit(0); })(); } diff --git a/src/common.cc b/src/common.cc index 308af52..37ab226 100644 --- a/src/common.cc +++ b/src/common.cc @@ -80,9 +80,10 @@ void Start(Napi::Env env) { // Called when the last watcher is stopped. void Stop(Napi::Env env) { - // std::cout << "Stop" << std::endl; - PlatformStop(env); auto addonData = env.GetInstanceData(); + std::cout << "Stop for ID: " << addonData->id << std::endl; + PlatformStop(env); + std::cout << "PlatformStop exited for ID: " << addonData->id << std::endl; if (addonData->worker) { addonData->worker->Stop(); } diff --git a/src/common.h b/src/common.h index 8fadc84..905af1d 100644 --- a/src/common.h +++ b/src/common.h @@ -98,8 +98,6 @@ struct PathWatcherEvent { using namespace Napi; -inline Function EMPTY_OK = *(new Napi::Function()); - class PathWatcherWorker: public AsyncProgressQueueWorker { public: PathWatcherWorker(Napi::Env env, Function &progressCallback); diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc index 9866054..8e5a51f 100644 --- a/src/pathwatcher_win.cc +++ b/src/pathwatcher_win.cc @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -21,6 +22,7 @@ struct ThreadData { const PathWatcherWorker::ExecutionProgress* progress; bool should_stop = false; bool is_main = false; + bool is_working = false; }; class ThreadManager { @@ -39,18 +41,50 @@ class ThreadManager { } } - bool unregister_thread(int id) { - std::lock_guard lock(mutex_); - if (id == this->main_id) { - this->main_id = -1; - for (const auto& pair : threads_) { - if (pair.first != id) { - std::cout << "Unregistering the main thread. Promoting " << pair.first << " to be the new boss thread." << std::endl; - promote(pair.first); - break; + bool clock_out(int id) { + std::cout << "omg clocking out sanity check " << id << std::endl; + if (id != this->main_id) { + std::cout << "Doesn't think it's the boss thread! " << id << std::endl; + return false; + } + // std::lock_guard lock(mutex_); + auto current_boss_data = this->get_thread_data(this->main_id); + // Pretend this is still the main thread until we can hand off duties to + // another thread. + current_boss_data->is_main = true; + current_boss_data->is_working = false; + + std::cout << "Finding a new boss thread." << std::endl; + + for (const auto& pair : threads_) { + if (pair.first != id) { + std::cout << "Promoting " << pair.first << " to be the new boss thread." << std::endl; + promote(pair.first); + if (this->main_id != pair.first) { + std::cout << "WHY AM I GOING INSANE?" << std::endl; } + auto new_boss_data = this->get_thread_data(pair.first); + new_boss_data->is_main = true; + return true; } } + return false; + } + + bool clock_in(int id) { + std::cout << "@@@@ clock_in called: Thread " << id << std::endl; + std::lock_guard lock(mutex_); + if (id != this->main_id) return false; + std::cout << "@@@@ CLOCKING IN: Thread " << id << std::endl; + + auto boss_data = unsafe_get_thread_data(id); + boss_data->is_working = true; + boss_data->cv.notify_one(); + return true; + } + + bool unregister_thread(int id) { + std::lock_guard lock(mutex_); return threads_.erase(id) > 0; } @@ -62,6 +96,13 @@ class ThreadManager { return id == this->main_id; } + void wake_up_new_main() { + if (this->main_id == -1) return; + std::cout << "Attempting to wake up new main thread: " << this->main_id << std::endl; + auto new_boss_data = this->get_thread_data(this->main_id); + new_boss_data->cv.notify_one(); + } + void promote(int id) { auto data = this->get_thread_data(id); data->is_main = true; @@ -91,6 +132,11 @@ class ThreadManager { return threads_.empty(); } + int size() { + std::lock_guard lock(mutex_); + return threads_.size(); + } + bool has_thread(int id) const { std::lock_guard lock(mutex_); return threads_.find(id) != threads_.end(); @@ -102,9 +148,21 @@ class ThreadManager { return it != threads_.end() ? it->second.get() : nullptr; } + ThreadData* get_main_data() { + if (this->main_id == -1) return nullptr; + return this->get_thread_data(this->main_id); + } + std::unordered_map> threads_; private: + + ThreadData* unsafe_get_thread_data(int id) { + auto it = threads_.find(id); + return it != threads_.end() ? it->second.get() : nullptr; + } + + mutable std::mutex mutex_; int main_id = -1; }; @@ -291,9 +349,11 @@ void PlatformThread( ThreadData* thread_data = g_thread_manager.get_thread_data(addonData->id); + std::cout << "Is " << addonData->id << " the boss thread? " << g_thread_manager.is_main(addonData->id) << std::endl; + if (!g_thread_manager.is_main(addonData->id)) { while (!g_thread_manager.is_main(addonData->id)) { - std::cout << "Thread with ID: " << addonData->id << " in holding pattern" << std::endl; + std::cout << "[WAIT WAIT " << addonData->id << "] Thread with ID: " << addonData->id << " in holding pattern" << std::endl; // A holding-pattern loop for threads that aren't the “boss” thread. ThreadData* thread_data = g_thread_manager.get_thread_data(addonData->id); if (!thread_data) break; // (thread was unregistered) @@ -303,9 +363,12 @@ void PlatformThread( thread_data->cv.wait(lock, [thread_data] { if (thread_data->should_stop) return true; if (!thread_data->event_queue.empty()) return true; + if (thread_data->is_main) return true; return false; }); + std::cout << "PEON Thread with ID: " << addonData->id << " unblocked because: (should_stop? " << thread_data->should_stop << ") (is_main? " << thread_data->is_main << ") (event queue? " << !thread_data->event_queue.empty() << ")" << std::endl; + if (thread_data->should_stop && thread_data->event_queue.empty()) { break; } @@ -320,17 +383,24 @@ void PlatformThread( if (thread_data->should_stop) break; } + if (thread_data->is_main) { + std::cout << "THIS IS WHERE WE WOULD HAVE THE NEW GUY CLOCK IN! " << addonData->id << std::endl; + g_thread_manager.clock_in(addonData->id); + break; + } + if (thread_data->should_stop) break; } } - if (!g_thread_manager.is_main(addonData->id)) { - // If we get to this point and this still isn't the “boss” thread, then - // we’ve broken out of the above loop but should not proceed. This thread - // hasn't been promoted; it should stop. - g_thread_manager.unregister_thread(addonData->id); - return; - } + // if (!g_thread_manager.is_main(addonData->id)) { + // // If we get to this point and this still isn't the “boss” thread, then + // // we’ve broken out of the above loop but should not proceed. This thread + // // hasn't been promoted; it should stop. + // std::cout << "Thread with ID: " << addonData->id << " is not the new boss thread, so it must be exiting." + // g_thread_manager.unregister_thread(addonData->id); + // return; + // } // If we get this far, then this is the main thread — either because it was // the first to be created or because it's been promoted after another thread @@ -358,6 +428,12 @@ void PlatformThread( ); SetEvent(g_file_handles_free_event); + std::cout << "BOSS Thread with ID: " << addonData->id << " unblocked because: (should_stop? " << thread_data->should_stop << ") (shouldStop? " << shouldStop << ") (is_main? " << thread_data->is_main << ") (event queue? " << (copied_events.size() > 1) << ") [THREAD SIZE: " << g_thread_manager.size() << "]" << std::endl; + + if (!thread_data->is_main) { + break; + } + if (r == WAIT_TIMEOUT) { continue; } @@ -368,6 +444,7 @@ void PlatformThread( // It's a wake up event; there is no FS event. if (copied_events[i] == g_wake_up_event) { std::cout << "Thread with ID: " << addonData->id << " received wake-up event. Continuing." << std::endl; + if (!thread_data->is_main) break; continue; } @@ -498,9 +575,21 @@ void PlatformThread( } // while } - std::cout << "PlatformThread with ID: " << addonData->id << " is exiting! " << std::endl; - // g_thread_manager.stop_all(); + + std::cout << "ABOUT TO UNREGISTER!!!!!!" << addonData->id << std::endl; g_thread_manager.unregister_thread(addonData->id); + std::cout << "PlatformThread with ID: " << addonData->id << " is exiting! Thread size: " << g_thread_manager.size() << std::endl; + + if (!g_thread_manager.is_empty()) { + std::cout << "[???] Waking up new main!" << std::endl; + // g_thread_manager.wake_up_new_main(); + // Sleep briefly to allow time for another thread to wake up. + std::cout << "[???] Sleeping!" << std::endl; + + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + std::cout << "[???] Done sleeping!" << std::endl; + } + // g_platform_thread_running = false; } @@ -581,11 +670,51 @@ int PlatformInvalidHandleToErrorNumber(WatcherHandle handle) { void PlatformStop(Napi::Env env) { auto addonData = env.GetInstanceData(); + std::cout << "@@@@ PlatformStop ID: " << addonData->id << std::endl; auto thread_data = g_thread_manager.get_thread_data(addonData->id); + + // if (g_thread_manager.is_main(addonData->id)) { + // std::cout << "CLOCKING OUT!!!!!!" << addonData->id << std::endl; + // // Warm hand-off. + // g_thread_manager.clock_out(addonData->id); + // + // if (g_thread_manager.size() > 1) { + // auto new_main_thread_data = g_thread_manager.get_main_data(); + // g_thread_manager.wake_up_new_main(); + // std::unique_lock lock(new_main_thread_data->mutex); + // // Wait until the new boss clocks in. + // new_main_thread_data->cv.wait(lock, [new_main_thread_data] { + // return new_main_thread_data->is_working; + // }); + // } + // } + if (thread_data) { + if (thread_data->is_main) { + std::cout << "CLOCKING OUT!!!!!!" << addonData->id << " thread size: " << g_thread_manager.size() << std::endl; + g_thread_manager.clock_out(addonData->id); + + if (g_thread_manager.size() > 1) { + std::cout << "WAITING FOR NEW BOSS!!!!!!" << addonData->id << std::endl; + auto new_main_thread_data = g_thread_manager.get_main_data(); + g_thread_manager.wake_up_new_main(); + std::unique_lock lock(new_main_thread_data->mutex); + // Wait until the new boss clocks in. + new_main_thread_data->cv.wait(lock, [new_main_thread_data] { + return new_main_thread_data->is_working; + }); + std::cout << "PROCEEDING!!!!!!" << addonData->id << std::endl; + } else { + std::cout << "WAS LAST THREAD!!!!!!" << addonData->id << std::endl; + } + } else { + std::cout << "NO THREAD DATA!!!!!!" << addonData->id << std::endl; + } + + std::lock_guard lock(thread_data->mutex); thread_data->should_stop = true; thread_data->cv.notify_one(); - g_thread_manager.unregister_thread(addonData->id); + // g_thread_manager.unregister_thread(addonData->id); } } From 7af1f491ebfc9ed378a8bf6e7fe6fe3bca5679f7 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 10 Oct 2024 15:57:42 -0700 Subject: [PATCH 068/168] Kill the tests if they hang indefinitely --- package.json | 2 +- spec/context-safety.js | 15 +++++++++- spec/directory-spec.js | 1 - spec/file-spec.js | 1 - spec/{spec-helper.js => helpers/all.js} | 0 spec/pathwatcher-spec.js | 1 - spec/run.js | 37 +++++++++++++++++++++++++ spec/support/jasmine.json | 18 ++++++++++++ 8 files changed, 70 insertions(+), 5 deletions(-) rename spec/{spec-helper.js => helpers/all.js} (100%) create mode 100644 spec/run.js create mode 100644 spec/support/jasmine.json diff --git a/package.json b/package.json index afc6f91..775d592 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "homepage": "http://atom.github.io/node-pathwatcher", "scripts": { "prepublish": "grunt prepublish", - "test": "jasmine spec/*-spec.js", + "test": "node spec/run.js", "test-context-safety": "node spec/context-safety.js" }, "devDependencies": { diff --git a/spec/context-safety.js b/spec/context-safety.js index c7dd947..f46d5b4 100644 --- a/spec/context-safety.js +++ b/spec/context-safety.js @@ -7,8 +7,21 @@ const spawnThread = require('./worker'); const NUM_WORKERS = 2; +const MAX_DURATION = 20 * 1000; -const earlyReturn = Math.floor(Math.random() * NUM_WORKERS); +// Pick one of the workers to return earlier than the others. +let earlyReturn = null; +if (NUM_WORKERS > 1) { + earlyReturn = Math.floor(Math.random() * NUM_WORKERS); +} + +function bail () { + console.error(`Script ran for more than ${MAX_DURATION / 1000} seconds; there's an open handle somewhere!`); + process.exit(2); +} + +let failsafe = setTimeout(bail, MAX_DURATION); +failsafe.unref(); for (let i = 0; i < NUM_WORKERS; i++) { spawnThread(i, earlyReturn); diff --git a/spec/directory-spec.js b/spec/directory-spec.js index 30bce3d..9bc3f49 100644 --- a/spec/directory-spec.js +++ b/spec/directory-spec.js @@ -3,7 +3,6 @@ const fs = require('fs-plus'); const temp = require('temp'); const Directory = require('../src/directory'); const PathWatcher = require('../src/main'); -require('./spec-helper.js'); describe('Directory', () => { let directory; diff --git a/spec/file-spec.js b/spec/file-spec.js index 043915c..89b0602 100644 --- a/spec/file-spec.js +++ b/spec/file-spec.js @@ -3,7 +3,6 @@ const fs = require('fs-plus'); const temp = require('temp'); const File = require('../src/file'); const PathWatcher = require('../src/main'); -require('./spec-helper.js'); describe('File', () => { let filePath; diff --git a/spec/spec-helper.js b/spec/helpers/all.js similarity index 100% rename from spec/spec-helper.js rename to spec/helpers/all.js diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index c3d5e9e..feccc4c 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -2,7 +2,6 @@ const PathWatcher = require('../src/main'); const fs = require('fs-plus'); const path = require('path'); const temp = require('temp'); -require('./spec-helper.js'); temp.track(); diff --git a/spec/run.js b/spec/run.js new file mode 100644 index 0000000..5504ec8 --- /dev/null +++ b/spec/run.js @@ -0,0 +1,37 @@ +// This script exists so that we can add some extra logic that runs once the +// suite has finished. This is necessary so that we can detect when the task +// fails to finish (perhaps because of an open handle somewhere) and prevent +// it from running in CI for hours while doing nothing. +const Path = require('path'); +const Jasmine = require('jasmine'); +const jasmine = new Jasmine(); + +// Load the config from the typical place… +const CONFIG = require(Path.resolve(__dirname, 'support', 'jasmine.json')); + +// …but still allow the user to override the standard suite of specs. +if (process.argv[2]) { + CONFIG.spec_files = [process.argv[2]]; +} +jasmine.loadConfig(CONFIG); + +const MAX_DURATION = 10 * 1000; + +function bail () { + console.error(`Script ran for more than ${MAX_DURATION / 1000} seconds after the end of the suite; there's an open handle somewhere!`); + process.exit(2); +} + +// Theory: the indefinite waiting that happens in CI sometimes might be the +// result of an open handle somewhere. If so, then the test task will keep +// running even though we haven't told Jasmine not to exit on completion. This +// approach might detect such scenarios and turn them into CI failures. +(async () => { + await jasmine.execute(); + // Wait to see if the script is still running MAX_DURATION milliseconds from + // now… + let failsafe = setTimeout(bail, MAX_DURATION); + // …but `unref` ourselves so that we're not the reason why the script keeps + // running! + failsafe.unref(); +})(); diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json new file mode 100644 index 0000000..a37a37e --- /dev/null +++ b/spec/support/jasmine.json @@ -0,0 +1,18 @@ +{ + "spec_dir": ".", + + "spec_files": [ + "spec/*-spec.js" + ], + + "helpers": [ + "spec/helpers/all.js" + ], + + "env": { + "failSpecWithNoExpectations": false, + "stopSpecOnExpectationFailure": false, + "stopOnSpecFailure": false, + "random": true + } +} From d4e1a7b282895cd728fbbea1be931a2bb7d63980 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 10 Oct 2024 16:44:14 -0700 Subject: [PATCH 069/168] Try again on `context-safety.js` --- spec/context-safety.js | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/spec/context-safety.js b/spec/context-safety.js index f46d5b4..11a5c64 100644 --- a/spec/context-safety.js +++ b/spec/context-safety.js @@ -7,7 +7,7 @@ const spawnThread = require('./worker'); const NUM_WORKERS = 2; -const MAX_DURATION = 20 * 1000; +const MAX_DURATION = 30 * 1000; // Pick one of the workers to return earlier than the others. let earlyReturn = null; @@ -20,15 +20,34 @@ function bail () { process.exit(2); } +// Wait to see if the script is still running MAX_DURATION milliseconds from +// now… let failsafe = setTimeout(bail, MAX_DURATION); +// …but `unref` ourselves so that we're not the reason why the script keeps +// running! failsafe.unref(); +let promises = []; +let errors = []; for (let i = 0; i < NUM_WORKERS; i++) { - spawnThread(i, earlyReturn); - // .catch((err) => { - // console.error(`Worker ${i + 1} threw error:`); - // console.error(err); - // }).finally(() => { - // console.log(`Worker ${i + 1} finished.`); - // }); + let promise = spawnThread(i, earlyReturn); + + // We want to prevent unhandled promise rejections. The errors from any + // rejected promises will be collected and handled once all workers are done. + promise.catch((err) => errors.push(err)); + promises.push(promise); } + +(async () => { + await Promise.allSettled(promises); + if (errors.length > 0) { + console.error('Errors:'); + for (let error of errors) { + console.error(` ${error}`); + } + // Don't call `process.exit`; we want to be able to detect whether there + // are open handles. If there aren't, the process will exit on its own; + // if there are, then the failsafe will detect it and tell us about it. + process.exitCode = 1; + } +})(); From 3d484b17a04b56242036a25dc5486a81a08b04d3 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sat, 12 Oct 2024 09:59:32 -0700 Subject: [PATCH 070/168] A first pass at moving to `efsw` This seems to work well, except that there's an open handle being left somewhere. This might get chaotic to track down, so this is a good commit point. --- .gitignore | 2 +- .gitmodules | 3 + binding.gyp | 221 ++++++++++++----- lib/addon-data.h | 15 ++ lib/binding.cc | 18 ++ lib/core.cc | 161 ++++++++++++ lib/core.h | 106 ++++++++ lib/directory.js | 379 ++++++++++++++++++++++++++++ lib/efsw_core.h | 42 ++++ lib/efsw_core_listener.h | 41 ++++ lib/file.js | 518 +++++++++++++++++++++++++++++++++++++++ lib/main.js | 266 ++++++++++++++++++++ spec/worker.js | 9 +- src/main.js | 23 +- vendor/efsw | 1 + 15 files changed, 1725 insertions(+), 80 deletions(-) create mode 100644 .gitmodules create mode 100644 lib/addon-data.h create mode 100644 lib/binding.cc create mode 100644 lib/core.cc create mode 100644 lib/core.h create mode 100644 lib/directory.js create mode 100644 lib/efsw_core.h create mode 100644 lib/efsw_core_listener.h create mode 100644 lib/file.js create mode 100644 lib/main.js create mode 160000 vendor/efsw diff --git a/.gitignore b/.gitignore index 79c895b..a563453 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ *.swp build/ node_modules/ -lib/ .node-version npm-debug.log api.json package-lock.json +.cache/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..bbb578a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/efsw"] + path = vendor/efsw + url = git@github.com:SpartanJ/efsw.git diff --git a/binding.gyp b/binding.gyp index 502e3ac..59efcaf 100644 --- a/binding.gyp +++ b/binding.gyp @@ -1,65 +1,162 @@ { - "targets": [ - { - "target_name": "pathwatcher", - "cflags!": ["-fno-exceptions"], - "cflags_cc!": ["-fno-exceptions"], - "xcode_settings": { - "GCC_ENABLE_CPP_EXCEPTIONS": "YES", - "CLANG_CXX_LIBRARY": "libc++", - "MACOSX_DEPLOYMENT_TARGET": "10.7", - }, - "msvs_settings": { - "VCCLCompilerTool": {"ExceptionHandling": 1}, - }, - "sources": [ - "src/main.cc", - "src/common.cc", - "src/common.h" - ], - "include_dirs": [ - " + +void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEvent* event); + +PathWatcherListener::PathWatcherListener(Napi::Env env, Napi::Function fn) + : callback(fn) { + std::cout << "new PathWatcherListener" << std::endl; + tsfn = Napi::ThreadSafeFunction::New( + env, + callback, + "pathwatcher-efsw-listener", + 0, + 2 + ); +} + +std::string EventType(efsw::Action action, bool isChild) { + switch (action) { + case efsw::Actions::Add: + return isChild ? "child-create" : "create"; + case efsw::Actions::Delete: + return isChild ? "child-delete" : "delete"; + case efsw::Actions::Modified: + return isChild ? "child-change" : "change"; + case efsw::Actions::Moved: + return isChild ? "child-rename" : "rename"; + default: + std::cout << "Unknown action: " << action; + return "unknown"; + } +} + +void PathWatcherListener::handleFileAction( + efsw::WatchID watchId, + const std::string& dir, + const std::string& filename, + efsw::Action action, + std::string oldFilename +) { + std::cout << "PathWatcherListener::handleFileAction" << std::endl; + std::cout << "Action: " << action << ", Dir: " << dir << ", Filename: " << filename << ", Old Filename: " << oldFilename << std::endl; + + std::string newPathStr = dir + PATH_SEPARATOR + filename; + std::vector newPath(newPathStr.begin(), newPathStr.end()); + + std::vector oldPath; + if (!oldFilename.empty()) { + std::string oldPathStr = dir + PATH_SEPARATOR + oldFilename; + oldPath.assign(oldPathStr.begin(), oldPathStr.end()); + } + + PathWatcherEvent* event = new PathWatcherEvent(action, watchId, newPath, oldPath); + napi_status status = tsfn.BlockingCall(event, ProcessEvent); + if (status != napi_ok) { + std::cerr << "Error in BlockingCall: " << status << std::endl; + delete event; // Clean up if BlockingCall fails + } +} + +void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEvent* event) { + if (event == nullptr) { + std::cerr << "ProcessEvent: event is null" << std::endl; + return; + } + + std::string eventName = EventType(event->type, true); + std::cout << "ProcessEvent! " << eventName << std::endl; + + std::string newPath; + std::string oldPath; + + if (!event->new_path.empty()) { + newPath.assign(event->new_path.begin(), event->new_path.end()); + std::cout << "new path: " << newPath << std::endl; + } else { + std::cout << "new path is empty" << std::endl; + } + + if (!event->old_path.empty()) { + oldPath.assign(event->old_path.begin(), event->old_path.end()); + std::cout << "old path: " << oldPath << std::endl; + } else { + std::cout << "old path is empty" << std::endl; + } + + // Use a try-catch block only for the Node-API call, which might throw + try { + callback.Call({ + Napi::String::New(env, eventName), + Napi::Number::New(env, event->handle), + Napi::String::New(env, newPath), + Napi::String::New(env, oldPath) + }); + } catch (const Napi::Error& e) { + std::cerr << "Napi error in callback.Call: " << e.what() << std::endl; + } +} + +Napi::Value EFSW::Watch(const Napi::CallbackInfo& info) { + std::cout << "Watch" << std::endl; + auto env = info.Env(); + auto addonData = env.GetInstanceData(); + Napi::HandleScope scope(env); + + if (!info[0].IsString()) { + Napi::TypeError::New(env, "String required").ThrowAsJavaScriptException(); + return env.Null(); + } + + Napi::String path = info[0].ToString(); + std::string cppPath(path); + + if (!info[1].IsFunction()) { + Napi::TypeError::New(env, "Function required").ThrowAsJavaScriptException(); + return env.Null(); + } + + Napi::Function fn = info[1].As(); + + PathWatcherListener* listener = new PathWatcherListener(env, fn); + + std::cout << "About to add handle for path: " << cppPath << std::endl; + + + WatcherHandle handle = addonData->fileWatcher->addWatch(path, listener, true); + std::cout << "Watcher handle: " << handle << std::endl; + addonData->fileWatcher->watch(); + return WatcherHandleToV8Value(handle, env); +} + +Napi::Value EFSW::Unwatch(const Napi::CallbackInfo& info) { + std::cout << "Unwatch" << std::endl; + auto env = info.Env(); + auto addonData = env.GetInstanceData(); + Napi::HandleScope scope(env); + + if (!IsV8ValueWatcherHandle(info[0])) { + Napi::TypeError::New(env, "Argument must be a number").ThrowAsJavaScriptException(); + return env.Null(); + } + + WatcherHandle handle = V8ValueToWatcherHandle(info[0].As()); + addonData->fileWatcher->removeWatch(handle); + + return env.Undefined(); +} + +void EFSW::Init(Napi::Env env) { + auto addonData = env.GetInstanceData(); + std::cout << "Addon data created!" << addonData->id << std::endl; + if (!addonData) { + std::cout << "WHAT THE FUCK" << std::endl; + } + addonData->fileWatcher = new efsw::FileWatcher(); + addonData->fileWatcher->followSymlinks(true); + // addonData->fileWatcher->watch(); +} diff --git a/lib/core.h b/lib/core.h new file mode 100644 index 0000000..ce284cd --- /dev/null +++ b/lib/core.h @@ -0,0 +1,106 @@ +#pragma once + +#define DEBUG 1 +#include +#include +#include +#include + +#include "../vendor/efsw/include/efsw/efsw.hpp" + +typedef efsw::WatchID WatcherHandle; + +#define WatcherHandleToV8Value(h, e) Napi::Number::New(e, h) +#define V8ValueToWatcherHandle(v) v.Int32Value() +#define IsV8ValueWatcherHandle(v) v.IsNumber() + +#ifdef _WIN32 +#define PATH_SEPARATOR "\\" +#else +#define PATH_SEPARATOR "/" +#endif + +struct PathWatcherEvent { + efsw::Action type; + efsw::WatchID handle; + std::vector new_path; + std::vector old_path; + + // Default constructor + PathWatcherEvent() = default; + + // Constructor + PathWatcherEvent(efsw::Action t, efsw::WatchID h, const std::vector& np, const std::vector& op = std::vector()) + : type(t), handle(h), new_path(np), old_path(op) {} + + // Copy constructor + PathWatcherEvent(const PathWatcherEvent& other) + : type(other.type), handle(other.handle), new_path(other.new_path), old_path(other.old_path) {} + + // Copy assignment operator + PathWatcherEvent& operator=(const PathWatcherEvent& other) { + if (this != &other) { + type = other.type; + handle = other.handle; + new_path = other.new_path; + old_path = other.old_path; + } + return *this; + } + + // Move constructor + PathWatcherEvent(PathWatcherEvent&& other) noexcept + : type(other.type), handle(other.handle), + new_path(std::move(other.new_path)), old_path(std::move(other.old_path)) {} + + // Move assignment operator + PathWatcherEvent& operator=(PathWatcherEvent&& other) noexcept { + if (this != &other) { + type = other.type; + handle = other.handle; + new_path = std::move(other.new_path); + old_path = std::move(other.old_path); + } + return *this; + } +}; + + +class PathWatcherListener: public efsw::FileWatchListener { +public: + PathWatcherListener(Napi::Env env, Napi::Function fn); + void handleFileAction( + efsw::WatchID watchId, + const std::string& dir, + const std::string& filename, + efsw::Action action, + std::string oldFilename + ) override; + + void unref(); + +private: + Napi::Function callback; + Napi::ThreadSafeFunction tsfn; +}; + +namespace EFSW { + class Watcher { + public: + Watcher(const char* path, Napi::Function fn, Napi::Env env); + ~Watcher(); + + WatcherHandle Start(); + void Stop(); + private: + const char* path; + Napi::Env env; + Napi::FunctionReference callback; + }; + + void Init(Napi::Env env); + + Napi::Value Watch(const Napi::CallbackInfo& info); + Napi::Value Unwatch(const Napi::CallbackInfo& info); + Napi::Value SetCallback(const Napi::CallbackInfo& info); +} diff --git a/lib/directory.js b/lib/directory.js new file mode 100644 index 0000000..2e6f8ec --- /dev/null +++ b/lib/directory.js @@ -0,0 +1,379 @@ +(function() { + var Directory, Disposable, Emitter, EmitterMixin, File, Grim, PathWatcher, async, fs, path, _ref, + __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + __slice = [].slice; + + path = require('path'); + + async = require('async'); + + _ref = require('event-kit'), Emitter = _ref.Emitter, Disposable = _ref.Disposable; + + fs = require('fs-plus'); + + Grim = require('grim'); + + File = require('./file'); + + PathWatcher = require('./main'); + + module.exports = Directory = (function() { + Directory.prototype.realPath = null; + + Directory.prototype.subscriptionCount = 0; + + + /* + Section: Construction + */ + + function Directory(directoryPath, symlink, includeDeprecatedAPIs) { + this.symlink = symlink != null ? symlink : false; + if (includeDeprecatedAPIs == null) { + includeDeprecatedAPIs = Grim.includeDeprecatedAPIs; + } + this.didRemoveSubscription = __bind(this.didRemoveSubscription, this); + this.willAddSubscription = __bind(this.willAddSubscription, this); + this.emitter = new Emitter; + if (includeDeprecatedAPIs) { + this.on('contents-changed-subscription-will-be-added', this.willAddSubscription); + this.on('contents-changed-subscription-removed', this.didRemoveSubscription); + } + if (directoryPath) { + directoryPath = path.normalize(directoryPath); + if (directoryPath.length > 1 && directoryPath[directoryPath.length - 1] === path.sep) { + directoryPath = directoryPath.substring(0, directoryPath.length - 1); + } + } + this.path = directoryPath; + if (fs.isCaseInsensitive()) { + this.lowerCasePath = this.path.toLowerCase(); + } + if (Grim.includeDeprecatedAPIs) { + this.reportOnDeprecations = true; + } + } + + Directory.prototype.create = function(mode) { + if (mode == null) { + mode = 0x1ff; + } + return this.exists().then((function(_this) { + return function(isExistingDirectory) { + if (isExistingDirectory) { + return false; + } + if (_this.isRoot()) { + throw Error("Root directory does not exist: " + (_this.getPath())); + } + return _this.getParent().create().then(function() { + return new Promise(function(resolve, reject) { + return fs.mkdir(_this.getPath(), mode, function(error) { + if (error) { + return reject(error); + } else { + return resolve(true); + } + }); + }); + }); + }; + })(this)); + }; + + + /* + Section: Event Subscription + */ + + Directory.prototype.onDidChange = function(callback) { + this.willAddSubscription(); + return this.trackUnsubscription(this.emitter.on('did-change', callback)); + }; + + Directory.prototype.willAddSubscription = function() { + if (this.subscriptionCount === 0) { + this.subscribeToNativeChangeEvents(); + } + return this.subscriptionCount++; + }; + + Directory.prototype.didRemoveSubscription = function() { + this.subscriptionCount--; + if (this.subscriptionCount === 0) { + return this.unsubscribeFromNativeChangeEvents(); + } + }; + + Directory.prototype.trackUnsubscription = function(subscription) { + return new Disposable((function(_this) { + return function() { + subscription.dispose(); + return _this.didRemoveSubscription(); + }; + })(this)); + }; + + + /* + Section: Directory Metadata + */ + + Directory.prototype.isFile = function() { + return false; + }; + + Directory.prototype.isDirectory = function() { + return true; + }; + + Directory.prototype.isSymbolicLink = function() { + return this.symlink; + }; + + Directory.prototype.exists = function() { + return new Promise((function(_this) { + return function(resolve) { + return fs.exists(_this.getPath(), resolve); + }; + })(this)); + }; + + Directory.prototype.existsSync = function() { + return fs.existsSync(this.getPath()); + }; + + Directory.prototype.isRoot = function() { + return this.getParent().getRealPathSync() === this.getRealPathSync(); + }; + + + /* + Section: Managing Paths + */ + + Directory.prototype.getPath = function() { + return this.path; + }; + + Directory.prototype.getRealPathSync = function() { + var e; + if (this.realPath == null) { + try { + this.realPath = fs.realpathSync(this.path); + if (fs.isCaseInsensitive()) { + this.lowerCaseRealPath = this.realPath.toLowerCase(); + } + } catch (_error) { + e = _error; + this.realPath = this.path; + if (fs.isCaseInsensitive()) { + this.lowerCaseRealPath = this.lowerCasePath; + } + } + } + return this.realPath; + }; + + Directory.prototype.getBaseName = function() { + return path.basename(this.path); + }; + + Directory.prototype.relativize = function(fullPath) { + var directoryPath, pathToCheck; + if (!fullPath) { + return fullPath; + } + if (process.platform === 'win32') { + fullPath = fullPath.replace(/\//g, '\\'); + } + if (fs.isCaseInsensitive()) { + pathToCheck = fullPath.toLowerCase(); + directoryPath = this.lowerCasePath; + } else { + pathToCheck = fullPath; + directoryPath = this.path; + } + if (pathToCheck === directoryPath) { + return ''; + } else if (this.isPathPrefixOf(directoryPath, pathToCheck)) { + return fullPath.substring(directoryPath.length + 1); + } + this.getRealPathSync(); + if (fs.isCaseInsensitive()) { + directoryPath = this.lowerCaseRealPath; + } else { + directoryPath = this.realPath; + } + if (pathToCheck === directoryPath) { + return ''; + } else if (this.isPathPrefixOf(directoryPath, pathToCheck)) { + return fullPath.substring(directoryPath.length + 1); + } else { + return fullPath; + } + }; + + Directory.prototype.resolve = function(relativePath) { + if (!relativePath) { + return; + } + if (relativePath != null ? relativePath.match(/[A-Za-z0-9+-.]+:\/\//) : void 0) { + return relativePath; + } else if (fs.isAbsolute(relativePath)) { + return path.normalize(fs.resolveHome(relativePath)); + } else { + return path.normalize(fs.resolveHome(path.join(this.getPath(), relativePath))); + } + }; + + + /* + Section: Traversing + */ + + Directory.prototype.getParent = function() { + return new Directory(path.join(this.path, '..')); + }; + + Directory.prototype.getFile = function() { + var filename; + filename = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + return new File(path.join.apply(path, [this.getPath()].concat(__slice.call(filename)))); + }; + + Directory.prototype.getSubdirectory = function() { + var dirname; + dirname = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + return new Directory(path.join.apply(path, [this.path].concat(__slice.call(dirname)))); + }; + + Directory.prototype.getEntriesSync = function() { + var directories, entryPath, files, stat, symlink, _i, _len, _ref1; + directories = []; + files = []; + _ref1 = fs.listSync(this.path); + for (_i = 0, _len = _ref1.length; _i < _len; _i++) { + entryPath = _ref1[_i]; + try { + stat = fs.lstatSync(entryPath); + symlink = stat.isSymbolicLink(); + if (symlink) { + stat = fs.statSync(entryPath); + } + } catch (_error) {} + if (stat != null ? stat.isDirectory() : void 0) { + directories.push(new Directory(entryPath, symlink)); + } else if (stat != null ? stat.isFile() : void 0) { + files.push(new File(entryPath, symlink)); + } + } + return directories.concat(files); + }; + + Directory.prototype.getEntries = function(callback) { + return fs.list(this.path, function(error, entries) { + var addEntry, directories, files, statEntry; + if (error != null) { + return callback(error); + } + directories = []; + files = []; + addEntry = function(entryPath, stat, symlink, callback) { + if (stat != null ? stat.isDirectory() : void 0) { + directories.push(new Directory(entryPath, symlink)); + } else if (stat != null ? stat.isFile() : void 0) { + files.push(new File(entryPath, symlink)); + } + return callback(); + }; + statEntry = function(entryPath, callback) { + return fs.lstat(entryPath, function(error, stat) { + if (stat != null ? stat.isSymbolicLink() : void 0) { + return fs.stat(entryPath, function(error, stat) { + return addEntry(entryPath, stat, true, callback); + }); + } else { + return addEntry(entryPath, stat, false, callback); + } + }); + }; + return async.eachLimit(entries, 1, statEntry, function() { + return callback(null, directories.concat(files)); + }); + }); + }; + + Directory.prototype.contains = function(pathToCheck) { + var directoryPath; + if (!pathToCheck) { + return false; + } + if (process.platform === 'win32') { + pathToCheck = pathToCheck.replace(/\//g, '\\'); + } + if (fs.isCaseInsensitive()) { + directoryPath = this.lowerCasePath; + pathToCheck = pathToCheck.toLowerCase(); + } else { + directoryPath = this.path; + } + if (this.isPathPrefixOf(directoryPath, pathToCheck)) { + return true; + } + this.getRealPathSync(); + if (fs.isCaseInsensitive()) { + directoryPath = this.lowerCaseRealPath; + } else { + directoryPath = this.realPath; + } + return this.isPathPrefixOf(directoryPath, pathToCheck); + }; + + + /* + Section: Private + */ + + Directory.prototype.subscribeToNativeChangeEvents = function() { + return this.watchSubscription != null ? this.watchSubscription : this.watchSubscription = PathWatcher.watch(this.path, (function(_this) { + return function(eventType) { + if (eventType === 'change') { + if (Grim.includeDeprecatedAPIs) { + _this.emit('contents-changed'); + } + return _this.emitter.emit('did-change'); + } + }; + })(this)); + }; + + Directory.prototype.unsubscribeFromNativeChangeEvents = function() { + if (this.watchSubscription != null) { + this.watchSubscription.close(); + return this.watchSubscription = null; + } + }; + + Directory.prototype.isPathPrefixOf = function(prefix, fullPath) { + return fullPath.indexOf(prefix) === 0 && fullPath[prefix.length] === path.sep; + }; + + return Directory; + + })(); + + if (Grim.includeDeprecatedAPIs) { + EmitterMixin = require('emissary').Emitter; + EmitterMixin.includeInto(Directory); + Directory.prototype.on = function(eventName) { + if (eventName === 'contents-changed') { + Grim.deprecate("Use Directory::onDidChange instead"); + } else if (this.reportOnDeprecations) { + Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead."); + } + return EmitterMixin.prototype.on.apply(this, arguments); + }; + } + +}).call(this); diff --git a/lib/efsw_core.h b/lib/efsw_core.h new file mode 100644 index 0000000..c342c66 --- /dev/null +++ b/lib/efsw_core.h @@ -0,0 +1,42 @@ +/** + * node-efsw - Node.js binding for EFSW + * + * Copyright (c) 2017 XadillaX + * + * MIT License + */ +#ifndef __EFSW_CORE_H__ +#define __EFSW_CORE_H__ +#include +#include +#include "../vendor/efsw/include/efsw/efsw.hpp" +#include "efsw_core_listener.h" + +namespace efsw_core { + +class EFSWCore : public Nan::ObjectWrap { +public: + static NAN_MODULE_INIT(Init); + +private: + explicit EFSWCore(const char* path, Nan::Callback* listener); + ~EFSWCore(); + + efsw::WatchId Start(); + void Stop(); + + static NAN_METHOD(New); + static NAN_METHOD(Start); + static NAN_METHOD(Stop); + +private: + std::string path; + Nan::Callback* listener; + efsw::FileWatcher* watcher; + efsw::WatchId watch_id; + EFSWCoreListener core_listener; +}; + +} + +#endif diff --git a/lib/efsw_core_listener.h b/lib/efsw_core_listener.h new file mode 100644 index 0000000..7ff76f7 --- /dev/null +++ b/lib/efsw_core_listener.h @@ -0,0 +1,41 @@ +/** + * node-efsw - Node.js binding for EFSW + * + * Copyright (c) 2017 XadillaX + * + * MIT License + */ +#ifndef __EFSW_CORE_LISTENER_H__ +#define __EFSW_CORE_LISTENER_H__ +#include +#include +#include "./deps/efsw/include/efsw/efsw.hpp" + +namespace efsw_core { + +#define Watch watch +#define AddWatch addWatch +#define RemoveWatch removeWatch +#define HandleFileAction handleFileAction +#define GetLastErrorLog getLastErrorLog +#define WatchId WatchID + +// class EFSWCoreListener : public efsw::FileWatchListener { +// public: +// EFSWCoreListener(Nan::Callback* listener); +// ~EFSWCoreListener(); +// +// void HandleFileAction( +// efsw::WatchId, +// const std::string& dir, +// const std::string& filename, +// efsw::Action action, +// std::string old_filename = ""); +// +// private: +// Nan::Callback* listener; +// }; +// +// } + +#endif diff --git a/lib/file.js b/lib/file.js new file mode 100644 index 0000000..37243d1 --- /dev/null +++ b/lib/file.js @@ -0,0 +1,518 @@ +(function() { + var Directory, Disposable, Emitter, EmitterMixin, File, Grim, PathWatcher, crypto, fs, iconv, path, _, _ref, + __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + __slice = [].slice; + + crypto = require('crypto'); + + path = require('path'); + + _ = require('underscore-plus'); + + _ref = require('event-kit'), Emitter = _ref.Emitter, Disposable = _ref.Disposable; + + fs = require('fs-plus'); + + Grim = require('grim'); + + iconv = null; + + Directory = null; + + PathWatcher = require('./main'); + + module.exports = File = (function() { + File.prototype.encoding = 'utf8'; + + File.prototype.realPath = null; + + File.prototype.subscriptionCount = 0; + + + /* + Section: Construction + */ + + function File(filePath, symlink, includeDeprecatedAPIs) { + this.symlink = symlink != null ? symlink : false; + if (includeDeprecatedAPIs == null) { + includeDeprecatedAPIs = Grim.includeDeprecatedAPIs; + } + this.didRemoveSubscription = __bind(this.didRemoveSubscription, this); + this.willAddSubscription = __bind(this.willAddSubscription, this); + if (filePath) { + filePath = path.normalize(filePath); + } + this.path = filePath; + this.emitter = new Emitter; + if (includeDeprecatedAPIs) { + this.on('contents-changed-subscription-will-be-added', this.willAddSubscription); + this.on('moved-subscription-will-be-added', this.willAddSubscription); + this.on('removed-subscription-will-be-added', this.willAddSubscription); + this.on('contents-changed-subscription-removed', this.didRemoveSubscription); + this.on('moved-subscription-removed', this.didRemoveSubscription); + this.on('removed-subscription-removed', this.didRemoveSubscription); + } + this.cachedContents = null; + this.reportOnDeprecations = true; + } + + File.prototype.create = function() { + return this.exists().then((function(_this) { + return function(isExistingFile) { + var parent; + if (!isExistingFile) { + parent = _this.getParent(); + return parent.create().then(function() { + return _this.write('').then(function() { + return true; + }); + }); + } else { + return false; + } + }; + })(this)); + }; + + + /* + Section: Event Subscription + */ + + File.prototype.onDidChange = function(callback) { + this.willAddSubscription(); + return this.trackUnsubscription(this.emitter.on('did-change', callback)); + }; + + File.prototype.onDidRename = function(callback) { + this.willAddSubscription(); + return this.trackUnsubscription(this.emitter.on('did-rename', callback)); + }; + + File.prototype.onDidDelete = function(callback) { + this.willAddSubscription(); + return this.trackUnsubscription(this.emitter.on('did-delete', callback)); + }; + + File.prototype.onWillThrowWatchError = function(callback) { + return this.emitter.on('will-throw-watch-error', callback); + }; + + File.prototype.willAddSubscription = function() { + this.subscriptionCount++; + try { + return this.subscribeToNativeChangeEvents(); + } catch (_error) {} + }; + + File.prototype.didRemoveSubscription = function() { + this.subscriptionCount--; + if (this.subscriptionCount === 0) { + return this.unsubscribeFromNativeChangeEvents(); + } + }; + + File.prototype.trackUnsubscription = function(subscription) { + return new Disposable((function(_this) { + return function() { + subscription.dispose(); + return _this.didRemoveSubscription(); + }; + })(this)); + }; + + + /* + Section: File Metadata + */ + + File.prototype.isFile = function() { + return true; + }; + + File.prototype.isDirectory = function() { + return false; + }; + + File.prototype.isSymbolicLink = function() { + return this.symlink; + }; + + File.prototype.exists = function() { + return new Promise((function(_this) { + return function(resolve) { + return fs.exists(_this.getPath(), resolve); + }; + })(this)); + }; + + File.prototype.existsSync = function() { + return fs.existsSync(this.getPath()); + }; + + File.prototype.getDigest = function() { + if (this.digest != null) { + return Promise.resolve(this.digest); + } else { + return this.read().then((function(_this) { + return function() { + return _this.digest; + }; + })(this)); + } + }; + + File.prototype.getDigestSync = function() { + if (!this.digest) { + this.readSync(); + } + return this.digest; + }; + + File.prototype.setDigest = function(contents) { + return this.digest = crypto.createHash('sha1').update(contents != null ? contents : '').digest('hex'); + }; + + File.prototype.setEncoding = function(encoding) { + if (encoding == null) { + encoding = 'utf8'; + } + if (encoding !== 'utf8') { + if (iconv == null) { + iconv = require('iconv-lite'); + } + iconv.getCodec(encoding); + } + return this.encoding = encoding; + }; + + File.prototype.getEncoding = function() { + return this.encoding; + }; + + + /* + Section: Managing Paths + */ + + File.prototype.getPath = function() { + return this.path; + }; + + File.prototype.setPath = function(path) { + this.path = path; + return this.realPath = null; + }; + + File.prototype.getRealPathSync = function() { + var error; + if (this.realPath == null) { + try { + this.realPath = fs.realpathSync(this.path); + } catch (_error) { + error = _error; + this.realPath = this.path; + } + } + return this.realPath; + }; + + File.prototype.getRealPath = function() { + if (this.realPath != null) { + return Promise.resolve(this.realPath); + } else { + return new Promise((function(_this) { + return function(resolve, reject) { + return fs.realpath(_this.path, function(err, result) { + if (err != null) { + return reject(err); + } else { + return resolve(_this.realPath = result); + } + }); + }; + })(this)); + } + }; + + File.prototype.getBaseName = function() { + return path.basename(this.path); + }; + + + /* + Section: Traversing + */ + + File.prototype.getParent = function() { + if (Directory == null) { + Directory = require('./directory'); + } + return new Directory(path.dirname(this.path)); + }; + + + /* + Section: Reading and Writing + */ + + File.prototype.readSync = function(flushCache) { + var encoding; + if (!this.existsSync()) { + this.cachedContents = null; + } else if ((this.cachedContents == null) || flushCache) { + encoding = this.getEncoding(); + if (encoding === 'utf8') { + this.cachedContents = fs.readFileSync(this.getPath(), encoding); + } else { + if (iconv == null) { + iconv = require('iconv-lite'); + } + this.cachedContents = iconv.decode(fs.readFileSync(this.getPath()), encoding); + } + } + this.setDigest(this.cachedContents); + return this.cachedContents; + }; + + File.prototype.writeFileSync = function(filePath, contents) { + var encoding; + encoding = this.getEncoding(); + if (encoding === 'utf8') { + return fs.writeFileSync(filePath, contents, { + encoding: encoding + }); + } else { + if (iconv == null) { + iconv = require('iconv-lite'); + } + return fs.writeFileSync(filePath, iconv.encode(contents, encoding)); + } + }; + + File.prototype.read = function(flushCache) { + var promise; + if ((this.cachedContents != null) && !flushCache) { + promise = Promise.resolve(this.cachedContents); + } else { + promise = new Promise((function(_this) { + return function(resolve, reject) { + var content, readStream; + content = []; + readStream = _this.createReadStream(); + readStream.on('data', function(chunk) { + return content.push(chunk); + }); + readStream.on('end', function() { + return resolve(content.join('')); + }); + return readStream.on('error', function(error) { + if (error.code === 'ENOENT') { + return resolve(null); + } else { + return reject(error); + } + }); + }; + })(this)); + } + return promise.then((function(_this) { + return function(contents) { + _this.setDigest(contents); + return _this.cachedContents = contents; + }; + })(this)); + }; + + File.prototype.createReadStream = function() { + var encoding; + encoding = this.getEncoding(); + if (encoding === 'utf8') { + return fs.createReadStream(this.getPath(), { + encoding: encoding + }); + } else { + if (iconv == null) { + iconv = require('iconv-lite'); + } + return fs.createReadStream(this.getPath()).pipe(iconv.decodeStream(encoding)); + } + }; + + File.prototype.write = function(text) { + return this.exists().then((function(_this) { + return function(previouslyExisted) { + return _this.writeFile(_this.getPath(), text).then(function() { + _this.cachedContents = text; + _this.setDigest(text); + if (!previouslyExisted && _this.hasSubscriptions()) { + _this.subscribeToNativeChangeEvents(); + } + return void 0; + }); + }; + })(this)); + }; + + File.prototype.createWriteStream = function() { + var encoding, stream; + encoding = this.getEncoding(); + if (encoding === 'utf8') { + return fs.createWriteStream(this.getPath(), { + encoding: encoding + }); + } else { + if (iconv == null) { + iconv = require('iconv-lite'); + } + stream = iconv.encodeStream(encoding); + stream.pipe(fs.createWriteStream(this.getPath())); + return stream; + } + }; + + File.prototype.writeSync = function(text) { + var previouslyExisted; + previouslyExisted = this.existsSync(); + this.writeFileSync(this.getPath(), text); + this.cachedContents = text; + this.setDigest(text); + if (Grim.includeDeprecatedAPIs) { + this.emit('contents-changed'); + } + this.emitter.emit('did-change'); + if (!previouslyExisted && this.hasSubscriptions()) { + this.subscribeToNativeChangeEvents(); + } + return void 0; + }; + + File.prototype.writeFile = function(filePath, contents) { + var encoding; + encoding = this.getEncoding(); + if (encoding === 'utf8') { + return new Promise(function(resolve, reject) { + return fs.writeFile(filePath, contents, { + encoding: encoding + }, function(err, result) { + if (err != null) { + return reject(err); + } else { + return resolve(result); + } + }); + }); + } else { + if (iconv == null) { + iconv = require('iconv-lite'); + } + return new Promise(function(resolve, reject) { + return fs.writeFile(filePath, iconv.encode(contents, encoding), function(err, result) { + if (err != null) { + return reject(err); + } else { + return resolve(result); + } + }); + }); + } + }; + + + /* + Section: Private + */ + + File.prototype.handleNativeChangeEvent = function(eventType, eventPath) { + switch (eventType) { + case 'delete': + this.unsubscribeFromNativeChangeEvents(); + return this.detectResurrectionAfterDelay(); + case 'rename': + this.setPath(eventPath); + if (Grim.includeDeprecatedAPIs) { + this.emit('moved'); + } + return this.emitter.emit('did-rename'); + case 'change': + case 'resurrect': + this.cachedContents = null; + return this.emitter.emit('did-change'); + } + }; + + File.prototype.detectResurrectionAfterDelay = function() { + return _.delay(((function(_this) { + return function() { + return _this.detectResurrection(); + }; + })(this)), 50); + }; + + File.prototype.detectResurrection = function() { + return this.exists().then((function(_this) { + return function(exists) { + if (exists) { + _this.subscribeToNativeChangeEvents(); + return _this.handleNativeChangeEvent('resurrect'); + } else { + _this.cachedContents = null; + if (Grim.includeDeprecatedAPIs) { + _this.emit('removed'); + } + return _this.emitter.emit('did-delete'); + } + }; + })(this)); + }; + + File.prototype.subscribeToNativeChangeEvents = function() { + return this.watchSubscription != null ? this.watchSubscription : this.watchSubscription = PathWatcher.watch(this.path, (function(_this) { + return function() { + var args; + args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + return _this.handleNativeChangeEvent.apply(_this, args); + }; + })(this)); + }; + + File.prototype.unsubscribeFromNativeChangeEvents = function() { + if (this.watchSubscription != null) { + this.watchSubscription.close(); + return this.watchSubscription = null; + } + }; + + return File; + + })(); + + if (Grim.includeDeprecatedAPIs) { + EmitterMixin = require('emissary').Emitter; + EmitterMixin.includeInto(File); + File.prototype.on = function(eventName) { + switch (eventName) { + case 'contents-changed': + Grim.deprecate("Use File::onDidChange instead"); + break; + case 'moved': + Grim.deprecate("Use File::onDidRename instead"); + break; + case 'removed': + Grim.deprecate("Use File::onDidDelete instead"); + break; + default: + if (this.reportOnDeprecations) { + Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead."); + } + } + return EmitterMixin.prototype.on.apply(this, arguments); + }; + } else { + File.prototype.hasSubscriptions = function() { + return this.subscriptionCount > 0; + }; + } + +}).call(this); diff --git a/lib/main.js b/lib/main.js new file mode 100644 index 0000000..9090027 --- /dev/null +++ b/lib/main.js @@ -0,0 +1,266 @@ +(function() { + var Emitter, HandleMap, HandleWatcher, PathWatcher, binding, fs, handleWatchers, path; + + binding = require('../build/Release/pathwatcher.node'); + + HandleMap = binding.HandleMap; + + Emitter = require('event-kit').Emitter; + + fs = require('fs'); + + path = require('path'); + + handleWatchers = null; + + HandleWatcher = (function() { + function HandleWatcher(path) { + this.path = path; + this.emitter = new Emitter(); + this.start(); + } + + HandleWatcher.prototype.onEvent = function(event, filePath, oldFilePath) { + var detectRename; + if (filePath) { + filePath = path.normalize(filePath); + } + if (oldFilePath) { + oldFilePath = path.normalize(oldFilePath); + } + switch (event) { + case 'rename': + this.close(); + detectRename = (function(_this) { + return function() { + return fs.stat(_this.path, function(err) { + if (err) { + _this.path = filePath; + if (process.platform === 'darwin' && /\/\.Trash\//.test(filePath)) { + _this.emitter.emit('did-change', { + event: 'delete', + newFilePath: null + }); + return _this.close(); + } else { + _this.start(); + return _this.emitter.emit('did-change', { + event: 'rename', + newFilePath: filePath + }); + } + } else { + _this.start(); + return _this.emitter.emit('did-change', { + event: 'change', + newFilePath: null + }); + } + }); + }; + })(this); + return setTimeout(detectRename, 100); + case 'delete': + this.emitter.emit('did-change', { + event: 'delete', + newFilePath: null + }); + return this.close(); + case 'unknown': + throw new Error("Received unknown event for path: " + this.path); + break; + default: + return this.emitter.emit('did-change', { + event: event, + newFilePath: filePath, + oldFilePath: oldFilePath + }); + } + }; + + HandleWatcher.prototype.onDidChange = function(callback) { + return this.emitter.on('did-change', callback); + }; + + HandleWatcher.prototype.start = function() { + var troubleWatcher; + this.handle = binding.watch(this.path); + if (handleWatchers.has(this.handle)) { + troubleWatcher = handleWatchers.get(this.handle); + troubleWatcher.close(); + console.error("The handle(" + this.handle + ") returned by watching " + this.path + " is the same with an already watched path(" + troubleWatcher.path + ")"); + } + return handleWatchers.add(this.handle, this); + }; + + HandleWatcher.prototype.closeIfNoListener = function() { + if (this.emitter.getTotalListenerCount() === 0) { + return this.close(); + } + }; + + HandleWatcher.prototype.close = function() { + if (handleWatchers.has(this.handle)) { + binding.unwatch(this.handle); + return handleWatchers.remove(this.handle); + } + }; + + return HandleWatcher; + + })(); + + PathWatcher = (function() { + PathWatcher.prototype.isWatchingParent = false; + + PathWatcher.prototype.path = null; + + PathWatcher.prototype.handleWatcher = null; + + function PathWatcher(filePath, callback) { + var stats, watcher, _i, _len, _ref; + this.path = filePath; + this.emitter = new Emitter(); + if (process.platform === 'win32') { + stats = fs.statSync(filePath); + this.isWatchingParent = !stats.isDirectory(); + } + if (this.isWatchingParent) { + filePath = path.dirname(filePath); + } + _ref = handleWatchers.values(); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + watcher = _ref[_i]; + if (watcher.path === filePath) { + this.handleWatcher = watcher; + break; + } + } + if (this.handleWatcher == null) { + this.handleWatcher = new HandleWatcher(filePath); + } + this.onChange = (function(_this) { + return function(_arg) { + var event, newFilePath, oldFilePath; + event = _arg.event, newFilePath = _arg.newFilePath, oldFilePath = _arg.oldFilePath; + switch (event) { + case 'rename': + case 'change': + case 'delete': + if (event === 'rename') { + _this.path = newFilePath; + } + if (typeof callback === 'function') { + callback.call(_this, event, newFilePath); + } + return _this.emitter.emit('did-change', { + event: event, + newFilePath: newFilePath + }); + case 'child-rename': + if (_this.isWatchingParent) { + if (_this.path === oldFilePath) { + return _this.onChange({ + event: 'rename', + newFilePath: newFilePath + }); + } + } else { + return _this.onChange({ + event: 'change', + newFilePath: '' + }); + } + break; + case 'child-delete': + if (_this.isWatchingParent) { + if (_this.path === newFilePath) { + return _this.onChange({ + event: 'delete', + newFilePath: null + }); + } + } else { + return _this.onChange({ + event: 'change', + newFilePath: '' + }); + } + break; + case 'child-change': + if (_this.isWatchingParent && _this.path === newFilePath) { + return _this.onChange({ + event: 'change', + newFilePath: '' + }); + } + break; + case 'child-create': + if (!_this.isWatchingParent) { + return _this.onChange({ + event: 'change', + newFilePath: '' + }); + } + } + }; + })(this); + this.disposable = this.handleWatcher.onDidChange(this.onChange); + } + + PathWatcher.prototype.onDidChange = function(callback) { + return this.emitter.on('did-change', callback); + }; + + PathWatcher.prototype.close = function() { + this.emitter.dispose(); + this.disposable.dispose(); + return this.handleWatcher.closeIfNoListener(); + }; + + return PathWatcher; + + })(); + + exports.watch = function(pathToWatch, callback) { + if (handleWatchers == null) { + handleWatchers = new HandleMap; + binding.setCallback(function(event, handle, filePath, oldFilePath) { + if (handleWatchers.has(handle)) { + return handleWatchers.get(handle).onEvent(event, filePath, oldFilePath); + } + }); + } + return new PathWatcher(path.resolve(pathToWatch), callback); + }; + + exports.closeAllWatchers = function() { + var watcher, _i, _len, _ref; + if (handleWatchers != null) { + _ref = handleWatchers.values(); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + watcher = _ref[_i]; + watcher.close(); + } + return handleWatchers.clear(); + } + }; + + exports.getWatchedPaths = function() { + var paths, watcher, _i, _len, _ref; + paths = []; + if (handleWatchers != null) { + _ref = handleWatchers.values(); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + watcher = _ref[_i]; + paths.push(watcher.path); + } + } + return paths; + }; + + exports.File = require('./file'); + + exports.Directory = require('./directory'); + +}).call(this); diff --git a/spec/worker.js b/spec/worker.js index 6cec9e0..916cb3b 100644 --- a/spec/worker.js +++ b/spec/worker.js @@ -51,7 +51,7 @@ if (isMainThread) { }); }; } else { - const tempDir = temp.mkdirSync('node-pathwatcher-directory'); + let tempDir = temp.mkdirSync('node-pathwatcher-directory'); const tempFile = path.join(tempDir, 'file'); const { watch, closeAllWatchers } = require('../src/main'); @@ -69,7 +69,7 @@ if (isMainThread) { console.log('Scheduler', this.id, 'starting at', performance.now(), 'watching path:', this.path); this.watcher = watch(this.path, (event) => { this.callCount++; - console.log('PathWatcher event for worker', this.id, event) + console.warn('\x1b[33m%s\x1b[0m', 'PathWatcher event for worker', this.id, event) console.log('callCount is now:', this.callCount); }); console.log('Scheduler', this.id, 'ready at:', performance.now()); @@ -84,7 +84,7 @@ if (isMainThread) { console.log('Worker', workerData.id, 'creating file:', tempFile); fs.writeFileSync(tempFile, ''); await wait(500); - const scheduler = new Scheduler(workerData.id, tempFile); + const scheduler = new Scheduler(workerData.id, tempDir); scheduler.start(); await wait(2000); @@ -95,10 +95,11 @@ if (isMainThread) { console.log('Worker', scheduler.id, 'changing file again:', tempFile); // Should generate another event: fs.writeFileSync(tempFile, 'changed again'); + await wait(500); if (workerData.earlyReturn) { console.log('Worker', scheduler.id, 'returning early!'); } else { - await wait(1000); + await wait(500); // Should generate a final event (total count 3 or 4): console.log('Worker', scheduler.id, 'deleting file:', tempFile); fs.rmSync(tempFile); diff --git a/src/main.js b/src/main.js index c21716c..8a4f873 100644 --- a/src/main.js +++ b/src/main.js @@ -88,7 +88,7 @@ class HandleWatcher { start () { let troubleWatcher; - this.handle = binding.watch(this.path); + this.handle = binding.watch(this.path, callback); if (HANDLE_WATCHERS.has(this.handle)) { troubleWatcher = HANDLE_WATCHERS.get(this.handle); troubleWatcher.close(); @@ -119,10 +119,10 @@ class PathWatcher { constructor(filePath, callback) { this.path = filePath; this.emitter = new Emitter(); - if (process.platform === 'win32') { - let stats = fs.statSync(filePath); - this.isWatchingParent = !stats.isDirectory(); - } + + let stats = fs.statSync(filePath); + this.isWatchingParent = !stats.isDirectory(); + if (this.isWatchingParent) { filePath = path.dirname(filePath); } @@ -194,15 +194,12 @@ class PathWatcher { } } -function watch (pathToWatch, callback) { - if (!initialized) { - initialized = true; - binding.setCallback((event, handle, filePath, oldFilePath) => { - if (!HANDLE_WATCHERS.has(handle)) return; - HANDLE_WATCHERS.get(handle).onEvent(event, filePath, oldFilePath); - }); - } +function callback(event, handle, filePath, oldFilePath) { + if (!HANDLE_WATCHERS.has(handle)) return; + HANDLE_WATCHERS.get(handle).onEvent(event, filePath, oldFilePath); +} +function watch (pathToWatch, callback) { return new PathWatcher(path.resolve(pathToWatch), callback); } diff --git a/vendor/efsw b/vendor/efsw new file mode 160000 index 0000000..a064eb2 --- /dev/null +++ b/vendor/efsw @@ -0,0 +1 @@ +Subproject commit a064eb20e1312634813c724acc3c8229cc04e0a2 From 3198532f9871f095ae4613f471e06cd65c400a8c Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 13:40:06 -0700 Subject: [PATCH 071/168] Fix some specs --- src/main.js | 50 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/src/main.js b/src/main.js index 8a4f873..c223891 100644 --- a/src/main.js +++ b/src/main.js @@ -116,8 +116,17 @@ class PathWatcher { isWatchingParent = false; path = null; handleWatcher = null; + constructor(filePath, callback) { this.path = filePath; + + if (!fs.existsSync(filePath)) { + let err = new Error(`Unable to watch path`); + err.code = 'ENOENT'; + throw err; + } + + this.assignRealPath(); this.emitter = new Emitter(); let stats = fs.statSync(filePath); @@ -152,7 +161,7 @@ class PathWatcher { return; case 'child-rename': if (this.isWatchingParent) { - if (this.path === oldFilePath) { + if (this.matches(oldFilePath)) { return this.onChange({ event: 'rename', newFilePath }); } } else { @@ -161,7 +170,7 @@ class PathWatcher { break; case 'child-delete': if (this.isWatchingParent) { - if (this.path === newFilePath) { + if (this.matches(newFilePath)) { return this.onChange({ event: 'delete', newFilePath: null }); } } else { @@ -169,7 +178,7 @@ class PathWatcher { } break; case 'child-change': - if (this.isWatchingParent && this.path === newFilePath) { + if (this.isWatchingParent && this.matches(newFilePath)) { return this.onChange({ event: 'change', newFilePath: '' }); } break; @@ -183,6 +192,25 @@ class PathWatcher { this.disposable = this.handleWatcher.onDidChange(this.onChange); } + matches (otherPath) { + if (this.realPath) { + return this.realPath === otherPath; + } else { + return this.path === otherPath; + } + } + + assignRealPath () { + try { + this.realPath = fs.realpathSync(this.path); + if (this.realPath) { + // console.log('We think the real path is:', this.realPath); + } + } catch (_error) { + this.realPath = null; + } + } + onDidChange (callback) { return this.emitter.on('did-change', callback); } @@ -194,8 +222,22 @@ class PathWatcher { } } -function callback(event, handle, filePath, oldFilePath) { +const LAST_EVENT_PER_PATH = new Map(); + +async function callback(event, handle, filePath, oldFilePath) { if (!HANDLE_WATCHERS.has(handle)) return; + LAST_EVENT_PER_PATH.set(filePath, event); + + await wait(10); + let lastEvent = LAST_EVENT_PER_PATH.get(filePath); + if (lastEvent !== event) { + return; + } + + if (event.includes('delete') && fs.existsSync(filePath)) { + return; + } + HANDLE_WATCHERS.get(handle).onEvent(event, filePath, oldFilePath); } From 683a10ad6f0cfb998cd934e71a4dd413230b6bbb Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 13:53:51 -0700 Subject: [PATCH 072/168] Fix issue introduced by slight buffering of events --- src/main.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index c223891..7c50828 100644 --- a/src/main.js +++ b/src/main.js @@ -228,6 +228,10 @@ async function callback(event, handle, filePath, oldFilePath) { if (!HANDLE_WATCHERS.has(handle)) return; LAST_EVENT_PER_PATH.set(filePath, event); + // Grab a reference to the watcher before we wait; it might be deleted from + // the registry after we wait. + let watcher = HANDLE_WATCHERS.get(handle); + await wait(10); let lastEvent = LAST_EVENT_PER_PATH.get(filePath); if (lastEvent !== event) { @@ -238,7 +242,7 @@ async function callback(event, handle, filePath, oldFilePath) { return; } - HANDLE_WATCHERS.get(handle).onEvent(event, filePath, oldFilePath); + watcher.onEvent(event, filePath, oldFilePath); } function watch (pathToWatch, callback) { From e3b6d4f2cc6019aa5283d0a15bd6a69d45e364ad Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 15:29:10 -0700 Subject: [PATCH 073/168] =?UTF-8?q?Get=20the=20specs=20passing=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …by moving some of the de-duping to a higher level. The raw `PathWatcher.watch` function can drink from the firehose, whereas `File` can be more discerning. --- src/file.js | 14 +++++++++++++- src/main.js | 33 +++++++++++++++++++-------------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/file.js b/src/file.js index 1fd6a2f..3ee5cb9 100644 --- a/src/file.js +++ b/src/file.js @@ -8,6 +8,10 @@ const Grim = require('grim'); let iconv; let Directory; +async function wait (ms) { + return new Promise(r => setTimeout(r, ms)); +} + const PathWatcher = require('./main'); class File { @@ -60,7 +64,15 @@ class File { onDidChange (callback) { this.willAddSubscription(); - return this.trackUnsubscription(this.emitter.on('did-change', callback)); + // Add a small buffer here. If a file has changed, we want to wait briefly + // to see if it's prelude to a delete event (as EFSW sometimes does). The + // good news is that we don't have to wait very long at all. + let wrappedCallback = async (...args) => { + await wait(0); + if (!(await this.exists())) return; + callback(...args); + }; + return this.trackUnsubscription(this.emitter.on('did-change', wrappedCallback)); } onDidRename (callback) { diff --git a/src/main.js b/src/main.js index 7c50828..e255549 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,3 @@ - let binding; try { binding = require('../build/Debug/pathwatcher.node'); @@ -10,7 +9,6 @@ const fs = require('fs'); const path = require('path'); const HANDLE_WATCHERS = new Map(); -let initialized = false; function wait (ms) { return new Promise(r => setTimeout(r, ms)); @@ -77,7 +75,7 @@ class HandleWatcher { default: this.emitter.emit( 'did-change', - { event, newFilePath: filePath, oldFilePath } + { event, newFilePath: filePath, oldFilePath, rawFilePath: filePath } ); } } @@ -143,13 +141,23 @@ class PathWatcher { } this.handleWatcher ??= new HandleWatcher(filePath); - this.onChange = ({ event, newFilePath, oldFilePath }) => { + this.onChange = ({ event, newFilePath, oldFilePath, rawFilePath }) => { + // Filter out strange events. + let comparisonPath = this.path ?? this.realPath; + if (rawFilePath && (comparisonPath.length > rawFilePath.length)) { + // This is weird. Not sure why this happens yet. It's most likely an + // event for a parent directory of what we're watching. Ideally we can + // filter this out earlier in the process, like in the native code, but + // that would involve doing earlier symlink resolution. + return; + } switch (event) { case 'rename': case 'change': case 'delete': if (event === 'rename') { this.path = newFilePath; + this.assignRealPath(); } if (typeof callback === 'function') { callback.call(this, event, newFilePath); @@ -184,7 +192,13 @@ class PathWatcher { break; case 'child-create': if (!this.isWatchingParent) { - return this.onChange({ event: 'change', newFilePath: '' }); + if (this.matches(newFilePath)) { + // If we are watching a file already, it must exist. There is no + // `create` event. This should not be handled because it's + // invalid. + return; + } + return this.onChange({ event: 'change', newFilePath: '', rawFilePath }); } } }; @@ -222,22 +236,13 @@ class PathWatcher { } } -const LAST_EVENT_PER_PATH = new Map(); - async function callback(event, handle, filePath, oldFilePath) { if (!HANDLE_WATCHERS.has(handle)) return; - LAST_EVENT_PER_PATH.set(filePath, event); // Grab a reference to the watcher before we wait; it might be deleted from // the registry after we wait. let watcher = HANDLE_WATCHERS.get(handle); - await wait(10); - let lastEvent = LAST_EVENT_PER_PATH.get(filePath); - if (lastEvent !== event) { - return; - } - if (event.includes('delete') && fs.existsSync(filePath)) { return; } From 26d1ed68dacd301f57f1c1bba75ca5c378ee033c Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 16:23:56 -0700 Subject: [PATCH 074/168] Cleanup --- spec/context-safety.js | 2 +- spec/pathwatcher-spec.js | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/spec/context-safety.js b/spec/context-safety.js index 11a5c64..1a174fc 100644 --- a/spec/context-safety.js +++ b/spec/context-safety.js @@ -7,7 +7,7 @@ const spawnThread = require('./worker'); const NUM_WORKERS = 2; -const MAX_DURATION = 30 * 1000; +const MAX_DURATION = 15 * 1000; // Pick one of the workers to return earlier than the others. let earlyReturn = null; diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index feccc4c..7fa9b0b 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -71,7 +71,6 @@ describe('PathWatcher', () => { }); let tempRenamed = path.join(tempDir, 'renamed'); - await wait(100); fs.renameSync(tempFile, tempRenamed); await condition(() => !!eventType); @@ -123,8 +122,6 @@ describe('PathWatcher', () => { if (fs.existsSync(newFile)) { fs.unlinkSync(newFile); } - console.log('ABOUT TO WATCH FAILING TEST'); - console.log('==========================='); PathWatcher.watch(tempDir, (type, path) => { fs.unlinkSync(newFile); expect(type).toBe('change'); @@ -132,8 +129,6 @@ describe('PathWatcher', () => { done(); }); - console.log('WRITING NEW FILE'); - console.log('================'); fs.writeFileSync(newFile, 'x'); }); }); From caca25ee50957ce394645eb8e5b59816b523d693 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 16:30:05 -0700 Subject: [PATCH 075/168] Fix open handle issue --- lib/addon-data.h | 3 +- lib/core.cc | 100 +++++++++++++++++++++++++++++++++-------------- lib/core.h | 5 ++- 3 files changed, 77 insertions(+), 31 deletions(-) diff --git a/lib/addon-data.h b/lib/addon-data.h index a7b57da..b44b6b6 100644 --- a/lib/addon-data.h +++ b/lib/addon-data.h @@ -10,6 +10,7 @@ class AddonData final { } int id; - unsigned int watchCount = 0; + int watchCount = 0; efsw::FileWatcher* fileWatcher; + std::unordered_map listeners; }; diff --git a/lib/core.cc b/lib/core.cc index 5068188..d24a3eb 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -8,16 +8,25 @@ void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEvent* even PathWatcherListener::PathWatcherListener(Napi::Env env, Napi::Function fn) : callback(fn) { - std::cout << "new PathWatcherListener" << std::endl; tsfn = Napi::ThreadSafeFunction::New( env, callback, "pathwatcher-efsw-listener", 0, - 2 + 1 ); } +PathWatcherListener::~PathWatcherListener() { + Stop(); +} + +void PathWatcherListener::Stop() { + if (tsfn) { + tsfn.Release(); + } +} + std::string EventType(efsw::Action action, bool isChild) { switch (action) { case efsw::Actions::Add: @@ -29,7 +38,7 @@ std::string EventType(efsw::Action action, bool isChild) { case efsw::Actions::Moved: return isChild ? "child-rename" : "rename"; default: - std::cout << "Unknown action: " << action; + // std::cout << "Unknown action: " << action; return "unknown"; } } @@ -41,50 +50,46 @@ void PathWatcherListener::handleFileAction( efsw::Action action, std::string oldFilename ) { - std::cout << "PathWatcherListener::handleFileAction" << std::endl; - std::cout << "Action: " << action << ", Dir: " << dir << ", Filename: " << filename << ", Old Filename: " << oldFilename << std::endl; + // std::cout << "PathWatcherListener::handleFileAction" << std::endl; + // std::cout << "Action: " << EventType(action, true) << ", Dir: " << dir << ", Filename: " << filename << ", Old Filename: " << oldFilename << std::endl; - std::string newPathStr = dir + PATH_SEPARATOR + filename; + std::string newPathStr = dir + filename; std::vector newPath(newPathStr.begin(), newPathStr.end()); std::vector oldPath; if (!oldFilename.empty()) { - std::string oldPathStr = dir + PATH_SEPARATOR + oldFilename; + std::string oldPathStr = dir + oldFilename; oldPath.assign(oldPathStr.begin(), oldPathStr.end()); } PathWatcherEvent* event = new PathWatcherEvent(action, watchId, newPath, oldPath); napi_status status = tsfn.BlockingCall(event, ProcessEvent); if (status != napi_ok) { - std::cerr << "Error in BlockingCall: " << status << std::endl; + // std::cerr << "Error in BlockingCall: " << status << std::endl; delete event; // Clean up if BlockingCall fails } } void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEvent* event) { if (event == nullptr) { - std::cerr << "ProcessEvent: event is null" << std::endl; + // std::cerr << "ProcessEvent: event is null" << std::endl; return; } std::string eventName = EventType(event->type, true); - std::cout << "ProcessEvent! " << eventName << std::endl; + // std::cout << "ProcessEvent! " << eventName << std::endl; std::string newPath; std::string oldPath; if (!event->new_path.empty()) { newPath.assign(event->new_path.begin(), event->new_path.end()); - std::cout << "new path: " << newPath << std::endl; - } else { - std::cout << "new path is empty" << std::endl; + // std::cout << "new path: " << newPath << std::endl; } if (!event->old_path.empty()) { oldPath.assign(event->old_path.begin(), event->old_path.end()); - std::cout << "old path: " << oldPath << std::endl; - } else { - std::cout << "old path is empty" << std::endl; + // std::cout << "old path: " << oldPath << std::endl; } // Use a try-catch block only for the Node-API call, which might throw @@ -96,12 +101,15 @@ void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEvent* even Napi::String::New(env, oldPath) }); } catch (const Napi::Error& e) { - std::cerr << "Napi error in callback.Call: " << e.what() << std::endl; + // TODO: Unsure why this would happen; but if it's plausible that it would + // happen sometimes, then figure out how to surface it. + + // std::cerr << "Napi error in callback.Call: " << e.what() << std::endl; } } Napi::Value EFSW::Watch(const Napi::CallbackInfo& info) { - std::cout << "Watch" << std::endl; + // std::cout << "Watch" << std::endl; auto env = info.Env(); auto addonData = env.GetInstanceData(); Napi::HandleScope scope(env); @@ -123,19 +131,33 @@ Napi::Value EFSW::Watch(const Napi::CallbackInfo& info) { PathWatcherListener* listener = new PathWatcherListener(env, fn); - std::cout << "About to add handle for path: " << cppPath << std::endl; + // std::cout << "About to add handle for path: " << cppPath << std::endl; + if (!addonData->fileWatcher) { + // std::cout << "CREATING WATCHER!!!" << std::endl; + addonData->fileWatcher = new efsw::FileWatcher(); + addonData->fileWatcher->followSymlinks(true); + addonData->fileWatcher->watch(); + } WatcherHandle handle = addonData->fileWatcher->addWatch(path, listener, true); - std::cout << "Watcher handle: " << handle << std::endl; - addonData->fileWatcher->watch(); + if (handle >= 0) { + addonData->listeners[handle] = listener; + } else { + delete listener; + Napi::Error::New(env, "Failed to add watch").ThrowAsJavaScriptException(); + return env.Null(); + } + // std::cout << "Watcher handle: " << handle << std::endl; + addonData->watchCount++; + return WatcherHandleToV8Value(handle, env); } Napi::Value EFSW::Unwatch(const Napi::CallbackInfo& info) { - std::cout << "Unwatch" << std::endl; auto env = info.Env(); auto addonData = env.GetInstanceData(); + // std::cout << "Unwatch ID:" << addonData->id << std::endl; Napi::HandleScope scope(env); if (!IsV8ValueWatcherHandle(info[0])) { @@ -144,18 +166,38 @@ Napi::Value EFSW::Unwatch(const Napi::CallbackInfo& info) { } WatcherHandle handle = V8ValueToWatcherHandle(info[0].As()); + // std::cout << "About to unwatch handle:" << handle << std::endl; + addonData->fileWatcher->removeWatch(handle); + auto it = addonData->listeners.find(handle); + if (it != addonData->listeners.end()) { + it->second->Stop(); // Release the ThreadSafeFunction + addonData->listeners.erase(it); // Remove from the map + } + + addonData->watchCount--; + if (addonData->watchCount == 0) { + EFSW::Cleanup(env); + } + return env.Undefined(); } -void EFSW::Init(Napi::Env env) { +void EFSW::Cleanup(Napi::Env env) { auto addonData = env.GetInstanceData(); - std::cout << "Addon data created!" << addonData->id << std::endl; - if (!addonData) { - std::cout << "WHAT THE FUCK" << std::endl; + delete addonData->fileWatcher; + if (addonData && addonData->fileWatcher) { + // Clean up all listeners + for (auto& pair : addonData->listeners) { + pair.second->Stop(); + } + addonData->fileWatcher = nullptr; } - addonData->fileWatcher = new efsw::FileWatcher(); - addonData->fileWatcher->followSymlinks(true); - // addonData->fileWatcher->watch(); +} + +void EFSW::Init(Napi::Env env) { + auto addonData = env.GetInstanceData(); + // std::cout << "Addon data created!" << addonData->id << std::endl; + addonData->watchCount = 0; } diff --git a/lib/core.h b/lib/core.h index ce284cd..84375fa 100644 --- a/lib/core.h +++ b/lib/core.h @@ -69,6 +69,7 @@ struct PathWatcherEvent { class PathWatcherListener: public efsw::FileWatchListener { public: PathWatcherListener(Napi::Env env, Napi::Function fn); + ~PathWatcherListener(); void handleFileAction( efsw::WatchID watchId, const std::string& dir, @@ -77,9 +78,10 @@ class PathWatcherListener: public efsw::FileWatchListener { std::string oldFilename ) override; - void unref(); + void Stop(); private: + const std::chrono::milliseconds delayDuration{100}; // 100ms delay Napi::Function callback; Napi::ThreadSafeFunction tsfn; }; @@ -99,6 +101,7 @@ namespace EFSW { }; void Init(Napi::Env env); + void Cleanup(Napi::Env env); Napi::Value Watch(const Napi::CallbackInfo& info); Napi::Value Unwatch(const Napi::CallbackInfo& info); From 49758ee097565d594c5ee65208e5266e20f50798 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 16:30:42 -0700 Subject: [PATCH 076/168] Add dev dependencies --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 775d592..3707f59 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "test-context-safety": "node spec/context-safety.js" }, "devDependencies": { + "chalk": "^4.1.2", "grunt": "~0.4.1", "grunt-atomdoc": "^1.0", "grunt-cli": "~0.1.7", @@ -34,7 +35,8 @@ "node-cpplint": "~0.1.5", "rimraf": "~2.2.0", "segfault-handler": "^1.3.0", - "temp": "~0.9.0" + "temp": "~0.9.0", + "why-is-node-running": "^2.3.0" }, "dependencies": { "async": "~0.2.10", From 6c610c4548ec9c32ea3aa17229d855edba53b37f Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 16:32:18 -0700 Subject: [PATCH 077/168] Clean up specs --- spec/directory-spec.js | 11 +---------- spec/file-spec.js | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/spec/directory-spec.js b/spec/directory-spec.js index 9bc3f49..a4c8234 100644 --- a/spec/directory-spec.js +++ b/spec/directory-spec.js @@ -214,22 +214,13 @@ describe('Directory', () => { }); it('no longer triggers events', async () => { - console.log('\nABOUT TO WATCH FAILING FILE TEST'); - console.log('================================'); - - let changeHandler = jasmine.createSpy('changeHandler', () => { - console.log('[[[CHANGE HANDLER!]]]'); - }); + let changeHandler = jasmine.createSpy('changeHandler'); let subscription = directory.onDidChange(changeHandler); - console.log('\nWAITING'); - console.log('======='); await wait(1000); fs.writeFileSync(temporaryFilePath, ''); - console.log('\nWROTE FILE'); - console.log('=========='); await condition(() => changeHandler.calls.count() > 0); changeHandler.calls.reset(); diff --git a/spec/file-spec.js b/spec/file-spec.js index 89b0602..335b9e1 100644 --- a/spec/file-spec.js +++ b/spec/file-spec.js @@ -194,13 +194,15 @@ describe('File', () => { describe('when a file is moved (via the filesystem)', () => { let newPath = null; - beforeEach(() => { + beforeEach(async () => { newPath = path.join(path.dirname(filePath), 'file-was-moved-test.txt'); }); afterEach(async () => { if (!fs.existsSync(newPath)) return; + // console.log(chalk.red('removing…')); fs.removeSync(newPath); + // console.log(chalk.red('…removed.')); let deleteHandler = jasmine.createSpy('deleteHandler'); file.onDidDelete(deleteHandler); await condition(() => deleteHandler.calls.count() > 0, 30000); @@ -210,7 +212,9 @@ describe('File', () => { let moveHandler = jasmine.createSpy('moveHandler'); file.onDidRename(moveHandler); + // console.log(chalk.blue('moving…')); fs.moveSync(filePath, newPath); + // console.log(chalk.blue('…moved.')); await condition(() => moveHandler.calls.count() > 0, 30000); @@ -243,14 +247,20 @@ describe('File', () => { file.onDidDelete(deleteHandler); expect(changeHandler).not.toHaveBeenCalled(); + // console.log(chalk.blue('deleting…')); fs.removeSync(filePath); + // console.log(chalk.blue('…deleted.')); expect(changeHandler).not.toHaveBeenCalled(); await wait(20); - fs.writeFileSync(filePath, 'HE HAS RISEN!'); expect(changeHandler).not.toHaveBeenCalled(); + // console.log(chalk.blue('resurrecting…')); + fs.writeFileSync(filePath, 'HE HAS RISEN!'); + // console.log(chalk.blue('resurrected.')); + + await condition(() => changeHandler.calls.count() === 1); expect(deleteHandler).not.toHaveBeenCalled(); From 3f464fb9f921fcf4471fcba1fe1ad90ef905d01f Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 16:33:56 -0700 Subject: [PATCH 078/168] Use 3 threads on the context-safety test --- spec/context-safety.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/context-safety.js b/spec/context-safety.js index 1a174fc..b63c6d3 100644 --- a/spec/context-safety.js +++ b/spec/context-safety.js @@ -6,7 +6,7 @@ // script segfaults or runs indefinitely. const spawnThread = require('./worker'); -const NUM_WORKERS = 2; +const NUM_WORKERS = 3; const MAX_DURATION = 15 * 1000; // Pick one of the workers to return earlier than the others. From f92eeadb0a685cb0ff562edf6cb288b5b41e7256 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 16:40:21 -0700 Subject: [PATCH 079/168] Checkout submodules in CI --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e591447..9c751a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,8 @@ jobs: - 20 steps: - uses: actions/checkout@v4 + with: + submodules: true - name: Install Node ${{ matrix.node }} uses: actions/setup-node@v4 From dd3323b8a42e142d7876fb17ce2e576ac85783a8 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 16:40:31 -0700 Subject: [PATCH 080/168] Reformat `binding.gyp` --- binding.gyp | 301 +++++++++++++++++++++++++--------------------------- 1 file changed, 143 insertions(+), 158 deletions(-) diff --git a/binding.gyp b/binding.gyp index 59efcaf..6b40361 100644 --- a/binding.gyp +++ b/binding.gyp @@ -1,162 +1,147 @@ { - "targets": [ - { - "target_name": "efsw", - "type": "static_library", - "sources": [ - "./vendor/efsw/src/efsw/Debug.cpp", - "./vendor/efsw/src/efsw/DirWatcherGeneric.cpp", - "./vendor/efsw/src/efsw/DirectorySnapshot.cpp", - "./vendor/efsw/src/efsw/DirectorySnapshotDiff.cpp", - "./vendor/efsw/src/efsw/FileInfo.cpp", - "./vendor/efsw/src/efsw/FileSystem.cpp", - "./vendor/efsw/src/efsw/FileWatcher.cpp", - "./vendor/efsw/src/efsw/FileWatcherCWrapper.cpp", - "./vendor/efsw/src/efsw/FileWatcherFSEvents.cpp", - "./vendor/efsw/src/efsw/FileWatcherGeneric.cpp", - "./vendor/efsw/src/efsw/FileWatcherImpl.cpp", - "./vendor/efsw/src/efsw/FileWatcherInotify.cpp", - "./vendor/efsw/src/efsw/FileWatcherKqueue.cpp", - "./vendor/efsw/src/efsw/FileWatcherWin32.cpp", - "./vendor/efsw/src/efsw/Log.cpp", - "./vendor/efsw/src/efsw/Mutex.cpp", - "./vendor/efsw/src/efsw/String.cpp", - "./vendor/efsw/src/efsw/System.cpp", - "./vendor/efsw/src/efsw/Thread.cpp", - "./vendor/efsw/src/efsw/Watcher.cpp", - "./vendor/efsw/src/efsw/WatcherFSEvents.cpp", - "./vendor/efsw/src/efsw/WatcherGeneric.cpp", - "./vendor/efsw/src/efsw/WatcherInotify.cpp", - "./vendor/efsw/src/efsw/WatcherKqueue.cpp", - "./vendor/efsw/src/efsw/WatcherWin32.cpp" - ], - "include_dirs": [ - "./vendor/efsw/include", - "./vendor/efsw/src" - ], - "conditions": [ - ["OS==\"win\"", { - "sources!": [ - "./vendor/efsw/src/efsw/WatcherKqueue.cpp", - "./vendor/efsw/src/efsw/WatcherFSEvents.cpp", - "./vendor/efsw/src/efsw/WatcherInotify.cpp", - "./vendor/efsw/src/efsw/FileWatcherKqueue.cpp", - "./vendor/efsw/src/efsw/FileWatcherInotify.cpp", - "./vendor/efsw/src/efsw/FileWatcherFSEvents.cpp" - ], - "sources": [ - "./vendor/efsw/src/efsw/platform/win/FileSystemImpl.cpp", - "./vendor/efsw/src/efsw/platform/win/MutexImpl.cpp", - "./vendor/efsw/src/efsw/platform/win/SystemImpl.cpp", - "./vendor/efsw/src/efsw/platform/win/ThreadImpl.cpp" - ], - }], - ["OS!=\"win\"", { - "sources": [ - "./vendor/efsw/src/efsw/platform/posix/FileSystemImpl.cpp", - "./vendor/efsw/src/efsw/platform/posix/MutexImpl.cpp", - "./vendor/efsw/src/efsw/platform/posix/SystemImpl.cpp", - "./vendor/efsw/src/efsw/platform/posix/ThreadImpl.cpp" - ], - "cflags": ["-Wall", "-Wno-long-long"] - }], - ["OS==\"linux\"", { - "sources!": [ - "./vendor/efsw/src/efsw/WatcherKqueue.cpp", - "./vendor/efsw/src/efsw/WatcherFSEvents.cpp", - "./vendor/efsw/src/efsw/WatcherWin32.cpp", - "./vendor/efsw/src/efsw/FileWatcherKqueue.cpp", - "./vendor/efsw/src/efsw/FileWatcherWin32.cpp", - "./vendor/efsw/src/efsw/FileWatcherFSEvents.cpp" - ], - "libraries": [ - "-lpthread" - ], - "defines": [ - "EFSW_VERBOSE" - ] - }], - ["OS==\"mac\"", { - "sources!": [ - "./vendor/efsw/src/efsw/WatcherInotify.cpp", - "./vendor/efsw/src/efsw/WatcherWin32.cpp", - "./vendor/efsw/src/efsw/FileWatcherInotify.cpp", - "./vendor/efsw/src/efsw/FileWatcherWin32.cpp" - ], - "defines": [ - "EFSW_FSEVENTS_SUPPORTED" - ], - "xcode_settings": { - "OTHER_LDFLAGS": [ - "-framework CoreFoundation -framework CoreServices" - ] - } - }] + "targets": [ + { + "target_name": "efsw", + "type": "static_library", + "sources": [ + "./vendor/efsw/src/efsw/Debug.cpp", + "./vendor/efsw/src/efsw/DirWatcherGeneric.cpp", + "./vendor/efsw/src/efsw/DirectorySnapshot.cpp", + "./vendor/efsw/src/efsw/DirectorySnapshotDiff.cpp", + "./vendor/efsw/src/efsw/FileInfo.cpp", + "./vendor/efsw/src/efsw/FileSystem.cpp", + "./vendor/efsw/src/efsw/FileWatcher.cpp", + "./vendor/efsw/src/efsw/FileWatcherCWrapper.cpp", + "./vendor/efsw/src/efsw/FileWatcherFSEvents.cpp", + "./vendor/efsw/src/efsw/FileWatcherGeneric.cpp", + "./vendor/efsw/src/efsw/FileWatcherImpl.cpp", + "./vendor/efsw/src/efsw/FileWatcherInotify.cpp", + "./vendor/efsw/src/efsw/FileWatcherKqueue.cpp", + "./vendor/efsw/src/efsw/FileWatcherWin32.cpp", + "./vendor/efsw/src/efsw/Log.cpp", + "./vendor/efsw/src/efsw/Mutex.cpp", + "./vendor/efsw/src/efsw/String.cpp", + "./vendor/efsw/src/efsw/System.cpp", + "./vendor/efsw/src/efsw/Thread.cpp", + "./vendor/efsw/src/efsw/Watcher.cpp", + "./vendor/efsw/src/efsw/WatcherFSEvents.cpp", + "./vendor/efsw/src/efsw/WatcherGeneric.cpp", + "./vendor/efsw/src/efsw/WatcherInotify.cpp", + "./vendor/efsw/src/efsw/WatcherKqueue.cpp", + "./vendor/efsw/src/efsw/WatcherWin32.cpp" + ], + "include_dirs": [ + "./vendor/efsw/include", + "./vendor/efsw/src" + ], + "conditions": [ + ["OS==\"win\"", { + "sources!": [ + "./vendor/efsw/src/efsw/WatcherKqueue.cpp", + "./vendor/efsw/src/efsw/WatcherFSEvents.cpp", + "./vendor/efsw/src/efsw/WatcherInotify.cpp", + "./vendor/efsw/src/efsw/FileWatcherKqueue.cpp", + "./vendor/efsw/src/efsw/FileWatcherInotify.cpp", + "./vendor/efsw/src/efsw/FileWatcherFSEvents.cpp" + ], + "sources": [ + "./vendor/efsw/src/efsw/platform/win/FileSystemImpl.cpp", + "./vendor/efsw/src/efsw/platform/win/MutexImpl.cpp", + "./vendor/efsw/src/efsw/platform/win/SystemImpl.cpp", + "./vendor/efsw/src/efsw/platform/win/ThreadImpl.cpp" + ], + }], + ["OS!=\"win\"", { + "sources": [ + "./vendor/efsw/src/efsw/platform/posix/FileSystemImpl.cpp", + "./vendor/efsw/src/efsw/platform/posix/MutexImpl.cpp", + "./vendor/efsw/src/efsw/platform/posix/SystemImpl.cpp", + "./vendor/efsw/src/efsw/platform/posix/ThreadImpl.cpp" + ], + "cflags": ["-Wall", "-Wno-long-long"] + }], + ["OS==\"linux\"", { + "sources!": [ + "./vendor/efsw/src/efsw/WatcherKqueue.cpp", + "./vendor/efsw/src/efsw/WatcherFSEvents.cpp", + "./vendor/efsw/src/efsw/WatcherWin32.cpp", + "./vendor/efsw/src/efsw/FileWatcherKqueue.cpp", + "./vendor/efsw/src/efsw/FileWatcherWin32.cpp", + "./vendor/efsw/src/efsw/FileWatcherFSEvents.cpp" + ], + "libraries": [ + "-lpthread" + ], + "defines": [ + "EFSW_VERBOSE" + ] + }], + ["OS==\"mac\"", { + "sources!": [ + "./vendor/efsw/src/efsw/WatcherInotify.cpp", + "./vendor/efsw/src/efsw/WatcherWin32.cpp", + "./vendor/efsw/src/efsw/FileWatcherInotify.cpp", + "./vendor/efsw/src/efsw/FileWatcherWin32.cpp" + ], + "defines": [ + "EFSW_FSEVENTS_SUPPORTED" + ], + "xcode_settings": { + "OTHER_LDFLAGS": [ + "-framework CoreFoundation -framework CoreServices" ] - }, - { - "target_name": "pathwatcher", - "dependencies": ["efsw"], - "cflags!": ["-fno-exceptions"], - "cflags_cc!": ["-fno-exceptions"], - "xcode_settings": { - "GCC_ENABLE_CPP_EXCEPTIONS": "YES", - "CLANG_CXX_LIBRARY": "libc++", - "MACOSX_DEPLOYMENT_TARGET": "10.7", + } + }] + ] + }, + { + "target_name": "pathwatcher", + "dependencies": ["efsw"], + "cflags!": ["-fno-exceptions"], + "cflags_cc!": ["-fno-exceptions"], + "xcode_settings": { + "GCC_ENABLE_CPP_EXCEPTIONS": "YES", + "CLANG_CXX_LIBRARY": "libc++", + "MACOSX_DEPLOYMENT_TARGET": "10.7", + }, + "msvs_settings": { + "VCCLCompilerTool": {"ExceptionHandling": 1}, + }, + "sources": [ + "lib/binding.cc", + "lib/core.cc", + "lib/core.h" + ], + "include_dirs": [ + " Date: Sun, 13 Oct 2024 16:40:43 -0700 Subject: [PATCH 081/168] Remove unnecessary files --- lib/directory.js | 379 --------------------- lib/file.js | 518 ---------------------------- lib/main.js | 266 --------------- src/addon-data.h | 28 -- src/common.cc | 173 ---------- src/common.h | 130 ------- src/main.cc | 22 -- src/pathwatcher_linux.cc | 114 ------- src/pathwatcher_unix.cc | 128 ------- src/pathwatcher_win.cc | 720 --------------------------------------- 10 files changed, 2478 deletions(-) delete mode 100644 lib/directory.js delete mode 100644 lib/file.js delete mode 100644 lib/main.js delete mode 100644 src/addon-data.h delete mode 100644 src/common.cc delete mode 100644 src/common.h delete mode 100644 src/main.cc delete mode 100644 src/pathwatcher_linux.cc delete mode 100644 src/pathwatcher_unix.cc delete mode 100644 src/pathwatcher_win.cc diff --git a/lib/directory.js b/lib/directory.js deleted file mode 100644 index 2e6f8ec..0000000 --- a/lib/directory.js +++ /dev/null @@ -1,379 +0,0 @@ -(function() { - var Directory, Disposable, Emitter, EmitterMixin, File, Grim, PathWatcher, async, fs, path, _ref, - __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, - __slice = [].slice; - - path = require('path'); - - async = require('async'); - - _ref = require('event-kit'), Emitter = _ref.Emitter, Disposable = _ref.Disposable; - - fs = require('fs-plus'); - - Grim = require('grim'); - - File = require('./file'); - - PathWatcher = require('./main'); - - module.exports = Directory = (function() { - Directory.prototype.realPath = null; - - Directory.prototype.subscriptionCount = 0; - - - /* - Section: Construction - */ - - function Directory(directoryPath, symlink, includeDeprecatedAPIs) { - this.symlink = symlink != null ? symlink : false; - if (includeDeprecatedAPIs == null) { - includeDeprecatedAPIs = Grim.includeDeprecatedAPIs; - } - this.didRemoveSubscription = __bind(this.didRemoveSubscription, this); - this.willAddSubscription = __bind(this.willAddSubscription, this); - this.emitter = new Emitter; - if (includeDeprecatedAPIs) { - this.on('contents-changed-subscription-will-be-added', this.willAddSubscription); - this.on('contents-changed-subscription-removed', this.didRemoveSubscription); - } - if (directoryPath) { - directoryPath = path.normalize(directoryPath); - if (directoryPath.length > 1 && directoryPath[directoryPath.length - 1] === path.sep) { - directoryPath = directoryPath.substring(0, directoryPath.length - 1); - } - } - this.path = directoryPath; - if (fs.isCaseInsensitive()) { - this.lowerCasePath = this.path.toLowerCase(); - } - if (Grim.includeDeprecatedAPIs) { - this.reportOnDeprecations = true; - } - } - - Directory.prototype.create = function(mode) { - if (mode == null) { - mode = 0x1ff; - } - return this.exists().then((function(_this) { - return function(isExistingDirectory) { - if (isExistingDirectory) { - return false; - } - if (_this.isRoot()) { - throw Error("Root directory does not exist: " + (_this.getPath())); - } - return _this.getParent().create().then(function() { - return new Promise(function(resolve, reject) { - return fs.mkdir(_this.getPath(), mode, function(error) { - if (error) { - return reject(error); - } else { - return resolve(true); - } - }); - }); - }); - }; - })(this)); - }; - - - /* - Section: Event Subscription - */ - - Directory.prototype.onDidChange = function(callback) { - this.willAddSubscription(); - return this.trackUnsubscription(this.emitter.on('did-change', callback)); - }; - - Directory.prototype.willAddSubscription = function() { - if (this.subscriptionCount === 0) { - this.subscribeToNativeChangeEvents(); - } - return this.subscriptionCount++; - }; - - Directory.prototype.didRemoveSubscription = function() { - this.subscriptionCount--; - if (this.subscriptionCount === 0) { - return this.unsubscribeFromNativeChangeEvents(); - } - }; - - Directory.prototype.trackUnsubscription = function(subscription) { - return new Disposable((function(_this) { - return function() { - subscription.dispose(); - return _this.didRemoveSubscription(); - }; - })(this)); - }; - - - /* - Section: Directory Metadata - */ - - Directory.prototype.isFile = function() { - return false; - }; - - Directory.prototype.isDirectory = function() { - return true; - }; - - Directory.prototype.isSymbolicLink = function() { - return this.symlink; - }; - - Directory.prototype.exists = function() { - return new Promise((function(_this) { - return function(resolve) { - return fs.exists(_this.getPath(), resolve); - }; - })(this)); - }; - - Directory.prototype.existsSync = function() { - return fs.existsSync(this.getPath()); - }; - - Directory.prototype.isRoot = function() { - return this.getParent().getRealPathSync() === this.getRealPathSync(); - }; - - - /* - Section: Managing Paths - */ - - Directory.prototype.getPath = function() { - return this.path; - }; - - Directory.prototype.getRealPathSync = function() { - var e; - if (this.realPath == null) { - try { - this.realPath = fs.realpathSync(this.path); - if (fs.isCaseInsensitive()) { - this.lowerCaseRealPath = this.realPath.toLowerCase(); - } - } catch (_error) { - e = _error; - this.realPath = this.path; - if (fs.isCaseInsensitive()) { - this.lowerCaseRealPath = this.lowerCasePath; - } - } - } - return this.realPath; - }; - - Directory.prototype.getBaseName = function() { - return path.basename(this.path); - }; - - Directory.prototype.relativize = function(fullPath) { - var directoryPath, pathToCheck; - if (!fullPath) { - return fullPath; - } - if (process.platform === 'win32') { - fullPath = fullPath.replace(/\//g, '\\'); - } - if (fs.isCaseInsensitive()) { - pathToCheck = fullPath.toLowerCase(); - directoryPath = this.lowerCasePath; - } else { - pathToCheck = fullPath; - directoryPath = this.path; - } - if (pathToCheck === directoryPath) { - return ''; - } else if (this.isPathPrefixOf(directoryPath, pathToCheck)) { - return fullPath.substring(directoryPath.length + 1); - } - this.getRealPathSync(); - if (fs.isCaseInsensitive()) { - directoryPath = this.lowerCaseRealPath; - } else { - directoryPath = this.realPath; - } - if (pathToCheck === directoryPath) { - return ''; - } else if (this.isPathPrefixOf(directoryPath, pathToCheck)) { - return fullPath.substring(directoryPath.length + 1); - } else { - return fullPath; - } - }; - - Directory.prototype.resolve = function(relativePath) { - if (!relativePath) { - return; - } - if (relativePath != null ? relativePath.match(/[A-Za-z0-9+-.]+:\/\//) : void 0) { - return relativePath; - } else if (fs.isAbsolute(relativePath)) { - return path.normalize(fs.resolveHome(relativePath)); - } else { - return path.normalize(fs.resolveHome(path.join(this.getPath(), relativePath))); - } - }; - - - /* - Section: Traversing - */ - - Directory.prototype.getParent = function() { - return new Directory(path.join(this.path, '..')); - }; - - Directory.prototype.getFile = function() { - var filename; - filename = 1 <= arguments.length ? __slice.call(arguments, 0) : []; - return new File(path.join.apply(path, [this.getPath()].concat(__slice.call(filename)))); - }; - - Directory.prototype.getSubdirectory = function() { - var dirname; - dirname = 1 <= arguments.length ? __slice.call(arguments, 0) : []; - return new Directory(path.join.apply(path, [this.path].concat(__slice.call(dirname)))); - }; - - Directory.prototype.getEntriesSync = function() { - var directories, entryPath, files, stat, symlink, _i, _len, _ref1; - directories = []; - files = []; - _ref1 = fs.listSync(this.path); - for (_i = 0, _len = _ref1.length; _i < _len; _i++) { - entryPath = _ref1[_i]; - try { - stat = fs.lstatSync(entryPath); - symlink = stat.isSymbolicLink(); - if (symlink) { - stat = fs.statSync(entryPath); - } - } catch (_error) {} - if (stat != null ? stat.isDirectory() : void 0) { - directories.push(new Directory(entryPath, symlink)); - } else if (stat != null ? stat.isFile() : void 0) { - files.push(new File(entryPath, symlink)); - } - } - return directories.concat(files); - }; - - Directory.prototype.getEntries = function(callback) { - return fs.list(this.path, function(error, entries) { - var addEntry, directories, files, statEntry; - if (error != null) { - return callback(error); - } - directories = []; - files = []; - addEntry = function(entryPath, stat, symlink, callback) { - if (stat != null ? stat.isDirectory() : void 0) { - directories.push(new Directory(entryPath, symlink)); - } else if (stat != null ? stat.isFile() : void 0) { - files.push(new File(entryPath, symlink)); - } - return callback(); - }; - statEntry = function(entryPath, callback) { - return fs.lstat(entryPath, function(error, stat) { - if (stat != null ? stat.isSymbolicLink() : void 0) { - return fs.stat(entryPath, function(error, stat) { - return addEntry(entryPath, stat, true, callback); - }); - } else { - return addEntry(entryPath, stat, false, callback); - } - }); - }; - return async.eachLimit(entries, 1, statEntry, function() { - return callback(null, directories.concat(files)); - }); - }); - }; - - Directory.prototype.contains = function(pathToCheck) { - var directoryPath; - if (!pathToCheck) { - return false; - } - if (process.platform === 'win32') { - pathToCheck = pathToCheck.replace(/\//g, '\\'); - } - if (fs.isCaseInsensitive()) { - directoryPath = this.lowerCasePath; - pathToCheck = pathToCheck.toLowerCase(); - } else { - directoryPath = this.path; - } - if (this.isPathPrefixOf(directoryPath, pathToCheck)) { - return true; - } - this.getRealPathSync(); - if (fs.isCaseInsensitive()) { - directoryPath = this.lowerCaseRealPath; - } else { - directoryPath = this.realPath; - } - return this.isPathPrefixOf(directoryPath, pathToCheck); - }; - - - /* - Section: Private - */ - - Directory.prototype.subscribeToNativeChangeEvents = function() { - return this.watchSubscription != null ? this.watchSubscription : this.watchSubscription = PathWatcher.watch(this.path, (function(_this) { - return function(eventType) { - if (eventType === 'change') { - if (Grim.includeDeprecatedAPIs) { - _this.emit('contents-changed'); - } - return _this.emitter.emit('did-change'); - } - }; - })(this)); - }; - - Directory.prototype.unsubscribeFromNativeChangeEvents = function() { - if (this.watchSubscription != null) { - this.watchSubscription.close(); - return this.watchSubscription = null; - } - }; - - Directory.prototype.isPathPrefixOf = function(prefix, fullPath) { - return fullPath.indexOf(prefix) === 0 && fullPath[prefix.length] === path.sep; - }; - - return Directory; - - })(); - - if (Grim.includeDeprecatedAPIs) { - EmitterMixin = require('emissary').Emitter; - EmitterMixin.includeInto(Directory); - Directory.prototype.on = function(eventName) { - if (eventName === 'contents-changed') { - Grim.deprecate("Use Directory::onDidChange instead"); - } else if (this.reportOnDeprecations) { - Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead."); - } - return EmitterMixin.prototype.on.apply(this, arguments); - }; - } - -}).call(this); diff --git a/lib/file.js b/lib/file.js deleted file mode 100644 index 37243d1..0000000 --- a/lib/file.js +++ /dev/null @@ -1,518 +0,0 @@ -(function() { - var Directory, Disposable, Emitter, EmitterMixin, File, Grim, PathWatcher, crypto, fs, iconv, path, _, _ref, - __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, - __slice = [].slice; - - crypto = require('crypto'); - - path = require('path'); - - _ = require('underscore-plus'); - - _ref = require('event-kit'), Emitter = _ref.Emitter, Disposable = _ref.Disposable; - - fs = require('fs-plus'); - - Grim = require('grim'); - - iconv = null; - - Directory = null; - - PathWatcher = require('./main'); - - module.exports = File = (function() { - File.prototype.encoding = 'utf8'; - - File.prototype.realPath = null; - - File.prototype.subscriptionCount = 0; - - - /* - Section: Construction - */ - - function File(filePath, symlink, includeDeprecatedAPIs) { - this.symlink = symlink != null ? symlink : false; - if (includeDeprecatedAPIs == null) { - includeDeprecatedAPIs = Grim.includeDeprecatedAPIs; - } - this.didRemoveSubscription = __bind(this.didRemoveSubscription, this); - this.willAddSubscription = __bind(this.willAddSubscription, this); - if (filePath) { - filePath = path.normalize(filePath); - } - this.path = filePath; - this.emitter = new Emitter; - if (includeDeprecatedAPIs) { - this.on('contents-changed-subscription-will-be-added', this.willAddSubscription); - this.on('moved-subscription-will-be-added', this.willAddSubscription); - this.on('removed-subscription-will-be-added', this.willAddSubscription); - this.on('contents-changed-subscription-removed', this.didRemoveSubscription); - this.on('moved-subscription-removed', this.didRemoveSubscription); - this.on('removed-subscription-removed', this.didRemoveSubscription); - } - this.cachedContents = null; - this.reportOnDeprecations = true; - } - - File.prototype.create = function() { - return this.exists().then((function(_this) { - return function(isExistingFile) { - var parent; - if (!isExistingFile) { - parent = _this.getParent(); - return parent.create().then(function() { - return _this.write('').then(function() { - return true; - }); - }); - } else { - return false; - } - }; - })(this)); - }; - - - /* - Section: Event Subscription - */ - - File.prototype.onDidChange = function(callback) { - this.willAddSubscription(); - return this.trackUnsubscription(this.emitter.on('did-change', callback)); - }; - - File.prototype.onDidRename = function(callback) { - this.willAddSubscription(); - return this.trackUnsubscription(this.emitter.on('did-rename', callback)); - }; - - File.prototype.onDidDelete = function(callback) { - this.willAddSubscription(); - return this.trackUnsubscription(this.emitter.on('did-delete', callback)); - }; - - File.prototype.onWillThrowWatchError = function(callback) { - return this.emitter.on('will-throw-watch-error', callback); - }; - - File.prototype.willAddSubscription = function() { - this.subscriptionCount++; - try { - return this.subscribeToNativeChangeEvents(); - } catch (_error) {} - }; - - File.prototype.didRemoveSubscription = function() { - this.subscriptionCount--; - if (this.subscriptionCount === 0) { - return this.unsubscribeFromNativeChangeEvents(); - } - }; - - File.prototype.trackUnsubscription = function(subscription) { - return new Disposable((function(_this) { - return function() { - subscription.dispose(); - return _this.didRemoveSubscription(); - }; - })(this)); - }; - - - /* - Section: File Metadata - */ - - File.prototype.isFile = function() { - return true; - }; - - File.prototype.isDirectory = function() { - return false; - }; - - File.prototype.isSymbolicLink = function() { - return this.symlink; - }; - - File.prototype.exists = function() { - return new Promise((function(_this) { - return function(resolve) { - return fs.exists(_this.getPath(), resolve); - }; - })(this)); - }; - - File.prototype.existsSync = function() { - return fs.existsSync(this.getPath()); - }; - - File.prototype.getDigest = function() { - if (this.digest != null) { - return Promise.resolve(this.digest); - } else { - return this.read().then((function(_this) { - return function() { - return _this.digest; - }; - })(this)); - } - }; - - File.prototype.getDigestSync = function() { - if (!this.digest) { - this.readSync(); - } - return this.digest; - }; - - File.prototype.setDigest = function(contents) { - return this.digest = crypto.createHash('sha1').update(contents != null ? contents : '').digest('hex'); - }; - - File.prototype.setEncoding = function(encoding) { - if (encoding == null) { - encoding = 'utf8'; - } - if (encoding !== 'utf8') { - if (iconv == null) { - iconv = require('iconv-lite'); - } - iconv.getCodec(encoding); - } - return this.encoding = encoding; - }; - - File.prototype.getEncoding = function() { - return this.encoding; - }; - - - /* - Section: Managing Paths - */ - - File.prototype.getPath = function() { - return this.path; - }; - - File.prototype.setPath = function(path) { - this.path = path; - return this.realPath = null; - }; - - File.prototype.getRealPathSync = function() { - var error; - if (this.realPath == null) { - try { - this.realPath = fs.realpathSync(this.path); - } catch (_error) { - error = _error; - this.realPath = this.path; - } - } - return this.realPath; - }; - - File.prototype.getRealPath = function() { - if (this.realPath != null) { - return Promise.resolve(this.realPath); - } else { - return new Promise((function(_this) { - return function(resolve, reject) { - return fs.realpath(_this.path, function(err, result) { - if (err != null) { - return reject(err); - } else { - return resolve(_this.realPath = result); - } - }); - }; - })(this)); - } - }; - - File.prototype.getBaseName = function() { - return path.basename(this.path); - }; - - - /* - Section: Traversing - */ - - File.prototype.getParent = function() { - if (Directory == null) { - Directory = require('./directory'); - } - return new Directory(path.dirname(this.path)); - }; - - - /* - Section: Reading and Writing - */ - - File.prototype.readSync = function(flushCache) { - var encoding; - if (!this.existsSync()) { - this.cachedContents = null; - } else if ((this.cachedContents == null) || flushCache) { - encoding = this.getEncoding(); - if (encoding === 'utf8') { - this.cachedContents = fs.readFileSync(this.getPath(), encoding); - } else { - if (iconv == null) { - iconv = require('iconv-lite'); - } - this.cachedContents = iconv.decode(fs.readFileSync(this.getPath()), encoding); - } - } - this.setDigest(this.cachedContents); - return this.cachedContents; - }; - - File.prototype.writeFileSync = function(filePath, contents) { - var encoding; - encoding = this.getEncoding(); - if (encoding === 'utf8') { - return fs.writeFileSync(filePath, contents, { - encoding: encoding - }); - } else { - if (iconv == null) { - iconv = require('iconv-lite'); - } - return fs.writeFileSync(filePath, iconv.encode(contents, encoding)); - } - }; - - File.prototype.read = function(flushCache) { - var promise; - if ((this.cachedContents != null) && !flushCache) { - promise = Promise.resolve(this.cachedContents); - } else { - promise = new Promise((function(_this) { - return function(resolve, reject) { - var content, readStream; - content = []; - readStream = _this.createReadStream(); - readStream.on('data', function(chunk) { - return content.push(chunk); - }); - readStream.on('end', function() { - return resolve(content.join('')); - }); - return readStream.on('error', function(error) { - if (error.code === 'ENOENT') { - return resolve(null); - } else { - return reject(error); - } - }); - }; - })(this)); - } - return promise.then((function(_this) { - return function(contents) { - _this.setDigest(contents); - return _this.cachedContents = contents; - }; - })(this)); - }; - - File.prototype.createReadStream = function() { - var encoding; - encoding = this.getEncoding(); - if (encoding === 'utf8') { - return fs.createReadStream(this.getPath(), { - encoding: encoding - }); - } else { - if (iconv == null) { - iconv = require('iconv-lite'); - } - return fs.createReadStream(this.getPath()).pipe(iconv.decodeStream(encoding)); - } - }; - - File.prototype.write = function(text) { - return this.exists().then((function(_this) { - return function(previouslyExisted) { - return _this.writeFile(_this.getPath(), text).then(function() { - _this.cachedContents = text; - _this.setDigest(text); - if (!previouslyExisted && _this.hasSubscriptions()) { - _this.subscribeToNativeChangeEvents(); - } - return void 0; - }); - }; - })(this)); - }; - - File.prototype.createWriteStream = function() { - var encoding, stream; - encoding = this.getEncoding(); - if (encoding === 'utf8') { - return fs.createWriteStream(this.getPath(), { - encoding: encoding - }); - } else { - if (iconv == null) { - iconv = require('iconv-lite'); - } - stream = iconv.encodeStream(encoding); - stream.pipe(fs.createWriteStream(this.getPath())); - return stream; - } - }; - - File.prototype.writeSync = function(text) { - var previouslyExisted; - previouslyExisted = this.existsSync(); - this.writeFileSync(this.getPath(), text); - this.cachedContents = text; - this.setDigest(text); - if (Grim.includeDeprecatedAPIs) { - this.emit('contents-changed'); - } - this.emitter.emit('did-change'); - if (!previouslyExisted && this.hasSubscriptions()) { - this.subscribeToNativeChangeEvents(); - } - return void 0; - }; - - File.prototype.writeFile = function(filePath, contents) { - var encoding; - encoding = this.getEncoding(); - if (encoding === 'utf8') { - return new Promise(function(resolve, reject) { - return fs.writeFile(filePath, contents, { - encoding: encoding - }, function(err, result) { - if (err != null) { - return reject(err); - } else { - return resolve(result); - } - }); - }); - } else { - if (iconv == null) { - iconv = require('iconv-lite'); - } - return new Promise(function(resolve, reject) { - return fs.writeFile(filePath, iconv.encode(contents, encoding), function(err, result) { - if (err != null) { - return reject(err); - } else { - return resolve(result); - } - }); - }); - } - }; - - - /* - Section: Private - */ - - File.prototype.handleNativeChangeEvent = function(eventType, eventPath) { - switch (eventType) { - case 'delete': - this.unsubscribeFromNativeChangeEvents(); - return this.detectResurrectionAfterDelay(); - case 'rename': - this.setPath(eventPath); - if (Grim.includeDeprecatedAPIs) { - this.emit('moved'); - } - return this.emitter.emit('did-rename'); - case 'change': - case 'resurrect': - this.cachedContents = null; - return this.emitter.emit('did-change'); - } - }; - - File.prototype.detectResurrectionAfterDelay = function() { - return _.delay(((function(_this) { - return function() { - return _this.detectResurrection(); - }; - })(this)), 50); - }; - - File.prototype.detectResurrection = function() { - return this.exists().then((function(_this) { - return function(exists) { - if (exists) { - _this.subscribeToNativeChangeEvents(); - return _this.handleNativeChangeEvent('resurrect'); - } else { - _this.cachedContents = null; - if (Grim.includeDeprecatedAPIs) { - _this.emit('removed'); - } - return _this.emitter.emit('did-delete'); - } - }; - })(this)); - }; - - File.prototype.subscribeToNativeChangeEvents = function() { - return this.watchSubscription != null ? this.watchSubscription : this.watchSubscription = PathWatcher.watch(this.path, (function(_this) { - return function() { - var args; - args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; - return _this.handleNativeChangeEvent.apply(_this, args); - }; - })(this)); - }; - - File.prototype.unsubscribeFromNativeChangeEvents = function() { - if (this.watchSubscription != null) { - this.watchSubscription.close(); - return this.watchSubscription = null; - } - }; - - return File; - - })(); - - if (Grim.includeDeprecatedAPIs) { - EmitterMixin = require('emissary').Emitter; - EmitterMixin.includeInto(File); - File.prototype.on = function(eventName) { - switch (eventName) { - case 'contents-changed': - Grim.deprecate("Use File::onDidChange instead"); - break; - case 'moved': - Grim.deprecate("Use File::onDidRename instead"); - break; - case 'removed': - Grim.deprecate("Use File::onDidDelete instead"); - break; - default: - if (this.reportOnDeprecations) { - Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead."); - } - } - return EmitterMixin.prototype.on.apply(this, arguments); - }; - } else { - File.prototype.hasSubscriptions = function() { - return this.subscriptionCount > 0; - }; - } - -}).call(this); diff --git a/lib/main.js b/lib/main.js deleted file mode 100644 index 9090027..0000000 --- a/lib/main.js +++ /dev/null @@ -1,266 +0,0 @@ -(function() { - var Emitter, HandleMap, HandleWatcher, PathWatcher, binding, fs, handleWatchers, path; - - binding = require('../build/Release/pathwatcher.node'); - - HandleMap = binding.HandleMap; - - Emitter = require('event-kit').Emitter; - - fs = require('fs'); - - path = require('path'); - - handleWatchers = null; - - HandleWatcher = (function() { - function HandleWatcher(path) { - this.path = path; - this.emitter = new Emitter(); - this.start(); - } - - HandleWatcher.prototype.onEvent = function(event, filePath, oldFilePath) { - var detectRename; - if (filePath) { - filePath = path.normalize(filePath); - } - if (oldFilePath) { - oldFilePath = path.normalize(oldFilePath); - } - switch (event) { - case 'rename': - this.close(); - detectRename = (function(_this) { - return function() { - return fs.stat(_this.path, function(err) { - if (err) { - _this.path = filePath; - if (process.platform === 'darwin' && /\/\.Trash\//.test(filePath)) { - _this.emitter.emit('did-change', { - event: 'delete', - newFilePath: null - }); - return _this.close(); - } else { - _this.start(); - return _this.emitter.emit('did-change', { - event: 'rename', - newFilePath: filePath - }); - } - } else { - _this.start(); - return _this.emitter.emit('did-change', { - event: 'change', - newFilePath: null - }); - } - }); - }; - })(this); - return setTimeout(detectRename, 100); - case 'delete': - this.emitter.emit('did-change', { - event: 'delete', - newFilePath: null - }); - return this.close(); - case 'unknown': - throw new Error("Received unknown event for path: " + this.path); - break; - default: - return this.emitter.emit('did-change', { - event: event, - newFilePath: filePath, - oldFilePath: oldFilePath - }); - } - }; - - HandleWatcher.prototype.onDidChange = function(callback) { - return this.emitter.on('did-change', callback); - }; - - HandleWatcher.prototype.start = function() { - var troubleWatcher; - this.handle = binding.watch(this.path); - if (handleWatchers.has(this.handle)) { - troubleWatcher = handleWatchers.get(this.handle); - troubleWatcher.close(); - console.error("The handle(" + this.handle + ") returned by watching " + this.path + " is the same with an already watched path(" + troubleWatcher.path + ")"); - } - return handleWatchers.add(this.handle, this); - }; - - HandleWatcher.prototype.closeIfNoListener = function() { - if (this.emitter.getTotalListenerCount() === 0) { - return this.close(); - } - }; - - HandleWatcher.prototype.close = function() { - if (handleWatchers.has(this.handle)) { - binding.unwatch(this.handle); - return handleWatchers.remove(this.handle); - } - }; - - return HandleWatcher; - - })(); - - PathWatcher = (function() { - PathWatcher.prototype.isWatchingParent = false; - - PathWatcher.prototype.path = null; - - PathWatcher.prototype.handleWatcher = null; - - function PathWatcher(filePath, callback) { - var stats, watcher, _i, _len, _ref; - this.path = filePath; - this.emitter = new Emitter(); - if (process.platform === 'win32') { - stats = fs.statSync(filePath); - this.isWatchingParent = !stats.isDirectory(); - } - if (this.isWatchingParent) { - filePath = path.dirname(filePath); - } - _ref = handleWatchers.values(); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - watcher = _ref[_i]; - if (watcher.path === filePath) { - this.handleWatcher = watcher; - break; - } - } - if (this.handleWatcher == null) { - this.handleWatcher = new HandleWatcher(filePath); - } - this.onChange = (function(_this) { - return function(_arg) { - var event, newFilePath, oldFilePath; - event = _arg.event, newFilePath = _arg.newFilePath, oldFilePath = _arg.oldFilePath; - switch (event) { - case 'rename': - case 'change': - case 'delete': - if (event === 'rename') { - _this.path = newFilePath; - } - if (typeof callback === 'function') { - callback.call(_this, event, newFilePath); - } - return _this.emitter.emit('did-change', { - event: event, - newFilePath: newFilePath - }); - case 'child-rename': - if (_this.isWatchingParent) { - if (_this.path === oldFilePath) { - return _this.onChange({ - event: 'rename', - newFilePath: newFilePath - }); - } - } else { - return _this.onChange({ - event: 'change', - newFilePath: '' - }); - } - break; - case 'child-delete': - if (_this.isWatchingParent) { - if (_this.path === newFilePath) { - return _this.onChange({ - event: 'delete', - newFilePath: null - }); - } - } else { - return _this.onChange({ - event: 'change', - newFilePath: '' - }); - } - break; - case 'child-change': - if (_this.isWatchingParent && _this.path === newFilePath) { - return _this.onChange({ - event: 'change', - newFilePath: '' - }); - } - break; - case 'child-create': - if (!_this.isWatchingParent) { - return _this.onChange({ - event: 'change', - newFilePath: '' - }); - } - } - }; - })(this); - this.disposable = this.handleWatcher.onDidChange(this.onChange); - } - - PathWatcher.prototype.onDidChange = function(callback) { - return this.emitter.on('did-change', callback); - }; - - PathWatcher.prototype.close = function() { - this.emitter.dispose(); - this.disposable.dispose(); - return this.handleWatcher.closeIfNoListener(); - }; - - return PathWatcher; - - })(); - - exports.watch = function(pathToWatch, callback) { - if (handleWatchers == null) { - handleWatchers = new HandleMap; - binding.setCallback(function(event, handle, filePath, oldFilePath) { - if (handleWatchers.has(handle)) { - return handleWatchers.get(handle).onEvent(event, filePath, oldFilePath); - } - }); - } - return new PathWatcher(path.resolve(pathToWatch), callback); - }; - - exports.closeAllWatchers = function() { - var watcher, _i, _len, _ref; - if (handleWatchers != null) { - _ref = handleWatchers.values(); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - watcher = _ref[_i]; - watcher.close(); - } - return handleWatchers.clear(); - } - }; - - exports.getWatchedPaths = function() { - var paths, watcher, _i, _len, _ref; - paths = []; - if (handleWatchers != null) { - _ref = handleWatchers.values(); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - watcher = _ref[_i]; - paths.push(watcher.path); - } - } - return paths; - }; - - exports.File = require('./file'); - - exports.Directory = require('./directory'); - -}).call(this); diff --git a/src/addon-data.h b/src/addon-data.h deleted file mode 100644 index df0dc5c..0000000 --- a/src/addon-data.h +++ /dev/null @@ -1,28 +0,0 @@ -#include "common.h" -#pragma once - -static int g_next_addon_data_id = 1; - -class AddonData final { -public: - explicit AddonData(Napi::Env env) { - id = g_next_addon_data_id++; - } - - Napi::FunctionReference callback; - PathWatcherWorker* worker; - int watch_count; - int id; - -#ifdef __APPLE__ - // macOS. - int kqueue; - int init_errno; -#endif - -#ifdef __linux__ - // Linux. - int inotify; - int init_errno; -#endif -}; diff --git a/src/common.cc b/src/common.cc deleted file mode 100644 index 37ab226..0000000 --- a/src/common.cc +++ /dev/null @@ -1,173 +0,0 @@ -#include "common.h" -#include "addon-data.h" -#include "uv.h" - -#include - -using namespace Napi; - -void CommonInit(Napi::Env env) { - auto addonData = env.GetInstanceData(); - addonData->watch_count = 0; -} - -PathWatcherWorker::PathWatcherWorker(Napi::Env env, Function &progressCallback) : - AsyncProgressQueueWorker(env), _env(env) { - shouldStop = false; - this->progressCallback.Reset(progressCallback); -} - -void PathWatcherWorker::Execute( - const PathWatcherWorker::ExecutionProgress& progress -) { - PlatformThread(progress, shouldStop, _env); -} - -void PathWatcherWorker::Stop() { - Napi::Env env = Env(); - auto addonData = env.GetInstanceData(); - std::cout << "PathWatcherWorker::Stop for ID: " << addonData->id << std::endl; - - shouldStop = true; -} - -const char* PathWatcherWorker::GetEventTypeString(EVENT_TYPE type) { - switch (type) { - case EVENT_CHANGE: return "change"; - case EVENT_DELETE: return "delete"; - case EVENT_RENAME: return "rename"; - case EVENT_CHILD_CREATE: return "child-create"; - case EVENT_CHILD_CHANGE: return "child-change"; - case EVENT_CHILD_DELETE: return "child-delete"; - case EVENT_CHILD_RENAME: return "child-rename"; - default: return "unknown"; - } -} - -void PathWatcherWorker::OnProgress(const PathWatcherEvent* data, size_t) { - Napi::Env env = Env(); - HandleScope scope(env); - auto addonData = env.GetInstanceData(); - std::cout << "OnProgress reporting event for environment with ID: " << addonData->id << std::endl; - if (this->progressCallback.IsEmpty()) return; - std::string newPath(data->new_path.begin(), data->new_path.end()); - std::string oldPath(data->old_path.begin(), data->old_path.end()); - - this->progressCallback.Call({ - Napi::String::New(Env(), GetEventTypeString(data->type)), - WatcherHandleToV8Value(data->handle, Env()), - Napi::String::New(Env(), newPath), - Napi::String::New(Env(), oldPath) - }); -} - -void PathWatcherWorker::OnOK() {} - -// Called when the first watcher is created. -void Start(Napi::Env env) { - // std::cout << "Start" << std::endl; - Napi::HandleScope scope(env); - auto addonData = env.GetInstanceData(); - if (!addonData->callback) { - return; - } - - Napi::Function fn = addonData->callback.Value(); - - addonData->worker = new PathWatcherWorker(env, fn); - addonData->worker->Queue(); -} - -// Called when the last watcher is stopped. -void Stop(Napi::Env env) { - auto addonData = env.GetInstanceData(); - std::cout << "Stop for ID: " << addonData->id << std::endl; - PlatformStop(env); - std::cout << "PlatformStop exited for ID: " << addonData->id << std::endl; - if (addonData->worker) { - addonData->worker->Stop(); - } -} - -Napi::Value SetCallback(const Napi::CallbackInfo& info) { - // std::cout << "SetCallback" << std::endl; - auto env = info.Env(); - Napi::HandleScope scope(env); - - if (!info[0].IsFunction()) { - Napi::TypeError::New(env, "Function required").ThrowAsJavaScriptException(); - return env.Null(); - } - - auto addonData = env.GetInstanceData(); - if (addonData->worker) { - // std::cout << "Worker already exists" << std::endl; - } - addonData->callback.Reset(info[0].As(), 1); - - return env.Undefined(); -} - -Napi::Value Watch(const Napi::CallbackInfo& info) { - // std::cout << "Watch" << std::endl; - auto env = info.Env(); - auto addonData = env.GetInstanceData(); - Napi::HandleScope scope(env); - - if (!info[0].IsString()) { - Napi::TypeError::New(env, "String required").ThrowAsJavaScriptException(); - return env.Null(); - } - - Napi::String path = info[0].ToString(); - std::string cppPath(path); - WatcherHandle handle = PlatformWatch(cppPath.c_str(), env); - - if (!PlatformIsHandleValid(handle)) { - int error_number = PlatformInvalidHandleToErrorNumber(handle); - Napi::Error err = Napi::Error::New(env, "Unable to watch path"); - - if (error_number != 0) { - err.Set("errno", Napi::Number::New(env, error_number)); - err.Set( - "code", - Napi::String::New(env, uv_err_name(-error_number)) - ); - } - err.ThrowAsJavaScriptException(); - return env.Undefined(); - } - - if (addonData->watch_count++ == 0) - Start(env); - - return WatcherHandleToV8Value(handle, info.Env()); -} - -Napi::Value Unwatch(const Napi::CallbackInfo& info) { - // std::cout << "Unwatch" << std::endl; - auto env = info.Env(); - auto addonData = env.GetInstanceData(); - Napi::HandleScope scope(env); - - if (!IsV8ValueWatcherHandle(info[0])) { - Napi::TypeError::New( - env, - "Local type required" - ).ThrowAsJavaScriptException(); - return env.Null(); - } - -#ifdef _WIN32 - Napi::Value num = info[0]; -#else - Napi::Number num = info[0].ToNumber(); -#endif - - PlatformUnwatch(V8ValueToWatcherHandle(num), env); - - if (--addonData->watch_count == 0) - Stop(env); - - return env.Undefined(); -} diff --git a/src/common.h b/src/common.h deleted file mode 100644 index 905af1d..0000000 --- a/src/common.h +++ /dev/null @@ -1,130 +0,0 @@ -#ifndef SRC_COMMON_H_ -#define SRC_COMMON_H_ - -#ifdef _WIN32 -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include -#include -#include -#endif - -#include - -#include "napi.h" -using namespace Napi; - -#ifdef _WIN32 -// Platform-dependent definition of HANDLE. -typedef HANDLE WatcherHandle; - -// Conversion between V8 value and WatcherHandle. -Napi::Value WatcherHandleToV8Value(WatcherHandle handle, Napi::Env env); -WatcherHandle V8ValueToWatcherHandle(Napi::Value value); -bool IsV8ValueWatcherHandle(Napi::Value value); -#else -// Correspoding definitions on OS X and Linux. -typedef int32_t WatcherHandle; -#define WatcherHandleToV8Value(h, e) Napi::Number::New(e, h) -#define V8ValueToWatcherHandle(v) v.Int32Value() -#define IsV8ValueWatcherHandle(v) v.IsNumber() -#endif - -void PlatformInit(Napi::Env env); -WatcherHandle PlatformWatch(const char* path, Napi::Env env); -void PlatformUnwatch(WatcherHandle handle, Napi::Env env); -bool PlatformIsHandleValid(WatcherHandle handle); -int PlatformInvalidHandleToErrorNumber(WatcherHandle handle); - -enum EVENT_TYPE { - EVENT_NONE, - EVENT_CHANGE, - EVENT_RENAME, - EVENT_DELETE, - EVENT_CHILD_CHANGE, - EVENT_CHILD_RENAME, - EVENT_CHILD_DELETE, - EVENT_CHILD_CREATE, -}; - -struct PathWatcherEvent { - EVENT_TYPE type; - WatcherHandle handle; - std::vector new_path; - std::vector old_path; - - // Default constructor - PathWatcherEvent() = default; - - // Constructor - PathWatcherEvent(EVENT_TYPE t, WatcherHandle h, const std::vector& np, const std::vector& op = std::vector()) - : type(t), handle(h), new_path(np), old_path(op) {} - - // Copy constructor - PathWatcherEvent(const PathWatcherEvent& other) - : type(other.type), handle(other.handle), new_path(other.new_path), old_path(other.old_path) {} - - // Copy assignment operator - PathWatcherEvent& operator=(const PathWatcherEvent& other) { - if (this != &other) { - type = other.type; - handle = other.handle; - new_path = other.new_path; - old_path = other.old_path; - } - return *this; - } - - // Move constructor - PathWatcherEvent(PathWatcherEvent&& other) noexcept - : type(other.type), handle(other.handle), - new_path(std::move(other.new_path)), old_path(std::move(other.old_path)) {} - - // Move assignment operator - PathWatcherEvent& operator=(PathWatcherEvent&& other) noexcept { - if (this != &other) { - type = other.type; - handle = other.handle; - new_path = std::move(other.new_path); - old_path = std::move(other.old_path); - } - return *this; - } -}; - -using namespace Napi; - -class PathWatcherWorker: public AsyncProgressQueueWorker { - public: - PathWatcherWorker(Napi::Env env, Function &progressCallback); - - ~PathWatcherWorker() {} - - void Execute(const PathWatcherWorker::ExecutionProgress& progress) override; - void OnOK() override; - - void OnProgress(const PathWatcherEvent* data, size_t) override; - void Stop(); - - private: - Napi::Env _env; - bool shouldStop = false; - FunctionReference progressCallback; - - const char* GetEventTypeString(EVENT_TYPE type); -}; - -void PlatformThread(const PathWatcherWorker::ExecutionProgress& progress, bool& shouldStop, Napi::Env env); -void PlatformStop(Napi::Env env); - -void CommonInit(Napi::Env env); - -Napi::Value SetCallback(const Napi::CallbackInfo& info); -Napi::Value Watch(const Napi::CallbackInfo& info); -Napi::Value Unwatch(const Napi::CallbackInfo& info); - -#endif // SRC_COMMON_H_ diff --git a/src/main.cc b/src/main.cc deleted file mode 100644 index 3acbc1c..0000000 --- a/src/main.cc +++ /dev/null @@ -1,22 +0,0 @@ -#include "common.h" -#include "addon-data.h" - -namespace { - - Napi::Object Init(Napi::Env env, Napi::Object exports) { - auto* data = new AddonData(env); - env.SetInstanceData(data); - - CommonInit(env); - PlatformInit(env); - - exports.Set("setCallback", Napi::Function::New(env, SetCallback)); - exports.Set("watch", Napi::Function::New(env, Watch)); - exports.Set("unwatch", Napi::Function::New(env, Unwatch)); - - return exports; - } - -} // namespace - -NODE_API_MODULE(pathwatcher, Init) diff --git a/src/pathwatcher_linux.cc b/src/pathwatcher_linux.cc deleted file mode 100644 index 5d64951..0000000 --- a/src/pathwatcher_linux.cc +++ /dev/null @@ -1,114 +0,0 @@ -#include -#include - -#include -#include -#include -#include - -#include -#include - -#include "common.h" -#include "addon-data.h" - -// static int g_inotify; -// static int g_init_errno; - -void PlatformInit(Napi::Env env) { - auto addonData = env.GetInstanceData(); - addonData->inotify = inotify_init(); - if (addonData->inotify == -1) { - addonData->init_errno = errno; - return; - } -} - -void PlatformThread( - const PathWatcherWorker::ExecutionProgress& progress, - bool& shouldStop, - Napi::Env env -) { - auto addonData = env.GetInstanceData(); - // std::cout << "PlatformThread START" << std::endl; - // Needs to be large enough for sizeof(inotify_event) + strlen(filename). - char buf[4096]; - - while (!shouldStop) { - std::cout << "PlatformThread loop ID: " << addonData->id << std::endl; - fd_set read_fds; - FD_ZERO(&read_fds); - FD_SET(addonData->inotify, &read_fds); - - struct timeval tv; - tv.tv_sec = 0; - tv.tv_usec = 100000; // 100ms timeout - - int ret = select(addonData->inotify + 1, &read_fds, NULL, NULL, &tv); - - if (ret == -1 && errno != EINTR) { - break; - } - - if (ret == 0) { - // Timeout. - continue; - } - - int size = read(addonData->inotify, buf, sizeof(buf)); - if (size <= 0) break; - - inotify_event* e; - for (char* p = buf; p < buf + size; p += sizeof(*e) + e->len) { - e = reinterpret_cast(p); - - int fd = e->wd; - EVENT_TYPE type; - std::vector path; - - // Note that inotify won't tell us where the file or directory has been - // moved to, so we just treat IN_MOVE_SELF as file being deleted. - if (e->mask & (IN_ATTRIB | IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVE)) { - type = EVENT_CHANGE; - } else if (e->mask & (IN_DELETE_SELF | IN_MOVE_SELF)) { - type = EVENT_DELETE; - } else { - continue; - } - - PathWatcherEvent event(type, fd, path); - progress.Send(&event, 1); - } - } - - // std::cout << "PlatformThread END" << std::endl; -} - -WatcherHandle PlatformWatch(const char* path, Napi::Env env) { - auto addonData = env.GetInstanceData(); - if (addonData->inotify == -1) { - return -addonData->init_errno; - } - - int fd = inotify_add_watch(addonData->inotify, path, IN_ATTRIB | IN_CREATE | - IN_DELETE | IN_MODIFY | IN_MOVE | IN_MOVE_SELF | IN_DELETE_SELF); - if (fd == -1) { - return -errno; - } - return fd; -} - -void PlatformUnwatch(WatcherHandle fd, Napi::Env env) { - auto addonData = env.GetInstanceData(); - inotify_rm_watch(addonData->inotify, fd); -} - -bool PlatformIsHandleValid(WatcherHandle handle) { - return handle >= 0; -} - -int PlatformInvalidHandleToErrorNumber(WatcherHandle handle) { - return -handle; -} - -void PlatformStop(Napi::Env env) {} diff --git a/src/pathwatcher_unix.cc b/src/pathwatcher_unix.cc deleted file mode 100644 index 1e0a47c..0000000 --- a/src/pathwatcher_unix.cc +++ /dev/null @@ -1,128 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "common.h" -#include "addon-data.h" - -// test for descriptor event notification, if not available set to O_RDONLY -#ifndef O_EVTONLY -#define O_EVTONLY O_RDONLY -#endif - -// test for flag to return full path of the fd -// if not then set value as defined by mac -// see: http://fxr.watson.org/fxr/source/bsd/sys/fcntl.h?v=xnu-792.6.70 -#ifndef F_GETPATH -#define F_GETPATH 50 - -#endif - -// NOTE: You might see the globals and get nervous here. Our working theory is -// that this this is fine; this is thread-safe without having to be isolated -// between contexts. -// static int g_kqueue; -// static int g_init_errno; - -void PlatformInit(Napi::Env env) { - auto addonData = env.GetInstanceData(); - addonData->kqueue = kqueue(); - if (addonData->kqueue == -1) { - addonData->init_errno = errno; - return; - } -} - -void PlatformThread( - const PathWatcherWorker::ExecutionProgress& progress, - bool& shouldStop, - Napi::Env env -) { - auto addonData = env.GetInstanceData(); - int l_kqueue = addonData->kqueue; - // std::cout << "PlatformThread " << std::this_thread::get_id() << std::endl; - struct kevent event; - struct timespec timeout = { 0, 500000000 }; - - while (!shouldStop) { - int r; - do { - if (shouldStop) return; - r = kevent(l_kqueue, NULL, 0, &event, 1, &timeout); - } while ((r == -1 && errno == EINTR) || r == 0); - - EVENT_TYPE type; - int fd = static_cast(event.ident); - std::vector path; - - if (event.fflags & NOTE_WRITE) { - type = EVENT_CHANGE; - } else if (event.fflags & NOTE_DELETE) { - type = EVENT_DELETE; - } else if (event.fflags & NOTE_RENAME) { - type = EVENT_RENAME; - char buffer[MAXPATHLEN] = { 0 }; - fcntl(fd, F_GETPATH, buffer); - close(fd); - - int length = strlen(buffer); - path.resize(length); - std::copy(buffer, buffer + length, path.data()); - } else if (event.fflags & NOTE_ATTRIB && lseek(fd, 0, SEEK_END) == 0) { - // The file became empty, this does not fire as a NOTE_WRITE event for - // some reason. - type = EVENT_CHANGE; - } else { - continue; - } - - // std::cout << "PlatformThread EVENT " << std::this_thread::get_id() << std::endl; - PathWatcherEvent event(type, fd, path); - progress.Send(&event, 1); - } -} - -WatcherHandle PlatformWatch(const char* path, Napi::Env env) { - auto addonData = env.GetInstanceData(); - if (addonData->kqueue == -1) { - return -addonData->init_errno; - } - - int fd = open(path, O_EVTONLY, 0); - if (fd < 0) { - return -errno; - } - - struct timespec timeout = { 0, 50000000 }; - struct kevent event; - int filter = EVFILT_VNODE; - int flags = EV_ADD | EV_ENABLE | EV_CLEAR; - int fflags = NOTE_WRITE | NOTE_DELETE | NOTE_RENAME | NOTE_ATTRIB; - EV_SET(&event, fd, filter, flags, fflags, 0, reinterpret_cast(const_cast(path))); - int r = kevent(addonData->kqueue, &event, 1, NULL, 0, &timeout); - if (r == -1) { - return -errno; - } - - return fd; -} - - -void PlatformUnwatch(WatcherHandle fd, Napi::Env _env) { - close(fd); -} - -bool PlatformIsHandleValid(WatcherHandle handle) { - return handle >= 0; -} - -int PlatformInvalidHandleToErrorNumber(WatcherHandle handle) { - return -handle; -} - -void PlatformStop(Napi::Env env) {} diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc deleted file mode 100644 index 8e5a51f..0000000 --- a/src/pathwatcher_win.cc +++ /dev/null @@ -1,720 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "common.h" -#include "addon-data.h" -#include "js_native_api_types.h" -#include "napi.h" -#include "uv.h" - -struct ThreadData { - std::queue event_queue; - std::mutex mutex; - std::condition_variable cv; - const PathWatcherWorker::ExecutionProgress* progress; - bool should_stop = false; - bool is_main = false; - bool is_working = false; -}; - -class ThreadManager { -public: - void register_thread( - int id, - const PathWatcherWorker::ExecutionProgress* progress, - bool is_main - ) { - std::lock_guard lock(mutex_); - threads_[id] = std::make_unique(); - threads_[id]->progress = progress; - threads_[id]->is_main = is_main; - if (is_main) { - this->main_id = id; - } - } - - bool clock_out(int id) { - std::cout << "omg clocking out sanity check " << id << std::endl; - if (id != this->main_id) { - std::cout << "Doesn't think it's the boss thread! " << id << std::endl; - return false; - } - // std::lock_guard lock(mutex_); - auto current_boss_data = this->get_thread_data(this->main_id); - // Pretend this is still the main thread until we can hand off duties to - // another thread. - current_boss_data->is_main = true; - current_boss_data->is_working = false; - - std::cout << "Finding a new boss thread." << std::endl; - - for (const auto& pair : threads_) { - if (pair.first != id) { - std::cout << "Promoting " << pair.first << " to be the new boss thread." << std::endl; - promote(pair.first); - if (this->main_id != pair.first) { - std::cout << "WHY AM I GOING INSANE?" << std::endl; - } - auto new_boss_data = this->get_thread_data(pair.first); - new_boss_data->is_main = true; - return true; - } - } - return false; - } - - bool clock_in(int id) { - std::cout << "@@@@ clock_in called: Thread " << id << std::endl; - std::lock_guard lock(mutex_); - if (id != this->main_id) return false; - std::cout << "@@@@ CLOCKING IN: Thread " << id << std::endl; - - auto boss_data = unsafe_get_thread_data(id); - boss_data->is_working = true; - boss_data->cv.notify_one(); - return true; - } - - bool unregister_thread(int id) { - std::lock_guard lock(mutex_); - return threads_.erase(id) > 0; - } - - int has_main () { - return this->main_id > -1; - } - - bool is_main (int id) { - return id == this->main_id; - } - - void wake_up_new_main() { - if (this->main_id == -1) return; - std::cout << "Attempting to wake up new main thread: " << this->main_id << std::endl; - auto new_boss_data = this->get_thread_data(this->main_id); - new_boss_data->cv.notify_one(); - } - - void promote(int id) { - auto data = this->get_thread_data(id); - data->is_main = true; - this->main_id = id; - } - - void queue_event(int id, PathWatcherEvent event) { - std::lock_guard lock(mutex_); - if (threads_.count(id) > 0) { - std::lock_guard thread_lock(threads_[id]->mutex); - threads_[id]->event_queue.push(std::move(event)); - threads_[id]->cv.notify_one(); - } - } - - void stop_all() { - std::lock_guard lock(mutex_); - for (auto& pair : threads_) { - std::lock_guard thread_lock(pair.second->mutex); - pair.second->should_stop = true; - pair.second->cv.notify_one(); - } - } - - bool is_empty() const { - std::lock_guard lock(mutex_); - return threads_.empty(); - } - - int size() { - std::lock_guard lock(mutex_); - return threads_.size(); - } - - bool has_thread(int id) const { - std::lock_guard lock(mutex_); - return threads_.find(id) != threads_.end(); - } - - ThreadData* get_thread_data(int id) { - std::lock_guard lock(mutex_); - auto it = threads_.find(id); - return it != threads_.end() ? it->second.get() : nullptr; - } - - ThreadData* get_main_data() { - if (this->main_id == -1) return nullptr; - return this->get_thread_data(this->main_id); - } - - std::unordered_map> threads_; - -private: - - ThreadData* unsafe_get_thread_data(int id) { - auto it = threads_.find(id); - return it != threads_.end() ? it->second.get() : nullptr; - } - - - mutable std::mutex mutex_; - int main_id = -1; -}; - -// Global instance -ThreadManager g_thread_manager; - -// Global atomic flag to ensure only one PlatformThread is running -std::atomic g_platform_thread_running(false); - -void ThreadWorker(int id) { - while (true) { - ThreadData* thread_data = g_thread_manager.get_thread_data(id); - if (!thread_data) break; // (thread was unregistered) - - std::unique_lock lock(thread_data->mutex); - std::cout << "[WAIT WAIT WAIT] ThreadWorker with ID: " << id << " has should_stop of: " << thread_data->should_stop << std::endl; - thread_data->cv.wait(lock, [thread_data] { - if (thread_data->should_stop) return true; - if (!thread_data->event_queue.empty()) return true; - return false; - }); - - // std::cout << "ThreadWorker with ID: " << id << "is unblocked. Why? " << "(internal_stop? " << thread_data->internal_stop << ") (should_stop? " << *(thread_data->should_stop) << ") (items in queue? " << !thread_data->event_queue.empty() << ")" << std::endl; - - if (thread_data->should_stop && thread_data->event_queue.empty()) { - break; - } - - while (!thread_data->event_queue.empty()) { - auto event = thread_data->event_queue.front(); - thread_data->event_queue.pop(); - lock.unlock(); - std::cout << "ThreadWorker with ID: " << id << " is sending event!" << std::endl; - thread_data->progress->Send(&event, 1); - lock.lock(); - - if (thread_data->should_stop) break; - } - - if (thread_data->should_stop) break; - } -} - -// Global instance of VectorMap -// ProgressMap g_progress_map; - -// Size of the buffer to store result of ReadDirectoryChangesW. -static const unsigned int kDirectoryWatcherBufferSize = 4096; - -// Mutex for the HandleWrapper map. -static uv_mutex_t g_handle_wrap_map_mutex; - -// The events to be waited on. -static std::vector g_events; - -// The dummy event to wakeup the thread. -static HANDLE g_wake_up_event; - -// The dummy event to ensure we are not waiting on a file handle when destroying it. -static HANDLE g_file_handles_free_event; - -// static bool g_is_running = false; -// static int g_env_count = 0; - -struct ScopedLocker { - explicit ScopedLocker(uv_mutex_t& mutex) : mutex_(&mutex) { uv_mutex_lock(mutex_); } - ~ScopedLocker() { Unlock(); } - - void Unlock() { uv_mutex_unlock(mutex_); } - - uv_mutex_t* mutex_; -}; - -struct HandleWrapper { - HandleWrapper(WatcherHandle handle, const char* path_str, int addon_data_id) - : addonDataId(addon_data_id), - dir_handle(handle), - path(strlen(path_str)), - canceled(false) { - memset(&overlapped, 0, sizeof(overlapped)); - overlapped.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); - g_events.push_back(overlapped.hEvent); - - std::copy(path_str, path_str + path.size(), path.data()); - map_[overlapped.hEvent] = this; - } - - ~HandleWrapper() { - if (!canceled) { - Cancel(); - } - - CloseHandle(dir_handle); - CloseHandle(overlapped.hEvent); - } - - void Cancel() { - canceled = true; - CancelIoEx(dir_handle, &overlapped); - g_events.erase(std::remove(g_events.begin(), g_events.end(), overlapped.hEvent), g_events.end()); - map_.erase(overlapped.hEvent); - } - - int addonDataId; - WatcherHandle dir_handle; - std::vector path; - bool canceled; - OVERLAPPED overlapped; - char buffer[kDirectoryWatcherBufferSize]; - - static HandleWrapper* Get(HANDLE key) { return map_[key]; } - - static std::map map_; -}; - -std::map HandleWrapper::map_; - -struct WatcherEvent { - EVENT_TYPE type; - WatcherHandle handle; - std::vector new_path; - std::vector old_path; -}; - -static bool QueueReaddirchanges(HandleWrapper* handle) { - return ReadDirectoryChangesW( - handle->dir_handle, - handle->buffer, - kDirectoryWatcherBufferSize, - FALSE, - FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | - FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_LAST_ACCESS | - FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_SECURITY, - NULL, - &handle->overlapped, - NULL - ) == TRUE; -} - -Napi::Value WatcherHandleToV8Value(WatcherHandle handle, Napi::Env env) { - uint64_t handleInt = reinterpret_cast(handle); - return Napi::BigInt::New(env, handleInt); -} - -WatcherHandle V8ValueToWatcherHandle(Napi::Value value) { - if (!value.IsBigInt()) { - return NULL; - } - bool lossless; - uint64_t handleInt = value.As().Uint64Value(&lossless); - if (!lossless) { - return NULL; - } - return reinterpret_cast(handleInt); -} - -bool IsV8ValueWatcherHandle(Napi::Value value) { - return value.IsBigInt(); -} - -void PlatformInit(Napi::Env _env) { - uv_mutex_init(&g_handle_wrap_map_mutex); - - g_file_handles_free_event = CreateEvent(NULL, TRUE, TRUE, NULL); - g_wake_up_event = CreateEvent(NULL, FALSE, FALSE, NULL); - g_events.push_back(g_wake_up_event); -} - -void PlatformThread( - const PathWatcherWorker::ExecutionProgress& progress, - bool& shouldStop, - Napi::Env env -) { - auto addonData = env.GetInstanceData(); - - bool hasMainThread = !g_thread_manager.is_empty(); - // bool expected = false; - // bool hasMainThread = g_platform_thread_running.compare_exchange_strong(expected, true); - - if (!g_thread_manager.has_thread(addonData->id)) { - g_thread_manager.register_thread(addonData->id, &progress, !hasMainThread); - } - - ThreadData* thread_data = g_thread_manager.get_thread_data(addonData->id); - - std::cout << "Is " << addonData->id << " the boss thread? " << g_thread_manager.is_main(addonData->id) << std::endl; - - if (!g_thread_manager.is_main(addonData->id)) { - while (!g_thread_manager.is_main(addonData->id)) { - std::cout << "[WAIT WAIT " << addonData->id << "] Thread with ID: " << addonData->id << " in holding pattern" << std::endl; - // A holding-pattern loop for threads that aren't the “boss” thread. - ThreadData* thread_data = g_thread_manager.get_thread_data(addonData->id); - if (!thread_data) break; // (thread was unregistered) - if (g_thread_manager.is_main(addonData->id)) break; - - std::unique_lock lock(thread_data->mutex); - thread_data->cv.wait(lock, [thread_data] { - if (thread_data->should_stop) return true; - if (!thread_data->event_queue.empty()) return true; - if (thread_data->is_main) return true; - return false; - }); - - std::cout << "PEON Thread with ID: " << addonData->id << " unblocked because: (should_stop? " << thread_data->should_stop << ") (is_main? " << thread_data->is_main << ") (event queue? " << !thread_data->event_queue.empty() << ")" << std::endl; - - if (thread_data->should_stop && thread_data->event_queue.empty()) { - break; - } - - while (!thread_data->event_queue.empty()) { - auto event = thread_data->event_queue.front(); - thread_data->event_queue.pop(); - lock.unlock(); - thread_data->progress->Send(&event, 1); - lock.lock(); - - if (thread_data->should_stop) break; - } - - if (thread_data->is_main) { - std::cout << "THIS IS WHERE WE WOULD HAVE THE NEW GUY CLOCK IN! " << addonData->id << std::endl; - g_thread_manager.clock_in(addonData->id); - break; - } - - if (thread_data->should_stop) break; - } - } - - // if (!g_thread_manager.is_main(addonData->id)) { - // // If we get to this point and this still isn't the “boss” thread, then - // // we’ve broken out of the above loop but should not proceed. This thread - // // hasn't been promoted; it should stop. - // std::cout << "Thread with ID: " << addonData->id << " is not the new boss thread, so it must be exiting." - // g_thread_manager.unregister_thread(addonData->id); - // return; - // } - - // If we get this far, then this is the main thread — either because it was - // the first to be created or because it's been promoted after another thread - // was stopped. - - std::cout << "PlatformThread ID: " << addonData->id << std::endl; - - // std::cout << "PlatformThread" << std::endl; - if (g_thread_manager.is_main(addonData->id)) { - while (!thread_data->should_stop && !shouldStop) { - // Do not use g_events directly, since reallocation could happen when there - // are new watchers adding to g_events when WaitForMultipleObjects is still - // polling. - ScopedLocker locker(g_handle_wrap_map_mutex); - std::vector copied_events(g_events); - locker.Unlock(); - - ResetEvent(g_file_handles_free_event); - std::cout << "Thread with ID: " << addonData->id << " is waiting..." << std::endl; - DWORD r = WaitForMultipleObjects( - copied_events.size(), - copied_events.data(), - FALSE, - 100 - ); - SetEvent(g_file_handles_free_event); - - std::cout << "BOSS Thread with ID: " << addonData->id << " unblocked because: (should_stop? " << thread_data->should_stop << ") (shouldStop? " << shouldStop << ") (is_main? " << thread_data->is_main << ") (event queue? " << (copied_events.size() > 1) << ") [THREAD SIZE: " << g_thread_manager.size() << "]" << std::endl; - - if (!thread_data->is_main) { - break; - } - - if (r == WAIT_TIMEOUT) { - continue; - } - std::cout << "Thread with ID: " << addonData->id << " is done waiting." << std::endl; - - int i = r - WAIT_OBJECT_0; - if (i >= 0 && i < copied_events.size()) { - // It's a wake up event; there is no FS event. - if (copied_events[i] == g_wake_up_event) { - std::cout << "Thread with ID: " << addonData->id << " received wake-up event. Continuing." << std::endl; - if (!thread_data->is_main) break; - continue; - } - - ScopedLocker locker(g_handle_wrap_map_mutex); - - // Match up the filesystem event with the handle responsible for it. - HandleWrapper* handle = HandleWrapper::Get(copied_events[i]); - if (!handle || handle->canceled) { - continue; - } - - if (!g_thread_manager.has_thread(handle->addonDataId)) { - // Ignore handles that belong to stale environments. - std::cout << "Unrecognized environment: " << handle->addonDataId << std::endl; - continue; - } - - DWORD bytes_transferred; - if (!GetOverlappedResult(handle->dir_handle, &handle->overlapped, &bytes_transferred, FALSE)) { - std::cout << "Nothing for thread: " << addonData->id << std::endl; - continue; - } - if (bytes_transferred == 0) { - std::cout << "Nothing for thread: " << addonData->id << std::endl; - continue; - } - - std::vector old_path; - std::vector events; - - DWORD offset = 0; - while (true) { - FILE_NOTIFY_INFORMATION* file_info = - reinterpret_cast(handle->buffer + offset); - - // Emit events for children. - EVENT_TYPE event = EVENT_NONE; - switch (file_info->Action) { - case FILE_ACTION_ADDED: - event = EVENT_CHILD_CREATE; - break; - case FILE_ACTION_REMOVED: - event = EVENT_CHILD_DELETE; - break; - case FILE_ACTION_RENAMED_OLD_NAME: - event = EVENT_CHILD_RENAME; - break; - case FILE_ACTION_RENAMED_NEW_NAME: - event = EVENT_CHILD_RENAME; - break; - case FILE_ACTION_MODIFIED: - event = EVENT_CHILD_CHANGE; - break; - } - - if (event != EVENT_NONE) { - // The FileNameLength is in "bytes", but the WideCharToMultiByte - // requires the length to be in "characters"! - int file_name_length_in_characters = - file_info->FileNameLength / sizeof(wchar_t); - - char filename[MAX_PATH] = { 0 }; - int size = WideCharToMultiByte( - CP_UTF8, - 0, - file_info->FileName, - file_name_length_in_characters, - filename, - MAX_PATH, - NULL, - NULL - ); - - // Convert file name to file path, same with: - // path = handle->path + '\\' + filename - std::vector path(handle->path.size() + 1 + size); - std::vector::iterator iter = path.begin(); - iter = std::copy(handle->path.begin(), handle->path.end(), iter); - *(iter++) = '\\'; - std::copy(filename, filename + size, iter); - - if (file_info->Action == FILE_ACTION_RENAMED_OLD_NAME) { - // Do not send rename event until the NEW_NAME event, but still keep - // a record of old name. - old_path.swap(path); - } else if (file_info->Action == FILE_ACTION_RENAMED_NEW_NAME) { - WatcherEvent e = { event, handle->overlapped.hEvent }; - e.new_path.swap(path); - e.old_path.swap(old_path); - events.push_back(e); - } else { - WatcherEvent e = { event, handle->overlapped.hEvent }; - e.new_path.swap(path); - events.push_back(e); - } - } - - if (file_info->NextEntryOffset == 0) break; - offset += file_info->NextEntryOffset; - } - - // Restart the monitor, it was reset after each call. - QueueReaddirchanges(handle); - - locker.Unlock(); - - std::cout << "Total events processed on thread " << addonData->id << ": " << events.size() << std::endl; - - for (size_t i = 0; i < events.size(); ++i) { - std::cout << "Emitting " << events[i].type << " event on thread " << addonData->id << " for path: " << events[i].new_path.data() << " for worker with ID: " << handle->addonDataId << std::endl; - PathWatcherEvent event( - events[i].type, - events[i].handle, - events[i].new_path, - events[i].old_path - ); - if (handle->addonDataId == addonData->id) { - // This event belongs to our thread, so we can handle it directly. - std::cout << "Invoking directly " << addonData->id << std::endl; - progress.Send(&event, 1); - } else { - // Since it's not ours, we should enqueue it to be handled by the - // thread responsible for it. - g_thread_manager.queue_event(handle->addonDataId, event); - } - } - } - } // while - } - - - std::cout << "ABOUT TO UNREGISTER!!!!!!" << addonData->id << std::endl; - g_thread_manager.unregister_thread(addonData->id); - std::cout << "PlatformThread with ID: " << addonData->id << " is exiting! Thread size: " << g_thread_manager.size() << std::endl; - - if (!g_thread_manager.is_empty()) { - std::cout << "[???] Waking up new main!" << std::endl; - // g_thread_manager.wake_up_new_main(); - // Sleep briefly to allow time for another thread to wake up. - std::cout << "[???] Sleeping!" << std::endl; - - std::this_thread::sleep_for(std::chrono::milliseconds(200)); - std::cout << "[???] Done sleeping!" << std::endl; - } - - // g_platform_thread_running = false; -} - -// // Function to get the vector for a given AddonData -// std::shared_ptr GetVectorForAddonData(AddonData* addonData) { -// return g_vector_map.get_or_create(addonData->id); -// } - -WatcherHandle PlatformWatch(const char* path, Napi::Env env) { - auto addonData = env.GetInstanceData(); - std::cout << "PlatformWatch ID: " << addonData->id << " Path: " << path << std::endl; - wchar_t wpath[MAX_PATH] = { 0 }; - MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, MAX_PATH); - - // Requires a directory, file watching is emulated in js. - DWORD attr = GetFileAttributesW(wpath); - if (attr == INVALID_FILE_ATTRIBUTES || !(attr & FILE_ATTRIBUTE_DIRECTORY)) { - return INVALID_HANDLE_VALUE; - } - - WatcherHandle dir_handle = CreateFileW( - wpath, - FILE_LIST_DIRECTORY, - FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE, - NULL, - OPEN_EXISTING, - FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, - NULL - ); - - if (!PlatformIsHandleValid(dir_handle)) { - return INVALID_HANDLE_VALUE; - } - - std::unique_ptr handle; - { - ScopedLocker locker(g_handle_wrap_map_mutex); - handle.reset(new HandleWrapper(dir_handle, path, addonData->id)); - } - - if (!QueueReaddirchanges(handle.get())) { - return INVALID_HANDLE_VALUE; - } - - // Wake up the thread to add the new event. - SetEvent(g_wake_up_event); - - // The pointer is leaked if no error happened. - return handle.release()->overlapped.hEvent; -} - -void PlatformUnwatch(WatcherHandle key, Napi::Env env) { - auto addonData = env.GetInstanceData(); - std::cout << "PlatformUnwatch ID: " << addonData->id << std::endl; - if (PlatformIsHandleValid(key)) { - HandleWrapper* handle; - { - ScopedLocker locker(g_handle_wrap_map_mutex); - handle = HandleWrapper::Get(key); - handle->Cancel(); - } - - do { - SetEvent(g_wake_up_event); - } while (WaitForSingleObject(g_file_handles_free_event, 50) == WAIT_TIMEOUT); - delete handle; - } -} - -bool PlatformIsHandleValid(WatcherHandle handle) { - return handle != INVALID_HANDLE_VALUE; -} - -// We have no errno on Windows. -int PlatformInvalidHandleToErrorNumber(WatcherHandle handle) { - return 0; -} - -void PlatformStop(Napi::Env env) { - auto addonData = env.GetInstanceData(); - std::cout << "@@@@ PlatformStop ID: " << addonData->id << std::endl; - auto thread_data = g_thread_manager.get_thread_data(addonData->id); - - // if (g_thread_manager.is_main(addonData->id)) { - // std::cout << "CLOCKING OUT!!!!!!" << addonData->id << std::endl; - // // Warm hand-off. - // g_thread_manager.clock_out(addonData->id); - // - // if (g_thread_manager.size() > 1) { - // auto new_main_thread_data = g_thread_manager.get_main_data(); - // g_thread_manager.wake_up_new_main(); - // std::unique_lock lock(new_main_thread_data->mutex); - // // Wait until the new boss clocks in. - // new_main_thread_data->cv.wait(lock, [new_main_thread_data] { - // return new_main_thread_data->is_working; - // }); - // } - // } - - if (thread_data) { - if (thread_data->is_main) { - std::cout << "CLOCKING OUT!!!!!!" << addonData->id << " thread size: " << g_thread_manager.size() << std::endl; - g_thread_manager.clock_out(addonData->id); - - if (g_thread_manager.size() > 1) { - std::cout << "WAITING FOR NEW BOSS!!!!!!" << addonData->id << std::endl; - auto new_main_thread_data = g_thread_manager.get_main_data(); - g_thread_manager.wake_up_new_main(); - std::unique_lock lock(new_main_thread_data->mutex); - // Wait until the new boss clocks in. - new_main_thread_data->cv.wait(lock, [new_main_thread_data] { - return new_main_thread_data->is_working; - }); - std::cout << "PROCEEDING!!!!!!" << addonData->id << std::endl; - } else { - std::cout << "WAS LAST THREAD!!!!!!" << addonData->id << std::endl; - } - } else { - std::cout << "NO THREAD DATA!!!!!!" << addonData->id << std::endl; - } - - - std::lock_guard lock(thread_data->mutex); - thread_data->should_stop = true; - thread_data->cv.notify_one(); - // g_thread_manager.unregister_thread(addonData->id); - } -} From cc6b9027ddebbbeac9cdeae24e53800b7b6fc36e Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 16:50:28 -0700 Subject: [PATCH 082/168] Make Visual Studio happy --- binding.gyp | 1 + 1 file changed, 1 insertion(+) diff --git a/binding.gyp b/binding.gyp index 6b40361..2abf3e4 100644 --- a/binding.gyp +++ b/binding.gyp @@ -136,6 +136,7 @@ 4996, # function was declared deprecated 2220, # warning treated as error - no object file generated 4309, # 'conversion' : truncation of constant value + 4101, # unreferenced local variable ], 'defines': [ '_WIN32_WINNT=0x0600', From 262dac60dcfffd7cbfa54c50f990a052ce18181b Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 16:53:14 -0700 Subject: [PATCH 083/168] Streamline prepublish tasks --- Gruntfile.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gruntfile.coffee b/Gruntfile.coffee index ed9c3a1..edd1b89 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -63,13 +63,13 @@ module.exports = (grunt) -> grunt.loadNpmTasks('grunt-contrib-coffee') grunt.loadNpmTasks('grunt-shell') grunt.loadNpmTasks('grunt-coffeelint') - grunt.loadNpmTasks('node-cpplint') + # grunt.loadNpmTasks('node-cpplint') grunt.loadNpmTasks('grunt-atomdoc') - grunt.registerTask('lint', ['coffeelint', 'cpplint']) + # grunt.registerTask('lint', ['coffeelint', 'cpplint']) grunt.registerTask('default', ['coffee', 'lint', 'shell:rebuild']) grunt.registerTask('test', ['default', 'shell:test']) - grunt.registerTask('prepublish', ['coffee', 'lint', 'shell:update-atomdoc', 'atomdoc']) + grunt.registerTask('prepublish', ['shell:update-atomdoc', 'atomdoc']) grunt.registerTask 'clean', -> rm = require('rimraf').sync rm 'build' From d55a10a8fdf6e3c468125eaded93536a9a073a6b Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 16:55:18 -0700 Subject: [PATCH 084/168] Use different URI for submodule --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index bbb578a..c9ebeba 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "vendor/efsw"] path = vendor/efsw - url = git@github.com:SpartanJ/efsw.git + url = https://github.com/SpartanJ/efsw.git From 640b2f47fa64a896f8c05090d3780c1fe00118e5 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 18:06:18 -0700 Subject: [PATCH 085/168] Script should be testing file monitoring, not directory monitoring --- spec/worker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/worker.js b/spec/worker.js index 916cb3b..ad5bc40 100644 --- a/spec/worker.js +++ b/spec/worker.js @@ -84,7 +84,7 @@ if (isMainThread) { console.log('Worker', workerData.id, 'creating file:', tempFile); fs.writeFileSync(tempFile, ''); await wait(500); - const scheduler = new Scheduler(workerData.id, tempDir); + const scheduler = new Scheduler(workerData.id, tempFile); scheduler.start(); await wait(2000); From 09809a8297cb329bdded2e5b941b81cd1917eb3b Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 18:19:26 -0700 Subject: [PATCH 086/168] Filter out too-eager creation events on macOS --- lib/core.cc | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/core.cc b/lib/core.cc index d24a3eb..a30b8db 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -3,6 +3,9 @@ #include "include/efsw/efsw.hpp" #include "napi.h" #include +#ifdef __APPLE__ +#include +#endif void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEvent* event); @@ -50,12 +53,33 @@ void PathWatcherListener::handleFileAction( efsw::Action action, std::string oldFilename ) { - // std::cout << "PathWatcherListener::handleFileAction" << std::endl; - // std::cout << "Action: " << EventType(action, true) << ", Dir: " << dir << ", Filename: " << filename << ", Old Filename: " << oldFilename << std::endl; + std::cout << "PathWatcherListener::handleFileAction" << std::endl; + std::cout << "Action: " << EventType(action, true) << ", Dir: " << dir << ", Filename: " << filename << ", Old Filename: " << oldFilename << std::endl; std::string newPathStr = dir + filename; std::vector newPath(newPathStr.begin(), newPathStr.end()); +#ifdef __APPLE__ + // macOS seems to think that lots of file creations happen that aren't + // actually creations; for instance, multiple successive writes to the same + // file will sometimes nonsensically produce a `child-create` event preceding + // each `child-change` event. + // + // Luckily, we can easily check whether or not a file has actually been + // created on macOS; we can compare creation time to modification time. + if (action == efsw::Action::Add) { + struct stat file; + if (stat(newPathStr.c_str(), &file) != 0) { + return; + } + if (file.st_birthtimespec.tv_sec != file.st_mtimespec.tv_sec) { + // std::cout << "Skipping spurious creation event!" << std::endl; + return; + } + } + +#endif + std::vector oldPath; if (!oldFilename.empty()) { std::string oldPathStr = dir + oldFilename; From 3e6527471d4815a61a41a2e50fa5392c9ff3957c Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 18:21:12 -0700 Subject: [PATCH 087/168] (oops) --- lib/core.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core.cc b/lib/core.cc index a30b8db..3ccf03d 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -53,8 +53,8 @@ void PathWatcherListener::handleFileAction( efsw::Action action, std::string oldFilename ) { - std::cout << "PathWatcherListener::handleFileAction" << std::endl; - std::cout << "Action: " << EventType(action, true) << ", Dir: " << dir << ", Filename: " << filename << ", Old Filename: " << oldFilename << std::endl; + // std::cout << "PathWatcherListener::handleFileAction" << std::endl; + // std::cout << "Action: " << EventType(action, true) << ", Dir: " << dir << ", Filename: " << filename << ", Old Filename: " << oldFilename << std::endl; std::string newPathStr = dir + filename; std::vector newPath(newPathStr.begin(), newPathStr.end()); From 04397a4c5ca875e67aa52d6fb264746d2caeec1c Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 18:26:44 -0700 Subject: [PATCH 088/168] (I don't care about Node 18) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c751a8..d03f1dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - windows-latest node_version: # - 14 - - 18 + # - 18 - 20 steps: - uses: actions/checkout@v4 From fede4060dba816a5c56fa5780cf7302b587d8b76 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 18:27:33 -0700 Subject: [PATCH 089/168] Run the context-safety test before the unit tests --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d03f1dc..918017d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,8 +31,8 @@ jobs: - name: Install dependencies run: npm install - - name: Run tests - run: npm test - - name: Run context safety test run: npm run test-context-safety + + - name: Run tests + run: npm test From b633337d73a3c9e42cfdde0bbe3a572f2376787d Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 20:02:04 -0700 Subject: [PATCH 090/168] =?UTF-8?q?Protect=20against=20segfaults=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …by ensuring that `handleFileAction` isn't called while we're releasing the thread-safe function. --- lib/core.cc | 6 ++++++ lib/core.h | 7 ++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/core.cc b/lib/core.cc index 3ccf03d..3ee2c63 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -25,6 +25,8 @@ PathWatcherListener::~PathWatcherListener() { } void PathWatcherListener::Stop() { + std::lock_guard lock(shutdownMutex); + isShuttingDown = true; if (tsfn) { tsfn.Release(); } @@ -53,6 +55,9 @@ void PathWatcherListener::handleFileAction( efsw::Action action, std::string oldFilename ) { + if (isShuttingDown) return; + std::lock_guard lock(shutdownMutex); + if (isShuttingDown); // std::cout << "PathWatcherListener::handleFileAction" << std::endl; // std::cout << "Action: " << EventType(action, true) << ", Dir: " << dir << ", Filename: " << filename << ", Old Filename: " << oldFilename << std::endl; @@ -95,6 +100,7 @@ void PathWatcherListener::handleFileAction( } void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEvent* event) { + std::unique_ptr eventPtr(event); if (event == nullptr) { // std::cerr << "ProcessEvent: event is null" << std::endl; return; diff --git a/lib/core.h b/lib/core.h index 84375fa..1347914 100644 --- a/lib/core.h +++ b/lib/core.h @@ -3,8 +3,8 @@ #define DEBUG 1 #include #include -#include -#include +#include +#include #include "../vendor/efsw/include/efsw/efsw.hpp" @@ -81,7 +81,8 @@ class PathWatcherListener: public efsw::FileWatchListener { void Stop(); private: - const std::chrono::milliseconds delayDuration{100}; // 100ms delay + std::atomic isShuttingDown{false}; + std::mutex shutdownMutex; Napi::Function callback; Napi::ThreadSafeFunction tsfn; }; From 05b2849daea19ac7ca4480174eb80dde5ca6a01f Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 20:06:00 -0700 Subject: [PATCH 091/168] Adjust how we react to deletions --- src/main.js | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/main.js b/src/main.js index e255549..96cbced 100644 --- a/src/main.js +++ b/src/main.js @@ -22,7 +22,7 @@ class HandleWatcher { this.start(); } - onEvent (event, filePath, oldFilePath) { + async onEvent (event, filePath, oldFilePath) { filePath &&= path.normalize(filePath); oldFilePath &&= path.normalize(oldFilePath); @@ -64,12 +64,28 @@ class HandleWatcher { setTimeout(detectRename, 100); return; case 'delete': + // Wait for a very short interval to protect against brief deletions or + // spurious deletion events. Git will sometimes briefly delete a file + // before restoring it with different contents. + await wait(20); + if (fs.existsSync(filePath)) return; this.emitter.emit( 'did-change', { event: 'delete', newFilePath: null } ); this.close(); return; + case 'child-delete': + // Wait for a very short interval to protect against brief deletions or + // spurious deletion events. Git will sometimes briefly delete a file + // before restoring it with different contents. + await wait(20); + if (fs.existsSync(filePath)) return; + this.emitter.emit( + 'did-change', + { event, newFilePath: filePath, oldFilePath, rawFilePath: filePath } + ); + return; case 'unknown': throw new Error("Received unknown event for path: " + this.path); default: @@ -101,12 +117,10 @@ class HandleWatcher { } } - async close () { + close () { if (!HANDLE_WATCHERS.has(this.handle)) return; binding.unwatch(this.handle); HANDLE_WATCHERS.delete(this.handle); - // Watchers take 100ms to realize they're closed. - await wait(100); } } @@ -217,9 +231,6 @@ class PathWatcher { assignRealPath () { try { this.realPath = fs.realpathSync(this.path); - if (this.realPath) { - // console.log('We think the real path is:', this.realPath); - } } catch (_error) { this.realPath = null; } @@ -239,14 +250,7 @@ class PathWatcher { async function callback(event, handle, filePath, oldFilePath) { if (!HANDLE_WATCHERS.has(handle)) return; - // Grab a reference to the watcher before we wait; it might be deleted from - // the registry after we wait. let watcher = HANDLE_WATCHERS.get(handle); - - if (event.includes('delete') && fs.existsSync(filePath)) { - return; - } - watcher.onEvent(event, filePath, oldFilePath); } From 01ed45d3704ff156a29ae716462bdee4895ef25b Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 20:25:33 -0700 Subject: [PATCH 092/168] Make file specs more robust --- spec/file-spec.js | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/spec/file-spec.js b/spec/file-spec.js index 335b9e1..74d59e0 100644 --- a/spec/file-spec.js +++ b/spec/file-spec.js @@ -21,6 +21,9 @@ describe('File', () => { file.unsubscribeFromNativeChangeEvents(); fs.removeSync(filePath); await PathWatcher.closeAllWatchers(); + // Without a brief pause between tests, events from previous tests can echo + // into the current ones. + await wait(50); }); it('normalizes the specified path', () => { @@ -200,11 +203,13 @@ describe('File', () => { afterEach(async () => { if (!fs.existsSync(newPath)) return; - // console.log(chalk.red('removing…')); - fs.removeSync(newPath); - // console.log(chalk.red('…removed.')); + if (file.getPath() !== newPath) { + fs.removeSync(newPath); + return; + } let deleteHandler = jasmine.createSpy('deleteHandler'); file.onDidDelete(deleteHandler); + fs.removeSync(newPath); await condition(() => deleteHandler.calls.count() > 0, 30000); }); @@ -212,9 +217,9 @@ describe('File', () => { let moveHandler = jasmine.createSpy('moveHandler'); file.onDidRename(moveHandler); - // console.log(chalk.blue('moving…')); + await wait(1000); + fs.moveSync(filePath, newPath); - // console.log(chalk.blue('…moved.')); await condition(() => moveHandler.calls.count() > 0, 30000); @@ -223,17 +228,20 @@ describe('File', () => { it('maintains ::onDidChange observers that were subscribed on the previous path', async () => { let moveHandler = jasmine.createSpy('moveHandler'); + file.onDidRename(moveHandler); + let changeHandler = jasmine.createSpy('changeHandler'); file.onDidChange(changeHandler); fs.moveSync(filePath, newPath); await condition(() => moveHandler.calls.count() > 0); - expect(changeHandler).not.toHaveBeenCalled(); + fs.writeFileSync(file.getPath(), 'this is new!'); + changeHandler.calls.reset(); await condition(() => changeHandler.calls.count() > 0); }); @@ -247,21 +255,16 @@ describe('File', () => { file.onDidDelete(deleteHandler); expect(changeHandler).not.toHaveBeenCalled(); - // console.log(chalk.blue('deleting…')); fs.removeSync(filePath); - // console.log(chalk.blue('…deleted.')); expect(changeHandler).not.toHaveBeenCalled(); await wait(20); expect(changeHandler).not.toHaveBeenCalled(); - // console.log(chalk.blue('resurrecting…')); fs.writeFileSync(filePath, 'HE HAS RISEN!'); - // console.log(chalk.blue('resurrected.')); - - await condition(() => changeHandler.calls.count() === 1); + await condition(() => changeHandler.calls.count() > 0); expect(deleteHandler).not.toHaveBeenCalled(); fs.writeFileSync(filePath, 'Hallelujah!'); From 11cd7519860e7d3a6e881ec69c818c7b6473d4ef Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 20:27:04 -0700 Subject: [PATCH 093/168] Move some specs away from `done` callbacks --- spec/pathwatcher-spec.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index 7fa9b0b..26625a4 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -85,6 +85,7 @@ describe('PathWatcher', () => { it('fires the callback with the event type and null path', async () => { let deleted = false; PathWatcher.watch(tempFile, (type, path) => { + if (type === 'delete' && path === null) { deleted = true; } @@ -117,48 +118,56 @@ describe('PathWatcher', () => { }); describe('when a new file is created under a watched directory', () => { - it('fires the callback with the change event and empty path', (done) => { + it('fires the callback with the change event and empty path', async () => { let newFile = path.join(tempDir, 'file'); if (fs.existsSync(newFile)) { fs.unlinkSync(newFile); } + let done = false; PathWatcher.watch(tempDir, (type, path) => { - fs.unlinkSync(newFile); + if (fs.existsSync(newFile)) { + fs.unlinkSync(newFile); + } expect(type).toBe('change'); expect(path).toBe(''); - done(); + done = true; }); fs.writeFileSync(newFile, 'x'); + await condition(() => done); }); }); describe('when a file under a watched directory is moved', () => { - it('fires the callback with the change event and empty path', (done) => { + it('fires the callback with the change event and empty path', async () => { let newName = path.join(tempDir, 'file2'); + let done = false; PathWatcher.watch(tempDir, (type, path) => { expect(type).toBe('change'); expect(path).toBe(''); - done(); + done = true; }); fs.renameSync(tempFile, newName); + await condition(() => done); }); }); describe('when an exception is thrown in the closed watcher’s callback', () => { - it('does not crash', (done) => { + it('does not crash', async () => { + let done = false; let watcher = PathWatcher.watch(tempFile, (_type, _path) => { watcher.close(); try { throw new Error('test'); } catch (e) { - done(); + done = true; } }); fs.writeFileSync(tempFile, 'changed'); + await condition(() => done); }); }); From 4c5390a30ce90a92c084c61351b061cffe72f7e9 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 13 Oct 2024 20:33:18 -0700 Subject: [PATCH 094/168] (oops) --- lib/core.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core.cc b/lib/core.cc index 3ee2c63..602772a 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -57,7 +57,7 @@ void PathWatcherListener::handleFileAction( ) { if (isShuttingDown) return; std::lock_guard lock(shutdownMutex); - if (isShuttingDown); + if (isShuttingDown) return; // std::cout << "PathWatcherListener::handleFileAction" << std::endl; // std::cout << "Action: " << EventType(action, true) << ", Dir: " << dir << ", Filename: " << filename << ", Old Filename: " << oldFilename << std::endl; From 018fb3a3fd6b07ce5cccf55721e20f30816dd4a3 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 14 Oct 2024 10:56:54 -0700 Subject: [PATCH 095/168] Initialize `fileWatcher` as `nullptr` --- lib/addon-data.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/addon-data.h b/lib/addon-data.h index b44b6b6..5627faa 100644 --- a/lib/addon-data.h +++ b/lib/addon-data.h @@ -11,6 +11,6 @@ class AddonData final { int id; int watchCount = 0; - efsw::FileWatcher* fileWatcher; + efsw::FileWatcher* fileWatcher = nullptr; std::unordered_map listeners; }; From b22e5581231f6a75f6758bddf6e1bedeb0ddb8f7 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 14 Oct 2024 11:48:58 -0700 Subject: [PATCH 096/168] Add back AtomDoc for `File` and `Directory` --- src/directory.js | 97 ++++++++++++++++++++-- src/file.js | 209 +++++++++++++++++++++++++++++++++++------------ 2 files changed, 248 insertions(+), 58 deletions(-) diff --git a/src/directory.js b/src/directory.js index 7acdc0c..f099c5e 100644 --- a/src/directory.js +++ b/src/directory.js @@ -7,10 +7,24 @@ const { Emitter, Disposable } = require('event-kit'); const File = require('./file'); const PathWatcher = require('./main'); +// Extended: Represents a directory on disk that can be traversed or watched +// for changes. class Directory { realPath = null; subscriptionCount = 0; + /* + Section: Construction + */ + + // Public: Configures a new {Directory} instance. + // + // No files are accessed. The directory does not yet need to exist. + // + // * `directoryPath` A {String} containing the absolute path to the + // directory. + // * `symlink` (optional) A {Boolean} indicating if the path is a symlink + // (default: `false`). constructor(directoryPath, symlink = false, includeDeprecatedAPIs = Grim.includeDeprecatedAPIs) { this.emitter = new Emitter(); this.symlink = symlink; @@ -35,6 +49,14 @@ class Directory { } } + // Public: Creates the directory on disk that corresponds to {::getPath} if + // no such directory already exists. + // + // * `mode` (optional) {Number} that defaults to `0777` and represents the + // default permissions of the directory on supported platforms. + // + // Returns a {Promise} that resolves to a {Boolean}: `true` if the directory + // was created and `false` if it already existed. async create (mode = 0o0777) { let isExistingDirectory = await this.exists(); if (isExistingDirectory) return false; @@ -53,6 +75,21 @@ class Directory { }); } + /* + Section: Event Subscription + */ + + // Public: Invoke the given callback when the directory’s contents change. + // + // A directory’s contents are considered to have changed when one of its + // children is added, deleted, or renamed. This callback will not fire when + // one of its children has its contents changed. + // + // * `callback` {Function} to be called when the directory’s contents change. + // Takes no arguments. + // + // Returns a {Disposable} on which {Disposable::dispose} can be called to + // unsubscribe. onDidChange (callback) { this.willAddSubscription(); return this.trackUnsubscription( @@ -81,37 +118,62 @@ class Directory { }); } + // Public: Returns a {Boolean}; always `false`. isFile () { return false; } + // Public: Returns a {Boolean}; always `true`. isDirectory() { return true; } + // Public: Returns a {Boolean} indicating whetehr or not this is a symbolic + // link. isSymbolicLink () { return this.symlink; } + // Public: Returns a {Promise} that resolves to a {Boolean}: `true` if the + // directory exists, `false` otherwise. exists () { return new Promise((resolve) => { FS.exists(this.getPath(), resolve) }); } + // Public: Returns a {Boolean}: `true` if the directory exists, `false` + // otherwise. existsSync () { return FS.existsSync(this.getPath()); } + // Public: Returns a {Boolean}: `true` if this {Directory} is the root + // directory of the filesystem, or `false` if it isn’t. isRoot () { let realPath = this.getRealPathSync(); return realPath === this.getParent().getRealPathSync(); } + /* + Section: Managing Paths + */ + + // Public: Returns the directory’s {String} path. + // + // This may include unfollowed symlinks or relative directory entries; or it + // may be fully resolved. It depends on what you give it. Anything that + // Node’s builtin `fs` and `path` libraries can resolve will also be + // understood by {Directory}. getPath () { return this.path; } + // Public: Returns the directory’s resolved {String} path, resolving symlinks + // if necessary. + // + // This will always be an absolute path; all relative paths are resolved and + // all symlinks are followed. getRealPathSync () { if (!this.realPath) { try { @@ -129,10 +191,17 @@ class Directory { return this.realPath; } + // Public: Returns the {String} basename of the directory. getBaseName () { return Path.basename(this.path); } + // Public: Returns the relative {String} path to the given path from this + // directory. If the given path is not a descendant of this directory, will + // return its full absolute path. + // + // * `fullPath` A path to compare against the real, absolute path of this + // {Directory}. relativize (fullPath) { if (!fullPath) return fullPath; @@ -173,6 +242,14 @@ class Directory { } } + // Public: Resolves the given relative path to an absolute path relative to + // this directory. If the path is already absolute or prefixed with a URI + // scheme, it is returned unchanged. + // + // * `uri` A {String} containing the path to resolve. + // + // Returns a {String} containing an absolute path, or `undefined` if the + // given URI is falsy (like an empty string). resolve (relativePath) { if (!relativePath) return; @@ -188,8 +265,13 @@ class Directory { } } - // TRAVERSING + /* + Section: Traversing + */ + // Public: Traverse to the parent directory. + // + // Returns a {Directory}. getParent () { return new Directory(Path.join(this.path, '..')); } @@ -209,9 +291,9 @@ class Directory { return new File(Path.join(this.getPath(), ...fileName)); } - // Public: Traverse within this a {Directory} to a child {Directory}. This - // method doesn't actually check to see if the {Directory} exists; it just - // creates the Directory object. + // Public: Traverse within this {Directory} to a child {Directory}. This + // method doesn't actually check to see if the directory exists; it just + // creates the {Directory} object. // // You can also access descendant directories by passing multiple arguments. // In this usage, all segments should be directory names. @@ -223,7 +305,8 @@ class Directory { return new Directory(Path.join(this.getPath(), ...dirName)); } - // Public: Reads file entries in this directory from disk asynchronously. + // Public: Reads file entries in this directory from disk asynchronously and + // applies a function to each. // // * `callback` A {Function} to call with the following arguments: // * `error` An {Error}, may be null. @@ -331,7 +414,9 @@ class Directory { return this.isPathPrefixOf(directoryPath, pathToCheck); } - // PRIVATE + /* + Section: Private + */ subscribeToNativeChangeEvents () { this.watchSubscription ??= PathWatcher.watch( diff --git a/src/file.js b/src/file.js index 3ee5cb9..300b3f4 100644 --- a/src/file.js +++ b/src/file.js @@ -14,11 +14,24 @@ async function wait (ms) { const PathWatcher = require('./main'); +// Extended: Represents an individual file that can be watched, read from, and +// written to. class File { encoding = 'utf8'; realPath = null; subscriptionCount = 0; + /* + Section: Construction + */ + + // Public: Configures a new {File} instance. + // + // No files are accessed. The file does not yet need to exist. + // + // * `filePath` A {String} containing the absolute path to the file. + // * `symlink` (optional) A {Boolean} indicating if the path is a symlink + // (default: false). constructor( filePath, symlink = false, @@ -45,6 +58,13 @@ class File { this.reportOnDeprecations = true; } + // Public: Creates the file on disk that corresponds to {::getPath} if no + // such file already exists. + // + // Returns a {Promise} that resolves once the file is created on disk. It + // resolves to a boolean value that is `true` if the file was created or + // `false` if it already existed. + // async create () { let isExistingFile = await this.exists(); let parent; @@ -62,6 +82,13 @@ class File { Section: Event Subscription */ + // Public: Invoke the given callback when the file’s contents change. + // + // * `callback` {Function} to be called when the file’s contents change. + // Takes no arguments. + // + // Returns a {Disposable} on which {Disposable::dispose} can be called to + // unsubscribe. onDidChange (callback) { this.willAddSubscription(); // Add a small buffer here. If a file has changed, we want to wait briefly @@ -75,11 +102,25 @@ class File { return this.trackUnsubscription(this.emitter.on('did-change', wrappedCallback)); } + // Public: Invoke the given callback when the file’s path changes. + // + // * `callback` {Function} to be called when the file’s path changes. + // Takes no arguments. + // + // Returns a {Disposable} on which {Disposable::dispose} can be called to + // unsubscribe. onDidRename (callback) { this.willAddSubscription(); return this.trackUnsubscription(this.emitter.on('did-rename', callback)); } + // Public: Invoke the given callback when the file is deleted. + // + // * `callback` {Function} to be called when the file is deleted. + // Takes no arguments. + // + // Returns a {Disposable} on which {Disposable::dispose} can be called to + // unsubscribe. onDidDelete (callback) { this.willAddSubscription(); return this.trackUnsubscription(this.emitter.on('did-delete', callback)); @@ -114,26 +155,36 @@ class File { Section: File Metadata */ + // Public: Returns a {Boolean}; always `true`. isFile () { return true; } + // Public: Returns a {Boolean}; always `false`. isDirectory () { return false; } + // Public: Returns a {Boolean} indicating whether or not this is a symbolic + // link. isSymbolicLink () { return this.symlink; } + // Public: Returns a {Promise} that resolves to a {Boolean}: `true` if the + // file exists; `false` otherwise. async exists () { return new Promise((resolve) => FS.exists(this.getPath(), resolve)); } + // Public: Returns a {Boolean}: `true` if the file exists; `false` otherwise. existsSync () { return FS.existsSync(this.getPath()); } + // Public: Get the SHA-1 digest of this file. + // + // Returns a {Promise} that resolves to a {String}. async getDigest () { if (this.digest != null) { return this.digest; @@ -142,6 +193,9 @@ class File { return this.digest; } + // Public: Get the SHA-1 digest of this file. + // + // Returns a {String}. getDigestSync () { if (this.digest == null) { this.readSync(); @@ -149,6 +203,7 @@ class File { return this.digest; } + setDigest (contents) { this.digest = crypto .createHash('sha1') @@ -157,6 +212,10 @@ class File { return this.digest; } + // Public: Sets the file's character set encoding name. + // + // Supports `utf8` natively and whichever other encodings are supported by + // the `iconv-lite` package. setEncoding (encoding = 'utf8') { if (encoding !== 'utf8') { iconv ??= require('iconv-lite'); @@ -166,6 +225,8 @@ class File { return encoding; } + // Public: Returns the {String} encoding name for this file; default is + // `utf8`. getEncoding () { return this.encoding; } @@ -174,26 +235,25 @@ class File { Section: Managing Paths */ + // Public: Returns the {String} path for this file. getPath () { return this.path; } + // Public: Sets the path for the file. + // + // This should not normally need to be called; use it only when you know a + // file’s path has changed and you don’t want to rely on the internal + // renaming detection. + // + // * `path` {String} The new path to set; should be absolute. setPath (path) { this.path = path; this.realPath = null; } - getRealPathSync () { - if (this.realPath == null) { - try { - this.realPath = FS.realpathSync(this.path); - } catch (_error) { - this.realPath = this.path; - } - } - return this.realPath; - } - + // Public: Returns a {Promise} that resolves to this file’s completely + // resolved {String} path, following symlinks if necessary. async getRealPath () { if (this.realPath != null) { return this.realPath; @@ -207,6 +267,21 @@ class File { }); } + // Public: Returns this file’s completely resolved {String} path, following + // symlinks if necessary. + getRealPathSync () { + if (this.realPath == null) { + try { + this.realPath = FS.realpathSync(this.path); + } catch (_error) { + this.realPath = this.path; + } + } + return this.realPath; + } + + // Public: Returns the {String} filename of this file without its directory + // context. getBaseName () { return Path.basename(this.path); } @@ -215,6 +290,7 @@ class File { Section: Traversing */ + // Public: Returns the {Directory} that contains this file. getParent () { Directory ??= require('./directory'); return new Directory(Path.dirname(this.path)); @@ -225,35 +301,13 @@ class File { Section: Reading and Writing */ - readSync (flushCache) { - if (!this.existsSync()) { - this.cachedContents = null; - } else if ((this.cachedContents == null) || flushCache) { - let encoding = this.getEncoding(); - if (encoding === 'utf8') { - this.cachedContents = FS.readFileSync(this.getPath(), encoding); - } else { - iconv ??= require('iconv-lite'); - this.cachedContents = iconv.decode( - FS.readFileSync(this.getPath()), - encoding - ); - } - } - this.setDigest(this.cachedContents); - return this.cachedContents; - } - - writeFileSync (filePath, contents) { - let encoding = this.getEncoding(); - if (encoding === 'utf8') { - return FS.writeFileSync(filePath, contents, { encoding }); - } else { - iconv ??= require('iconv-lite'); - return FS.writeFileSync(filePath, iconv.encode(contents, encoding)); - } - } - + // Public: Reads the contents of the file. + // + // * `flushCache` A {Boolean} indicating whether to require a direct read or + // if a cached copy is acceptable. + // + // Returns a {Promise} that resolves to a {String} (if the file exists) or + // `null` (if it does not). async read (flushCache) { let contents; if (!flushCache && this.cachedContents != null) { @@ -278,6 +332,34 @@ class File { return contents; } + // Public: Reads the contents of the file synchronously. + // + // * `flushCache` A {Boolean} indicating whether to require a direct read or + // if a cached copy is acceptable. + // + // Returns a {String} (if the file exists) or `null` (if it does not). + readSync (flushCache) { + if (!this.existsSync()) { + this.cachedContents = null; + } else if ((this.cachedContents == null) || flushCache) { + let encoding = this.getEncoding(); + if (encoding === 'utf8') { + this.cachedContents = FS.readFileSync(this.getPath(), encoding); + } else { + iconv ??= require('iconv-lite'); + this.cachedContents = iconv.decode( + FS.readFileSync(this.getPath()), + encoding + ); + } + } + this.setDigest(this.cachedContents); + return this.cachedContents; + } + + // Public: Returns a stream to read the content of the file. + // + // Returns a {ReadStream} object. createReadStream () { let encoding = this.getEncoding(); if (encoding === 'utf8') { @@ -289,6 +371,11 @@ class File { } } + // Public: Overwrites the file with the given text. + // + // * `text` The {String} text to write to the underlying file. + // + // Returns a {Promise} that resolves when the file has been written. async write (text) { let previouslyExisted = await this.exists(); await this.writeFile(this.getPath(), text); @@ -299,18 +386,9 @@ class File { } } - createWriteStream () { - let encoding = this.getEncoding(); - if (encoding === 'utf8') { - return FS.createWriteStream(this.getPath(), { encoding }); - } else { - iconv ??= require('iconv-lite'); - let stream = iconv.encodeStream(encoding); - stream.pipe(FS.createWriteStream(this.getPath())); - return stream; - } - } - + // Public: Overwrites the file with the given text. + // + // * `text` The {String} text to write to the underlying file. writeSync (text) { let previouslyExisted = this.existsSync(); this.writeFileSync(this.getPath(), text); @@ -325,6 +403,22 @@ class File { } } + // Public: Returns a stream to write content to the file. + // + // Returns a {WriteStream} object. + createWriteStream () { + let encoding = this.getEncoding(); + if (encoding === 'utf8') { + return FS.createWriteStream(this.getPath(), { encoding }); + } else { + iconv ??= require('iconv-lite'); + let stream = iconv.encodeStream(encoding); + stream.pipe(FS.createWriteStream(this.getPath())); + return stream; + } + } + + // Internal helper method for writing to a file. async writeFile (filePath, contents) { let encoding = this.getEncoding(); if (encoding === 'utf8') { @@ -360,6 +454,17 @@ class File { } } + // Internal helper method for writing to a file. + writeFileSync (filePath, contents) { + let encoding = this.getEncoding(); + if (encoding === 'utf8') { + return FS.writeFileSync(filePath, contents, { encoding }); + } else { + iconv ??= require('iconv-lite'); + return FS.writeFileSync(filePath, iconv.encode(contents, encoding)); + } + } + /* Section: Private */ From b64e56493be33290112e2073c550a51944e388da Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 14 Oct 2024 12:26:40 -0700 Subject: [PATCH 097/168] Add some comments --- lib/addon-data.h | 5 ++++ lib/core.cc | 71 ++++++++++++++++++++++++++++++------------------ lib/core.h | 2 ++ 3 files changed, 51 insertions(+), 27 deletions(-) diff --git a/lib/addon-data.h b/lib/addon-data.h index 5627faa..1d683d8 100644 --- a/lib/addon-data.h +++ b/lib/addon-data.h @@ -9,8 +9,13 @@ class AddonData final { id = g_next_addon_data_id++; } + // A unique identifier for each environment. int id; + // The number of watchers active in this environment. int watchCount = 0; efsw::FileWatcher* fileWatcher = nullptr; + + // A map that associates `WatcherHandle` values with their + // `PathWatcherListener` instances. std::unordered_map listeners; }; diff --git a/lib/core.cc b/lib/core.cc index 602772a..12f0114 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -7,8 +7,6 @@ #include #endif -void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEvent* event); - PathWatcherListener::PathWatcherListener(Napi::Env env, Napi::Function fn) : callback(fn) { tsfn = Napi::ThreadSafeFunction::New( @@ -25,6 +23,7 @@ PathWatcherListener::~PathWatcherListener() { } void PathWatcherListener::Stop() { + // Prevent responders from acting while we shut down. std::lock_guard lock(shutdownMutex); isShuttingDown = true; if (tsfn) { @@ -55,11 +54,10 @@ void PathWatcherListener::handleFileAction( efsw::Action action, std::string oldFilename ) { + // Don't try to proceed if we've already started the shutdown process. if (isShuttingDown) return; std::lock_guard lock(shutdownMutex); if (isShuttingDown) return; - // std::cout << "PathWatcherListener::handleFileAction" << std::endl; - // std::cout << "Action: " << EventType(action, true) << ", Dir: " << dir << ", Filename: " << filename << ", Old Filename: " << oldFilename << std::endl; std::string newPathStr = dir + filename; std::vector newPath(newPathStr.begin(), newPathStr.end()); @@ -71,7 +69,7 @@ void PathWatcherListener::handleFileAction( // each `child-change` event. // // Luckily, we can easily check whether or not a file has actually been - // created on macOS; we can compare creation time to modification time. + // created on macOS: we can compare creation time to modification time. if (action == efsw::Action::Add) { struct stat file; if (stat(newPathStr.c_str(), &file) != 0) { @@ -82,7 +80,6 @@ void PathWatcherListener::handleFileAction( return; } } - #endif std::vector oldPath; @@ -94,20 +91,33 @@ void PathWatcherListener::handleFileAction( PathWatcherEvent* event = new PathWatcherEvent(action, watchId, newPath, oldPath); napi_status status = tsfn.BlockingCall(event, ProcessEvent); if (status != napi_ok) { - // std::cerr << "Error in BlockingCall: " << status << std::endl; - delete event; // Clean up if BlockingCall fails + // TODO: Not sure how this could fail, or how we should present it to the + // user if it does fail. This action runs on a separate thread and it's not + // immediately clear how we'd surface an exception from here. + delete event; } } void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEvent* event) { std::unique_ptr eventPtr(event); if (event == nullptr) { - // std::cerr << "ProcessEvent: event is null" << std::endl; + Napi::TypeError::New(env, "Unknown error handling filesystem event").ThrowAsJavaScriptException(); return; } + // Translate the event type to the expected event name in the JS code. + // + // NOTE: This library previously envisioned that some platforms would allow + // watching of files directly and some would require watching of a file's + // parent folder. EFSW uses the parent-folder approach on all platforms, so + // in practice we're not using half of the event names we used to use. That's + // why the second argument below is `true`. + // + // There might be some edge cases that we need to handle here; for instance, + // if we're watching a directory and that directory itself is deleted, then + // that should be `delete` rather than `child-delete`. Right now we deal with + // that in JavaScript, but we could handle it here instead. std::string eventName = EventType(event->type, true); - // std::cout << "ProcessEvent! " << eventName << std::endl; std::string newPath; std::string oldPath; @@ -131,19 +141,17 @@ void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEvent* even Napi::String::New(env, oldPath) }); } catch (const Napi::Error& e) { - // TODO: Unsure why this would happen; but if it's plausible that it would - // happen sometimes, then figure out how to surface it. - - // std::cerr << "Napi error in callback.Call: " << e.what() << std::endl; + // TODO: Unsure why this would happen. + Napi::TypeError::New(env, "Unknown error handling filesystem event").ThrowAsJavaScriptException(); } } Napi::Value EFSW::Watch(const Napi::CallbackInfo& info) { - // std::cout << "Watch" << std::endl; auto env = info.Env(); auto addonData = env.GetInstanceData(); Napi::HandleScope scope(env); + // First argument must be a string. if (!info[0].IsString()) { Napi::TypeError::New(env, "String required").ThrowAsJavaScriptException(); return env.Null(); @@ -152,62 +160,72 @@ Napi::Value EFSW::Watch(const Napi::CallbackInfo& info) { Napi::String path = info[0].ToString(); std::string cppPath(path); + // Second argument must be a callback. if (!info[1].IsFunction()) { Napi::TypeError::New(env, "Function required").ThrowAsJavaScriptException(); return env.Null(); } Napi::Function fn = info[1].As(); - PathWatcherListener* listener = new PathWatcherListener(env, fn); - // std::cout << "About to add handle for path: " << cppPath << std::endl; - + // The first call to `Watch` initializes a `FileWatcher`. if (!addonData->fileWatcher) { - // std::cout << "CREATING WATCHER!!!" << std::endl; addonData->fileWatcher = new efsw::FileWatcher(); addonData->fileWatcher->followSymlinks(true); addonData->fileWatcher->watch(); } + // EFSW represents watchers as unsigned `int`s; we can easily convert these + // to JavaScript. WatcherHandle handle = addonData->fileWatcher->addWatch(path, listener, true); + if (handle >= 0) { addonData->listeners[handle] = listener; } else { delete listener; - Napi::Error::New(env, "Failed to add watch").ThrowAsJavaScriptException(); + Napi::Error::New(env, "Failed to add watch; unknown error").ThrowAsJavaScriptException(); return env.Null(); } - // std::cout << "Watcher handle: " << handle << std::endl; + addonData->watchCount++; + // The `watch` function returns a JavaScript number much like `setTimeout` or + // `setInterval` would; this is the handle that the consumer can use to + // unwatch the path later. return WatcherHandleToV8Value(handle, env); } Napi::Value EFSW::Unwatch(const Napi::CallbackInfo& info) { auto env = info.Env(); auto addonData = env.GetInstanceData(); - // std::cout << "Unwatch ID:" << addonData->id << std::endl; Napi::HandleScope scope(env); + // Our sole argument must be a JavaScript number; we convert it to a watcher + // handle. if (!IsV8ValueWatcherHandle(info[0])) { Napi::TypeError::New(env, "Argument must be a number").ThrowAsJavaScriptException(); return env.Null(); } WatcherHandle handle = V8ValueToWatcherHandle(info[0].As()); - // std::cout << "About to unwatch handle:" << handle << std::endl; + // EFSW doesn’t mind if we give it a handle that it doesn’t recognize; it’ll + // just silently do nothing. addonData->fileWatcher->removeWatch(handle); + // Since we’re not listening anymore, we have to stop the associated + // `PathWatcherListener` or else Node will think there’s an open handle. auto it = addonData->listeners.find(handle); if (it != addonData->listeners.end()) { - it->second->Stop(); // Release the ThreadSafeFunction - addonData->listeners.erase(it); // Remove from the map + it->second->Stop(); + addonData->listeners.erase(it); } addonData->watchCount--; if (addonData->watchCount == 0) { + // When this environment isn’t watching any files, we can stop the + // `FileWatcher` instance. We’ll start it up again if `Watch` is called. EFSW::Cleanup(env); } @@ -218,7 +236,7 @@ void EFSW::Cleanup(Napi::Env env) { auto addonData = env.GetInstanceData(); delete addonData->fileWatcher; if (addonData && addonData->fileWatcher) { - // Clean up all listeners + // Clean up all outstanding listeners. for (auto& pair : addonData->listeners) { pair.second->Stop(); } @@ -228,6 +246,5 @@ void EFSW::Cleanup(Napi::Env env) { void EFSW::Init(Napi::Env env) { auto addonData = env.GetInstanceData(); - // std::cout << "Addon data created!" << addonData->id << std::endl; addonData->watchCount = 0; } diff --git a/lib/core.h b/lib/core.h index 1347914..1802866 100644 --- a/lib/core.h +++ b/lib/core.h @@ -87,6 +87,8 @@ class PathWatcherListener: public efsw::FileWatchListener { Napi::ThreadSafeFunction tsfn; }; +void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEvent* event); + namespace EFSW { class Watcher { public: From 6c64f15441c248bdf9ceaa2f3cbe4609a780fb94 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 14 Oct 2024 12:44:20 -0700 Subject: [PATCH 098/168] Convert the `Gruntfile` to JS --- Gruntfile.coffee | 77 ------------------------------------------------ Gruntfile.js | 48 ++++++++++++++++++++++++++++++ package.json | 2 -- 3 files changed, 48 insertions(+), 79 deletions(-) delete mode 100644 Gruntfile.coffee create mode 100644 Gruntfile.js diff --git a/Gruntfile.coffee b/Gruntfile.coffee deleted file mode 100644 index edd1b89..0000000 --- a/Gruntfile.coffee +++ /dev/null @@ -1,77 +0,0 @@ -module.exports = (grunt) -> - grunt.initConfig - pkg: grunt.file.readJSON('package.json') - - coffee: - glob_to_multiple: - expand: true - cwd: 'src' - src: ['*.coffee'] - dest: 'lib' - ext: '.js' - - coffeelint: - options: - no_empty_param_list: - level: 'error' - max_line_length: - level: 'ignore' - - src: ['src/**/*.coffee'] - test: ['spec/**/*.coffee'] - gruntfile: ['Gruntfile.coffee'] - - cpplint: - files: ['src/**/*.cc'] - reporter: 'spec' - verbosity: 1 - filters: - build: - include: false - legal: - copyright: false - readability: - braces: false - runtime: - references: false - sizeof: false - whitespace: - line_length: false - - shell: - rebuild: - command: 'npm build .' - options: - stdout: true - stderr: true - failOnError: true - - test: - command: 'node node_modules/jasmine-tagged/bin/jasmine-tagged --captureExceptions --coffee spec/' - options: - stdout: true - stderr: true - failOnError: true - - 'update-atomdoc': - command: 'npm update grunt-atomdoc' - options: - stdout: true - stderr: true - failOnError: true - - grunt.loadNpmTasks('grunt-contrib-coffee') - grunt.loadNpmTasks('grunt-shell') - grunt.loadNpmTasks('grunt-coffeelint') - # grunt.loadNpmTasks('node-cpplint') - grunt.loadNpmTasks('grunt-atomdoc') - - # grunt.registerTask('lint', ['coffeelint', 'cpplint']) - grunt.registerTask('default', ['coffee', 'lint', 'shell:rebuild']) - grunt.registerTask('test', ['default', 'shell:test']) - grunt.registerTask('prepublish', ['shell:update-atomdoc', 'atomdoc']) - grunt.registerTask 'clean', -> - rm = require('rimraf').sync - rm 'build' - rm 'lib' - rm 'api.json' diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..9560bef --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,48 @@ +const DEFAULT_COMMAND_OPTIONS = { + stdout: true, + stderr: true, + failOnError: true +}; + +function defineTasks (grunt) { + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + + shell: { + rebuild: { + command: `npm build .`, + options: DEFAULT_COMMAND_OPTIONS + }, + + test: { + command: `npm test`, + options: DEFAULT_COMMAND_OPTIONS + }, + + 'update-atomdoc': { + command: 'npm update grunt-atomdoc', + options: DEFAULT_COMMAND_OPTIONS + } + } + }); + + grunt.loadNpmTasks('grunt-shell'); + grunt.loadNpmTasks('grunt-atomdoc'); + + grunt.registerTask('default', ['shell:rebuild']); + grunt.registerTask('test', ['default', 'shell:test']); + + // TODO: AtomDoc is not being generated now that we've decaffeinated the + // source files. We should use `joanna` instead, but it needs some + // modernization to understand current JS syntax. + grunt.registerTask('prepublish', ['shell:update-atomdoc', 'atomdoc']); + + grunt.registerTask('clean', () => { + let rm = require('rimraf').sync; + rm('build'); + rm('lib'); + rm('api.json'); + }); +} + +module.exports = defineTasks; diff --git a/package.json b/package.json index 3707f59..3f1c1e4 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,6 @@ "grunt": "~0.4.1", "grunt-atomdoc": "^1.0", "grunt-cli": "~0.1.7", - "grunt-coffeelint": "git+https://github.com/atom/grunt-coffeelint.git", - "grunt-contrib-coffee": "~0.9.0", "grunt-shell": "~0.2.2", "jasmine": "^5.3.1", "node-addon-api": "^8.1.0", From a45e957a3dd6dbc50e0ae8f3769c5e46ac0dee8b Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 14 Oct 2024 12:44:48 -0700 Subject: [PATCH 099/168] Update `.gitignore` --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index a563453..60622ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.DS_Store *.swp build/ node_modules/ @@ -6,3 +7,4 @@ npm-debug.log api.json package-lock.json .cache/ +compile_commands.json From c59bbb181f4f2968e22fdd2049d35969b49f0106 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 14 Oct 2024 12:56:54 -0700 Subject: [PATCH 100/168] Amend `README` --- README.md | 48 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index ff4e73e..c9f5915 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ -# Path Watcher Node module -![ci](https://github.com/atom/node-pathwatcher/workflows/ci/badge.svg) -[![Dependency Status](https://david-dm.org/atom/node-pathwatcher/status.svg)](https://david-dm.org/atom/node-pathwatcher) +# node-pathwatcher + +Watch files and directories for changes. + + +> [!IMPORTANT] +> This library is used in [Pulsar][] in several places for compatibility reasons. The [nsfw](https://www.npmjs.com/package/nsfw) library is more robust and more widely used; it is available in Pulsar via `atom.watchPath` and is usually a better choice. +> +> The purpose of this library’s continued inclusion in Pulsar is to provide the [File][] and [Directory][] classes that have long been available as exports via `require('atom')`. ## Installing @@ -16,24 +22,34 @@ npm install pathwatcher ## Using -```coffeescript -PathWatcher = require 'pathwatcher' +```js +const PathWatcher = require('pathwatcher'); ``` -### PathWatcher.watch(filename, [listener]) +### PathWatcher.watch(filename, listener) -Watch for changes on `filename`, where `filename` is either a file or a -directory. The returned object is a `PathWatcher`. +Watch for changes on `filename`, where `filename` is either a file or a directory. Returns a number that represents a specific watcher instance. -The listener callback gets two arguments `(event, path)`. `event` can be `rename`, -`delete` or `change`, and `path` is the path of the file which triggered the -event. +The listener callback gets two arguments: `(event, path)`. `event` can be `rename`, `delete` or `change`, and `path` is the path of the file which triggered the event. -For directories, the `change` event is emitted when a file or directory under -the watched directory got created or deleted. And the `PathWatcher.watch` is -not recursive, so changes of subdirectories under the watched directory would -not be detected. +For directories, the `change` event is emitted when a file or directory under the watched directory is created, deleted, or renamed. The watcher is not recursive; changes to the contents of subdirectories will not be detected. -### PathWatcher.close() +### PathWatcher.close(handle) Stop watching for changes on the given `PathWatcher`. + +The `handle` argument is a number and should be the return value from the initial call to `PathWatcher.watch`. + +### File and Directory + +These are convenience wrappers around some filesystem operations. They also wrap `PathWatcher.watch` via their `onDidChange` (and similar) methods. + +Documentation can be found on the Pulsar documentation site: + +* [File][] +* [Directory][] + + +[File]: https://docs.pulsar-edit.dev/api/pulsar/latest/File/ +[Directory]: https://docs.pulsar-edit.dev/api/pulsar/latest/Directory/ +[Pulsar]: https://pulsar-edit.dev From 90fd3f0cac6a49b3c0521c54dd2c887cd7b37018 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 14 Oct 2024 15:35:24 -0700 Subject: [PATCH 101/168] Start replacing the Grunt tasks --- package.json | 4 ++-- scripts/clean.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 scripts/clean.js diff --git a/package.json b/package.json index 3f1c1e4..cecd2b7 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,9 @@ }, "homepage": "http://atom.github.io/node-pathwatcher", "scripts": { - "prepublish": "grunt prepublish", "test": "node spec/run.js", - "test-context-safety": "node spec/context-safety.js" + "test-context-safety": "node spec/context-safety.js", + "clean": "node scripts/clean.js" }, "devDependencies": { "chalk": "^4.1.2", diff --git a/scripts/clean.js b/scripts/clean.js new file mode 100644 index 0000000..f7344c2 --- /dev/null +++ b/scripts/clean.js @@ -0,0 +1,17 @@ +const fs = require('fs/promises'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '..'); + +async function rimraf (filePath) { + if (!path.isAbsolute(filePath)) { + filePath = path.resolve(ROOT, filePath); + } + await fs.rm(filePath, { recursive: true, force: true }); +} + + +(async () => { + await rimraf('build'); + await rimraf('api.json'); +})(); From 187909640e0bb3ebb68180e265594e0ecbf18e23 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Mon, 14 Oct 2024 15:35:54 -0700 Subject: [PATCH 102/168] Update `README` to mention submodule init --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c9f5915..b1ca46b 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,10 @@ npm install pathwatcher ## Building - * Clone the repository - * Run `npm install` to install the dependencies - * Run `npm test` to run the specs +* Clone the repository +* `git submodule init && git submodule update` +* Run `npm install` to install the dependencies +* Run `npm test` to run the specs ## Using From 0ad1f4d884409df6a5dcf546306fa8b562996a40 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 15 Oct 2024 11:52:16 -0700 Subject: [PATCH 103/168] Add preinstall script to initialize submodules when necessary --- package.json | 2 ++ scripts/preinstall.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 scripts/preinstall.js diff --git a/package.json b/package.json index cecd2b7..8876823 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "homepage": "http://atom.github.io/node-pathwatcher", "scripts": { + "preinstall": "node scripts/preinstall.js", "test": "node spec/run.js", "test-context-safety": "node spec/context-safety.js", "clean": "node scripts/clean.js" @@ -34,6 +35,7 @@ "rimraf": "~2.2.0", "segfault-handler": "^1.3.0", "temp": "~0.9.0", + "tinyexec": "^0.3.0", "why-is-node-running": "^2.3.0" }, "dependencies": { diff --git a/scripts/preinstall.js b/scripts/preinstall.js new file mode 100644 index 0000000..f1c3a40 --- /dev/null +++ b/scripts/preinstall.js @@ -0,0 +1,15 @@ +const FS = require('fs'); +const Path = require('path'); +const { x } = require('tinyexec'); + +async function initSubmodules () { + await x('git', ['submodule', 'init']); + await x('git', ['submodule', 'update']); +} + +if (!FS.existsSync(Path.resolve(__dirname, '..', 'vendor', 'efsw'))) { + console.log('Initializing EFSW submodule…'); + initSubmodules().then(() => console.log('…done.')); +} else { + console.log('EFSW already present; skipping submodule init'); +} From 46087e90c6347b248129e09ff8b5d60c0497a323 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 15 Oct 2024 11:57:12 -0700 Subject: [PATCH 104/168] Ehh, just use `child_process` instead of a new dependency --- package.json | 1 - scripts/preinstall.js | 20 +++++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 8876823..a053745 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "rimraf": "~2.2.0", "segfault-handler": "^1.3.0", "temp": "~0.9.0", - "tinyexec": "^0.3.0", "why-is-node-running": "^2.3.0" }, "dependencies": { diff --git a/scripts/preinstall.js b/scripts/preinstall.js index f1c3a40..71a1061 100644 --- a/scripts/preinstall.js +++ b/scripts/preinstall.js @@ -1,10 +1,24 @@ const FS = require('fs'); const Path = require('path'); -const { x } = require('tinyexec'); +const CP = require('child_process'); + +async function exec (command, args) { + return new Promise((resolve, reject) => { + let proc = CP.spawn(command, args); + let stderr = []; + let stdout = []; + proc.stdout.on('data', (data) => stdout.push(data.toString())); + proc.stdout.on('error', (error) => stderr.push(error.toString())); + proc.on('close', () => { + if (stderr.length > 9) reject(stderr.join('')); + else resolve(stdout.join('')); + }); + }); +} async function initSubmodules () { - await x('git', ['submodule', 'init']); - await x('git', ['submodule', 'update']); + await exec('git', ['submodule', 'init']); + await exec('git', ['submodule', 'update']); } if (!FS.existsSync(Path.resolve(__dirname, '..', 'vendor', 'efsw'))) { From 3f7001c8f9095e4196adef26e9c1caf980571941 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 15 Oct 2024 12:16:26 -0700 Subject: [PATCH 105/168] Do it through Grunt instead --- Gruntfile.js | 9 +++++++-- package.json | 2 +- scripts/preinstall.js | 29 ----------------------------- 3 files changed, 8 insertions(+), 32 deletions(-) delete mode 100644 scripts/preinstall.js diff --git a/Gruntfile.js b/Gruntfile.js index 9560bef..cdc2025 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -9,8 +9,13 @@ function defineTasks (grunt) { pkg: grunt.file.readJSON('package.json'), shell: { + 'submodule-update': { + command: 'git submodule update --init', + options: DEFAULT_COMMAND_OPTIONS + }, + rebuild: { - command: `npm build .`, + command: `node-gyp rebuild`, options: DEFAULT_COMMAND_OPTIONS }, @@ -29,7 +34,7 @@ function defineTasks (grunt) { grunt.loadNpmTasks('grunt-shell'); grunt.loadNpmTasks('grunt-atomdoc'); - grunt.registerTask('default', ['shell:rebuild']); + grunt.registerTask('default', ['shell:submodule-update', 'shell:rebuild']); grunt.registerTask('test', ['default', 'shell:test']); // TODO: AtomDoc is not being generated now that we've decaffeinated the diff --git a/package.json b/package.json index a053745..c192f98 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ }, "homepage": "http://atom.github.io/node-pathwatcher", "scripts": { - "preinstall": "node scripts/preinstall.js", + "install": "grunt", "test": "node spec/run.js", "test-context-safety": "node spec/context-safety.js", "clean": "node scripts/clean.js" diff --git a/scripts/preinstall.js b/scripts/preinstall.js deleted file mode 100644 index 71a1061..0000000 --- a/scripts/preinstall.js +++ /dev/null @@ -1,29 +0,0 @@ -const FS = require('fs'); -const Path = require('path'); -const CP = require('child_process'); - -async function exec (command, args) { - return new Promise((resolve, reject) => { - let proc = CP.spawn(command, args); - let stderr = []; - let stdout = []; - proc.stdout.on('data', (data) => stdout.push(data.toString())); - proc.stdout.on('error', (error) => stderr.push(error.toString())); - proc.on('close', () => { - if (stderr.length > 9) reject(stderr.join('')); - else resolve(stdout.join('')); - }); - }); -} - -async function initSubmodules () { - await exec('git', ['submodule', 'init']); - await exec('git', ['submodule', 'update']); -} - -if (!FS.existsSync(Path.resolve(__dirname, '..', 'vendor', 'efsw'))) { - console.log('Initializing EFSW submodule…'); - initSubmodules().then(() => console.log('…done.')); -} else { - console.log('EFSW already present; skipping submodule init'); -} From 95e85e14fe35ccb5d0da716b362dd53b7ad74aed Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 15 Oct 2024 12:20:55 -0700 Subject: [PATCH 106/168] No, we don't want Grunt to be a dependency --- package.json | 3 ++- scripts/preinstall.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 scripts/preinstall.js diff --git a/package.json b/package.json index c192f98..45ec372 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ }, "homepage": "http://atom.github.io/node-pathwatcher", "scripts": { - "install": "grunt", + "preinstall": "node scripts/preinstall.js", + "install": "node-gyp rebuild", "test": "node spec/run.js", "test-context-safety": "node spec/context-safety.js", "clean": "node scripts/clean.js" diff --git a/scripts/preinstall.js b/scripts/preinstall.js new file mode 100644 index 0000000..71a1061 --- /dev/null +++ b/scripts/preinstall.js @@ -0,0 +1,29 @@ +const FS = require('fs'); +const Path = require('path'); +const CP = require('child_process'); + +async function exec (command, args) { + return new Promise((resolve, reject) => { + let proc = CP.spawn(command, args); + let stderr = []; + let stdout = []; + proc.stdout.on('data', (data) => stdout.push(data.toString())); + proc.stdout.on('error', (error) => stderr.push(error.toString())); + proc.on('close', () => { + if (stderr.length > 9) reject(stderr.join('')); + else resolve(stdout.join('')); + }); + }); +} + +async function initSubmodules () { + await exec('git', ['submodule', 'init']); + await exec('git', ['submodule', 'update']); +} + +if (!FS.existsSync(Path.resolve(__dirname, '..', 'vendor', 'efsw'))) { + console.log('Initializing EFSW submodule…'); + initSubmodules().then(() => console.log('…done.')); +} else { + console.log('EFSW already present; skipping submodule init'); +} From 4f01c56acdd99978f86adf22047711cd0e5a1170 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 15 Oct 2024 12:22:15 -0700 Subject: [PATCH 107/168] Trial and error at this point --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 45ec372..94542e2 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,7 @@ }, "homepage": "http://atom.github.io/node-pathwatcher", "scripts": { - "preinstall": "node scripts/preinstall.js", - "install": "node-gyp rebuild", + "install": "node scripts/preinstall.js && node-gyp rebuild", "test": "node spec/run.js", "test-context-safety": "node spec/context-safety.js", "clean": "node scripts/clean.js" From 3725f254bf9b7cfaf3297dace66d0c9790593600 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 15 Oct 2024 12:23:45 -0700 Subject: [PATCH 108/168] Check for directory _contents_ --- scripts/preinstall.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/preinstall.js b/scripts/preinstall.js index 71a1061..0765674 100644 --- a/scripts/preinstall.js +++ b/scripts/preinstall.js @@ -21,7 +21,7 @@ async function initSubmodules () { await exec('git', ['submodule', 'update']); } -if (!FS.existsSync(Path.resolve(__dirname, '..', 'vendor', 'efsw'))) { +if (!FS.existsSync(Path.resolve(__dirname, '..', 'vendor', 'efsw', 'LICENSE'))) { console.log('Initializing EFSW submodule…'); initSubmodules().then(() => console.log('…done.')); } else { From f2a8ca2f338422f8a68b041a3f86386b55b111c4 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 15 Oct 2024 12:34:15 -0700 Subject: [PATCH 109/168] Un-submodule EFSW (too many hassles) --- .gitmodules | 3 - vendor/efsw | 1 - vendor/efsw/.DS_Store | Bin 0 -> 6148 bytes vendor/efsw/.clang-format | 18 + vendor/efsw/.ecode/project_build.json | 170 +++++ vendor/efsw/.github/workflows/main.yml | 52 ++ vendor/efsw/.gitignore | 21 + vendor/efsw/CMakeLists.txt | 198 +++++ vendor/efsw/LICENSE | 22 + vendor/efsw/README.md | 149 ++++ vendor/efsw/compile_flags.txt | 16 + vendor/efsw/efswConfig.cmake.in | 10 + vendor/efsw/include/efsw/efsw.h | 180 +++++ vendor/efsw/include/efsw/efsw.hpp | 242 ++++++ vendor/efsw/premake4.lua | 235 ++++++ vendor/efsw/premake5.lua | 238 ++++++ vendor/efsw/project/build.reldbginfo.sh | 9 + .../efsw/project/qtcreator-linux/efsw.cflags | 1 + .../efsw/project/qtcreator-linux/efsw.config | 1 + .../efsw/project/qtcreator-linux/efsw.creator | 1 + .../project/qtcreator-linux/efsw.creator.user | 344 +++++++++ .../project/qtcreator-linux/efsw.cxxflags | 1 + .../efsw/project/qtcreator-linux/efsw.files | 118 +++ .../project/qtcreator-linux/efsw.includes | 2 + vendor/efsw/project/qtcreator-osx/efsw.cflags | 1 + vendor/efsw/project/qtcreator-osx/efsw.config | 3 + .../efsw/project/qtcreator-osx/efsw.creator | 1 + .../project/qtcreator-osx/efsw.creator.user | 253 ++++++ .../efsw/project/qtcreator-osx/efsw.cxxflags | 1 + vendor/efsw/project/qtcreator-osx/efsw.files | 185 +++++ .../efsw/project/qtcreator-osx/efsw.includes | 7 + vendor/efsw/project/qtcreator-win/efsw.cflags | 1 + vendor/efsw/project/qtcreator-win/efsw.config | 1 + .../efsw/project/qtcreator-win/efsw.creator | 1 + .../project/qtcreator-win/efsw.creator.user | 211 +++++ .../efsw/project/qtcreator-win/efsw.cxxflags | 1 + vendor/efsw/project/qtcreator-win/efsw.files | 215 ++++++ .../efsw/project/qtcreator-win/efsw.includes | 2 + vendor/efsw/src/efsw/Atomic.hpp | 33 + vendor/efsw/src/efsw/Debug.cpp | 81 ++ vendor/efsw/src/efsw/Debug.hpp | 62 ++ vendor/efsw/src/efsw/DirWatcherGeneric.cpp | 388 ++++++++++ vendor/efsw/src/efsw/DirWatcherGeneric.hpp | 57 ++ vendor/efsw/src/efsw/DirectorySnapshot.cpp | 212 +++++ vendor/efsw/src/efsw/DirectorySnapshot.hpp | 45 ++ .../efsw/src/efsw/DirectorySnapshotDiff.cpp | 22 + .../efsw/src/efsw/DirectorySnapshotDiff.hpp | 35 + vendor/efsw/src/efsw/FileInfo.cpp | 240 ++++++ vendor/efsw/src/efsw/FileInfo.hpp | 64 ++ vendor/efsw/src/efsw/FileSystem.cpp | 136 ++++ vendor/efsw/src/efsw/FileSystem.hpp | 40 + vendor/efsw/src/efsw/FileWatcher.cpp | 120 +++ vendor/efsw/src/efsw/FileWatcherCWrapper.cpp | 131 ++++ vendor/efsw/src/efsw/FileWatcherFSEvents.cpp | 245 ++++++ vendor/efsw/src/efsw/FileWatcherFSEvents.hpp | 103 +++ vendor/efsw/src/efsw/FileWatcherGeneric.cpp | 158 ++++ vendor/efsw/src/efsw/FileWatcherGeneric.hpp | 61 ++ vendor/efsw/src/efsw/FileWatcherImpl.cpp | 34 + vendor/efsw/src/efsw/FileWatcherImpl.hpp | 64 ++ vendor/efsw/src/efsw/FileWatcherInotify.cpp | 562 ++++++++++++++ vendor/efsw/src/efsw/FileWatcherInotify.hpp | 86 +++ vendor/efsw/src/efsw/FileWatcherKqueue.cpp | 229 ++++++ vendor/efsw/src/efsw/FileWatcherKqueue.hpp | 81 ++ vendor/efsw/src/efsw/FileWatcherWin32.cpp | 267 +++++++ vendor/efsw/src/efsw/FileWatcherWin32.hpp | 71 ++ vendor/efsw/src/efsw/Lock.hpp | 21 + vendor/efsw/src/efsw/Log.cpp | 49 ++ vendor/efsw/src/efsw/Mutex.cpp | 20 + vendor/efsw/src/efsw/Mutex.hpp | 31 + vendor/efsw/src/efsw/String.cpp | 669 ++++++++++++++++ vendor/efsw/src/efsw/String.hpp | 630 +++++++++++++++ vendor/efsw/src/efsw/System.cpp | 22 + vendor/efsw/src/efsw/System.hpp | 25 + vendor/efsw/src/efsw/Thread.cpp | 41 + vendor/efsw/src/efsw/Thread.hpp | 100 +++ vendor/efsw/src/efsw/Utf.hpp | 721 ++++++++++++++++++ vendor/efsw/src/efsw/Utf.inl | 576 ++++++++++++++ vendor/efsw/src/efsw/Watcher.cpp | 10 + vendor/efsw/src/efsw/Watcher.hpp | 29 + vendor/efsw/src/efsw/WatcherFSEvents.cpp | 211 +++++ vendor/efsw/src/efsw/WatcherFSEvents.hpp | 64 ++ vendor/efsw/src/efsw/WatcherGeneric.cpp | 33 + vendor/efsw/src/efsw/WatcherGeneric.hpp | 29 + vendor/efsw/src/efsw/WatcherInotify.cpp | 21 + vendor/efsw/src/efsw/WatcherInotify.hpp | 23 + vendor/efsw/src/efsw/WatcherKqueue.cpp | 566 ++++++++++++++ vendor/efsw/src/efsw/WatcherKqueue.hpp | 97 +++ vendor/efsw/src/efsw/WatcherWin32.cpp | 263 +++++++ vendor/efsw/src/efsw/WatcherWin32.hpp | 79 ++ vendor/efsw/src/efsw/base.hpp | 129 ++++ vendor/efsw/src/efsw/inotify-nosys.h | 164 ++++ .../efsw/src/efsw/platform/platformimpl.hpp | 20 + .../efsw/platform/posix/FileSystemImpl.cpp | 251 ++++++ .../efsw/platform/posix/FileSystemImpl.hpp | 30 + .../src/efsw/platform/posix/MutexImpl.cpp | 28 + .../src/efsw/platform/posix/MutexImpl.hpp | 30 + .../src/efsw/platform/posix/SystemImpl.cpp | 168 ++++ .../src/efsw/platform/posix/SystemImpl.hpp | 25 + .../src/efsw/platform/posix/ThreadImpl.cpp | 62 ++ .../src/efsw/platform/posix/ThreadImpl.hpp | 39 + .../src/efsw/platform/win/FileSystemImpl.cpp | 111 +++ .../src/efsw/platform/win/FileSystemImpl.hpp | 31 + .../efsw/src/efsw/platform/win/MutexImpl.cpp | 25 + .../efsw/src/efsw/platform/win/MutexImpl.hpp | 33 + .../efsw/src/efsw/platform/win/SystemImpl.cpp | 46 ++ .../efsw/src/efsw/platform/win/SystemImpl.hpp | 25 + .../efsw/src/efsw/platform/win/ThreadImpl.cpp | 56 ++ .../efsw/src/efsw/platform/win/ThreadImpl.hpp | 42 + vendor/efsw/src/efsw/sophist.h | 147 ++++ vendor/efsw/src/test/efsw-test.c | 164 ++++ vendor/efsw/src/test/efsw-test.cpp | 139 ++++ 111 files changed, 12633 insertions(+), 4 deletions(-) delete mode 100644 .gitmodules delete mode 160000 vendor/efsw create mode 100644 vendor/efsw/.DS_Store create mode 100644 vendor/efsw/.clang-format create mode 100644 vendor/efsw/.ecode/project_build.json create mode 100644 vendor/efsw/.github/workflows/main.yml create mode 100644 vendor/efsw/.gitignore create mode 100644 vendor/efsw/CMakeLists.txt create mode 100644 vendor/efsw/LICENSE create mode 100644 vendor/efsw/README.md create mode 100644 vendor/efsw/compile_flags.txt create mode 100644 vendor/efsw/efswConfig.cmake.in create mode 100644 vendor/efsw/include/efsw/efsw.h create mode 100644 vendor/efsw/include/efsw/efsw.hpp create mode 100644 vendor/efsw/premake4.lua create mode 100644 vendor/efsw/premake5.lua create mode 100644 vendor/efsw/project/build.reldbginfo.sh create mode 100644 vendor/efsw/project/qtcreator-linux/efsw.cflags create mode 100644 vendor/efsw/project/qtcreator-linux/efsw.config create mode 100644 vendor/efsw/project/qtcreator-linux/efsw.creator create mode 100644 vendor/efsw/project/qtcreator-linux/efsw.creator.user create mode 100644 vendor/efsw/project/qtcreator-linux/efsw.cxxflags create mode 100644 vendor/efsw/project/qtcreator-linux/efsw.files create mode 100644 vendor/efsw/project/qtcreator-linux/efsw.includes create mode 100644 vendor/efsw/project/qtcreator-osx/efsw.cflags create mode 100644 vendor/efsw/project/qtcreator-osx/efsw.config create mode 100644 vendor/efsw/project/qtcreator-osx/efsw.creator create mode 100644 vendor/efsw/project/qtcreator-osx/efsw.creator.user create mode 100644 vendor/efsw/project/qtcreator-osx/efsw.cxxflags create mode 100644 vendor/efsw/project/qtcreator-osx/efsw.files create mode 100644 vendor/efsw/project/qtcreator-osx/efsw.includes create mode 100644 vendor/efsw/project/qtcreator-win/efsw.cflags create mode 100644 vendor/efsw/project/qtcreator-win/efsw.config create mode 100644 vendor/efsw/project/qtcreator-win/efsw.creator create mode 100644 vendor/efsw/project/qtcreator-win/efsw.creator.user create mode 100644 vendor/efsw/project/qtcreator-win/efsw.cxxflags create mode 100644 vendor/efsw/project/qtcreator-win/efsw.files create mode 100644 vendor/efsw/project/qtcreator-win/efsw.includes create mode 100644 vendor/efsw/src/efsw/Atomic.hpp create mode 100644 vendor/efsw/src/efsw/Debug.cpp create mode 100644 vendor/efsw/src/efsw/Debug.hpp create mode 100644 vendor/efsw/src/efsw/DirWatcherGeneric.cpp create mode 100644 vendor/efsw/src/efsw/DirWatcherGeneric.hpp create mode 100644 vendor/efsw/src/efsw/DirectorySnapshot.cpp create mode 100644 vendor/efsw/src/efsw/DirectorySnapshot.hpp create mode 100644 vendor/efsw/src/efsw/DirectorySnapshotDiff.cpp create mode 100644 vendor/efsw/src/efsw/DirectorySnapshotDiff.hpp create mode 100644 vendor/efsw/src/efsw/FileInfo.cpp create mode 100644 vendor/efsw/src/efsw/FileInfo.hpp create mode 100644 vendor/efsw/src/efsw/FileSystem.cpp create mode 100644 vendor/efsw/src/efsw/FileSystem.hpp create mode 100644 vendor/efsw/src/efsw/FileWatcher.cpp create mode 100644 vendor/efsw/src/efsw/FileWatcherCWrapper.cpp create mode 100644 vendor/efsw/src/efsw/FileWatcherFSEvents.cpp create mode 100644 vendor/efsw/src/efsw/FileWatcherFSEvents.hpp create mode 100644 vendor/efsw/src/efsw/FileWatcherGeneric.cpp create mode 100644 vendor/efsw/src/efsw/FileWatcherGeneric.hpp create mode 100644 vendor/efsw/src/efsw/FileWatcherImpl.cpp create mode 100644 vendor/efsw/src/efsw/FileWatcherImpl.hpp create mode 100644 vendor/efsw/src/efsw/FileWatcherInotify.cpp create mode 100644 vendor/efsw/src/efsw/FileWatcherInotify.hpp create mode 100644 vendor/efsw/src/efsw/FileWatcherKqueue.cpp create mode 100644 vendor/efsw/src/efsw/FileWatcherKqueue.hpp create mode 100644 vendor/efsw/src/efsw/FileWatcherWin32.cpp create mode 100644 vendor/efsw/src/efsw/FileWatcherWin32.hpp create mode 100644 vendor/efsw/src/efsw/Lock.hpp create mode 100644 vendor/efsw/src/efsw/Log.cpp create mode 100644 vendor/efsw/src/efsw/Mutex.cpp create mode 100644 vendor/efsw/src/efsw/Mutex.hpp create mode 100644 vendor/efsw/src/efsw/String.cpp create mode 100644 vendor/efsw/src/efsw/String.hpp create mode 100644 vendor/efsw/src/efsw/System.cpp create mode 100644 vendor/efsw/src/efsw/System.hpp create mode 100644 vendor/efsw/src/efsw/Thread.cpp create mode 100644 vendor/efsw/src/efsw/Thread.hpp create mode 100644 vendor/efsw/src/efsw/Utf.hpp create mode 100644 vendor/efsw/src/efsw/Utf.inl create mode 100644 vendor/efsw/src/efsw/Watcher.cpp create mode 100644 vendor/efsw/src/efsw/Watcher.hpp create mode 100644 vendor/efsw/src/efsw/WatcherFSEvents.cpp create mode 100644 vendor/efsw/src/efsw/WatcherFSEvents.hpp create mode 100644 vendor/efsw/src/efsw/WatcherGeneric.cpp create mode 100644 vendor/efsw/src/efsw/WatcherGeneric.hpp create mode 100644 vendor/efsw/src/efsw/WatcherInotify.cpp create mode 100644 vendor/efsw/src/efsw/WatcherInotify.hpp create mode 100644 vendor/efsw/src/efsw/WatcherKqueue.cpp create mode 100644 vendor/efsw/src/efsw/WatcherKqueue.hpp create mode 100644 vendor/efsw/src/efsw/WatcherWin32.cpp create mode 100644 vendor/efsw/src/efsw/WatcherWin32.hpp create mode 100644 vendor/efsw/src/efsw/base.hpp create mode 100644 vendor/efsw/src/efsw/inotify-nosys.h create mode 100644 vendor/efsw/src/efsw/platform/platformimpl.hpp create mode 100644 vendor/efsw/src/efsw/platform/posix/FileSystemImpl.cpp create mode 100644 vendor/efsw/src/efsw/platform/posix/FileSystemImpl.hpp create mode 100644 vendor/efsw/src/efsw/platform/posix/MutexImpl.cpp create mode 100644 vendor/efsw/src/efsw/platform/posix/MutexImpl.hpp create mode 100644 vendor/efsw/src/efsw/platform/posix/SystemImpl.cpp create mode 100644 vendor/efsw/src/efsw/platform/posix/SystemImpl.hpp create mode 100644 vendor/efsw/src/efsw/platform/posix/ThreadImpl.cpp create mode 100644 vendor/efsw/src/efsw/platform/posix/ThreadImpl.hpp create mode 100644 vendor/efsw/src/efsw/platform/win/FileSystemImpl.cpp create mode 100644 vendor/efsw/src/efsw/platform/win/FileSystemImpl.hpp create mode 100644 vendor/efsw/src/efsw/platform/win/MutexImpl.cpp create mode 100644 vendor/efsw/src/efsw/platform/win/MutexImpl.hpp create mode 100644 vendor/efsw/src/efsw/platform/win/SystemImpl.cpp create mode 100644 vendor/efsw/src/efsw/platform/win/SystemImpl.hpp create mode 100644 vendor/efsw/src/efsw/platform/win/ThreadImpl.cpp create mode 100644 vendor/efsw/src/efsw/platform/win/ThreadImpl.hpp create mode 100644 vendor/efsw/src/efsw/sophist.h create mode 100644 vendor/efsw/src/test/efsw-test.c create mode 100644 vendor/efsw/src/test/efsw-test.cpp diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index c9ebeba..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "vendor/efsw"] - path = vendor/efsw - url = https://github.com/SpartanJ/efsw.git diff --git a/vendor/efsw b/vendor/efsw deleted file mode 160000 index a064eb2..0000000 --- a/vendor/efsw +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a064eb20e1312634813c724acc3c8229cc04e0a2 diff --git a/vendor/efsw/.DS_Store b/vendor/efsw/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 + $ + $ + ) +endif() + +set(EFSW_CPP_SOURCE + src/efsw/Debug.cpp + src/efsw/DirectorySnapshot.cpp + src/efsw/DirectorySnapshotDiff.cpp + src/efsw/DirWatcherGeneric.cpp + src/efsw/FileInfo.cpp + src/efsw/FileSystem.cpp + src/efsw/FileWatcher.cpp + src/efsw/FileWatcherCWrapper.cpp + src/efsw/FileWatcherGeneric.cpp + src/efsw/FileWatcherImpl.cpp + src/efsw/Log.cpp + src/efsw/Mutex.cpp + src/efsw/String.cpp + src/efsw/System.cpp + src/efsw/Thread.cpp + src/efsw/Watcher.cpp + src/efsw/WatcherGeneric.cpp +) + +target_include_directories(efsw + PRIVATE src/ + PUBLIC + $ + $ + $ +) + +if(VERBOSE) + target_compile_definitions(efsw PRIVATE EFSW_VERBOSE) +endif() + +target_compile_features(efsw PRIVATE cxx_std_11) + +if(BUILD_SHARED_LIBS) + target_compile_definitions(efsw PRIVATE EFSW_DYNAMIC EFSW_EXPORTS) +endif() + +# platforms +if(WIN32) + list(APPEND EFSW_CPP_SOURCE + src/efsw/platform/win/FileSystemImpl.cpp + src/efsw/platform/win/MutexImpl.cpp + src/efsw/platform/win/SystemImpl.cpp + src/efsw/platform/win/ThreadImpl.cpp + ) +else() + list(APPEND EFSW_CPP_SOURCE + src/efsw/platform/posix/FileSystemImpl.cpp + src/efsw/platform/posix/MutexImpl.cpp + src/efsw/platform/posix/SystemImpl.cpp + src/efsw/platform/posix/ThreadImpl.cpp + ) +endif() + +# watcher implementations +if(APPLE) + list(APPEND EFSW_CPP_SOURCE + src/efsw/FileWatcherFSEvents.cpp + src/efsw/FileWatcherKqueue.cpp + src/efsw/WatcherFSEvents.cpp + src/efsw/WatcherKqueue.cpp + ) +elseif(WIN32) + list(APPEND EFSW_CPP_SOURCE + src/efsw/FileWatcherWin32.cpp + src/efsw/WatcherWin32.cpp + ) +elseif(${CMAKE_SYSTEM_NAME} MATCHES "Linux") + list(APPEND EFSW_CPP_SOURCE + src/efsw/FileWatcherInotify.cpp + src/efsw/WatcherInotify.cpp + ) + + find_path(EFSW_INOTIFY_H NAMES sys/inotify.h NO_CACHE) + if(EFSW_INOTIFY_H STREQUAL "EFSW_INOTIFY_H-NOTFOUND") + target_compile_definitions(efsw PRIVATE EFSW_INOTIFY_NOSYS) + endif() +elseif(${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD") + list(APPEND EFSW_CPP_SOURCE + src/efsw/FileWatcherKqueue.cpp + src/efsw/WatcherKqueue.cpp + ) +endif() + +if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" OR + (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC")) + target_compile_definitions(efsw PRIVATE _SCL_SECURE_NO_WARNINGS) +else() + target_compile_options(efsw PRIVATE -Wall -Wno-long-long -fPIC) +endif() + +target_compile_definitions(efsw PRIVATE $,DEBUG,NDEBUG>) + +if(APPLE) + set(MAC_LIBS "-framework CoreFoundation" "-framework CoreServices") + target_link_libraries(efsw PRIVATE ${MAC_LIBS}) + if(BUILD_STATIC_LIBS) + target_link_libraries(efsw-static PRIVATE ${MAC_LIBS}) + endif() +elseif(NOT(${CMAKE_SYSTEM_NAME} MATCHES "Haiku") AND NOT WIN32) + target_link_libraries(efsw PRIVATE Threads::Threads) +endif() + +target_sources(efsw PRIVATE ${EFSW_CPP_SOURCE}) + +if(BUILD_STATIC_LIBS) + target_sources(efsw-static PRIVATE ${EFSW_CPP_SOURCE}) +endif() + +include(CMakePackageConfigHelpers) + +set(packageDestDir "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}") + +configure_package_config_file( + ${CMAKE_CURRENT_SOURCE_DIR}/efswConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/cmake/efswConfig.cmake + INSTALL_DESTINATION "${packageDestDir}" + NO_SET_AND_CHECK_MACRO + NO_CHECK_REQUIRED_COMPONENTS_MACRO +) + +export(TARGETS efsw NAMESPACE efsw:: FILE ${CMAKE_CURRENT_BINARY_DIR}/cmake/${PROJECT_NAME}Targets.cmake) +if(BUILD_STATIC_LIBS) + export(TARGETS efsw-static NAMESPACE efsw:: APPEND FILE ${CMAKE_CURRENT_BINARY_DIR}/cmake/${PROJECT_NAME}Targets.cmake) +endif() + +if(EFSW_INSTALL) + install(TARGETS efsw EXPORT efswExport + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + ) + + install( + FILES + include/efsw/efsw.h include/efsw/efsw.hpp + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/efsw + ) + + if(BUILD_STATIC_LIBS) + install(TARGETS efsw-static EXPORT efswExport + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + ) + endif() + + install(EXPORT efswExport NAMESPACE efsw:: DESTINATION "${packageDestDir}" FILE ${PROJECT_NAME}Targets.cmake) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/cmake/efswConfig.cmake DESTINATION "${packageDestDir}") +endif() + +if(BUILD_TEST_APP) + # C++ test application + add_executable(efsw-test src/test/efsw-test.cpp) + target_link_libraries(efsw-test efsw-static) + + # C test application + add_executable(efsw-test-stdc src/test/efsw-test.c) + target_link_libraries(efsw-test-stdc efsw-static) +endif() diff --git a/vendor/efsw/LICENSE b/vendor/efsw/LICENSE new file mode 100644 index 0000000..37f354a --- /dev/null +++ b/vendor/efsw/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2020 Martín Lucas Golini + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +This software is a fork of the "simplefilewatcher" by James Wynn (james@jameswynn.com) +http://code.google.com/p/simplefilewatcher/ also MIT licensed. diff --git a/vendor/efsw/README.md b/vendor/efsw/README.md new file mode 100644 index 0000000..1c71940 --- /dev/null +++ b/vendor/efsw/README.md @@ -0,0 +1,149 @@ +Entropia File System Watcher ![efsw](https://web.ensoft.dev/efsw/efsw-logo.svg) +============================ + +[![build status](https://img.shields.io/github/actions/workflow/status/SpartanJ/efsw/main.yml?branch=master)](https://github.com/SpartanJ/efsw/actions?query=workflow%3Abuild) + +**efsw** is a C++ cross-platform file system watcher and notifier. + +**efsw** monitors the file system asynchronously for changes to files and directories by watching a list of specified paths, and raises events when a directory or file change. + +**efsw** supports recursive directories watch, tracking the entire sub directory tree. + +**efsw** currently supports the following platforms: + +* Linux via [inotify](http://en.wikipedia.org/wiki/Inotify) + +* Windows via [I/O Completion Ports](http://en.wikipedia.org/wiki/IOCP) + +* Mac OS X via [FSEvents](http://en.wikipedia.org/wiki/FSEvents) or [kqueue](http://en.wikipedia.org/wiki/Kqueue) + +* FreeBSD/BSD via [kqueue](http://en.wikipedia.org/wiki/Kqueue) + +* OS-independent generic watcher +(polling the disk for directory snapshots and comparing them periodically) + +If any of the backend fails to start for any reason, it will fallback to the OS-independent implementation. +This should never happen, except for the Kqueue implementation; see `Platform limitations and clarifications`. + +**Code License** +-------------- +[MIT License](http://www.opensource.org/licenses/mit-license.php) + +**Some example code:** +-------------------- + +```c++ +// Inherits from the abstract listener class, and implements the the file action handler +class UpdateListener : public efsw::FileWatchListener { + public: + void handleFileAction( efsw::WatchID watchid, const std::string& dir, + const std::string& filename, efsw::Action action, + std::string oldFilename ) override { + switch ( action ) { + case efsw::Actions::Add: + std::cout << "DIR (" << dir << ") FILE (" << filename << ") has event Added" + << std::endl; + break; + case efsw::Actions::Delete: + std::cout << "DIR (" << dir << ") FILE (" << filename << ") has event Delete" + << std::endl; + break; + case efsw::Actions::Modified: + std::cout << "DIR (" << dir << ") FILE (" << filename << ") has event Modified" + << std::endl; + break; + case efsw::Actions::Moved: + std::cout << "DIR (" << dir << ") FILE (" << filename << ") has event Moved from (" + << oldFilename << ")" << std::endl; + break; + default: + std::cout << "Should never happen!" << std::endl; + } + } +}; + +// Create the file system watcher instance +// efsw::FileWatcher allow a first boolean parameter that indicates if it should start with the +// generic file watcher instead of the platform specific backend +efsw::FileWatcher* fileWatcher = new efsw::FileWatcher(); + +// Create the instance of your efsw::FileWatcherListener implementation +UpdateListener* listener = new UpdateListener(); + +// Add a folder to watch, and get the efsw::WatchID +// It will watch the /tmp folder recursively ( the third parameter indicates that is recursive ) +// Reporting the files and directories changes to the instance of the listener +efsw::WatchID watchID = fileWatcher->addWatch( "/tmp", listener, true ); + +// Adds another directory to watch. This time as non-recursive. +efsw::WatchID watchID2 = fileWatcher->addWatch( "/usr", listener, false ); + +// For Windows, adds another watch, specifying to use a bigger buffer, to not miss events +// (do not use for network locations, see efsw.hpp for details). +efsw::WatchID watchID3 = fileWatcher->addWatch( "c:\\temp", listener, true, { (BufferSize, 128*1024) } ); + +// Start watching asynchronously the directories +fileWatcher->watch(); + +// Remove the second watcher added +// You can also call removeWatch by passing the watch path ( it must end with an slash or backslash +// in windows, since that's how internally it's saved ) +fileWatcher->removeWatch( watchID2 ); +``` + +**Dependencies** +-------------- +None :) + +**Compiling** +------------ +To generate project files you will need to [download and install](https://premake.github.io/download) [Premake](https://premake.github.io/docs/What-Is-Premake) + +Then you can generate the project for your platform by just going to the project directory where the premake4.lua file is located and executing: + +`premake5 gmake2` to generate project Makefiles, then `cd make/*YOURPLATFORM*/`, and finally `make` or `make config=release_x86_64` ( it will generate the static lib, the shared lib and the test application ). + +or + +`premake5 vs2022` to generate Visual Studio 2022 project. + +or + +`premake5 xcode4` to generate Xcode 4 project. + +There is also a cmake file that I don't officially support but it works just fine, provided by [Mohammed Nafees](https://github.com/mnafees) and improved by [Eugene Shalygin](https://github.com/zeule). + +**Platform limitations and clarifications** +------------------------------------------- + +Directory paths are expected to be encoded as UTF-8 strings in all platforms. + +handleFileAction returns UTF-8 strings in all platforms. + +Windows and FSEvents Mac OS X implementation can't follow symlinks ( it will ignore followSymlinks() and allowOutOfScopeLinks() ). + +Kqueue implementation is limited by the maximum number of file descriptors allowed per process by the OS. In the case of reaching the file descriptors limit ( in BSD around 18000 and in OS X around 10240 ), it will fallback to the generic file watcher. + +OS X will use only Kqueue if the OS X version is below 10.5. This implementation needs to be compiled separately from the OS X >= 10.5 implementation, since there's no way to compile FSEvents backend in OS X below 10.5. + +FSEvents for OS X Lion and beyond in some cases will generate more actions than in reality ocurred, since fine-grained implementation of FSEvents doesn't give the order of the actions retrieved. In some cases I need to guess/approximate the order of them. + +Generic watcher relies on the inode information to detect file and directories renames/move. Since Windows has no concept of inodes as Unix platforms do, there is no current reliable way of determining file/directory movement on Windows without help from the Windows API ( this is replaced with Add/Delete events ). + +Linux versions below 2.6.13 are not supported, since inotify wasn't implemented yet. I'm not interested in supporting older kernels, since I don't see the point. If someone needs this, open an issue in the issue tracker and I may consider implementing a dnotify backend. + +OS-independent watcher, Kqueue and FSEvents for OS X below 10.5 keep cache of the directories structures, to be able to detect changes in the directories. This means that there's a memory overhead for these backends. + +**Useful information** +-------------------- +The project also comes with a C API wrapper, contributed by [Sepul Sepehr Taghdisian](https://github.com/septag). + +There's a string manipulation class not exposed in the efsw header ( efsw::String ) that can be used to make string encoding conversion. + + +**Clarifications** +---------------- + +This software started as a fork of the [simplefilewatcher](http://code.google.com/p/simplefilewatcher/) by James Wynn (james[at]jameswynn.com), [MIT licensed](http://www.opensource.org/licenses/mit-license.html). + +The icon used for the project is part of the [Haiku®'s Icons](http://www.haiku-inc.org/haiku-icons.html), [MIT licensed](http://www.opensource.org/licenses/mit-license.html). diff --git a/vendor/efsw/compile_flags.txt b/vendor/efsw/compile_flags.txt new file mode 100644 index 0000000..d87aa27 --- /dev/null +++ b/vendor/efsw/compile_flags.txt @@ -0,0 +1,16 @@ +-Wno-documentation-unknown-command +-Wno-unknown-warning-option +-Wno-unknown-pragmas +-std=c++11 +-fsyntax-only +-Isrc +-Iinclude +-fmessage-length=0 +-fdiagnostics-show-note-include-stack +-fretain-comments-from-system-headers +-fmacro-backtrace-limit=0 +-ferror-limit=1000 +-Wall +-Wextra +-x +c++-header diff --git a/vendor/efsw/efswConfig.cmake.in b/vendor/efsw/efswConfig.cmake.in new file mode 100644 index 0000000..5340f33 --- /dev/null +++ b/vendor/efsw/efswConfig.cmake.in @@ -0,0 +1,10 @@ +# - Config file for the @CMAKE_PROJECT_NAME@ package + +@PACKAGE_INIT@ + +@DEPENDENCIES_SECTION@ +include(CMakeFindDependencyMacro) + +find_dependency(Threads) + +include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") diff --git a/vendor/efsw/include/efsw/efsw.h b/vendor/efsw/include/efsw/efsw.h new file mode 100644 index 0000000..ecb9ec4 --- /dev/null +++ b/vendor/efsw/include/efsw/efsw.h @@ -0,0 +1,180 @@ +/** + @author Sepul Sepehr Taghdisian + + Copyright (c) 2013 Martin Lucas Golini + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + This software is a fork of the "simplefilewatcher" by James Wynn (james@jameswynn.com) + http://code.google.com/p/simplefilewatcher/ also MIT licensed. +*/ +/** This is the C API wrapper of EFSW */ +#ifndef ESFW_H +#define ESFW_H + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(_WIN32) + #ifdef EFSW_DYNAMIC + // Windows platforms + #ifdef EFSW_EXPORTS + // From DLL side, we must export + #define EFSW_API __declspec(dllexport) + #else + // From client application side, we must import + #define EFSW_API __declspec(dllimport) + #endif + #else + // No specific directive needed for static build + #ifndef EFSW_API + #define EFSW_API + #endif + #endif +#else + #if ( __GNUC__ >= 4 ) && defined( EFSW_EXPORTS ) + #define EFSW_API __attribute__ ((visibility("default"))) + #endif + + // Other platforms don't need to define anything + #ifndef EFSW_API + #define EFSW_API + #endif +#endif + +/// Type for a watch id +typedef long efsw_watchid; + +/// Type for watcher +typedef void* efsw_watcher; + +enum efsw_action +{ + EFSW_ADD = 1, /// Sent when a file is created or renamed + EFSW_DELETE = 2, /// Sent when a file is deleted or renamed + EFSW_MODIFIED = 3, /// Sent when a file is modified + EFSW_MOVED = 4 /// Sent when a file is moved +}; + +enum efsw_error +{ + EFSW_NOTFOUND = -1, + EFSW_REPEATED = -2, + EFSW_OUTOFSCOPE = -3, + EFSW_NOTREADABLE = -4, + EFSW_REMOTE = -5, + EFSW_WATCHER_FAILED = -6, + EFSW_UNSPECIFIED = -7 +}; + +enum efsw_option +{ + /// For Windows, the default buffer size of 63*1024 bytes sometimes is not enough and + /// file system events may be dropped. For that, using a different (bigger) buffer size + /// can be defined here, but note that this does not work for network drives, + /// because a buffer larger than 64K will fail the folder being watched, see + /// http://msdn.microsoft.com/en-us/library/windows/desktop/aa365465(v=vs.85).aspx) + EFSW_OPT_WIN_BUFFER_SIZE = 1, + /// For Windows, per default all events are captured but we might only be interested + /// in a subset; the value of the option should be set to a bitwise or'ed set of + /// FILE_NOTIFY_CHANGE_* flags. + EFSW_OPT_WIN_NOTIFY_FILTER = 2 +}; + +/// Basic interface for listening for file events. +typedef void (*efsw_pfn_fileaction_callback) ( + efsw_watcher watcher, + efsw_watchid watchid, + const char* dir, + const char* filename, + enum efsw_action action, + const char* old_filename, + void* param +); + +typedef struct { + enum efsw_option option; + int value; +} efsw_watcher_option; + +/** + * Creates a new file-watcher + * @param generic_mode Force the use of the Generic file watcher + */ +efsw_watcher EFSW_API efsw_create(int generic_mode); + +/// Release the file-watcher and unwatch any directories +void EFSW_API efsw_release(efsw_watcher watcher); + +/// Retrieve last error occured by file-watcher +EFSW_API const char* efsw_getlasterror(); + +/// Reset file-watcher last error +EFSW_API void efsw_clearlasterror(); + +/// Add a directory watch +/// On error returns WatchID with Error type. +efsw_watchid EFSW_API efsw_addwatch(efsw_watcher watcher, const char* directory, + efsw_pfn_fileaction_callback callback_fn, int recursive, void* param); + +/// Add a directory watch, specifying options +/// @param options Pointer to an array of watcher options +/// @param nr_options Number of options referenced by \p options +efsw_watchid EFSW_API efsw_addwatch_withoptions(efsw_watcher watcher, const char* directory, + efsw_pfn_fileaction_callback callback_fn, int recursive, efsw_watcher_option *options, + int options_number, void* param); + +/// Remove a directory watch. This is a brute force search O(nlogn). +void EFSW_API efsw_removewatch(efsw_watcher watcher, const char* directory); + +/// Remove a directory watch. This is a map lookup O(logn). +void EFSW_API efsw_removewatch_byid(efsw_watcher watcher, efsw_watchid watchid); + +/// Starts watching ( in other thread ) +void EFSW_API efsw_watch(efsw_watcher watcher); + +/** + * Allow recursive watchers to follow symbolic links to other directories + * followSymlinks is disabled by default + */ +void EFSW_API efsw_follow_symlinks(efsw_watcher watcher, int enable); + +/** @return If can follow symbolic links to directorioes */ +int EFSW_API efsw_follow_symlinks_isenabled(efsw_watcher watcher); + +/** + * When enable this it will allow symlinks to watch recursively out of the pointed directory. + * follorSymlinks must be enabled to this work. + * For example, added symlink to /home/folder, and the symlink points to /, this by default is not allowed, + * it's only allowed to symlink anything from /home/ and deeper. This is to avoid great levels of recursion. + * Enabling this could lead in infinite recursion, and crash the watcher ( it will try not to avoid this ). + * Buy enabling out of scope links, it will allow this behavior. + * allowOutOfScopeLinks are disabled by default. + */ +void EFSW_API efsw_allow_outofscopelinks(efsw_watcher watcher, int allow); + +/// @return Returns if out of scope links are allowed +int EFSW_API efsw_outofscopelinks_isallowed(efsw_watcher watcher); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/vendor/efsw/include/efsw/efsw.hpp b/vendor/efsw/include/efsw/efsw.hpp new file mode 100644 index 0000000..b6519b6 --- /dev/null +++ b/vendor/efsw/include/efsw/efsw.hpp @@ -0,0 +1,242 @@ +/** + @author Martín Lucas Golini + + Copyright (c) 2013 Martín Lucas Golini + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + This software is a fork of the "simplefilewatcher" by James Wynn (james@jameswynn.com) + http://code.google.com/p/simplefilewatcher/ also MIT licensed. +*/ + +#ifndef ESFW_HPP +#define ESFW_HPP + +#include +#include +#include + +#if defined( _WIN32 ) +#ifdef EFSW_DYNAMIC +// Windows platforms +#ifdef EFSW_EXPORTS +// From DLL side, we must export +#define EFSW_API __declspec( dllexport ) +#else +// From client application side, we must import +#define EFSW_API __declspec( dllimport ) +#endif +#else +// No specific directive needed for static build +#ifndef EFSW_API +#define EFSW_API +#endif +#endif +#else +#if ( __GNUC__ >= 4 ) && defined( EFSW_EXPORTS ) +#ifndef EFSW_API +#define EFSW_API __attribute__( ( visibility( "default" ) ) ) +#endif +#endif + +// Other platforms don't need to define anything +#ifndef EFSW_API +#define EFSW_API +#endif +#endif + +namespace efsw { + +/// Type for a watch id +typedef long WatchID; + +// forward declarations +class FileWatcherImpl; +class FileWatchListener; +class WatcherOption; + +/// Actions to listen for. Rename will send two events, one for +/// the deletion of the old file, and one for the creation of the +/// new file. +namespace Actions { +enum Action { + /// Sent when a file is created or renamed + Add = 1, + /// Sent when a file is deleted or renamed + Delete = 2, + /// Sent when a file is modified + Modified = 3, + /// Sent when a file is moved + Moved = 4 +}; +} +typedef Actions::Action Action; + +/// Errors log namespace +namespace Errors { + +enum Error { + NoError = 0, + FileNotFound = -1, + FileRepeated = -2, + FileOutOfScope = -3, + FileNotReadable = -4, + /// Directory in remote file system + /// ( create a generic FileWatcher instance to watch this directory ). + FileRemote = -5, + /// File system watcher failed to watch for changes. + WatcherFailed = -6, + Unspecified = -7 +}; + +class EFSW_API Log { + public: + /// @return The last error logged + static std::string getLastErrorLog(); + + /// @return The code of the last error logged + static Error getLastErrorCode(); + + /// Reset last error + static void clearLastError(); + + /// Creates an error of the type specified + static Error createLastError( Error err, std::string log ); +}; + +} // namespace Errors +typedef Errors::Error Error; + +/// Optional file watcher settings. +namespace Options { +enum Option { + /// For Windows, the default buffer size of 63*1024 bytes sometimes is not enough and + /// file system events may be dropped. For that, using a different (bigger) buffer size + /// can be defined here, but note that this does not work for network drives, + /// because a buffer larger than 64K will fail the folder being watched, see + /// http://msdn.microsoft.com/en-us/library/windows/desktop/aa365465(v=vs.85).aspx) + WinBufferSize = 1, + /// For Windows, per default all events are captured but we might only be interested + /// in a subset; the value of the option should be set to a bitwise or'ed set of + /// FILE_NOTIFY_CHANGE_* flags. + WinNotifyFilter = 2 +}; +} +typedef Options::Option Option; + +/// Listens to files and directories and dispatches events +/// to notify the listener of files and directories changes. +/// @class FileWatcher +class EFSW_API FileWatcher { + public: + /// Default constructor, will use the default platform file watcher + FileWatcher(); + + /// Constructor that lets you force the use of the Generic File Watcher + explicit FileWatcher( bool useGenericFileWatcher ); + + virtual ~FileWatcher(); + + /// Add a directory watch. Same as the other addWatch, but doesn't have recursive option. + /// For backwards compatibility. + /// On error returns WatchID with Error type. + WatchID addWatch( const std::string& directory, FileWatchListener* watcher ); + + /// Add a directory watch + /// On error returns WatchID with Error type. + WatchID addWatch( const std::string& directory, FileWatchListener* watcher, bool recursive ); + + /// Add a directory watch, allowing customization with options + /// @param directory The folder to be watched + /// @param watcher The listener to receive events + /// @param recursive Set this to true to include subdirectories + /// @param options Allows customization of a watcher + /// @return Returns the watch id for the directory or, on error, a WatchID with Error type. + WatchID addWatch( const std::string& directory, FileWatchListener* watcher, bool recursive, + const std::vector &options ); + + /// Remove a directory watch. This is a brute force search O(nlogn). + void removeWatch( const std::string& directory ); + + /// Remove a directory watch. This is a map lookup O(logn). + void removeWatch( WatchID watchid ); + + /// Starts watching ( in other thread ) + void watch(); + + /// @return Returns a list of the directories that are being watched + std::vector directories(); + + /** Allow recursive watchers to follow symbolic links to other directories + * followSymlinks is disabled by default + */ + void followSymlinks( bool follow ); + + /** @return If can follow symbolic links to directorioes */ + const bool& followSymlinks() const; + + /** When enable this it will allow symlinks to watch recursively out of the pointed directory. + * follorSymlinks must be enabled to this work. + * For example, added symlink to /home/folder, and the symlink points to /, this by default is + * not allowed, it's only allowed to symlink anything from /home/ and deeper. This is to avoid + * great levels of recursion. Enabling this could lead in infinite recursion, and crash the + * watcher ( it will try not to avoid this ). Buy enabling out of scope links, it will allow + * this behavior. allowOutOfScopeLinks are disabled by default. + */ + void allowOutOfScopeLinks( bool allow ); + + /// @return Returns if out of scope links are allowed + const bool& allowOutOfScopeLinks() const; + + private: + /// The implementation + FileWatcherImpl* mImpl; + bool mFollowSymlinks; + bool mOutOfScopeLinks; +}; + +/// Basic interface for listening for file events. +/// @class FileWatchListener +class FileWatchListener { + public: + virtual ~FileWatchListener() {} + + /// Handles the action file action + /// @param watchid The watch id for the directory + /// @param dir The directory + /// @param filename The filename that was accessed (not full path) + /// @param action Action that was performed + /// @param oldFilename The name of the file or directory moved + virtual void handleFileAction( WatchID watchid, const std::string& dir, + const std::string& filename, Action action, + std::string oldFilename = "" ) = 0; +}; + +/// Optional, typically platform specific parameter for customization of a watcher. +/// @class WatcherOption +class WatcherOption { + public: + WatcherOption(Option option, int value) : mOption(option), mValue(value) {}; + Option mOption; + int mValue; +}; + +} // namespace efsw + +#endif diff --git a/vendor/efsw/premake4.lua b/vendor/efsw/premake4.lua new file mode 100644 index 0000000..2918390 --- /dev/null +++ b/vendor/efsw/premake4.lua @@ -0,0 +1,235 @@ +newoption { trigger = "verbose", description = "Build efsw with verbose mode." } +newoption { trigger = "strip-symbols", description = "Strip debugging symbols in other file ( only for relwithdbginfo configuration )." } +newoption { trigger = "thread-sanitizer", description ="Compile with ThreadSanitizer." } + +efsw_major_version = "1" +efsw_minor_version = "0" +efsw_patch_version = "2" +efsw_version = efsw_major_version .. "." .. efsw_minor_version .. "." .. efsw_patch_version + +function get_include_paths() + local function _insert_include_paths( file ) + local function _trim(s) + return (s:gsub("^%s*(.-)%s*$", "%1")) + end + + local paths = { } + local lines = file:read('*all') + + for line in string.gmatch(lines, '([^\n]+)') + do + table.insert( paths, _trim( line ) ) + end + + file:close() + + return paths + end + + local file = io.popen( "echo | gcc -Wp,-v -x c++ - -fsyntax-only 2>&1 | grep -v '#' | grep '/'", 'r' ) + local include_paths = _insert_include_paths( file ) + + if next(include_paths) == nil then + file = io.popen( "echo | clang++ -Wp,-v -x c++ - -fsyntax-only 2>&1 | grep -v '#' | grep '/' | grep -v 'nonexistent'", 'r' ) + + include_paths = _insert_include_paths( file ) + + if next(include_paths) == nil then + table.insert( include_paths, "/usr/include" ) + table.insert( include_paths, "/usr/local/include" ) + end + end + + return include_paths +end + +function inotify_header_exists() + local efsw_include_paths = get_include_paths() + + for _,v in pairs( efsw_include_paths ) + do + local cur_path = v .. "/sys/inotify.h" + + if os.isfile( cur_path ) then + return true + end + end + + return false +end + +function string.starts(String,Start) + if ( _ACTION ) then + return string.sub(String,1,string.len(Start))==Start + end + + return false +end + +function is_vs() + return ( string.starts(_ACTION,"vs") ) +end + +function conf_warnings() + if not is_vs() then + buildoptions{ "-Wall -Wno-long-long" } + + if not os.is("windows") then + buildoptions{ "-fPIC" } + end + else + defines { "_SCL_SECURE_NO_WARNINGS" } + end + + if _OPTIONS["thread-sanitizer"] then + buildoptions { "-fsanitize=thread" } + linkoptions { "-fsanitize=thread" } + if not os.is("macosx") then + links { "tsan" } + end + end +end + +function conf_links() + if not os.is("windows") and not os.is("haiku") then + links { "pthread" } + end + + if os.is("macosx") then + links { "CoreFoundation.framework", "CoreServices.framework" } + end +end + +function conf_excludes() + if os.is("windows") then + excludes { "src/efsw/WatcherKqueue.cpp", "src/efsw/WatcherFSEvents.cpp", "src/efsw/WatcherInotify.cpp", "src/efsw/FileWatcherKqueue.cpp", "src/efsw/FileWatcherInotify.cpp", "src/efsw/FileWatcherFSEvents.cpp" } + elseif os.is("linux") then + excludes { "src/efsw/WatcherKqueue.cpp", "src/efsw/WatcherFSEvents.cpp", "src/efsw/WatcherWin32.cpp", "src/efsw/FileWatcherKqueue.cpp", "src/efsw/FileWatcherWin32.cpp", "src/efsw/FileWatcherFSEvents.cpp" } + elseif os.is("macosx") then + excludes { "src/efsw/WatcherInotify.cpp", "src/efsw/WatcherWin32.cpp", "src/efsw/FileWatcherInotify.cpp", "src/efsw/FileWatcherWin32.cpp" } + elseif os.is("freebsd") then + excludes { "src/efsw/WatcherInotify.cpp", "src/efsw/WatcherWin32.cpp", "src/efsw/WatcherFSEvents.cpp", "src/efsw/FileWatcherInotify.cpp", "src/efsw/FileWatcherWin32.cpp", "src/efsw/FileWatcherFSEvents.cpp" } + end + + if os.is("linux") and not inotify_header_exists() then + defines { "EFSW_INOTIFY_NOSYS" } + end +end + +solution "efsw" + location("./make/" .. os.get() .. "/") + targetdir("./bin") + configurations { "debug", "release", "relwithdbginfo" } + + if os.is("windows") then + osfiles = "src/efsw/platform/win/*.cpp" + else + osfiles = "src/efsw/platform/posix/*.cpp" + end + + -- Activates verbose mode + if _OPTIONS["verbose"] then + defines { "EFSW_VERBOSE" } + end + + if not is_vs() then + buildoptions { "-std=c++11" } + end + + if os.is("macosx") then + -- Premake 4.4 needed for this + if not string.match(_PREMAKE_VERSION, "^4.[123]") then + local ver = os.getversion(); + + if not ( ver.majorversion >= 10 and ver.minorversion >= 5 ) then + defines { "EFSW_FSEVENTS_NOT_SUPPORTED" } + end + end + end + + objdir("obj/" .. os.get() .. "/") + + project "efsw-static-lib" + kind "StaticLib" + language "C++" + targetdir("./lib") + includedirs { "include", "src" } + files { "src/efsw/*.cpp", osfiles } + conf_excludes() + + configuration "debug" + defines { "DEBUG" } + flags { "Symbols" } + targetname "efsw-static-debug" + conf_warnings() + + configuration "release" + defines { "NDEBUG" } + flags { "Optimize" } + targetname "efsw-static-release" + conf_warnings() + + configuration "relwithdbginfo" + defines { "NDEBUG" } + flags { "Optimize", "Symbols" } + targetname "efsw-static-reldbginfo" + conf_warnings() + + project "efsw-test" + kind "ConsoleApp" + language "C++" + links { "efsw-static-lib" } + files { "src/test/*.cpp" } + includedirs { "include", "src" } + conf_links() + + configuration "debug" + defines { "DEBUG" } + flags { "Symbols" } + targetname "efsw-test-debug" + conf_warnings() + + configuration "release" + defines { "NDEBUG" } + flags { "Optimize" } + targetname "efsw-test-release" + conf_warnings() + + configuration "relwithdbginfo" + defines { "NDEBUG" } + flags { "Optimize", "Symbols" } + targetname "efsw-test-reldbginfo" + conf_warnings() + + project "efsw-shared-lib" + kind "SharedLib" + language "C++" + targetdir("./lib") + includedirs { "include", "src" } + files { "src/efsw/*.cpp", osfiles } + defines { "EFSW_DYNAMIC", "EFSW_EXPORTS" } + conf_excludes() + conf_links() + + configuration "debug" + defines { "DEBUG" } + flags { "Symbols" } + targetname "efsw-debug" + conf_warnings() + + configuration "release" + defines { "NDEBUG" } + flags { "Optimize" } + targetname "efsw" + conf_warnings() + + configuration "relwithdbginfo" + defines { "NDEBUG" } + flags { "Optimize", "Symbols" } + targetname "efsw" + conf_warnings() + + if os.is("linux") or os.is("bsd") or os.is("haiku") then + targetextension ( ".so." .. efsw_version ) + postbuildcommands { "sh ../../project/build.reldbginfo.sh " .. efsw_major_version .. " " .. efsw_minor_version .. " " .. efsw_patch_version .. " " .. iif( _OPTIONS["strip-symbols"], "strip-symbols", "" ) } + end diff --git a/vendor/efsw/premake5.lua b/vendor/efsw/premake5.lua new file mode 100644 index 0000000..9f993b1 --- /dev/null +++ b/vendor/efsw/premake5.lua @@ -0,0 +1,238 @@ +newoption { trigger = "verbose", description = "Build efsw with verbose mode." } +newoption { trigger = "strip-symbols", description = "Strip debugging symbols in other file ( only for relwithdbginfo configuration )." } +newoption { trigger = "thread-sanitizer", description ="Compile with ThreadSanitizer" } + +efsw_major_version = "1" +efsw_minor_version = "0" +efsw_patch_version = "2" +efsw_version = efsw_major_version .. "." .. efsw_minor_version .. "." .. efsw_patch_version + +function get_include_paths() + local function _insert_include_paths( file ) + local function _trim(s) + return (s:gsub("^%s*(.-)%s*$", "%1")) + end + + local paths = { } + local lines = file:read('*all') + + for line in string.gmatch(lines, '([^\n]+)') + do + table.insert( paths, _trim( line ) ) + end + + file:close() + + return paths + end + + local file = io.popen( "echo | gcc -Wp,-v -x c++ - -fsyntax-only 2>&1 | grep -v '#' | grep '/'", 'r' ) + local include_paths = _insert_include_paths( file ) + + if next(include_paths) == nil then + file = io.popen( "echo | clang++ -Wp,-v -x c++ - -fsyntax-only 2>&1 | grep -v '#' | grep '/' | grep -v 'nonexistent'", 'r' ) + + include_paths = _insert_include_paths( file ) + + if next(include_paths) == nil then + table.insert( include_paths, "/usr/include" ) + table.insert( include_paths, "/usr/local/include" ) + end + end + + return include_paths +end + +function inotify_header_exists() + local efsw_include_paths = get_include_paths() + + for _,v in pairs( efsw_include_paths ) + do + local cur_path = v .. "/sys/inotify.h" + + if os.isfile( cur_path ) then + return true + end + end + + return false +end + +function string.starts(String,Start) + if ( _ACTION ) then + return string.sub(String,1,string.len(Start))==Start + end + + return false +end + +function is_vs() + return ( string.starts(_ACTION,"vs") ) +end + +function conf_warnings() + if not is_vs() then + buildoptions{ "-Wall -Wno-long-long" } + + if not os.istarget("windows") then + buildoptions{ "-fPIC" } + end + else + defines { "_SCL_SECURE_NO_WARNINGS" } + end + + if _OPTIONS["thread-sanitizer"] then + buildoptions { "-fsanitize=thread" } + linkoptions { "-fsanitize=thread" } + if not os.istarget("macosx") then + links { "tsan" } + end + end +end + +function conf_links() + if not os.istarget("windows") and not os.istarget("haiku") then + links { "pthread" } + end + + if os.istarget("macosx") then + links { "CoreFoundation.framework", "CoreServices.framework" } + end +end + +function conf_excludes() + if os.istarget("windows") then + excludes { "src/efsw/WatcherKqueue.cpp", "src/efsw/WatcherFSEvents.cpp", "src/efsw/WatcherInotify.cpp", "src/efsw/FileWatcherKqueue.cpp", "src/efsw/FileWatcherInotify.cpp", "src/efsw/FileWatcherFSEvents.cpp" } + elseif os.istarget("linux") then + excludes { "src/efsw/WatcherKqueue.cpp", "src/efsw/WatcherFSEvents.cpp", "src/efsw/WatcherWin32.cpp", "src/efsw/FileWatcherKqueue.cpp", "src/efsw/FileWatcherWin32.cpp", "src/efsw/FileWatcherFSEvents.cpp" } + elseif os.istarget("macosx") then + excludes { "src/efsw/WatcherInotify.cpp", "src/efsw/WatcherWin32.cpp", "src/efsw/FileWatcherInotify.cpp", "src/efsw/FileWatcherWin32.cpp" } + elseif os.istarget("bsd") then + excludes { "src/efsw/WatcherInotify.cpp", "src/efsw/WatcherWin32.cpp", "src/efsw/WatcherFSEvents.cpp", "src/efsw/FileWatcherInotify.cpp", "src/efsw/FileWatcherWin32.cpp", "src/efsw/FileWatcherFSEvents.cpp" } + end + + if os.istarget("linux") and not inotify_header_exists() then + defines { "EFSW_INOTIFY_NOSYS" } + end +end + +workspace "efsw" + location("./make/" .. os.target() .. "/") + targetdir("./bin") + configurations { "debug", "release", "relwithdbginfo" } + platforms { "x86_64", "x86", "ARM", "ARM64" } + + if os.istarget("windows") then + osfiles = "src/efsw/platform/win/*.cpp" + else + osfiles = "src/efsw/platform/posix/*.cpp" + end + + -- Activates verbose mode + if _OPTIONS["verbose"] then + defines { "EFSW_VERBOSE" } + end + + cppdialect "C++11" + + objdir("obj/" .. os.target() .. "/") + + filter "platforms:x86" + architecture "x86" + + filter "platforms:x86_64" + architecture "x86_64" + + filter "platforms:arm" + architecture "ARM" + + filter "platforms:arm64" + architecture "ARM64" + + project "efsw-static-lib" + kind "StaticLib" + language "C++" + targetdir("./lib") + includedirs { "include", "src" } + files { "src/efsw/*.cpp", osfiles } + conf_excludes() + + filter "configurations:debug" + defines { "DEBUG" } + symbols "On" + targetname "efsw-static-debug" + conf_warnings() + + filter "configurations:release" + defines { "NDEBUG" } + optimize "On" + targetname "efsw-static-release" + conf_warnings() + + filter "configurations:relwithdbginfo" + defines { "NDEBUG" } + symbols "On" + optimize "On" + targetname "efsw-static-reldbginfo" + conf_warnings() + + project "efsw-test" + kind "ConsoleApp" + language "C++" + links { "efsw-static-lib" } + files { "src/test/*.cpp" } + includedirs { "include", "src" } + conf_links() + + filter "configurations:debug" + defines { "DEBUG" } + symbols "On" + targetname "efsw-test-debug" + conf_warnings() + + filter "configurations:release" + defines { "NDEBUG" } + optimize "On" + targetname "efsw-test-release" + conf_warnings() + + filter "configurations:relwithdbginfo" + defines { "NDEBUG" } + symbols "On" + optimize "On" + targetname "efsw-test-reldbginfo" + conf_warnings() + + project "efsw-shared-lib" + kind "SharedLib" + language "C++" + targetdir("./lib") + includedirs { "include", "src" } + files { "src/efsw/*.cpp", osfiles } + defines { "EFSW_DYNAMIC", "EFSW_EXPORTS" } + conf_excludes() + conf_links() + + filter "configurations:debug" + defines { "DEBUG" } + symbols "On" + targetname "efsw-debug" + conf_warnings() + + filter "configurations:release" + defines { "NDEBUG" } + optimize "On" + targetname "efsw" + conf_warnings() + + filter "configurations:relwithdbginfo" + defines { "NDEBUG" } + symbols "On" + optimize "On" + targetname "efsw" + conf_warnings() + + if os.istarget("linux") or os.istarget("bsd") or os.istarget("haiku") then + targetextension ( ".so." .. efsw_version ) + postbuildcommands { "sh ../../project/build.reldbginfo.sh " .. efsw_major_version .. " " .. efsw_minor_version .. " " .. efsw_patch_version .. " " .. iif( _OPTIONS["strip-symbols"], "strip-symbols", "" ) } + end diff --git a/vendor/efsw/project/build.reldbginfo.sh b/vendor/efsw/project/build.reldbginfo.sh new file mode 100644 index 0000000..5ec272b --- /dev/null +++ b/vendor/efsw/project/build.reldbginfo.sh @@ -0,0 +1,9 @@ +#!/bin/sh +cd ../../lib +ln -fs libefsw.so.$1.$2.$3 libefsw.so.$1 +ln -fs libefsw.so.$1 libefsw.so + +if [ "$4" == "strip-symbols" ]; then + objcopy --only-keep-debug libefsw.so.$1.$2.$3 libefsw.debug + objcopy --strip-debug libefsw.so.$1.$2.$3 +fi diff --git a/vendor/efsw/project/qtcreator-linux/efsw.cflags b/vendor/efsw/project/qtcreator-linux/efsw.cflags new file mode 100644 index 0000000..5905d6d --- /dev/null +++ b/vendor/efsw/project/qtcreator-linux/efsw.cflags @@ -0,0 +1 @@ +-std=c11 diff --git a/vendor/efsw/project/qtcreator-linux/efsw.config b/vendor/efsw/project/qtcreator-linux/efsw.config new file mode 100644 index 0000000..8cec188 --- /dev/null +++ b/vendor/efsw/project/qtcreator-linux/efsw.config @@ -0,0 +1 @@ +// ADD PREDEFINED MACROS HERE! diff --git a/vendor/efsw/project/qtcreator-linux/efsw.creator b/vendor/efsw/project/qtcreator-linux/efsw.creator new file mode 100644 index 0000000..e94cbbd --- /dev/null +++ b/vendor/efsw/project/qtcreator-linux/efsw.creator @@ -0,0 +1 @@ +[General] diff --git a/vendor/efsw/project/qtcreator-linux/efsw.creator.user b/vendor/efsw/project/qtcreator-linux/efsw.creator.user new file mode 100644 index 0000000..0bcd422 --- /dev/null +++ b/vendor/efsw/project/qtcreator-linux/efsw.creator.user @@ -0,0 +1,344 @@ + + + + + + EnvironmentId + {d43f4693-30c1-436c-b1d1-498aab2c2f8c} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + + Nim + + NimGlobal + + + 3 + UTF-8 + false + 4 + false + 80 + true + true + 1 + false + true + false + 0 + true + true + 0 + 8 + true + false + 1 + true + true + true + *.md, *.MD, Makefile + false + true + true + + + + ProjectExplorer.Project.PluginSettings + + + true + false + true + true + true + true + + + 0 + true + + true + Builtin.BuildSystem + + false + false + + 0 + + + + true + + false + + + + true + + true + + + + + ProjectExplorer.Project.Target.0 + + Desktop + Desktop + Desktop + {6d057187-158a-4883-8d5b-d470a6b6b025} + 1 + 0 + 0 + + ../../make/linux + + + true + gmake2 + premake5 + %{buildDir}../../../ + ProjectExplorer.ProcessStep + + + -j24 -e config=release_x86_64 + make + true + GenericProjectManager.GenericMakeStep + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + clean + + -e config=release_x86_64 + true + GenericProjectManager.GenericMakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + release + GenericProjectManager.GenericBuildConfiguration + + + ../../make/linux + + + true + --thread-sanitizer --verbose gmake2 + premake5 + %{buildDir}../../../ + ProjectExplorer.ProcessStep + + + -j24 + make + true + GenericProjectManager.GenericMakeStep + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + clean + + -e config=debug_x86_64 + true + GenericProjectManager.GenericMakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + debug + GenericProjectManager.GenericBuildConfiguration + + + ../../make/linux + + + true + gmake2 + premake5 + %{buildDir}../../../ + ProjectExplorer.ProcessStep + + + -e config=relwithdbginfo_x86_64 + make + true + GenericProjectManager.GenericMakeStep + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + clean + + -e config=relwithdbginfo_x86_64 + true + GenericProjectManager.GenericMakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + relwithdbginfo + GenericProjectManager.GenericBuildConfiguration + + 3 + + + 0 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + 1 + + dwarf + + cpu-cycles + + -F + true + 0 + true + true + + 2 + + %{buildDir}/../../bin/efsw-test-debug + debug + ProjectExplorer.CustomExecutableRunConfiguration + + /home/programming/thebricks/fe/ + true + false + false + true + true + %{buildDir}../../../ + + + dwarf + + cpu-cycles + + -F + true + 0 + true + true + + 2 + + %{buildDir}../../../bin/efsw-test-release + release + ProjectExplorer.CustomExecutableRunConfiguration + + true + false + false + true + true + %{buildDir}../../../ + + + dwarf + + cpu-cycles + + -F + true + 0 + true + true + + 2 + + %{buildDir}../../../bin/efsw-test-dbginfo + reldbginfo + ProjectExplorer.CustomExecutableRunConfiguration + + true + false + false + true + true + %{buildDir}../../../ + + 3 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.FileVersion + 22 + + + Version + 22 + + diff --git a/vendor/efsw/project/qtcreator-linux/efsw.cxxflags b/vendor/efsw/project/qtcreator-linux/efsw.cxxflags new file mode 100644 index 0000000..c24e3b5 --- /dev/null +++ b/vendor/efsw/project/qtcreator-linux/efsw.cxxflags @@ -0,0 +1 @@ +-std=c++11 diff --git a/vendor/efsw/project/qtcreator-linux/efsw.files b/vendor/efsw/project/qtcreator-linux/efsw.files new file mode 100644 index 0000000..2eb9989 --- /dev/null +++ b/vendor/efsw/project/qtcreator-linux/efsw.files @@ -0,0 +1,118 @@ +../../CMakeLists.txt +../../include/efsw/efsw.hpp +../../premake5.lua +../../src/efsw/Atomic.hpp +../../src/efsw/FileWatcher.cpp +../../src/efsw/FileWatcherInotify.cpp +../../src/efsw/FileWatcherKqueue.cpp +../../src/efsw/FileWatcherWin32.cpp +../../src/efsw/FileWatcherWin32.hpp +../../src/efsw/FileWatcherKqueue.hpp +../../src/efsw/FileWatcherInotify.hpp +../../src/efsw/FileWatcherImpl.hpp +../../src/efsw/FileWatcherWin32.cpp +../../src/efsw/FileWatcherKqueue.cpp +../../src/efsw/FileWatcherInotify.cpp +../../src/efsw/FileWatcher.cpp +../../src/efsw/Thread.cpp +../../src/efsw/Mutex.cpp +../../src/efsw/System.cpp +../../src/efsw/platform/platformimpl.hpp +../../src/efsw/platform/posix/ThreadImpl.hpp +../../src/efsw/platform/posix/MutexImpl.hpp +../../src/efsw/platform/posix/SystemImpl.hpp +../../src/efsw/platform/posix/ThreadImpl.cpp +../../src/efsw/platform/posix/MutexImpl.cpp +../../src/efsw/platform/posix/SystemImpl.cpp +../../src/efsw/platform/win/ThreadImpl.hpp +../../src/efsw/platform/win/MutexImpl.hpp +../../src/efsw/platform/win/SystemImpl.hpp +../../src/efsw/platform/win/ThreadImpl.cpp +../../src/efsw/platform/win/MutexImpl.cpp +../../src/efsw/platform/win/SystemImpl.cpp +../../src/efsw/base.hpp +../../src/efsw/FileWatcherGeneric.hpp +../../src/efsw/FileWatcherGeneric.cpp +../../src/efsw/FileSystem.cpp +../../src/efsw/platform/posix/FileSystemImpl.hpp +../../src/efsw/platform/posix/FileSystemImpl.cpp +../../src/efsw/platform/win/FileSystemImpl.hpp +../../src/efsw/platform/win/FileSystemImpl.cpp +../../src/efsw/FileInfo.cpp +../../src/efsw/base.hpp +../../src/efsw/Thread.hpp +../../src/efsw/System.hpp +../../src/efsw/Mutex.hpp +../../src/efsw/FileWatcherWin32.hpp +../../src/efsw/FileWatcherKqueue.hpp +../../src/efsw/FileWatcherInotify.hpp +../../src/efsw/FileWatcherImpl.hpp +../../src/efsw/FileWatcherGeneric.hpp +../../src/efsw/FileSystem.hpp +../../src/efsw/FileInfo.hpp +../../src/efsw/Thread.cpp +../../src/efsw/System.cpp +../../src/efsw/Mutex.cpp +../../src/efsw/FileWatcherWin32.cpp +../../src/efsw/FileWatcherKqueue.cpp +../../src/efsw/FileWatcherInotify.cpp +../../src/efsw/FileWatcherGeneric.cpp +../../src/efsw/FileWatcher.cpp +../../src/efsw/FileSystem.cpp +../../src/efsw/FileInfo.cpp +../../src/efsw/sophist.h +../../src/efsw/base.hpp +../../src/efsw/Utf.hpp +../../src/efsw/Thread.hpp +../../src/efsw/System.hpp +../../src/efsw/String.hpp +../../src/efsw/Mutex.hpp +../../src/efsw/FileWatcherWin32.hpp +../../src/efsw/FileWatcherKqueue.hpp +../../src/efsw/FileWatcherInotify.hpp +../../src/efsw/FileWatcherImpl.hpp +../../src/efsw/FileWatcherGeneric.hpp +../../src/efsw/FileSystem.hpp +../../src/efsw/FileInfo.hpp +../../src/efsw/Utf.inl +../../src/efsw/Thread.cpp +../../src/efsw/System.cpp +../../src/efsw/String.cpp +../../src/efsw/Mutex.cpp +../../src/efsw/FileWatcherWin32.cpp +../../src/efsw/FileWatcherKqueue.cpp +../../src/efsw/FileWatcherInotify.cpp +../../src/efsw/FileWatcherGeneric.cpp +../../src/efsw/FileWatcher.cpp +../../src/efsw/FileSystem.cpp +../../src/efsw/FileInfo.cpp +../../src/test/efsw-test.cpp +../../premake4.lua +../../src/efsw/WatcherKqueue.hpp +../../src/efsw/WatcherKqueue.cpp +../../src/efsw/Debug.hpp +../../src/efsw/Debug.cpp +../../src/efsw/WatcherGeneric.hpp +../../src/efsw/WatcherGeneric.cpp +../../src/efsw/DirWatcherGeneric.hpp +../../src/efsw/DirWatcherGeneric.cpp +../../src/efsw/Log.cpp +../../src/efsw/WatcherInotify.hpp +../../src/efsw/WatcherInotify.cpp +../../src/efsw/FileWatcherImpl.cpp +../../src/efsw/DirectorySnapshot.hpp +../../src/efsw/DirectorySnapshot.cpp +../../src/efsw/DirectorySnapshotDiff.hpp +../../src/efsw/DirectorySnapshotDiff.cpp +../../src/efsw/WatcherFSEvents.hpp +../../src/efsw/FileWatcherFSEvents.hpp +../../src/efsw/WatcherFSEvents.cpp +../../src/efsw/FileWatcherFSEvents.cpp +../../src/efsw/Watcher.hpp +../../src/efsw/Watcher.cpp +../../src/efsw/WatcherWin32.hpp +../../src/efsw/WatcherWin32.cpp +../../README.md +../../include/efsw/efsw.h +../../src/efsw/FileWatcherCWrapper.cpp +../../src/efsw/inotify-nosys.h diff --git a/vendor/efsw/project/qtcreator-linux/efsw.includes b/vendor/efsw/project/qtcreator-linux/efsw.includes new file mode 100644 index 0000000..1ab792a --- /dev/null +++ b/vendor/efsw/project/qtcreator-linux/efsw.includes @@ -0,0 +1,2 @@ +../../include +../../src diff --git a/vendor/efsw/project/qtcreator-osx/efsw.cflags b/vendor/efsw/project/qtcreator-osx/efsw.cflags new file mode 100644 index 0000000..68d5165 --- /dev/null +++ b/vendor/efsw/project/qtcreator-osx/efsw.cflags @@ -0,0 +1 @@ +-std=c17 \ No newline at end of file diff --git a/vendor/efsw/project/qtcreator-osx/efsw.config b/vendor/efsw/project/qtcreator-osx/efsw.config new file mode 100644 index 0000000..685bb2d --- /dev/null +++ b/vendor/efsw/project/qtcreator-osx/efsw.config @@ -0,0 +1,3 @@ +// ADD PREDEFINED MACROS HERE! +#define EFSW_FSEVENTS_SUPPORTED +#define EFSW_USE_CXX11 diff --git a/vendor/efsw/project/qtcreator-osx/efsw.creator b/vendor/efsw/project/qtcreator-osx/efsw.creator new file mode 100644 index 0000000..e94cbbd --- /dev/null +++ b/vendor/efsw/project/qtcreator-osx/efsw.creator @@ -0,0 +1 @@ +[General] diff --git a/vendor/efsw/project/qtcreator-osx/efsw.creator.user b/vendor/efsw/project/qtcreator-osx/efsw.creator.user new file mode 100644 index 0000000..c1e8795 --- /dev/null +++ b/vendor/efsw/project/qtcreator-osx/efsw.creator.user @@ -0,0 +1,253 @@ + + + + + + EnvironmentId + {49267ae2-f136-4b84-8041-cf11a20f6a32} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + true + 80 + true + true + 1 + 0 + false + true + false + 0 + true + true + 0 + 8 + true + false + 1 + true + true + true + *.md, *.MD, Makefile + false + true + true + + + + ProjectExplorer.Project.PluginSettings + + + true + false + true + true + true + true + + + 0 + true + + true + + true + true + Builtin.DefaultTidyAndClazy + 4 + false + + + + true + + + + + ProjectExplorer.Project.Target.0 + + Desktop + Desktop (arm-darwin-generic-mach_o-64bit) + Desktop (arm-darwin-generic-mach_o-64bit) + {6d6b6d62-1e99-4e76-b5e2-cf731a0dbd92} + 0 + 0 + 0 + + ../../make/macosx/ + + + true + --file=../../premake4.lua --thread-sanitizer --verbose gmake + /usr/local/bin/premake4 + %{buildDir} + ProjectExplorer.ProcessStep + + + -j4 + true + GenericProjectManager.GenericMakeStep + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + clean + + true + GenericProjectManager.GenericMakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + debug + GenericProjectManager.GenericBuildConfiguration + + + ../../make/macosx/ + + + true + --file=../../premake4.lua gmake + /usr/local/bin/premake4 + %{buildDir} + ProjectExplorer.ProcessStep + + + -j4 config=release + true + GenericProjectManager.GenericMakeStep + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + clean + + config=release + true + GenericProjectManager.GenericMakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + release + GenericProjectManager.GenericBuildConfiguration + + 2 + + + 0 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + 1 + + true + 0 + true + true + + 2 + + false + /Users/prognoz/programming/efsw/bin/efsw-test-debug + debug + ProjectExplorer.CustomExecutableRunConfiguration + + true + 0 + false + 1 + false + false + %{buildDir}/../../bin + + + true + 0 + true + true + + 2 + + false + efsw-test + release + ProjectExplorer.CustomExecutableRunConfiguration + + false + 1 + false + true + false + %{buildDir}/../../bin/ + + 2 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.FileVersion + 22 + + + Version + 22 + + diff --git a/vendor/efsw/project/qtcreator-osx/efsw.cxxflags b/vendor/efsw/project/qtcreator-osx/efsw.cxxflags new file mode 100644 index 0000000..6435dfc --- /dev/null +++ b/vendor/efsw/project/qtcreator-osx/efsw.cxxflags @@ -0,0 +1 @@ +-std=c++17 \ No newline at end of file diff --git a/vendor/efsw/project/qtcreator-osx/efsw.files b/vendor/efsw/project/qtcreator-osx/efsw.files new file mode 100644 index 0000000..0ab369f --- /dev/null +++ b/vendor/efsw/project/qtcreator-osx/efsw.files @@ -0,0 +1,185 @@ +../../include/efsw/efsw.hpp +../../src/efsw/FileWatcher.cpp +../../src/efsw/FileWatcherInotify.cpp +../../src/efsw/FileWatcherKqueue.cpp +../../src/efsw/FileWatcherWin32.cpp +../../src/efsw/FileWatcherWin32.hpp +../../src/efsw/FileWatcherKqueue.hpp +../../src/efsw/FileWatcherInotify.hpp +../../src/efsw/FileWatcherImpl.hpp +../../src/efsw/FileWatcherWin32.cpp +../../src/efsw/FileWatcherKqueue.cpp +../../src/efsw/FileWatcherInotify.cpp +../../src/efsw/FileWatcher.cpp +../../src/efsw/Thread.cpp +../../src/efsw/Mutex.cpp +../../src/efsw/System.cpp +../../src/efsw/platform/platformimpl.hpp +../../src/efsw/platform/posix/ThreadImpl.hpp +../../src/efsw/platform/posix/MutexImpl.hpp +../../src/efsw/platform/posix/SystemImpl.hpp +../../src/efsw/platform/posix/ThreadImpl.cpp +../../src/efsw/platform/posix/MutexImpl.cpp +../../src/efsw/platform/posix/SystemImpl.cpp +../../src/efsw/platform/win/ThreadImpl.hpp +../../src/efsw/platform/win/MutexImpl.hpp +../../src/efsw/platform/win/SystemImpl.hpp +../../src/efsw/platform/win/ThreadImpl.cpp +../../src/efsw/platform/win/MutexImpl.cpp +../../src/efsw/platform/win/SystemImpl.cpp +../../src/efsw/base.hpp +../../src/efsw/FileWatcherGeneric.hpp +../../src/efsw/FileWatcherGeneric.cpp +../../src/efsw/FileSystem.cpp +../../src/efsw/platform/posix/FileSystemImpl.hpp +../../src/efsw/platform/posix/FileSystemImpl.cpp +../../src/efsw/platform/win/FileSystemImpl.hpp +../../src/efsw/platform/win/FileSystemImpl.cpp +../../src/efsw/FileInfo.cpp +../../src/efsw/base.hpp +../../src/efsw/Thread.hpp +../../src/efsw/System.hpp +../../src/efsw/Mutex.hpp +../../src/efsw/FileWatcherWin32.hpp +../../src/efsw/FileWatcherKqueue.hpp +../../src/efsw/FileWatcherInotify.hpp +../../src/efsw/FileWatcherImpl.hpp +../../src/efsw/FileWatcherGeneric.hpp +../../src/efsw/FileSystem.hpp +../../src/efsw/FileInfo.hpp +../../src/efsw/Thread.cpp +../../src/efsw/System.cpp +../../src/efsw/Mutex.cpp +../../src/efsw/FileWatcherWin32.cpp +../../src/efsw/FileWatcherKqueue.cpp +../../src/efsw/FileWatcherInotify.cpp +../../src/efsw/FileWatcherGeneric.cpp +../../src/efsw/FileWatcher.cpp +../../src/efsw/FileSystem.cpp +../../src/efsw/FileInfo.cpp +../../src/efsw/sophist.h +../../src/efsw/base.hpp +../../src/efsw/Utf.hpp +../../src/efsw/Thread.hpp +../../src/efsw/System.hpp +../../src/efsw/String.hpp +../../src/efsw/Mutex.hpp +../../src/efsw/FileWatcherWin32.hpp +../../src/efsw/FileWatcherKqueue.hpp +../../src/efsw/FileWatcherInotify.hpp +../../src/efsw/FileWatcherImpl.hpp +../../src/efsw/FileWatcherGeneric.hpp +../../src/efsw/FileSystem.hpp +../../src/efsw/FileInfo.hpp +../../src/efsw/Utf.inl +../../src/efsw/Thread.cpp +../../src/efsw/System.cpp +../../src/efsw/String.cpp +../../src/efsw/Mutex.cpp +../../src/efsw/FileWatcherWin32.cpp +../../src/efsw/FileWatcherKqueue.cpp +../../src/efsw/FileWatcherInotify.cpp +../../src/efsw/FileWatcherGeneric.cpp +../../src/efsw/FileWatcher.cpp +../../src/efsw/FileSystem.cpp +../../src/efsw/FileInfo.cpp +../../src/test/efsw-test.cpp +../../premake4.lua +../../src/efsw/WatcherKqueue.hpp +../../src/efsw/WatcherKqueue.cpp +../../src/efsw/WatcherKqueue.hpp +../../src/efsw/WatcherInotify.hpp +../../src/efsw/WatcherGeneric.hpp +../../src/efsw/Utf.hpp +../../src/efsw/Thread.hpp +../../src/efsw/System.hpp +../../src/efsw/String.hpp +../../src/efsw/sophist.h +../../src/efsw/Mutex.hpp +../../src/efsw/FileWatcherWin32.hpp +../../src/efsw/FileWatcherKqueue.hpp +../../src/efsw/FileWatcherInotify.hpp +../../src/efsw/FileWatcherImpl.hpp +../../src/efsw/FileWatcherGeneric.hpp +../../src/efsw/FileSystem.hpp +../../src/efsw/FileInfo.hpp +../../src/efsw/DirWatcherGeneric.hpp +../../src/efsw/Debug.hpp +../../src/efsw/base.hpp +../../src/efsw/WatcherKqueue.cpp +../../src/efsw/WatcherInotify.cpp +../../src/efsw/WatcherGeneric.cpp +../../src/efsw/Utf.inl +../../src/efsw/Thread.cpp +../../src/efsw/System.cpp +../../src/efsw/String.cpp +../../src/efsw/Mutex.cpp +../../src/efsw/Log.cpp +../../src/efsw/FileWatcherWin32.cpp +../../src/efsw/FileWatcherKqueue.cpp +../../src/efsw/FileWatcherInotify.cpp +../../src/efsw/FileWatcherGeneric.cpp +../../src/efsw/FileWatcher.cpp +../../src/efsw/FileSystem.cpp +../../src/efsw/FileInfo.cpp +../../src/efsw/DirWatcherGeneric.cpp +../../src/efsw/Debug.cpp +../../src/efsw/FileWatcherImpl.cpp +../../src/efsw/DirectorySnapshotDiff.hpp +../../src/efsw/DirectorySnapshot.hpp +../../src/efsw/DirectorySnapshotDiff.cpp +../../src/efsw/DirectorySnapshot.cpp +../../src/efsw/WatcherFSEvents.hpp +../../src/efsw/FileWatcherFSEvents.hpp +../../src/efsw/WatcherFSEvents.cpp +../../src/efsw/FileWatcherFSEvents.cpp +../../src/efsw/WatcherWin32.hpp +../../src/efsw/WatcherKqueue.hpp +../../src/efsw/WatcherInotify.hpp +../../src/efsw/WatcherGeneric.hpp +../../src/efsw/WatcherFSEvents.hpp +../../src/efsw/Watcher.hpp +../../src/efsw/Utf.hpp +../../src/efsw/Thread.hpp +../../src/efsw/System.hpp +../../src/efsw/String.hpp +../../src/efsw/sophist.h +../../src/efsw/Mutex.hpp +../../src/efsw/FileWatcherWin32.hpp +../../src/efsw/FileWatcherKqueue.hpp +../../src/efsw/FileWatcherInotify.hpp +../../src/efsw/FileWatcherImpl.hpp +../../src/efsw/FileWatcherGeneric.hpp +../../src/efsw/FileWatcherFSEvents.hpp +../../src/efsw/FileSystem.hpp +../../src/efsw/FileInfo.hpp +../../src/efsw/DirWatcherGeneric.hpp +../../src/efsw/DirectorySnapshotDiff.hpp +../../src/efsw/DirectorySnapshot.hpp +../../src/efsw/Debug.hpp +../../src/efsw/base.hpp +../../src/efsw/WatcherWin32.cpp +../../src/efsw/WatcherKqueue.cpp +../../src/efsw/WatcherInotify.cpp +../../src/efsw/WatcherGeneric.cpp +../../src/efsw/WatcherFSEvents.cpp +../../src/efsw/Watcher.cpp +../../src/efsw/Utf.inl +../../src/efsw/Thread.cpp +../../src/efsw/System.cpp +../../src/efsw/String.cpp +../../src/efsw/Mutex.cpp +../../src/efsw/Log.cpp +../../src/efsw/FileWatcherWin32.cpp +../../src/efsw/FileWatcherKqueue.cpp +../../src/efsw/FileWatcherInotify.cpp +../../src/efsw/FileWatcherImpl.cpp +../../src/efsw/FileWatcherGeneric.cpp +../../src/efsw/FileWatcherFSEvents.cpp +../../src/efsw/FileWatcher.cpp +../../src/efsw/FileSystem.cpp +../../src/efsw/FileInfo.cpp +../../src/efsw/DirWatcherGeneric.cpp +../../src/efsw/DirectorySnapshotDiff.cpp +../../src/efsw/DirectorySnapshot.cpp +../../src/efsw/Debug.cpp diff --git a/vendor/efsw/project/qtcreator-osx/efsw.includes b/vendor/efsw/project/qtcreator-osx/efsw.includes new file mode 100644 index 0000000..1f6e72e --- /dev/null +++ b/vendor/efsw/project/qtcreator-osx/efsw.includes @@ -0,0 +1,7 @@ +../../src +../../include +/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../include/c++/v1 +/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/12.0.0/include +/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/usr/include +/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include +/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/System/Library/Frameworks diff --git a/vendor/efsw/project/qtcreator-win/efsw.cflags b/vendor/efsw/project/qtcreator-win/efsw.cflags new file mode 100644 index 0000000..68d5165 --- /dev/null +++ b/vendor/efsw/project/qtcreator-win/efsw.cflags @@ -0,0 +1 @@ +-std=c17 \ No newline at end of file diff --git a/vendor/efsw/project/qtcreator-win/efsw.config b/vendor/efsw/project/qtcreator-win/efsw.config new file mode 100644 index 0000000..8cec188 --- /dev/null +++ b/vendor/efsw/project/qtcreator-win/efsw.config @@ -0,0 +1 @@ +// ADD PREDEFINED MACROS HERE! diff --git a/vendor/efsw/project/qtcreator-win/efsw.creator b/vendor/efsw/project/qtcreator-win/efsw.creator new file mode 100644 index 0000000..e94cbbd --- /dev/null +++ b/vendor/efsw/project/qtcreator-win/efsw.creator @@ -0,0 +1 @@ +[General] diff --git a/vendor/efsw/project/qtcreator-win/efsw.creator.user b/vendor/efsw/project/qtcreator-win/efsw.creator.user new file mode 100644 index 0000000..deadd7e --- /dev/null +++ b/vendor/efsw/project/qtcreator-win/efsw.creator.user @@ -0,0 +1,211 @@ + + + + + + EnvironmentId + {55fc4913-4acc-49e6-b0d5-ebf25d4d498e} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 80 + true + true + 1 + true + false + 0 + true + true + 0 + 8 + true + 1 + true + true + true + false + + + + ProjectExplorer.Project.PluginSettings + + + + ProjectExplorer.Project.Target.0 + + Desktop + Desktop + {eb5b6178-a7a7-439e-ab01-e63b057196a1} + 0 + 0 + 0 + + C:\programming\efsw\make\windows + + + + all + + false + + + false + true + Make + + GenericProjectManager.GenericMakeStep + + 1 + Build + + ProjectExplorer.BuildSteps.Build + + + + + clean + + true + + + false + true + Make + + GenericProjectManager.GenericMakeStep + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Default + Default + GenericProjectManager.GenericBuildConfiguration + + 1 + + + 0 + Deploy + + ProjectExplorer.BuildSteps.Deploy + + 1 + Deploy locally + + ProjectExplorer.DefaultDeployConfiguration + + 1 + + + dwarf + + cpu-cycles + + + 250 + -F + true + 4096 + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + 2 + + %{buildDir}\..\..\bin\efsw-test-debug.exe + Run C:\programming\efsw\bin\efsw-test-debug.exe + + ProjectExplorer.CustomExecutableRunConfiguration + + 3768 + false + true + false + false + true + %{buildDir} + + + 1 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.FileVersion + 22 + + + Version + 22 + + diff --git a/vendor/efsw/project/qtcreator-win/efsw.cxxflags b/vendor/efsw/project/qtcreator-win/efsw.cxxflags new file mode 100644 index 0000000..6435dfc --- /dev/null +++ b/vendor/efsw/project/qtcreator-win/efsw.cxxflags @@ -0,0 +1 @@ +-std=c++17 \ No newline at end of file diff --git a/vendor/efsw/project/qtcreator-win/efsw.files b/vendor/efsw/project/qtcreator-win/efsw.files new file mode 100644 index 0000000..8efe50d --- /dev/null +++ b/vendor/efsw/project/qtcreator-win/efsw.files @@ -0,0 +1,215 @@ +../../include/efsw/efsw.hpp +../../premake5.lua +../../src/efsw/FileWatcher.cpp +../../src/efsw/FileWatcherInotify.cpp +../../src/efsw/FileWatcherKqueue.cpp +../../src/efsw/FileWatcherWin32.cpp +../../src/efsw/FileWatcherWin32.hpp +../../src/efsw/FileWatcherKqueue.hpp +../../src/efsw/FileWatcherInotify.hpp +../../src/efsw/FileWatcherImpl.hpp +../../src/efsw/FileWatcherWin32.cpp +../../src/efsw/FileWatcherKqueue.cpp +../../src/efsw/FileWatcherInotify.cpp +../../src/efsw/FileWatcher.cpp +../../src/efsw/Thread.cpp +../../src/efsw/Mutex.cpp +../../src/efsw/System.cpp +../../src/efsw/platform/platformimpl.hpp +../../src/efsw/platform/posix/ThreadImpl.hpp +../../src/efsw/platform/posix/MutexImpl.hpp +../../src/efsw/platform/posix/SystemImpl.hpp +../../src/efsw/platform/posix/ThreadImpl.cpp +../../src/efsw/platform/posix/MutexImpl.cpp +../../src/efsw/platform/posix/SystemImpl.cpp +../../src/efsw/platform/win/ThreadImpl.hpp +../../src/efsw/platform/win/MutexImpl.hpp +../../src/efsw/platform/win/SystemImpl.hpp +../../src/efsw/platform/win/ThreadImpl.cpp +../../src/efsw/platform/win/MutexImpl.cpp +../../src/efsw/platform/win/SystemImpl.cpp +../../src/efsw/base.hpp +../../src/efsw/FileWatcherGeneric.hpp +../../src/efsw/FileWatcherGeneric.cpp +../../src/efsw/FileSystem.cpp +../../src/efsw/platform/posix/FileSystemImpl.hpp +../../src/efsw/platform/posix/FileSystemImpl.cpp +../../src/efsw/platform/win/FileSystemImpl.hpp +../../src/efsw/platform/win/FileSystemImpl.cpp +../../src/efsw/FileInfo.cpp +../../src/efsw/base.hpp +../../src/efsw/Thread.hpp +../../src/efsw/System.hpp +../../src/efsw/Mutex.hpp +../../src/efsw/FileWatcherWin32.hpp +../../src/efsw/FileWatcherKqueue.hpp +../../src/efsw/FileWatcherInotify.hpp +../../src/efsw/FileWatcherImpl.hpp +../../src/efsw/FileWatcherGeneric.hpp +../../src/efsw/FileSystem.hpp +../../src/efsw/FileInfo.hpp +../../src/efsw/Thread.cpp +../../src/efsw/System.cpp +../../src/efsw/Mutex.cpp +../../src/efsw/FileWatcherWin32.cpp +../../src/efsw/FileWatcherKqueue.cpp +../../src/efsw/FileWatcherInotify.cpp +../../src/efsw/FileWatcherGeneric.cpp +../../src/efsw/FileWatcher.cpp +../../src/efsw/FileSystem.cpp +../../src/efsw/FileInfo.cpp +../../src/efsw/sophist.h +../../src/efsw/base.hpp +../../src/efsw/Utf.hpp +../../src/efsw/Thread.hpp +../../src/efsw/System.hpp +../../src/efsw/String.hpp +../../src/efsw/Mutex.hpp +../../src/efsw/FileWatcherWin32.hpp +../../src/efsw/FileWatcherKqueue.hpp +../../src/efsw/FileWatcherInotify.hpp +../../src/efsw/FileWatcherImpl.hpp +../../src/efsw/FileWatcherGeneric.hpp +../../src/efsw/FileSystem.hpp +../../src/efsw/FileInfo.hpp +../../src/efsw/Utf.inl +../../src/efsw/Thread.cpp +../../src/efsw/System.cpp +../../src/efsw/String.cpp +../../src/efsw/Mutex.cpp +../../src/efsw/FileWatcherWin32.cpp +../../src/efsw/FileWatcherKqueue.cpp +../../src/efsw/FileWatcherInotify.cpp +../../src/efsw/FileWatcherGeneric.cpp +../../src/efsw/FileWatcher.cpp +../../src/efsw/FileSystem.cpp +../../src/efsw/FileInfo.cpp +../../src/test/efsw-test.cpp +../../src/efsw/WatcherKqueue.hpp +../../src/efsw/WatcherInotify.hpp +../../src/efsw/WatcherGeneric.hpp +../../src/efsw/Utf.hpp +../../src/efsw/Thread.hpp +../../src/efsw/System.hpp +../../src/efsw/String.hpp +../../src/efsw/Mutex.hpp +../../src/efsw/FileWatcherWin32.hpp +../../src/efsw/FileWatcherKqueue.hpp +../../src/efsw/FileWatcherInotify.hpp +../../src/efsw/FileWatcherImpl.hpp +../../src/efsw/FileWatcherGeneric.hpp +../../src/efsw/FileSystem.hpp +../../src/efsw/FileInfo.hpp +../../src/efsw/DirWatcherGeneric.hpp +../../src/efsw/Debug.hpp +../../src/efsw/base.hpp +../../src/efsw/sophist.h +../../src/efsw/Utf.inl +../../src/efsw/WatcherKqueue.cpp +../../src/efsw/WatcherInotify.cpp +../../src/efsw/WatcherGeneric.cpp +../../src/efsw/Thread.cpp +../../src/efsw/System.cpp +../../src/efsw/String.cpp +../../src/efsw/Mutex.cpp +../../src/efsw/Log.cpp +../../src/efsw/FileWatcherWin32.cpp +../../src/efsw/FileWatcherKqueue.cpp +../../src/efsw/FileWatcherInotify.cpp +../../src/efsw/FileWatcherGeneric.cpp +../../src/efsw/FileWatcher.cpp +../../src/efsw/FileSystem.cpp +../../src/efsw/FileInfo.cpp +../../src/efsw/DirWatcherGeneric.cpp +../../src/efsw/Debug.cpp +../../premake4.lua +../../src/efsw/WatcherKqueue.hpp +../../src/efsw/WatcherInotify.hpp +../../src/efsw/WatcherGeneric.hpp +../../src/efsw/Utf.hpp +../../src/efsw/Thread.hpp +../../src/efsw/System.hpp +../../src/efsw/String.hpp +../../src/efsw/sophist.h +../../src/efsw/Mutex.hpp +../../src/efsw/FileWatcherWin32.hpp +../../src/efsw/FileWatcherKqueue.hpp +../../src/efsw/FileWatcherInotify.hpp +../../src/efsw/FileWatcherImpl.hpp +../../src/efsw/FileWatcherGeneric.hpp +../../src/efsw/FileSystem.hpp +../../src/efsw/FileInfo.hpp +../../src/efsw/DirWatcherGeneric.hpp +../../src/efsw/Debug.hpp +../../src/efsw/base.hpp +../../src/efsw/Utf.inl +../../src/efsw/WatcherKqueue.cpp +../../src/efsw/WatcherInotify.cpp +../../src/efsw/WatcherGeneric.cpp +../../src/efsw/Thread.cpp +../../src/efsw/System.cpp +../../src/efsw/String.cpp +../../src/efsw/Mutex.cpp +../../src/efsw/Log.cpp +../../src/efsw/FileWatcherWin32.cpp +../../src/efsw/FileWatcherKqueue.cpp +../../src/efsw/FileWatcherInotify.cpp +../../src/efsw/FileWatcherImpl.cpp +../../src/efsw/FileWatcherGeneric.cpp +../../src/efsw/FileWatcher.cpp +../../src/efsw/FileSystem.cpp +../../src/efsw/FileInfo.cpp +../../src/efsw/DirWatcherGeneric.cpp +../../src/efsw/Debug.cpp +../../src/efsw/WatcherWin32.hpp +../../src/efsw/WatcherKqueue.hpp +../../src/efsw/WatcherInotify.hpp +../../src/efsw/WatcherGeneric.hpp +../../src/efsw/WatcherFSEvents.hpp +../../src/efsw/Watcher.hpp +../../src/efsw/Utf.hpp +../../src/efsw/Thread.hpp +../../src/efsw/System.hpp +../../src/efsw/String.hpp +../../src/efsw/sophist.h +../../src/efsw/Mutex.hpp +../../src/efsw/FileWatcherWin32.hpp +../../src/efsw/FileWatcherKqueue.hpp +../../src/efsw/FileWatcherInotify.hpp +../../src/efsw/FileWatcherImpl.hpp +../../src/efsw/FileWatcherGeneric.hpp +../../src/efsw/FileWatcherFSEvents.hpp +../../src/efsw/FileSystem.hpp +../../src/efsw/FileInfo.hpp +../../src/efsw/DirWatcherGeneric.hpp +../../src/efsw/DirectorySnapshotDiff.hpp +../../src/efsw/DirectorySnapshot.hpp +../../src/efsw/Debug.hpp +../../src/efsw/base.hpp +../../src/efsw/Utf.inl +../../src/efsw/WatcherWin32.cpp +../../src/efsw/WatcherKqueue.cpp +../../src/efsw/WatcherInotify.cpp +../../src/efsw/WatcherGeneric.cpp +../../src/efsw/WatcherFSEvents.cpp +../../src/efsw/Watcher.cpp +../../src/efsw/Thread.cpp +../../src/efsw/System.cpp +../../src/efsw/String.cpp +../../src/efsw/Mutex.cpp +../../src/efsw/Log.cpp +../../src/efsw/FileWatcherWin32.cpp +../../src/efsw/FileWatcherKqueue.cpp +../../src/efsw/FileWatcherInotify.cpp +../../src/efsw/FileWatcherImpl.cpp +../../src/efsw/FileWatcherGeneric.cpp +../../src/efsw/FileWatcherFSEvents.cpp +../../src/efsw/FileWatcher.cpp +../../src/efsw/FileSystem.cpp +../../src/efsw/FileInfo.cpp +../../src/efsw/DirWatcherGeneric.cpp +../../src/efsw/DirectorySnapshotDiff.cpp +../../src/efsw/DirectorySnapshot.cpp +../../src/efsw/Debug.cpp +../../include/efsw/efsw.h +../../src/efsw/FileWatcherCWrapper.cpp diff --git a/vendor/efsw/project/qtcreator-win/efsw.includes b/vendor/efsw/project/qtcreator-win/efsw.includes new file mode 100644 index 0000000..0eeab56 --- /dev/null +++ b/vendor/efsw/project/qtcreator-win/efsw.includes @@ -0,0 +1,2 @@ +../../src +../../include diff --git a/vendor/efsw/src/efsw/Atomic.hpp b/vendor/efsw/src/efsw/Atomic.hpp new file mode 100644 index 0000000..9015c60 --- /dev/null +++ b/vendor/efsw/src/efsw/Atomic.hpp @@ -0,0 +1,33 @@ +#ifndef EFSW_ATOMIC_BOOL_HPP +#define EFSW_ATOMIC_BOOL_HPP + +#include + +#include + +namespace efsw { + +template class Atomic { + public: + explicit Atomic( T set = false ) : set_( set ) {} + + Atomic& operator=( T set ) { + set_.store( set, std::memory_order_release ); + return *this; + } + + explicit operator T() const { + return set_.load( std::memory_order_acquire ); + } + + T load() const { + return set_.load( std::memory_order_acquire ); + } + + private: + std::atomic set_; +}; + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/Debug.cpp b/vendor/efsw/src/efsw/Debug.cpp new file mode 100644 index 0000000..18cfd31 --- /dev/null +++ b/vendor/efsw/src/efsw/Debug.cpp @@ -0,0 +1,81 @@ +#include +#include + +#ifdef EFSW_COMPILER_MSVC +#define WIN32_LEAN_AND_MEAN +#include +#include +#endif + +#include +#include +#include + +namespace efsw { + +#ifdef DEBUG + +void efREPORT_ASSERT( const char* File, int Line, const char* Exp ) { +#ifdef EFSW_COMPILER_MSVC + _CrtDbgReport( _CRT_ASSERT, File, Line, "", Exp ); + + DebugBreak(); +#else + std::cout << "ASSERT: " << Exp << " file: " << File << " line: " << Line << std::endl; + +#if defined( EFSW_COMPILER_GCC ) && defined( EFSW_32BIT ) && !defined( EFSW_ARM ) + asm( "int3" ); +#else + assert( false ); +#endif +#endif +} + +void efPRINT( const char* format, ... ) { + char buf[2048]; + va_list args; + + va_start( args, format ); + +#ifdef EFSW_COMPILER_MSVC + _vsnprintf_s( buf, sizeof( buf ), sizeof( buf ) / sizeof( buf[0] ), format, args ); +#else + vsnprintf( buf, sizeof( buf ) / sizeof( buf[0] ), format, args ); +#endif + + va_end( args ); + +#ifdef EFSW_COMPILER_MSVC + OutputDebugStringA( buf ); +#else + std::cout << buf; +#endif +} + +void efPRINTC( unsigned int cond, const char* format, ... ) { + if ( 0 == cond ) + return; + + char buf[2048]; + va_list args; + + va_start( args, format ); + +#ifdef EFSW_COMPILER_MSVC + _vsnprintf_s( buf, efARRAY_SIZE( buf ), efARRAY_SIZE( buf ), format, args ); +#else + vsnprintf( buf, sizeof( buf ) / sizeof( buf[0] ), format, args ); +#endif + + va_end( args ); + +#ifdef EFSW_COMPILER_MSVC + OutputDebugStringA( buf ); +#else + std::cout << buf; +#endif +} + +#endif + +} // namespace efsw diff --git a/vendor/efsw/src/efsw/Debug.hpp b/vendor/efsw/src/efsw/Debug.hpp new file mode 100644 index 0000000..fefaec4 --- /dev/null +++ b/vendor/efsw/src/efsw/Debug.hpp @@ -0,0 +1,62 @@ +#ifndef EFSW_DEBUG_HPP +#define EFSW_DEBUG_HPP + +#include + +namespace efsw { + +#ifdef DEBUG + +void efREPORT_ASSERT( const char* File, const int Line, const char* Exp ); + +#define efASSERT( expr ) \ + if ( !( expr ) ) { \ + efREPORT_ASSERT( __FILE__, __LINE__, #expr ); \ + } +#define efASSERTM( expr, msg ) \ + if ( !( expr ) ) { \ + efREPORT_ASSERT( __FILE__, __LINE__, #msg ); \ + } + +void efPRINT( const char* format, ... ); +void efPRINTC( unsigned int cond, const char* format, ... ); + +#else + +#define efASSERT( expr ) +#define efASSERTM( expr, msg ) + +#ifndef EFSW_COMPILER_MSVC +#define efPRINT( format, args... ) \ + {} +#define efPRINTC( cond, format, args... ) \ + {} +#else +#define efPRINT +#define efPRINTC +#endif + +#endif + +#ifdef EFSW_VERBOSE +#define efDEBUG efPRINT +#define efDEBUGC efPRINTC +#else + +#ifndef EFSW_COMPILER_MSVC +#define efDEBUG( format, args... ) \ + {} +#define efDEBUGC( cond, format, args... ) \ + {} +#else +#define efDEBUG( ... ) \ + {} +#define efDEBUGC( ... ) \ + {} +#endif + +#endif + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/DirWatcherGeneric.cpp b/vendor/efsw/src/efsw/DirWatcherGeneric.cpp new file mode 100644 index 0000000..8b6bc8a --- /dev/null +++ b/vendor/efsw/src/efsw/DirWatcherGeneric.cpp @@ -0,0 +1,388 @@ +#include +#include +#include +#include + +namespace efsw { + +DirWatcherGeneric::DirWatcherGeneric( DirWatcherGeneric* parent, WatcherGeneric* ws, + const std::string& directory, bool recursive, + bool reportNewFiles ) : + Parent( parent ), Watch( ws ), Recursive( recursive ), Deleted( false ) { + resetDirectory( directory ); + + if ( !reportNewFiles ) { + DirSnap.scan(); + } else { + DirectorySnapshotDiff Diff = DirSnap.scan(); + + if ( Diff.changed() ) { + FileInfoList::iterator it; + + DiffIterator( FilesCreated ) { + handleAction( ( *it ).Filepath, Actions::Add ); + } + } + } +} + +DirWatcherGeneric::~DirWatcherGeneric() { + /// If the directory was deleted mark the files as deleted + if ( Deleted ) { + DirectorySnapshotDiff Diff = DirSnap.scan(); + + if ( !DirSnap.exists() ) { + FileInfoList::iterator it; + + DiffIterator( FilesDeleted ) { + handleAction( ( *it ).Filepath, Actions::Delete ); + } + + DiffIterator( DirsDeleted ) { + handleAction( ( *it ).Filepath, Actions::Delete ); + } + } + } + + DirWatchMap::iterator it = Directories.begin(); + + for ( ; it != Directories.end(); ++it ) { + if ( Deleted ) { + /// If the directory was deleted, mark the flag for file deletion + it->second->Deleted = true; + } + + efSAFE_DELETE( it->second ); + } +} + +void DirWatcherGeneric::resetDirectory( std::string directory ) { + std::string dir( directory ); + + /// Is this a recursive watch? + if ( Watch->Directory != directory ) { + if ( !( directory.size() && + ( directory.at( 0 ) == FileSystem::getOSSlash() || + directory.at( directory.size() - 1 ) == FileSystem::getOSSlash() ) ) ) { + /// Get the real directory + if ( NULL != Parent ) { + std::string parentPath( Parent->DirSnap.DirectoryInfo.Filepath ); + FileSystem::dirAddSlashAtEnd( parentPath ); + FileSystem::dirAddSlashAtEnd( directory ); + + dir = parentPath + directory; + } else { + efDEBUG( "resetDirectory(): Parent is NULL. Fatal error." ); + } + } + } + + DirSnap.setDirectoryInfo( dir ); +} + +void DirWatcherGeneric::handleAction( const std::string& filename, unsigned long action, + std::string oldFilename ) { + Watch->Listener->handleFileAction( Watch->ID, DirSnap.DirectoryInfo.Filepath, + FileSystem::fileNameFromPath( filename ), (Action)action, + oldFilename ); +} + +void DirWatcherGeneric::addChilds( bool reportNewFiles ) { + if ( Recursive ) { + /// Create the subdirectories watchers + std::string dir; + + for ( FileInfoMap::iterator it = DirSnap.Files.begin(); it != DirSnap.Files.end(); it++ ) { + if ( it->second.isDirectory() && it->second.isReadable() && + !FileSystem::isRemoteFS( it->second.Filepath ) ) { + /// Check if the directory is a symbolic link + std::string curPath; + std::string link( FileSystem::getLinkRealPath( it->second.Filepath, curPath ) ); + + dir = it->first; + + if ( "" != link ) { + /// Avoid adding symlinks directories if it's now enabled + if ( !Watch->WatcherImpl->mFileWatcher->followSymlinks() ) { + continue; + } + + /// If it's a symlink check if the realpath exists as a watcher, or + /// if the path is outside the current dir + if ( Watch->WatcherImpl->pathInWatches( link ) || + Watch->pathInWatches( link ) || + !Watch->WatcherImpl->linkAllowed( curPath, link ) ) { + continue; + } else { + dir = link; + } + } else { + if ( Watch->pathInWatches( dir ) || Watch->WatcherImpl->pathInWatches( dir ) ) { + continue; + } + } + + if ( reportNewFiles ) { + handleAction( dir, Actions::Add ); + } + + Directories[dir] = + new DirWatcherGeneric( this, Watch, dir, Recursive, reportNewFiles ); + + Directories[dir]->addChilds( reportNewFiles ); + } + } + } +} + +void DirWatcherGeneric::watch( bool reportOwnChange ) { + DirectorySnapshotDiff Diff = DirSnap.scan(); + + if ( reportOwnChange && Diff.DirChanged && NULL != Parent ) { + Watch->Listener->handleFileAction( + Watch->ID, FileSystem::pathRemoveFileName( DirSnap.DirectoryInfo.Filepath ), + FileSystem::fileNameFromPath( DirSnap.DirectoryInfo.Filepath ), Actions::Modified ); + } + + if ( Diff.changed() ) { + FileInfoList::iterator it; + MovedList::iterator mit; + + /// Files + DiffIterator( FilesCreated ) { + handleAction( ( *it ).Filepath, Actions::Add ); + } + + DiffIterator( FilesModified ) { + handleAction( ( *it ).Filepath, Actions::Modified ); + } + + DiffIterator( FilesDeleted ) { + handleAction( ( *it ).Filepath, Actions::Delete ); + } + + DiffMovedIterator( FilesMoved ) { + handleAction( ( *mit ).second.Filepath, Actions::Moved, ( *mit ).first ); + } + + /// Directories + DiffIterator( DirsCreated ) { + createDirectory( ( *it ).Filepath ); + } + + DiffIterator( DirsModified ) { + handleAction( ( *it ).Filepath, Actions::Modified ); + } + + DiffIterator( DirsDeleted ) { + handleAction( ( *it ).Filepath, Actions::Delete ); + removeDirectory( ( *it ).Filepath ); + } + + DiffMovedIterator( DirsMoved ) { + handleAction( ( *mit ).second.Filepath, Actions::Moved, ( *mit ).first ); + moveDirectory( ( *mit ).first, ( *mit ).second.Filepath ); + } + } + + /// Process the subdirectories looking for changes + for ( DirWatchMap::iterator dit = Directories.begin(); dit != Directories.end(); ++dit ) { + /// Just watch + dit->second->watch(); + } +} + +void DirWatcherGeneric::watchDir( std::string& dir ) { + DirWatcherGeneric* watcher = Watch->WatcherImpl->mFileWatcher->allowOutOfScopeLinks() + ? findDirWatcher( dir ) + : findDirWatcherFast( dir ); + + if ( NULL != watcher ) { + watcher->watch( true ); + } +} + +DirWatcherGeneric* DirWatcherGeneric::findDirWatcherFast( std::string dir ) { + // remove the common base ( dir should always start with the same base as the watcher ) + efASSERT( !dir.empty() ); + efASSERT( dir.size() >= DirSnap.DirectoryInfo.Filepath.size() ); + efASSERT( DirSnap.DirectoryInfo.Filepath == + dir.substr( 0, DirSnap.DirectoryInfo.Filepath.size() ) ); + + if ( dir.size() >= DirSnap.DirectoryInfo.Filepath.size() ) { + dir = dir.substr( DirSnap.DirectoryInfo.Filepath.size() - 1 ); + } + + if ( dir.size() == 1 ) { + efASSERT( dir[0] == FileSystem::getOSSlash() ); + return this; + } + + size_t level = 0; + std::vector dirv = String::split( dir, FileSystem::getOSSlash(), false ); + + DirWatcherGeneric* watcher = this; + + while ( level < dirv.size() ) { + // search the dir level in the current watcher + DirWatchMap::iterator it = watcher->Directories.find( dirv[level] ); + + // found? continue with the next level + if ( it != watcher->Directories.end() ) { + watcher = it->second; + + level++; + } else { + // couldn't found the folder level? + // directory not watched + return NULL; + } + } + + return watcher; +} + +DirWatcherGeneric* DirWatcherGeneric::findDirWatcher( std::string dir ) { + if ( DirSnap.DirectoryInfo.Filepath == dir ) { + return this; + } else { + DirWatcherGeneric* watcher = NULL; + + for ( DirWatchMap::iterator it = Directories.begin(); it != Directories.end(); ++it ) { + watcher = it->second->findDirWatcher( dir ); + + if ( NULL != watcher ) { + return watcher; + } + } + } + + return NULL; +} + +DirWatcherGeneric* DirWatcherGeneric::createDirectory( std::string newdir ) { + FileSystem::dirRemoveSlashAtEnd( newdir ); + newdir = FileSystem::fileNameFromPath( newdir ); + + DirWatcherGeneric* dw = NULL; + + /// Check if the directory is a symbolic link + std::string parentPath( DirSnap.DirectoryInfo.Filepath ); + FileSystem::dirAddSlashAtEnd( parentPath ); + std::string dir( parentPath + newdir ); + + FileSystem::dirAddSlashAtEnd( dir ); + + FileInfo fi( dir ); + + if ( !fi.isDirectory() || !fi.isReadable() || FileSystem::isRemoteFS( dir ) ) { + return NULL; + } + + std::string curPath; + std::string link( FileSystem::getLinkRealPath( dir, curPath ) ); + bool skip = false; + + if ( "" != link ) { + /// Avoid adding symlinks directories if it's now enabled + if ( !Watch->WatcherImpl->mFileWatcher->followSymlinks() ) { + skip = true; + } + + /// If it's a symlink check if the realpath exists as a watcher, or + /// if the path is outside the current dir + if ( Watch->WatcherImpl->pathInWatches( link ) || Watch->pathInWatches( link ) || + !Watch->WatcherImpl->linkAllowed( curPath, link ) ) { + skip = true; + } else { + dir = link; + } + } else { + if ( Watch->pathInWatches( dir ) || Watch->WatcherImpl->pathInWatches( dir ) ) { + skip = true; + } + } + + if ( !skip ) { + handleAction( newdir, Actions::Add ); + + /// Creates the new directory watcher of the subfolder and check for new files + dw = new DirWatcherGeneric( this, Watch, dir, Recursive ); + + dw->addChilds(); + + dw->watch(); + + /// Add it to the list of directories + Directories[newdir] = dw; + } + + return dw; +} + +void DirWatcherGeneric::removeDirectory( std::string dir ) { + FileSystem::dirRemoveSlashAtEnd( dir ); + dir = FileSystem::fileNameFromPath( dir ); + + DirWatcherGeneric* dw = NULL; + DirWatchMap::iterator dit; + + /// Folder deleted + + /// Search the folder, it should exists + dit = Directories.find( dir ); + + if ( dit != Directories.end() ) { + dw = dit->second; + + /// Flag it as deleted so it fire the event for every file inside deleted + dw->Deleted = true; + + /// Delete the DirWatcherGeneric + efSAFE_DELETE( dw ); + + /// Remove the directory from the map + Directories.erase( dit->first ); + } +} + +void DirWatcherGeneric::moveDirectory( std::string oldDir, std::string newDir ) { + FileSystem::dirRemoveSlashAtEnd( oldDir ); + oldDir = FileSystem::fileNameFromPath( oldDir ); + + FileSystem::dirRemoveSlashAtEnd( newDir ); + newDir = FileSystem::fileNameFromPath( newDir ); + + DirWatcherGeneric* dw = NULL; + DirWatchMap::iterator dit; + + /// Directory existed? + dit = Directories.find( oldDir ); + + if ( dit != Directories.end() ) { + dw = dit->second; + + /// Remove the directory from the map + Directories.erase( dit->first ); + + Directories[newDir] = dw; + + dw->resetDirectory( newDir ); + } +} + +bool DirWatcherGeneric::pathInWatches( std::string path ) { + if ( DirSnap.DirectoryInfo.Filepath == path ) { + return true; + } + + for ( DirWatchMap::iterator it = Directories.begin(); it != Directories.end(); ++it ) { + if ( it->second->pathInWatches( path ) ) { + return true; + } + } + + return false; +} + +} // namespace efsw diff --git a/vendor/efsw/src/efsw/DirWatcherGeneric.hpp b/vendor/efsw/src/efsw/DirWatcherGeneric.hpp new file mode 100644 index 0000000..ca52de7 --- /dev/null +++ b/vendor/efsw/src/efsw/DirWatcherGeneric.hpp @@ -0,0 +1,57 @@ +#ifndef EFSW_DIRWATCHERGENERIC_HPP +#define EFSW_DIRWATCHERGENERIC_HPP + +#include +#include +#include +#include + +namespace efsw { + +class DirWatcherGeneric { + public: + typedef std::map DirWatchMap; + + DirWatcherGeneric* Parent; + WatcherGeneric* Watch; + DirectorySnapshot DirSnap; + DirWatchMap Directories; + bool Recursive; + + DirWatcherGeneric( DirWatcherGeneric* parent, WatcherGeneric* ws, const std::string& directory, + bool recursive, bool reportNewFiles = false ); + + ~DirWatcherGeneric(); + + void watch( bool reportOwnChange = false ); + + void watchDir( std::string& dir ); + + static bool isDir( const std::string& directory ); + + bool pathInWatches( std::string path ); + + void addChilds( bool reportNewFiles = true ); + + DirWatcherGeneric* findDirWatcher( std::string dir ); + + DirWatcherGeneric* findDirWatcherFast( std::string dir ); + + protected: + bool Deleted; + + DirWatcherGeneric* createDirectory( std::string newdir ); + + void removeDirectory( std::string dir ); + + void moveDirectory( std::string oldDir, std::string newDir ); + + void resetDirectory( std::string directory ); + + void handleAction( const std::string& filename, unsigned long action, + std::string oldFilename = "" ); +}; + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/DirectorySnapshot.cpp b/vendor/efsw/src/efsw/DirectorySnapshot.cpp new file mode 100644 index 0000000..f78475f --- /dev/null +++ b/vendor/efsw/src/efsw/DirectorySnapshot.cpp @@ -0,0 +1,212 @@ +#include +#include + +namespace efsw { + +DirectorySnapshot::DirectorySnapshot() {} + +DirectorySnapshot::DirectorySnapshot( std::string directory ) { + init( directory ); +} + +DirectorySnapshot::~DirectorySnapshot() {} + +void DirectorySnapshot::init( std::string directory ) { + setDirectoryInfo( directory ); + initFiles(); +} + +bool DirectorySnapshot::exists() { + return DirectoryInfo.exists(); +} + +void DirectorySnapshot::deleteAll( DirectorySnapshotDiff& Diff ) { + FileInfo fi; + + for ( FileInfoMap::iterator it = Files.begin(); it != Files.end(); it++ ) { + fi = it->second; + + if ( fi.isDirectory() ) { + Diff.DirsDeleted.push_back( fi ); + } else { + Diff.FilesDeleted.push_back( fi ); + } + } + + Files.clear(); +} + +void DirectorySnapshot::setDirectoryInfo( std::string directory ) { + DirectoryInfo = FileInfo( directory ); +} + +void DirectorySnapshot::initFiles() { + Files = FileSystem::filesInfoFromPath( DirectoryInfo.Filepath ); + + FileInfoMap::iterator it = Files.begin(); + std::vector eraseFiles; + + /// Remove all non regular files and non directories + for ( ; it != Files.end(); it++ ) { + if ( !it->second.isRegularFile() && !it->second.isDirectory() ) { + eraseFiles.push_back( it->first ); + } + } + + for ( std::vector::iterator eit = eraseFiles.begin(); eit != eraseFiles.end(); + eit++ ) { + Files.erase( *eit ); + } +} + +DirectorySnapshotDiff DirectorySnapshot::scan() { + DirectorySnapshotDiff Diff; + + Diff.clear(); + + FileInfo curFI( DirectoryInfo.Filepath ); + + Diff.DirChanged = DirectoryInfo != curFI; + + if ( Diff.DirChanged ) { + DirectoryInfo = curFI; + } + + /// If the directory was erased, create the events for files and directories deletion + if ( !curFI.exists() ) { + deleteAll( Diff ); + + return Diff; + } + + FileInfoMap files = FileSystem::filesInfoFromPath( DirectoryInfo.Filepath ); + + if ( files.empty() && Files.empty() ) { + return Diff; + } + + FileInfo fi; + FileInfoMap FilesCpy; + FileInfoMap::iterator it; + FileInfoMap::iterator fiIt; + + if ( Diff.DirChanged ) { + FilesCpy = Files; + } + + for ( it = files.begin(); it != files.end(); it++ ) { + fi = it->second; + + /// File existed before? + fiIt = Files.find( it->first ); + + if ( fiIt != Files.end() ) { + /// Erase from the file list copy + FilesCpy.erase( it->first ); + + /// File changed? + if ( ( *fiIt ).second != fi ) { + /// Update the new file info + Files[it->first] = fi; + + /// handle modified event + if ( fi.isDirectory() ) { + Diff.DirsModified.push_back( fi ); + } else { + Diff.FilesModified.push_back( fi ); + } + } + } + /// Only add regular files or directories + else if ( fi.isRegularFile() || fi.isDirectory() ) { + /// New file found + Files[it->first] = fi; + + FileInfoMap::iterator fit; + std::string oldFile = ""; + + /// Check if the same inode already existed + if ( ( fit = nodeInFiles( fi ) ) != Files.end() ) { + oldFile = fit->first; + + /// Avoid firing a Delete event + FilesCpy.erase( fit->first ); + + /// Delete the old file name + Files.erase( fit->first ); + + if ( fi.isDirectory() ) { + Diff.DirsMoved.push_back( std::make_pair( oldFile, fi ) ); + } else { + Diff.FilesMoved.push_back( std::make_pair( oldFile, fi ) ); + } + } else { + if ( fi.isDirectory() ) { + Diff.DirsCreated.push_back( fi ); + } else { + Diff.FilesCreated.push_back( fi ); + } + } + } + } + + if ( !Diff.DirChanged ) { + return Diff; + } + + /// The files or directories that remains were deleted + for ( it = FilesCpy.begin(); it != FilesCpy.end(); it++ ) { + fi = it->second; + + if ( fi.isDirectory() ) { + Diff.DirsDeleted.push_back( fi ); + } else { + Diff.FilesDeleted.push_back( fi ); + } + + /// Remove the file or directory from the list of files + Files.erase( it->first ); + } + + return Diff; +} + +FileInfoMap::iterator DirectorySnapshot::nodeInFiles( FileInfo& fi ) { + FileInfoMap::iterator it; + + if ( FileInfo::inodeSupported() ) { + for ( it = Files.begin(); it != Files.end(); it++ ) { + if ( it->second.sameInode( fi ) && it->second.Filepath != fi.Filepath ) { + return it; + } + } + } + + return Files.end(); +} + +void DirectorySnapshot::addFile( std::string path ) { + std::string name( FileSystem::fileNameFromPath( path ) ); + Files[name] = FileInfo( path ); +} + +void DirectorySnapshot::removeFile( std::string path ) { + std::string name( FileSystem::fileNameFromPath( path ) ); + + FileInfoMap::iterator it = Files.find( name ); + + if ( Files.end() != it ) { + Files.erase( it ); + } +} + +void DirectorySnapshot::moveFile( std::string oldPath, std::string newPath ) { + removeFile( oldPath ); + addFile( newPath ); +} + +void DirectorySnapshot::updateFile( std::string path ) { + addFile( path ); +} + +} // namespace efsw diff --git a/vendor/efsw/src/efsw/DirectorySnapshot.hpp b/vendor/efsw/src/efsw/DirectorySnapshot.hpp new file mode 100644 index 0000000..0e60542 --- /dev/null +++ b/vendor/efsw/src/efsw/DirectorySnapshot.hpp @@ -0,0 +1,45 @@ +#ifndef EFSW_DIRECTORYSNAPSHOT_HPP +#define EFSW_DIRECTORYSNAPSHOT_HPP + +#include + +namespace efsw { + +class DirectorySnapshot { + public: + FileInfo DirectoryInfo; + FileInfoMap Files; + + void setDirectoryInfo( std::string directory ); + + DirectorySnapshot(); + + DirectorySnapshot( std::string directory ); + + ~DirectorySnapshot(); + + void init( std::string directory ); + + bool exists(); + + DirectorySnapshotDiff scan(); + + FileInfoMap::iterator nodeInFiles( FileInfo& fi ); + + void addFile( std::string path ); + + void removeFile( std::string path ); + + void moveFile( std::string oldPath, std::string newPath ); + + void updateFile( std::string path ); + + protected: + void initFiles(); + + void deleteAll( DirectorySnapshotDiff& Diff ); +}; + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/DirectorySnapshotDiff.cpp b/vendor/efsw/src/efsw/DirectorySnapshotDiff.cpp new file mode 100644 index 0000000..37ee507 --- /dev/null +++ b/vendor/efsw/src/efsw/DirectorySnapshotDiff.cpp @@ -0,0 +1,22 @@ +#include + +namespace efsw { + +void DirectorySnapshotDiff::clear() { + FilesCreated.clear(); + FilesModified.clear(); + FilesMoved.clear(); + FilesDeleted.clear(); + DirsCreated.clear(); + DirsModified.clear(); + DirsMoved.clear(); + DirsDeleted.clear(); +} + +bool DirectorySnapshotDiff::changed() { + return !FilesCreated.empty() || !FilesModified.empty() || !FilesMoved.empty() || + !FilesDeleted.empty() || !DirsCreated.empty() || !DirsModified.empty() || + !DirsMoved.empty() || !DirsDeleted.empty(); +} + +} // namespace efsw diff --git a/vendor/efsw/src/efsw/DirectorySnapshotDiff.hpp b/vendor/efsw/src/efsw/DirectorySnapshotDiff.hpp new file mode 100644 index 0000000..26a29ec --- /dev/null +++ b/vendor/efsw/src/efsw/DirectorySnapshotDiff.hpp @@ -0,0 +1,35 @@ +#ifndef EFSW_DIRECTORYSNAPSHOTDIFF_HPP +#define EFSW_DIRECTORYSNAPSHOTDIFF_HPP + +#include + +namespace efsw { + +class DirectorySnapshotDiff { + public: + FileInfoList FilesDeleted; + FileInfoList FilesCreated; + FileInfoList FilesModified; + MovedList FilesMoved; + FileInfoList DirsDeleted; + FileInfoList DirsCreated; + FileInfoList DirsModified; + MovedList DirsMoved; + bool DirChanged; + + void clear(); + + bool changed(); +}; + +#define DiffIterator( FileInfoListName ) \ + it = Diff.FileInfoListName.begin(); \ + for ( ; it != Diff.FileInfoListName.end(); it++ ) + +#define DiffMovedIterator( MovedListName ) \ + mit = Diff.MovedListName.begin(); \ + for ( ; mit != Diff.MovedListName.end(); mit++ ) + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/FileInfo.cpp b/vendor/efsw/src/efsw/FileInfo.cpp new file mode 100644 index 0000000..707f617 --- /dev/null +++ b/vendor/efsw/src/efsw/FileInfo.cpp @@ -0,0 +1,240 @@ +#include +#include +#include + +#ifndef _DARWIN_FEATURE_64_BIT_INODE +#define _DARWIN_FEATURE_64_BIT_INODE +#endif + +#ifndef _FILE_OFFSET_BITS +#define _FILE_OFFSET_BITS 64 +#endif + +#include + +#include +#include + +#ifdef EFSW_COMPILER_MSVC +#ifndef S_ISDIR +#define S_ISDIR( f ) ( (f)&_S_IFDIR ) +#endif + +#ifndef S_ISREG +#define S_ISREG( f ) ( (f)&_S_IFREG ) +#endif + +#ifndef S_ISRDBL +#define S_ISRDBL( f ) ( (f)&_S_IREAD ) +#endif +#else +#include + +#ifndef S_ISRDBL +#define S_ISRDBL( f ) ( (f)&S_IRUSR ) +#endif +#endif + +namespace efsw { + +bool FileInfo::exists( const std::string& filePath ) { + FileInfo fi( filePath ); + return fi.exists(); +} + +bool FileInfo::isLink( const std::string& filePath ) { + FileInfo fi( filePath, true ); + return fi.isLink(); +} + +bool FileInfo::inodeSupported() { +#if EFSW_PLATFORM != EFSW_PLATFORM_WIN32 + return true; +#else + return false; +#endif +} + +FileInfo::FileInfo() : + ModificationTime( 0 ), OwnerId( 0 ), GroupId( 0 ), Permissions( 0 ), Inode( 0 ) {} + +FileInfo::FileInfo( const std::string& filepath ) : + Filepath( filepath ), + ModificationTime( 0 ), + OwnerId( 0 ), + GroupId( 0 ), + Permissions( 0 ), + Inode( 0 ) { + getInfo(); +} + +FileInfo::FileInfo( const std::string& filepath, bool linkInfo ) : + Filepath( filepath ), + ModificationTime( 0 ), + OwnerId( 0 ), + GroupId( 0 ), + Permissions( 0 ), + Inode( 0 ) { + if ( linkInfo ) { + getRealInfo(); + } else { + getInfo(); + } +} + +void FileInfo::getInfo() { +#if EFSW_PLATFORM == EFSW_PLATFORM_WIN32 + if ( Filepath.size() == 3 && Filepath[1] == ':' && Filepath[2] == FileSystem::getOSSlash() ) { + Filepath += FileSystem::getOSSlash(); + } +#endif + + /// Why i'm doing this? stat in mingw32 doesn't work for directories if the dir path ends with a + /// path slash + bool slashAtEnd = FileSystem::slashAtEnd( Filepath ); + + if ( slashAtEnd ) { + FileSystem::dirRemoveSlashAtEnd( Filepath ); + } + +#if EFSW_PLATFORM != EFSW_PLATFORM_WIN32 + struct stat st; + int res = stat( Filepath.c_str(), &st ); +#else + struct _stat st; + int res = _wstat( String::fromUtf8( Filepath ).toWideString().c_str(), &st ); +#endif + + if ( 0 == res ) { + ModificationTime = st.st_mtime; + Size = st.st_size; + OwnerId = st.st_uid; + GroupId = st.st_gid; + Permissions = st.st_mode; + Inode = st.st_ino; + } + + if ( slashAtEnd ) { + FileSystem::dirAddSlashAtEnd( Filepath ); + } +} + +void FileInfo::getRealInfo() { + bool slashAtEnd = FileSystem::slashAtEnd( Filepath ); + + if ( slashAtEnd ) { + FileSystem::dirRemoveSlashAtEnd( Filepath ); + } + +#if EFSW_PLATFORM != EFSW_PLATFORM_WIN32 + struct stat st; + int res = lstat( Filepath.c_str(), &st ); +#else + struct _stat st; + int res = _wstat( String::fromUtf8( Filepath ).toWideString().c_str(), &st ); +#endif + + if ( 0 == res ) { + ModificationTime = st.st_mtime; + Size = st.st_size; + OwnerId = st.st_uid; + GroupId = st.st_gid; + Permissions = st.st_mode; + Inode = st.st_ino; + } + + if ( slashAtEnd ) { + FileSystem::dirAddSlashAtEnd( Filepath ); + } +} + +bool FileInfo::operator==( const FileInfo& Other ) const { + return ( ModificationTime == Other.ModificationTime && Size == Other.Size && + OwnerId == Other.OwnerId && GroupId == Other.GroupId && + Permissions == Other.Permissions && Inode == Other.Inode ); +} + +bool FileInfo::isDirectory() const { + return 0 != S_ISDIR( Permissions ); +} + +bool FileInfo::isRegularFile() const { + return 0 != S_ISREG( Permissions ); +} + +bool FileInfo::isReadable() const { +#if EFSW_PLATFORM != EFSW_PLATFORM_WIN32 + static bool isRoot = getuid() == 0; + return isRoot || 0 != S_ISRDBL( Permissions ); +#else + return 0 != S_ISRDBL( Permissions ); +#endif +} + +bool FileInfo::isLink() const { +#if EFSW_PLATFORM != EFSW_PLATFORM_WIN32 + return S_ISLNK( Permissions ); +#else + return false; +#endif +} + +std::string FileInfo::linksTo() { +#if EFSW_PLATFORM != EFSW_PLATFORM_WIN32 + if ( isLink() ) { + char* ch = realpath( Filepath.c_str(), NULL ); + + if ( NULL != ch ) { + std::string tstr( ch ); + + free( ch ); + + return tstr; + } + } +#endif + return std::string( "" ); +} + +bool FileInfo::exists() { + bool slashAtEnd = FileSystem::slashAtEnd( Filepath ); + + if ( slashAtEnd ) { + FileSystem::dirRemoveSlashAtEnd( Filepath ); + } + +#if EFSW_PLATFORM != EFSW_PLATFORM_WIN32 + struct stat st; + int res = stat( Filepath.c_str(), &st ); +#else + struct _stat st; + int res = _wstat( String::fromUtf8( Filepath ).toWideString().c_str(), &st ); +#endif + + if ( slashAtEnd ) { + FileSystem::dirAddSlashAtEnd( Filepath ); + } + + return 0 == res; +} + +FileInfo& FileInfo::operator=( const FileInfo& Other ) { + this->Filepath = Other.Filepath; + this->Size = Other.Size; + this->ModificationTime = Other.ModificationTime; + this->GroupId = Other.GroupId; + this->OwnerId = Other.OwnerId; + this->Permissions = Other.Permissions; + this->Inode = Other.Inode; + return *this; +} + +bool FileInfo::sameInode( const FileInfo& Other ) const { + return inodeSupported() && Inode == Other.Inode; +} + +bool FileInfo::operator!=( const FileInfo& Other ) const { + return !( *this == Other ); +} + +} // namespace efsw diff --git a/vendor/efsw/src/efsw/FileInfo.hpp b/vendor/efsw/src/efsw/FileInfo.hpp new file mode 100644 index 0000000..1aca2a8 --- /dev/null +++ b/vendor/efsw/src/efsw/FileInfo.hpp @@ -0,0 +1,64 @@ +#ifndef EFSW_FILEINFO_HPP +#define EFSW_FILEINFO_HPP + +#include +#include +#include +#include + +namespace efsw { + +class FileInfo { + public: + static bool exists( const std::string& filePath ); + + static bool isLink( const std::string& filePath ); + + static bool inodeSupported(); + + FileInfo(); + + FileInfo( const std::string& filepath ); + + FileInfo( const std::string& filepath, bool linkInfo ); + + bool operator==( const FileInfo& Other ) const; + + bool operator!=( const FileInfo& Other ) const; + + FileInfo& operator=( const FileInfo& Other ); + + bool isDirectory() const; + + bool isRegularFile() const; + + bool isReadable() const; + + bool sameInode( const FileInfo& Other ) const; + + bool isLink() const; + + std::string linksTo(); + + bool exists(); + + void getInfo(); + + void getRealInfo(); + + std::string Filepath; + Uint64 ModificationTime; + Uint64 Size; + Uint32 OwnerId; + Uint32 GroupId; + Uint32 Permissions; + Uint64 Inode; +}; + +typedef std::map FileInfoMap; +typedef std::vector FileInfoList; +typedef std::vector> MovedList; + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/FileSystem.cpp b/vendor/efsw/src/efsw/FileSystem.cpp new file mode 100644 index 0000000..b6d2d63 --- /dev/null +++ b/vendor/efsw/src/efsw/FileSystem.cpp @@ -0,0 +1,136 @@ +#include +#include +#include + +#if EFSW_OS == EFSW_OS_MACOSX +#include +#endif + +namespace efsw { + +bool FileSystem::isDirectory( const std::string& path ) { + return Platform::FileSystem::isDirectory( path ); +} + +FileInfoMap FileSystem::filesInfoFromPath( std::string path ) { + dirAddSlashAtEnd( path ); + + return Platform::FileSystem::filesInfoFromPath( path ); +} + +char FileSystem::getOSSlash() { + return Platform::FileSystem::getOSSlash(); +} + +bool FileSystem::slashAtEnd( std::string& dir ) { + return ( dir.size() && dir[dir.size() - 1] == getOSSlash() ); +} + +void FileSystem::dirAddSlashAtEnd( std::string& dir ) { + if ( dir.size() >= 1 && dir[dir.size() - 1] != getOSSlash() ) { + dir.push_back( getOSSlash() ); + } +} + +void FileSystem::dirRemoveSlashAtEnd( std::string& dir ) { + if ( dir.size() >= 1 && dir[dir.size() - 1] == getOSSlash() ) { + dir.erase( dir.size() - 1 ); + } +} + +std::string FileSystem::fileNameFromPath( std::string filepath ) { + dirRemoveSlashAtEnd( filepath ); + + size_t pos = filepath.find_last_of( getOSSlash() ); + + if ( pos != std::string::npos ) { + return filepath.substr( pos + 1 ); + } + + return filepath; +} + +std::string FileSystem::pathRemoveFileName( std::string filepath ) { + dirRemoveSlashAtEnd( filepath ); + + size_t pos = filepath.find_last_of( getOSSlash() ); + + if ( pos != std::string::npos ) { + return filepath.substr( 0, pos + 1 ); + } + + return filepath; +} + +std::string FileSystem::getLinkRealPath( std::string dir, std::string& curPath ) { + FileSystem::dirRemoveSlashAtEnd( dir ); + FileInfo fi( dir, true ); + + /// Check with lstat and see if it's a link + if ( fi.isLink() ) { + /// get the real path of the link + std::string link( fi.linksTo() ); + + /// get the current path of the directory without the link dir path + curPath = FileSystem::pathRemoveFileName( dir ); + + /// ensure that ends with the os directory slash + FileSystem::dirAddSlashAtEnd( link ); + + return link; + } + + /// if it's not a link return nothing + return ""; +} + +std::string FileSystem::precomposeFileName( const std::string& name ) { +#if EFSW_OS == EFSW_OS_MACOSX + CFStringRef cfStringRef = + CFStringCreateWithCString( kCFAllocatorDefault, name.c_str(), kCFStringEncodingUTF8 ); + CFMutableStringRef cfMutable = CFStringCreateMutableCopy( NULL, 0, cfStringRef ); + + CFStringNormalize( cfMutable, kCFStringNormalizationFormC ); + + const char* c_str = CFStringGetCStringPtr( cfMutable, kCFStringEncodingUTF8 ); + if ( c_str != NULL ) { + std::string result( c_str ); + CFRelease( cfStringRef ); + CFRelease( cfMutable ); + return result; + } + CFIndex length = CFStringGetLength( cfMutable ); + CFIndex maxSize = CFStringGetMaximumSizeForEncoding( length, kCFStringEncodingUTF8 ); + if ( maxSize == kCFNotFound ) { + CFRelease( cfStringRef ); + CFRelease( cfMutable ); + return std::string(); + } + + std::string result( maxSize + 1, '\0' ); + if ( CFStringGetCString( cfMutable, &result[0], result.size(), kCFStringEncodingUTF8 ) ) { + result.resize( std::strlen( result.c_str() ) ); + CFRelease( cfStringRef ); + CFRelease( cfMutable ); + } else { + result.clear(); + } + return result; +#else + return name; +#endif +} + +bool FileSystem::isRemoteFS( const std::string& directory ) { + return Platform::FileSystem::isRemoteFS( directory ); +} + +bool FileSystem::changeWorkingDirectory( const std::string& directory ) { + return Platform::FileSystem::changeWorkingDirectory( directory ); +} + +std::string FileSystem::getCurrentWorkingDirectory() { + return Platform::FileSystem::getCurrentWorkingDirectory(); +} + +} // namespace efsw diff --git a/vendor/efsw/src/efsw/FileSystem.hpp b/vendor/efsw/src/efsw/FileSystem.hpp new file mode 100644 index 0000000..d8e11a0 --- /dev/null +++ b/vendor/efsw/src/efsw/FileSystem.hpp @@ -0,0 +1,40 @@ +#ifndef EFSW_FILESYSTEM_HPP +#define EFSW_FILESYSTEM_HPP + +#include +#include + +namespace efsw { + +class FileSystem { + public: + static bool isDirectory( const std::string& path ); + + static FileInfoMap filesInfoFromPath( std::string path ); + + static char getOSSlash(); + + static bool slashAtEnd( std::string& dir ); + + static void dirAddSlashAtEnd( std::string& dir ); + + static void dirRemoveSlashAtEnd( std::string& dir ); + + static std::string fileNameFromPath( std::string filepath ); + + static std::string pathRemoveFileName( std::string filepath ); + + static std::string getLinkRealPath( std::string dir, std::string& curPath ); + + static std::string precomposeFileName( const std::string& name ); + + static bool isRemoteFS( const std::string& directory ); + + static bool changeWorkingDirectory( const std::string& path ); + + static std::string getCurrentWorkingDirectory(); +}; + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/FileWatcher.cpp b/vendor/efsw/src/efsw/FileWatcher.cpp new file mode 100644 index 0000000..f45b243 --- /dev/null +++ b/vendor/efsw/src/efsw/FileWatcher.cpp @@ -0,0 +1,120 @@ +#include +#include +#include +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_WIN32 +#include +#define FILEWATCHER_IMPL FileWatcherWin32 +#define BACKEND_NAME "Win32" +#elif EFSW_PLATFORM == EFSW_PLATFORM_INOTIFY +#include +#define FILEWATCHER_IMPL FileWatcherInotify +#define BACKEND_NAME "Inotify" +#elif EFSW_PLATFORM == EFSW_PLATFORM_KQUEUE +#include +#define FILEWATCHER_IMPL FileWatcherKqueue +#define BACKEND_NAME "Kqueue" +#elif EFSW_PLATFORM == EFSW_PLATFORM_FSEVENTS +#include +#define FILEWATCHER_IMPL FileWatcherFSEvents +#define BACKEND_NAME "FSEvents" +#else +#define FILEWATCHER_IMPL FileWatcherGeneric +#define BACKEND_NAME "Generic" +#endif + +#include + +namespace efsw { + +FileWatcher::FileWatcher() : mFollowSymlinks( false ), mOutOfScopeLinks( false ) { + efDEBUG( "Using backend: %s\n", BACKEND_NAME ); + + mImpl = new FILEWATCHER_IMPL( this ); + + if ( !mImpl->initOK() ) { + efSAFE_DELETE( mImpl ); + + efDEBUG( "Falled back to backend: %s\n", BACKEND_NAME ); + + mImpl = new FileWatcherGeneric( this ); + } +} + +FileWatcher::FileWatcher( bool useGenericFileWatcher ) : + mFollowSymlinks( false ), mOutOfScopeLinks( false ) { + if ( useGenericFileWatcher ) { + efDEBUG( "Using backend: Generic\n" ); + + mImpl = new FileWatcherGeneric( this ); + } else { + efDEBUG( "Using backend: %s\n", BACKEND_NAME ); + + mImpl = new FILEWATCHER_IMPL( this ); + + if ( !mImpl->initOK() ) { + efSAFE_DELETE( mImpl ); + + efDEBUG( "Falled back to backend: %s\n", BACKEND_NAME ); + + mImpl = new FileWatcherGeneric( this ); + } + } +} + +FileWatcher::~FileWatcher() { + efSAFE_DELETE( mImpl ); +} + +WatchID FileWatcher::addWatch( const std::string& directory, FileWatchListener* watcher ) { + return addWatch( directory, watcher, false, {} ); +} + +WatchID FileWatcher::addWatch( const std::string& directory, FileWatchListener* watcher, + bool recursive ) { + return addWatch( directory, watcher, recursive, {} ); +} + +WatchID FileWatcher::addWatch( const std::string& directory, FileWatchListener* watcher, + bool recursive, const std::vector& options ) { + if ( mImpl->mIsGeneric || !FileSystem::isRemoteFS( directory ) ) { + return mImpl->addWatch( directory, watcher, recursive, options ); + } else { + return Errors::Log::createLastError( Errors::FileRemote, directory ); + } +} + +void FileWatcher::removeWatch( const std::string& directory ) { + mImpl->removeWatch( directory ); +} + +void FileWatcher::removeWatch( WatchID watchid ) { + mImpl->removeWatch( watchid ); +} + +void FileWatcher::watch() { + mImpl->watch(); +} + +std::vector FileWatcher::directories() { + return mImpl->directories(); +} + +void FileWatcher::followSymlinks( bool follow ) { + mFollowSymlinks = follow; +} + +const bool& FileWatcher::followSymlinks() const { + return mFollowSymlinks; +} + +void FileWatcher::allowOutOfScopeLinks( bool allow ) { + mOutOfScopeLinks = allow; +} + +const bool& FileWatcher::allowOutOfScopeLinks() const { + return mOutOfScopeLinks; +} + +} // namespace efsw diff --git a/vendor/efsw/src/efsw/FileWatcherCWrapper.cpp b/vendor/efsw/src/efsw/FileWatcherCWrapper.cpp new file mode 100644 index 0000000..860d7d5 --- /dev/null +++ b/vendor/efsw/src/efsw/FileWatcherCWrapper.cpp @@ -0,0 +1,131 @@ +#include +#include +#include + +#define TOBOOL( i ) ( ( i ) == 0 ? false : true ) + +/*************************************************************************************************/ +class Watcher_CAPI : public efsw::FileWatchListener { + public: + efsw_watcher mWatcher; + efsw_pfn_fileaction_callback mFn; + void* mParam; + + public: + Watcher_CAPI( efsw_watcher watcher, efsw_pfn_fileaction_callback fn, void* param ) : + mWatcher( watcher ), mFn( fn ), mParam( param ) {} + + void handleFileAction( efsw::WatchID watchid, const std::string& dir, + const std::string& filename, efsw::Action action, + std::string oldFilename = "" ) { + mFn( mWatcher, watchid, dir.c_str(), filename.c_str(), (enum efsw_action)action, + oldFilename.c_str(), mParam ); + } +}; + +/************************************************************************************************* + * globals + */ +static std::vector g_callbacks; + +Watcher_CAPI* find_callback( efsw_watcher watcher, efsw_pfn_fileaction_callback fn ) { + for ( std::vector::iterator i = g_callbacks.begin(); i != g_callbacks.end(); + ++i ) { + Watcher_CAPI* callback = *i; + + if ( callback->mFn == fn && callback->mWatcher == watcher ) + return *i; + } + + return NULL; +} + +Watcher_CAPI* remove_callback( efsw_watcher watcher ) { + std::vector::iterator i = g_callbacks.begin(); + + while ( i != g_callbacks.end() ) { + Watcher_CAPI* callback = *i; + + if ( callback->mWatcher == watcher ) + i = g_callbacks.erase( i ); + else + ++i; + } + + return NULL; +} + +/*************************************************************************************************/ +efsw_watcher efsw_create( int generic_mode ) { + return ( efsw_watcher ) new efsw::FileWatcher( TOBOOL( generic_mode ) ); +} + +void efsw_release( efsw_watcher watcher ) { + remove_callback( watcher ); + delete (efsw::FileWatcher*)watcher; +} + +const char* efsw_getlasterror() { + static std::string log_str; + log_str = efsw::Errors::Log::getLastErrorLog(); + return log_str.c_str(); +} + +EFSW_API void efsw_clearlasterror() { + efsw::Errors::Log::clearLastError(); +} + +efsw_watchid efsw_addwatch( efsw_watcher watcher, const char* directory, + efsw_pfn_fileaction_callback callback_fn, int recursive, void* param ) { + return efsw_addwatch_withoptions( watcher, directory, callback_fn, recursive, 0, 0, param ); +} + +efsw_watchid efsw_addwatch_withoptions(efsw_watcher watcher, const char* directory, + efsw_pfn_fileaction_callback callback_fn, int recursive, + efsw_watcher_option *options, int options_number, + void* param) { + Watcher_CAPI* callback = find_callback( watcher, callback_fn ); + + if ( callback == NULL ) { + callback = new Watcher_CAPI( watcher, callback_fn, param ); + g_callbacks.push_back( callback ); + } + + std::vector watcher_options{}; + for ( int i = 0; i < options_number; i++ ) { + efsw_watcher_option* option = &options[i]; + watcher_options.emplace_back( efsw::WatcherOption{ + static_cast(option->option), option->value } ); + } + + return ( (efsw::FileWatcher*)watcher ) + ->addWatch( std::string( directory ), callback, TOBOOL( recursive ), watcher_options ); +} + +void efsw_removewatch( efsw_watcher watcher, const char* directory ) { + ( (efsw::FileWatcher*)watcher )->removeWatch( std::string( directory ) ); +} + +void efsw_removewatch_byid( efsw_watcher watcher, efsw_watchid watchid ) { + ( (efsw::FileWatcher*)watcher )->removeWatch( watchid ); +} + +void efsw_watch( efsw_watcher watcher ) { + ( (efsw::FileWatcher*)watcher )->watch(); +} + +void efsw_follow_symlinks( efsw_watcher watcher, int enable ) { + ( (efsw::FileWatcher*)watcher )->followSymlinks( TOBOOL( enable ) ); +} + +int efsw_follow_symlinks_isenabled( efsw_watcher watcher ) { + return (int)( (efsw::FileWatcher*)watcher )->followSymlinks(); +} + +void efsw_allow_outofscopelinks( efsw_watcher watcher, int allow ) { + ( (efsw::FileWatcher*)watcher )->allowOutOfScopeLinks( TOBOOL( allow ) ); +} + +int efsw_outofscopelinks_isallowed( efsw_watcher watcher ) { + return (int)( (efsw::FileWatcher*)watcher )->allowOutOfScopeLinks(); +} diff --git a/vendor/efsw/src/efsw/FileWatcherFSEvents.cpp b/vendor/efsw/src/efsw/FileWatcherFSEvents.cpp new file mode 100644 index 0000000..6ea960d --- /dev/null +++ b/vendor/efsw/src/efsw/FileWatcherFSEvents.cpp @@ -0,0 +1,245 @@ +#include +#include +#include +#include +#include +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_FSEVENTS + +#include + +namespace efsw { + +int getOSXReleaseNumber() { + static int osxR = -1; + + if ( -1 == osxR ) { + struct utsname os; + + if ( -1 != uname( &os ) ) { + std::string release( os.release ); + + size_t pos = release.find_first_of( '.' ); + + if ( pos != std::string::npos ) { + release = release.substr( 0, pos ); + } + + int rel = 0; + + if ( String::fromString( rel, release ) ) { + osxR = rel; + } + } + } + + return osxR; +} + +bool FileWatcherFSEvents::isGranular() { + return getOSXReleaseNumber() >= 11; +} + +static std::string convertCFStringToStdString( CFStringRef cfString ) { + // Try to get the C string pointer directly + const char* cStr = CFStringGetCStringPtr( cfString, kCFStringEncodingUTF8 ); + + if ( cStr ) { + // If the pointer is valid, directly return a std::string from it + return std::string( cStr ); + } else { + // If not, manually convert it + CFIndex length = CFStringGetLength( cfString ); + CFIndex maxSize = CFStringGetMaximumSizeForEncoding( length, kCFStringEncodingUTF8 ) + + 1; // +1 for null terminator + + char* buffer = new char[maxSize]; + + if ( CFStringGetCString( cfString, buffer, maxSize, kCFStringEncodingUTF8 ) ) { + std::string result( buffer ); + delete[] buffer; + return result; + } else { + delete[] buffer; + return ""; + } + } +} + +void FileWatcherFSEvents::FSEventCallback( ConstFSEventStreamRef streamRef, void* userData, + size_t numEvents, void* eventPaths, + const FSEventStreamEventFlags eventFlags[], + const FSEventStreamEventId eventIds[] ) { + WatcherFSEvents* watcher = static_cast( userData ); + + std::vector events; + events.reserve( numEvents ); + + for ( size_t i = 0; i < numEvents; i++ ) { + if ( isGranular() ) { + CFDictionaryRef pathInfoDict = + static_cast( CFArrayGetValueAtIndex( (CFArrayRef)eventPaths, i ) ); + CFStringRef path = static_cast( + CFDictionaryGetValue( pathInfoDict, kFSEventStreamEventExtendedDataPathKey ) ); + CFNumberRef cfInode = static_cast( + CFDictionaryGetValue( pathInfoDict, kFSEventStreamEventExtendedFileIDKey ) ); + + if ( cfInode ) { + unsigned long inode = 0; + CFNumberGetValue( cfInode, kCFNumberLongType, &inode ); + events.push_back( FSEvent( convertCFStringToStdString( path ), (long)eventFlags[i], + (Uint64)eventIds[i], inode ) ); + } + } else { + events.push_back( FSEvent( std::string( ( (char**)eventPaths )[i] ), + (long)eventFlags[i], (Uint64)eventIds[i] ) ); + } + } + + watcher->handleActions( events ); + + watcher->process(); + + efDEBUG( "\n" ); +} + +FileWatcherFSEvents::FileWatcherFSEvents( FileWatcher* parent ) : + FileWatcherImpl( parent ), mLastWatchID( 0 ) { + mInitOK = true; + + watch(); +} + +FileWatcherFSEvents::~FileWatcherFSEvents() { + mInitOK = false; + + mWatchCond.notify_all(); + + WatchMap::iterator iter = mWatches.begin(); + + for ( ; iter != mWatches.end(); ++iter ) { + WatcherFSEvents* watch = iter->second; + + efSAFE_DELETE( watch ); + } +} + +WatchID FileWatcherFSEvents::addWatch( const std::string& directory, FileWatchListener* watcher, + bool recursive, const std::vector &options ) { + std::string dir( directory ); + + FileInfo fi( dir ); + + if ( !fi.isDirectory() ) { + return Errors::Log::createLastError( Errors::FileNotFound, dir ); + } else if ( !fi.isReadable() ) { + return Errors::Log::createLastError( Errors::FileNotReadable, dir ); + } + + FileSystem::dirAddSlashAtEnd( dir ); + + if ( pathInWatches( dir ) ) { + return Errors::Log::createLastError( Errors::FileRepeated, directory ); + } + + /// Check if the directory is a symbolic link + std::string curPath; + std::string link( FileSystem::getLinkRealPath( dir, curPath ) ); + + if ( "" != link ) { + /// If it's a symlink check if the realpath exists as a watcher, or + /// if the path is outside the current dir + if ( pathInWatches( link ) ) { + return Errors::Log::createLastError( Errors::FileRepeated, directory ); + } else if ( !linkAllowed( curPath, link ) ) { + return Errors::Log::createLastError( Errors::FileOutOfScope, dir ); + } else { + dir = link; + } + } + + mLastWatchID++; + + WatcherFSEvents* pWatch = new WatcherFSEvents(); + pWatch->Listener = watcher; + pWatch->ID = mLastWatchID; + pWatch->Directory = dir; + pWatch->Recursive = recursive; + pWatch->FWatcher = this; + + pWatch->init(); + + { + Lock lock( mWatchesLock ); + mWatches.insert( std::make_pair( mLastWatchID, pWatch ) ); + } + + mWatchCond.notify_all(); + return pWatch->ID; +} + +void FileWatcherFSEvents::removeWatch( const std::string& directory ) { + Lock lock( mWatchesLock ); + + WatchMap::iterator iter = mWatches.begin(); + + for ( ; iter != mWatches.end(); ++iter ) { + if ( directory == iter->second->Directory ) { + removeWatch( iter->second->ID ); + return; + } + } +} + +void FileWatcherFSEvents::removeWatch( WatchID watchid ) { + Lock lock( mWatchesLock ); + + WatchMap::iterator iter = mWatches.find( watchid ); + + if ( iter == mWatches.end() ) + return; + + WatcherFSEvents* watch = iter->second; + + mWatches.erase( iter ); + + efDEBUG( "Removed watch %s\n", watch->Directory.c_str() ); + + efSAFE_DELETE( watch ); +} + +void FileWatcherFSEvents::watch() {} + +void FileWatcherFSEvents::handleAction( Watcher* watch, const std::string& filename, + unsigned long action, std::string oldFilename ) { + /// Not used +} + +std::vector FileWatcherFSEvents::directories() { + std::vector dirs; + + Lock lock( mWatchesLock ); + + dirs.reserve( mWatches.size() ); + + for ( WatchMap::iterator it = mWatches.begin(); it != mWatches.end(); ++it ) { + dirs.push_back( std::string( it->second->Directory ) ); + } + + return dirs; +} + +bool FileWatcherFSEvents::pathInWatches( const std::string& path ) { + for ( WatchMap::iterator it = mWatches.begin(); it != mWatches.end(); ++it ) { + if ( it->second->Directory == path ) { + return true; + } + } + + return false; +} + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/FileWatcherFSEvents.hpp b/vendor/efsw/src/efsw/FileWatcherFSEvents.hpp new file mode 100644 index 0000000..2a192a5 --- /dev/null +++ b/vendor/efsw/src/efsw/FileWatcherFSEvents.hpp @@ -0,0 +1,103 @@ +#ifndef EFSW_FILEWATCHERFSEVENTS_HPP +#define EFSW_FILEWATCHERFSEVENTS_HPP + +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_FSEVENTS + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace efsw { + +/* OSX < 10.7 has no file events */ +/* So i declare the events constants */ +enum FSEventEvents { + efswFSEventStreamCreateFlagUseCFTypes = 0x00000001, + efswFSEventStreamCreateFlagNoDefer = 0x00000002, + efswFSEventStreamCreateFlagFileEvents = 0x00000010, + efswFSEventStreamCreateFlagUseExtendedData = 0x00000040, + efswFSEventStreamEventFlagItemCreated = 0x00000100, + efswFSEventStreamEventFlagItemRemoved = 0x00000200, + efswFSEventStreamEventFlagItemInodeMetaMod = 0x00000400, + efswFSEventStreamEventFlagItemRenamed = 0x00000800, + efswFSEventStreamEventFlagItemModified = 0x00001000, + efswFSEventStreamEventFlagItemFinderInfoMod = 0x00002000, + efswFSEventStreamEventFlagItemChangeOwner = 0x00004000, + efswFSEventStreamEventFlagItemXattrMod = 0x00008000, + efswFSEventStreamEventFlagItemIsFile = 0x00010000, + efswFSEventStreamEventFlagItemIsDir = 0x00020000, + efswFSEventStreamEventFlagItemIsSymlink = 0x00040000, + efswFSEventsModified = efswFSEventStreamEventFlagItemFinderInfoMod | + efswFSEventStreamEventFlagItemModified | + efswFSEventStreamEventFlagItemInodeMetaMod +}; + +/// Implementation for Win32 based on ReadDirectoryChangesW. +/// @class FileWatcherFSEvents +class FileWatcherFSEvents : public FileWatcherImpl { + friend class WatcherFSEvents; + + public: + /// @return If FSEvents supports file-level notifications ( true if OS X >= 10.7 ) + static bool isGranular(); + + /// type for a map from WatchID to WatcherWin32 pointer + typedef std::map WatchMap; + + FileWatcherFSEvents( FileWatcher* parent ); + + virtual ~FileWatcherFSEvents(); + + /// Add a directory watch + /// On error returns WatchID with Error type. + WatchID addWatch( const std::string& directory, FileWatchListener* watcher, bool recursive, + const std::vector &options ) override; + + /// Remove a directory watch. This is a brute force lazy search O(nlogn). + void removeWatch( const std::string& directory ) override; + + /// Remove a directory watch. This is a map lookup O(logn). + void removeWatch( WatchID watchid ) override; + + /// Updates the watcher. Must be called often. + void watch() override; + + /// Handles the action + void handleAction( Watcher* watch, const std::string& filename, unsigned long action, + std::string oldFilename = "" ) override; + + /// @return Returns a list of the directories that are being watched + std::vector directories() override; + + protected: + static void FSEventCallback( ConstFSEventStreamRef streamRef, void* userData, size_t numEvents, + void* eventPaths, const FSEventStreamEventFlags eventFlags[], + const FSEventStreamEventId eventIds[] ); + + /// Vector of WatcherWin32 pointers + WatchMap mWatches; + + /// The last watchid + WatchID mLastWatchID; + + Mutex mWatchesLock; + + bool pathInWatches( const std::string& path ) override; + + std::mutex mWatchesMutex; + std::condition_variable mWatchCond; + +}; + +} // namespace efsw + +#endif + +#endif diff --git a/vendor/efsw/src/efsw/FileWatcherGeneric.cpp b/vendor/efsw/src/efsw/FileWatcherGeneric.cpp new file mode 100644 index 0000000..3f3c52e --- /dev/null +++ b/vendor/efsw/src/efsw/FileWatcherGeneric.cpp @@ -0,0 +1,158 @@ +#include +#include +#include +#include + +namespace efsw { + +FileWatcherGeneric::FileWatcherGeneric( FileWatcher* parent ) : + FileWatcherImpl( parent ), mThread( NULL ), mLastWatchID( 0 ) { + mInitOK = true; + mIsGeneric = true; +} + +FileWatcherGeneric::~FileWatcherGeneric() { + mInitOK = false; + + efSAFE_DELETE( mThread ); + + /// Delete the watches + WatchList::iterator it = mWatches.begin(); + + for ( ; it != mWatches.end(); ++it ) { + efSAFE_DELETE( ( *it ) ); + } +} + +WatchID FileWatcherGeneric::addWatch( const std::string& directory, FileWatchListener* watcher, + bool recursive, const std::vector& options ) { + std::string dir( directory ); + + FileSystem::dirAddSlashAtEnd( dir ); + + FileInfo fi( dir ); + + if ( !fi.isDirectory() ) { + return Errors::Log::createLastError( Errors::FileNotFound, dir ); + } else if ( !fi.isReadable() ) { + return Errors::Log::createLastError( Errors::FileNotReadable, dir ); + } else if ( pathInWatches( dir ) ) { + return Errors::Log::createLastError( Errors::FileRepeated, dir ); + } + + std::string curPath; + std::string link( FileSystem::getLinkRealPath( dir, curPath ) ); + + if ( "" != link ) { + if ( pathInWatches( link ) ) { + return Errors::Log::createLastError( Errors::FileRepeated, dir ); + } else if ( !linkAllowed( curPath, link ) ) { + return Errors::Log::createLastError( Errors::FileOutOfScope, dir ); + } else { + dir = link; + } + } + + mLastWatchID++; + + WatcherGeneric* pWatch = new WatcherGeneric( mLastWatchID, dir, watcher, this, recursive ); + + Lock lock( mWatchesLock ); + mWatches.push_back( pWatch ); + + return pWatch->ID; +} + +void FileWatcherGeneric::removeWatch( const std::string& directory ) { + WatchList::iterator it = mWatches.begin(); + + for ( ; it != mWatches.end(); ++it ) { + if ( ( *it )->Directory == directory ) { + WatcherGeneric* watch = ( *it ); + + Lock lock( mWatchesLock ); + + mWatches.erase( it ); + + efSAFE_DELETE( watch ); + + return; + } + } +} + +void FileWatcherGeneric::removeWatch( WatchID watchid ) { + WatchList::iterator it = mWatches.begin(); + + for ( ; it != mWatches.end(); ++it ) { + if ( ( *it )->ID == watchid ) { + WatcherGeneric* watch = ( *it ); + + Lock lock( mWatchesLock ); + + mWatches.erase( it ); + + efSAFE_DELETE( watch ); + + return; + } + } +} + +void FileWatcherGeneric::watch() { + if ( NULL == mThread ) { + mThread = new Thread( &FileWatcherGeneric::run, this ); + mThread->launch(); + } +} + +void FileWatcherGeneric::run() { + do { + { + Lock lock( mWatchesLock ); + + WatchList::iterator it = mWatches.begin(); + + for ( ; it != mWatches.end(); ++it ) { + ( *it )->watch(); + } + } + + if ( mInitOK ) + System::sleep( 1000 ); + } while ( mInitOK ); +} + +void FileWatcherGeneric::handleAction( Watcher*, const std::string&, unsigned long, std::string ) { + /// Not used +} + +std::vector FileWatcherGeneric::directories() { + std::vector dirs; + + Lock lock( mWatchesLock ); + + dirs.reserve( mWatches.size() ); + + WatchList::iterator it = mWatches.begin(); + + for ( ; it != mWatches.end(); ++it ) { + dirs.push_back( ( *it )->Directory ); + } + + return dirs; +} + +bool FileWatcherGeneric::pathInWatches( const std::string& path ) { + WatchList::iterator it = mWatches.begin(); + + for ( ; it != mWatches.end(); ++it ) { + if ( ( *it )->Directory == path || ( *it )->pathInWatches( path ) ) { + return true; + } + } + + return false; +} + +} // namespace efsw diff --git a/vendor/efsw/src/efsw/FileWatcherGeneric.hpp b/vendor/efsw/src/efsw/FileWatcherGeneric.hpp new file mode 100644 index 0000000..47f7e04 --- /dev/null +++ b/vendor/efsw/src/efsw/FileWatcherGeneric.hpp @@ -0,0 +1,61 @@ +#ifndef EFSW_FILEWATCHERGENERIC_HPP +#define EFSW_FILEWATCHERGENERIC_HPP + +#include +#include +#include +#include + +namespace efsw { + +/// Implementation for Generic File Watcher. +/// @class FileWatcherGeneric +class FileWatcherGeneric : public FileWatcherImpl { + public: + typedef std::vector WatchList; + + FileWatcherGeneric( FileWatcher* parent ); + + virtual ~FileWatcherGeneric(); + + /// Add a directory watch + /// On error returns WatchID with Error type. + WatchID addWatch( const std::string& directory, FileWatchListener* watcher, bool recursive, + const std::vector &options ) override; + + /// Remove a directory watch. This is a brute force lazy search O(nlogn). + void removeWatch( const std::string& directory ) override; + + /// Remove a directory watch. This is a map lookup O(logn). + void removeWatch( WatchID watchid ) override; + + /// Updates the watcher. Must be called often. + void watch() override; + + /// Handles the action + void handleAction( Watcher* watch, const std::string& filename, unsigned long action, + std::string oldFilename = "" ) override; + + /// @return Returns a list of the directories that are being watched + std::vector directories() override; + + protected: + Thread* mThread; + + /// The last watchid + WatchID mLastWatchID; + + /// Map of WatchID to WatchStruct pointers + WatchList mWatches; + + Mutex mWatchesLock; + + bool pathInWatches( const std::string& path ) override; + + private: + void run(); +}; + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/FileWatcherImpl.cpp b/vendor/efsw/src/efsw/FileWatcherImpl.cpp new file mode 100644 index 0000000..bf69a45 --- /dev/null +++ b/vendor/efsw/src/efsw/FileWatcherImpl.cpp @@ -0,0 +1,34 @@ +#include +#include +#include + +namespace efsw { + +FileWatcherImpl::FileWatcherImpl( FileWatcher* parent ) : + mFileWatcher( parent ), mInitOK( false ), mIsGeneric( false ) { + System::maxFD(); +} + +FileWatcherImpl::~FileWatcherImpl() {} + +bool FileWatcherImpl::initOK() { + return static_cast( mInitOK ); +} + +bool FileWatcherImpl::linkAllowed( const std::string& curPath, const std::string& link ) { + return ( mFileWatcher->followSymlinks() && mFileWatcher->allowOutOfScopeLinks() ) || + -1 != String::strStartsWith( curPath, link ); +} + +int FileWatcherImpl::getOptionValue( const std::vector& options, Option option, + int defaultValue ) { + for ( size_t i = 0; i < options.size(); i++ ) { + if ( options[i].mOption == option ) { + return options[i].mValue; + } + } + + return defaultValue; +} + +} // namespace efsw diff --git a/vendor/efsw/src/efsw/FileWatcherImpl.hpp b/vendor/efsw/src/efsw/FileWatcherImpl.hpp new file mode 100644 index 0000000..a6ec472 --- /dev/null +++ b/vendor/efsw/src/efsw/FileWatcherImpl.hpp @@ -0,0 +1,64 @@ +#ifndef EFSW_FILEWATCHERIMPL_HPP +#define EFSW_FILEWATCHERIMPL_HPP + +#include +#include +#include +#include +#include +#include + +namespace efsw { + +class FileWatcherImpl { + public: + FileWatcherImpl( FileWatcher* parent ); + + virtual ~FileWatcherImpl(); + + /// Add a directory watch + /// On error returns WatchID with Error type. + virtual WatchID addWatch( const std::string& directory, FileWatchListener* watcher, + bool recursive, const std::vector& options = {} ) = 0; + + /// Remove a directory watch. This is a brute force lazy search O(nlogn). + virtual void removeWatch( const std::string& directory ) = 0; + + /// Remove a directory watch. This is a map lookup O(logn). + virtual void removeWatch( WatchID watchid ) = 0; + + /// Updates the watcher. Must be called often. + virtual void watch() = 0; + + /// Handles the action + virtual void handleAction( Watcher* watch, const std::string& filename, unsigned long action, + std::string oldFilename = "" ) = 0; + + /// @return Returns a list of the directories that are being watched + virtual std::vector directories() = 0; + + /// @return true if the backend init successfully + virtual bool initOK(); + + /// @return If the link is allowed according to the current path and the state of out scope + /// links + virtual bool linkAllowed( const std::string& curPath, const std::string& link ); + + /// Search if a directory already exists in the watches + virtual bool pathInWatches( const std::string& path ) = 0; + + protected: + friend class FileWatcher; + friend class DirWatcherGeneric; + + FileWatcher* mFileWatcher; + Atomic mInitOK; + bool mIsGeneric; + + int getOptionValue( const std::vector& options, Option option, + int defaultValue ); +}; + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/FileWatcherInotify.cpp b/vendor/efsw/src/efsw/FileWatcherInotify.cpp new file mode 100644 index 0000000..29be12b --- /dev/null +++ b/vendor/efsw/src/efsw/FileWatcherInotify.cpp @@ -0,0 +1,562 @@ +#include +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_INOTIFY + +#include +#include +#include +#include +#include +#include + +#ifdef EFSW_INOTIFY_NOSYS +#include +#else +#include +#endif + +#include +#include +#include +#include +#include + +#define BUFF_SIZE ( ( sizeof( struct inotify_event ) + FILENAME_MAX ) * 1024 ) + +namespace efsw { + +FileWatcherInotify::FileWatcherInotify( FileWatcher* parent ) : + FileWatcherImpl( parent ), mFD( -1 ), mThread( NULL ), mIsTakingAction( false ) { + mFD = inotify_init(); + + if ( mFD < 0 ) { + efDEBUG( "Error: %s\n", strerror( errno ) ); + } else { + mInitOK = true; + } +} + +FileWatcherInotify::~FileWatcherInotify() { + mInitOK = false; + // There is deadlock when release FileWatcherInotify instance since its handAction + // function is still running and hangs in requiring lock without init lock captured. + while ( mIsTakingAction ) { + // It'd use condition-wait instead of sleep. Actually efsw has no such + // implementation so we just skip and sleep while for that to avoid deadlock. + usleep( 1000 ); + }; + Lock initLock( mInitLock ); + + efSAFE_DELETE( mThread ); + + Lock l( mWatchesLock ); + Lock l2( mRealWatchesLock ); + + WatchMap::iterator iter = mWatches.begin(); + WatchMap::iterator end = mWatches.end(); + + for ( ; iter != end; ++iter ) { + efSAFE_DELETE( iter->second ); + } + + mWatches.clear(); + + if ( mFD != -1 ) { + close( mFD ); + mFD = -1; + } +} + +WatchID FileWatcherInotify::addWatch( const std::string& directory, FileWatchListener* watcher, + bool recursive, const std::vector& ) { + if ( !mInitOK ) + return Errors::Log::createLastError( Errors::Unspecified, directory ); + Lock initLock( mInitLock ); + return addWatch( directory, watcher, recursive, NULL ); +} + +WatchID FileWatcherInotify::addWatch( const std::string& directory, FileWatchListener* watcher, + bool recursive, WatcherInotify* parent ) { + std::string dir( directory ); + + FileSystem::dirAddSlashAtEnd( dir ); + + FileInfo fi( dir ); + + if ( !fi.isDirectory() ) { + return Errors::Log::createLastError( Errors::FileNotFound, dir ); + } else if ( !fi.isReadable() ) { + return Errors::Log::createLastError( Errors::FileNotReadable, dir ); + } else if ( pathInWatches( dir ) ) { + return Errors::Log::createLastError( Errors::FileRepeated, directory ); + } else if ( NULL != parent && FileSystem::isRemoteFS( dir ) ) { + return Errors::Log::createLastError( Errors::FileRemote, dir ); + } + + /// Check if the directory is a symbolic link + std::string curPath; + std::string link( FileSystem::getLinkRealPath( dir, curPath ) ); + + if ( "" != link ) { + /// Avoid adding symlinks directories if it's now enabled + if ( NULL != parent && !mFileWatcher->followSymlinks() ) { + return Errors::Log::createLastError( Errors::FileOutOfScope, dir ); + } + + /// If it's a symlink check if the realpath exists as a watcher, or + /// if the path is outside the current dir + if ( pathInWatches( link ) ) { + return Errors::Log::createLastError( Errors::FileRepeated, directory ); + } else if ( !linkAllowed( curPath, link ) ) { + return Errors::Log::createLastError( Errors::FileOutOfScope, dir ); + } else { + dir = link; + } + } + + int wd = inotify_add_watch( mFD, dir.c_str(), + IN_CLOSE_WRITE | IN_MOVED_TO | IN_CREATE | IN_MOVED_FROM | + IN_DELETE | IN_MODIFY ); + + if ( wd < 0 ) { + if ( errno == ENOENT ) { + return Errors::Log::createLastError( Errors::FileNotFound, dir ); + } else { + return Errors::Log::createLastError( Errors::Unspecified, + std::string( strerror( errno ) ) ); + } + } + + efDEBUG( "Added watch %s with id: %d\n", dir.c_str(), wd ); + + WatcherInotify* pWatch = new WatcherInotify(); + pWatch->Listener = watcher; + pWatch->ID = parent ? parent->ID : wd; + pWatch->InotifyID = wd; + pWatch->Directory = dir; + pWatch->Recursive = recursive; + pWatch->Parent = parent; + + { + Lock lock( mWatchesLock ); + mWatches.insert( std::make_pair( wd, pWatch ) ); + mWatchesRef[pWatch->Directory] = wd; + } + + if ( NULL == pWatch->Parent ) { + Lock l( mRealWatchesLock ); + mRealWatches[pWatch->InotifyID] = pWatch; + } + + if ( pWatch->Recursive ) { + std::map files = FileSystem::filesInfoFromPath( pWatch->Directory ); + std::map::iterator it = files.begin(); + + for ( ; it != files.end(); ++it ) { + if ( !mInitOK ) + break; + + const FileInfo& cfi = it->second; + + if ( cfi.isDirectory() && cfi.isReadable() ) { + addWatch( cfi.Filepath, watcher, recursive, pWatch ); + } + } + } + + return wd; +} + +void FileWatcherInotify::removeWatchLocked( WatchID watchid ) { + WatchMap::iterator iter = mWatches.find( watchid ); + if ( iter == mWatches.end() ) + return; + + WatcherInotify* watch = iter->second; + + for ( std::vector>::iterator itm = + mMovedOutsideWatches.begin(); + mMovedOutsideWatches.end() != itm; ++itm ) { + if ( itm->first == watch ) { + mMovedOutsideWatches.erase( itm ); + break; + } + } + + if ( watch->Recursive && NULL == watch->Parent ) { + WatchMap::iterator it = mWatches.begin(); + std::vector eraseWatches; + + for ( ; it != mWatches.end(); ++it ) + if ( it->second != watch && it->second->inParentTree( watch ) ) + eraseWatches.push_back( it->second->InotifyID ); + + for ( std::vector::iterator eit = eraseWatches.begin(); eit != eraseWatches.end(); + ++eit ) { + removeWatch( *eit ); + } + } + + mWatchesRef.erase( watch->Directory ); + mWatches.erase( iter ); + + if ( NULL == watch->Parent ) { + WatchMap::iterator eraseit = mRealWatches.find( watch->InotifyID ); + + if ( eraseit != mRealWatches.end() ) { + mRealWatches.erase( eraseit ); + } + } + + int err = inotify_rm_watch( mFD, watchid ); + + if ( err < 0 ) { + efDEBUG( "Error removing watch %d: %s\n", watchid, strerror( errno ) ); + } else { + efDEBUG( "Removed watch %s with id: %d\n", watch->Directory.c_str(), watchid ); + } + + efSAFE_DELETE( watch ); +} + +void FileWatcherInotify::removeWatch( const std::string& directory ) { + if ( !mInitOK ) + return; + Lock initLock( mInitLock ); + Lock lock( mWatchesLock ); + Lock l( mRealWatchesLock ); + + std::unordered_map::iterator ref = mWatchesRef.find( directory ); + if ( ref == mWatchesRef.end() ) + return; + + removeWatchLocked( ref->second ); +} + +void FileWatcherInotify::removeWatch( WatchID watchid ) { + if ( !mInitOK ) + return; + Lock initLock( mInitLock ); + Lock lock( mWatchesLock ); + removeWatchLocked( watchid ); +} + +void FileWatcherInotify::watch() { + if ( NULL == mThread ) { + mThread = new Thread( &FileWatcherInotify::run, this ); + mThread->launch(); + } +} + +Watcher* FileWatcherInotify::watcherContainsDirectory( std::string dir ) { + FileSystem::dirRemoveSlashAtEnd( dir ); + std::string watcherPath = FileSystem::pathRemoveFileName( dir ); + FileSystem::dirAddSlashAtEnd( watcherPath ); + Lock lock( mWatchesLock ); + + for ( WatchMap::iterator it = mWatches.begin(); it != mWatches.end(); ++it ) { + Watcher* watcher = it->second; + if ( watcher->Directory == watcherPath ) + return watcher; + } + + return NULL; +} + +void FileWatcherInotify::run() { + char* buff = new char[BUFF_SIZE]; + memset( buff, 0, BUFF_SIZE ); + WatchMap::iterator wit; + + WatcherInotify* currentMoveFrom = NULL; + u_int32_t currentMoveCookie = -1; + bool lastWasMovedFrom = false; + std::string prevOldFileName; + + do { + fd_set rfds; + FD_ZERO( &rfds ); + FD_SET( mFD, &rfds ); + timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 100000; + + if ( select( FD_SETSIZE, &rfds, NULL, NULL, &timeout ) > 0 ) { + ssize_t len; + + len = read( mFD, buff, BUFF_SIZE ); + + if ( len != -1 ) { + ssize_t i = 0; + + while ( i < len ) { + struct inotify_event* pevent = (struct inotify_event*)&buff[i]; + + { + { + Lock lock( mWatchesLock ); + + wit = mWatches.find( pevent->wd ); + } + + if ( wit != mWatches.end() ) { + handleAction( wit->second, (char*)pevent->name, pevent->mask ); + + if ( ( pevent->mask & IN_MOVED_TO ) && wit->second == currentMoveFrom && + pevent->cookie == currentMoveCookie ) { + /// make pair success + currentMoveFrom = NULL; + currentMoveCookie = -1; + } else if ( pevent->mask & IN_MOVED_FROM ) { + // Previous event was moved from and current event is moved from + // Treat it as a DELETE or moved ouside watches + if ( lastWasMovedFrom && currentMoveFrom ) { + mMovedOutsideWatches.push_back( + std::make_pair( currentMoveFrom, prevOldFileName ) ); + } + + currentMoveFrom = wit->second; + currentMoveCookie = pevent->cookie; + } else { + /// Keep track of the IN_MOVED_FROM events to know + /// if the IN_MOVED_TO event is also fired + if ( currentMoveFrom ) { + mMovedOutsideWatches.push_back( + std::make_pair( currentMoveFrom, prevOldFileName ) ); + } + + currentMoveFrom = NULL; + currentMoveCookie = -1; + } + } + + lastWasMovedFrom = ( pevent->mask & IN_MOVED_FROM ) != 0; + if ( pevent->mask & IN_MOVED_FROM ) + prevOldFileName = std::string( (char*)pevent->name ); + } + + i += sizeof( struct inotify_event ) + pevent->len; + } + } + } else { + // Here means no event received + // If last event is IN_MOVED_FROM, we assume no IN_MOVED_TO + if ( currentMoveFrom ) { + mMovedOutsideWatches.push_back( + std::make_pair( currentMoveFrom, currentMoveFrom->OldFileName ) ); + } + + currentMoveFrom = NULL; + currentMoveCookie = -1; + } + + if ( !mMovedOutsideWatches.empty() ) { + // We need to make a copy since the element mMovedOutsideWatches could be modified + // during the iteration. + std::vector> movedOutsideWatches( + mMovedOutsideWatches ); + + /// In case that the IN_MOVED_TO is never fired means that the file was moved to other + /// folder + for ( std::vector>::iterator it = + movedOutsideWatches.begin(); + it != movedOutsideWatches.end(); ++it ) { + + // Skip if the watch has already being removed + if ( mMovedOutsideWatches.size() != movedOutsideWatches.size() ) { + bool found = false; + for ( std::vector>::iterator itm = + mMovedOutsideWatches.begin(); + mMovedOutsideWatches.end() != itm; ++itm ) { + if ( itm->first == it->first ) { + found = true; + break; + } + } + if ( !found ) + continue; + } + + Watcher* watch = ( *it ).first; + const std::string& oldFileName = ( *it ).second; + + /// Check if the file move was a folder already being watched + std::vector eraseWatches; + + { + Lock lock( mWatchesLock ); + + for ( ; wit != mWatches.end(); ++wit ) { + Watcher* oldWatch = wit->second; + + if ( oldWatch != watch && + -1 != String::strStartsWith( watch->Directory + oldFileName + "/", + oldWatch->Directory ) ) { + eraseWatches.push_back( oldWatch ); + } + } + } + + /// Remove invalid watches + std::stable_sort( eraseWatches.begin(), eraseWatches.end(), + []( const Watcher* left, const Watcher* right ) { + return left->Directory < right->Directory; + } ); + + if ( eraseWatches.empty() ) { + handleAction( watch, oldFileName, IN_DELETE ); + } else { + for ( std::vector::reverse_iterator eit = eraseWatches.rbegin(); + eit != eraseWatches.rend(); ++eit ) { + Watcher* rmWatch = *eit; + + /// Create Delete event for removed watches that have been moved too + if ( Watcher* cntWatch = watcherContainsDirectory( rmWatch->Directory ) ) { + handleAction( cntWatch, + FileSystem::fileNameFromPath( rmWatch->Directory ), + IN_DELETE ); + } + } + } + } + + mMovedOutsideWatches.clear(); + } + } while ( mInitOK ); + + delete[] buff; +} + +void FileWatcherInotify::checkForNewWatcher( Watcher* watch, std::string fpath ) { + FileSystem::dirAddSlashAtEnd( fpath ); + + /// If the watcher is recursive, checks if the new file is a folder, and creates a watcher + if ( watch->Recursive && FileSystem::isDirectory( fpath ) ) { + bool found = false; + + { + Lock lock( mWatchesLock ); + + /// First check if exists + for ( WatchMap::iterator it = mWatches.begin(); it != mWatches.end(); ++it ) { + if ( it->second->Directory == fpath ) { + found = true; + break; + } + } + } + + if ( !found ) { + addWatch( fpath, watch->Listener, watch->Recursive, + static_cast( watch ) ); + } + } +} + +void FileWatcherInotify::handleAction( Watcher* watch, const std::string& filename, + unsigned long action, std::string ) { + if ( !watch || !watch->Listener || !mInitOK ) { + return; + } + mIsTakingAction = true; + Lock initLock( mInitLock ); + + std::string fpath( watch->Directory + filename ); + + if ( ( IN_CLOSE_WRITE & action ) || ( IN_MODIFY & action ) ) { + watch->Listener->handleFileAction( watch->ID, watch->Directory, filename, + Actions::Modified ); + } else if ( IN_MOVED_TO & action ) { + /// If OldFileName doesn't exist means that the file has been moved from other folder, so we + /// just send the Add event + if ( watch->OldFileName.empty() ) { + watch->Listener->handleFileAction( watch->ID, watch->Directory, filename, + Actions::Add ); + + watch->Listener->handleFileAction( watch->ID, watch->Directory, filename, + Actions::Modified ); + + checkForNewWatcher( watch, fpath ); + } else { + watch->Listener->handleFileAction( watch->ID, watch->Directory, filename, + Actions::Moved, watch->OldFileName ); + } + + if ( watch->Recursive && FileSystem::isDirectory( fpath ) ) { + /// Update the new directory path + std::string opath( watch->Directory + watch->OldFileName ); + FileSystem::dirAddSlashAtEnd( opath ); + FileSystem::dirAddSlashAtEnd( fpath ); + + Lock lock( mWatchesLock ); + + for ( WatchMap::iterator it = mWatches.begin(); it != mWatches.end(); ++it ) { + if ( it->second->Directory == opath ) { + it->second->Directory = fpath; + it->second->DirInfo = FileInfo( fpath ); + } else if ( -1 != String::strStartsWith( opath, it->second->Directory ) ) { + it->second->Directory = fpath + it->second->Directory.substr( opath.size() ); + it->second->DirInfo.Filepath = it->second->Directory; + } + } + } + + watch->OldFileName = ""; + } else if ( IN_CREATE & action ) { + watch->Listener->handleFileAction( watch->ID, watch->Directory, filename, Actions::Add ); + + checkForNewWatcher( watch, fpath ); + } else if ( IN_MOVED_FROM & action ) { + watch->OldFileName = filename; + } else if ( IN_DELETE & action ) { + watch->Listener->handleFileAction( watch->ID, watch->Directory, filename, Actions::Delete ); + + FileSystem::dirAddSlashAtEnd( fpath ); + + /// If the file erased is a directory and recursive is enabled, removes the directory erased + if ( watch->Recursive ) { + Lock l( mWatchesLock ); + + for ( WatchMap::iterator it = mWatches.begin(); it != mWatches.end(); ++it ) { + if ( it->second->Directory == fpath ) { + removeWatchLocked( it->second->InotifyID ); + break; + } + } + } + } + mIsTakingAction = false; +} + +std::vector FileWatcherInotify::directories() { + std::vector dirs; + + Lock l( mRealWatchesLock ); + + dirs.reserve( mRealWatches.size() ); + + WatchMap::iterator it = mRealWatches.begin(); + + for ( ; it != mRealWatches.end(); ++it ) + dirs.push_back( it->second->Directory ); + + return dirs; +} + +bool FileWatcherInotify::pathInWatches( const std::string& path ) { + Lock l( mRealWatchesLock ); + + /// Search in the real watches, since it must allow adding a watch already watched as a subdir + WatchMap::iterator it = mRealWatches.begin(); + + for ( ; it != mRealWatches.end(); ++it ) + if ( it->second->Directory == path ) + return true; + + return false; +} + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/FileWatcherInotify.hpp b/vendor/efsw/src/efsw/FileWatcherInotify.hpp new file mode 100644 index 0000000..84174a0 --- /dev/null +++ b/vendor/efsw/src/efsw/FileWatcherInotify.hpp @@ -0,0 +1,86 @@ +#ifndef EFSW_FILEWATCHERLINUX_HPP +#define EFSW_FILEWATCHERLINUX_HPP + +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_INOTIFY + +#include +#include +#include +#include + +namespace efsw { + +/// Implementation for Linux based on inotify. +/// @class FileWatcherInotify +class FileWatcherInotify : public FileWatcherImpl { + public: + /// type for a map from WatchID to WatchStruct pointer + typedef std::map WatchMap; + + FileWatcherInotify( FileWatcher* parent ); + + virtual ~FileWatcherInotify(); + + /// Add a directory watch + /// On error returns WatchID with Error type. + WatchID addWatch( const std::string& directory, FileWatchListener* watcher, bool recursive, + const std::vector& options ) override; + + /// Remove a directory watch. This is a brute force lazy search O(nlogn). + void removeWatch( const std::string& directory ) override; + + /// Remove a directory watch. This is a map lookup O(logn). + void removeWatch( WatchID watchid ) override; + + /// Updates the watcher. Must be called often. + void watch() override; + + /// Handles the action + void handleAction( Watcher* watch, const std::string& filename, unsigned long action, + std::string oldFilename = "" ) override; + + /// @return Returns a list of the directories that are being watched + std::vector directories() override; + + protected: + /// Map of WatchID to WatchStruct pointers + WatchMap mWatches; + + /// User added watches + WatchMap mRealWatches; + + std::unordered_map mWatchesRef; + + /// inotify file descriptor + int mFD; + + Thread* mThread; + + Mutex mWatchesLock; + Mutex mRealWatchesLock; + Mutex mInitLock; + bool mIsTakingAction; + std::vector> mMovedOutsideWatches; + + WatchID addWatch( const std::string& directory, FileWatchListener* watcher, bool recursive, + WatcherInotify* parent = NULL ); + + bool pathInWatches( const std::string& path ) override; + + private: + void run(); + + void removeWatchLocked( WatchID watchid ); + + void checkForNewWatcher( Watcher* watch, std::string fpath ); + + Watcher* watcherContainsDirectory( std::string dir ); +}; + +} // namespace efsw + +#endif + +#endif diff --git a/vendor/efsw/src/efsw/FileWatcherKqueue.cpp b/vendor/efsw/src/efsw/FileWatcherKqueue.cpp new file mode 100644 index 0000000..32ef3dc --- /dev/null +++ b/vendor/efsw/src/efsw/FileWatcherKqueue.cpp @@ -0,0 +1,229 @@ +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_KQUEUE || EFSW_PLATFORM == EFSW_PLATFORM_FSEVENTS + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace efsw { + +FileWatcherKqueue::FileWatcherKqueue( FileWatcher* parent ) : + FileWatcherImpl( parent ), + mLastWatchID( 0 ), + mThread( NULL ), + mFileDescriptorCount( 1 ), + mAddingWatcher( false ) { + mTimeOut.tv_sec = 0; + mTimeOut.tv_nsec = 0; + mInitOK = true; +} + +FileWatcherKqueue::~FileWatcherKqueue() { + WatchMap::iterator iter = mWatches.begin(); + + for ( ; iter != mWatches.end(); ++iter ) { + efSAFE_DELETE( iter->second ); + } + + mWatches.clear(); + + mInitOK = false; + + efSAFE_DELETE( mThread ); +} + +WatchID FileWatcherKqueue::addWatch( const std::string& directory, FileWatchListener* watcher, + bool recursive, const std::vector& options ) { + static bool s_ug = false; + + std::string dir( directory ); + + FileSystem::dirAddSlashAtEnd( dir ); + + FileInfo fi( dir ); + + if ( !fi.isDirectory() ) { + return Errors::Log::createLastError( Errors::FileNotFound, dir ); + } else if ( !fi.isReadable() ) { + return Errors::Log::createLastError( Errors::FileNotReadable, dir ); + } else if ( pathInWatches( dir ) ) { + return Errors::Log::createLastError( Errors::FileRepeated, directory ); + } + + std::string curPath; + std::string link( FileSystem::getLinkRealPath( dir, curPath ) ); + + if ( "" != link ) { + if ( pathInWatches( link ) ) { + return Errors::Log::createLastError( Errors::FileRepeated, directory ); + } else if ( !linkAllowed( curPath, link ) ) { + return Errors::Log::createLastError( Errors::FileOutOfScope, dir ); + } else { + dir = link; + } + } + + /// Check first if are enough file descriptors available to create another kqueue watcher, + /// otherwise it creates a generic watcher + if ( availablesFD() ) { + mAddingWatcher = true; + + WatcherKqueue* watch = new WatcherKqueue( ++mLastWatchID, dir, watcher, recursive, this ); + + { + Lock lock( mWatchesLock ); + mWatches.insert( std::make_pair( mLastWatchID, watch ) ); + } + + watch->addAll(); + + // if failed to open the directory... erase the watcher + if ( !watch->initOK() ) { + int le = watch->lastErrno(); + + mWatches.erase( watch->ID ); + + efSAFE_DELETE( watch ); + + mLastWatchID--; + + // Probably the folder has too many files, create a generic watcher + if ( EACCES != le ) { + WatcherGeneric* genericWatch = + new WatcherGeneric( ++mLastWatchID, dir, watcher, this, recursive ); + + Lock lock( mWatchesLock ); + mWatches.insert( std::make_pair( mLastWatchID, genericWatch ) ); + } else { + return Errors::Log::createLastError( Errors::Unspecified, link ); + } + } + + mAddingWatcher = false; + } else { + if ( !s_ug ) { + efDEBUG( "Started using generic watcher, file descriptor limit reached: %ld\n", + mFileDescriptorCount ); + s_ug = true; + } + + WatcherGeneric* watch = new WatcherGeneric( ++mLastWatchID, dir, watcher, this, recursive ); + + Lock lock( mWatchesLock ); + mWatches.insert( std::make_pair( mLastWatchID, watch ) ); + } + + return mLastWatchID; +} + +void FileWatcherKqueue::removeWatch( const std::string& directory ) { + Lock lock( mWatchesLock ); + + WatchMap::iterator iter = mWatches.begin(); + + for ( ; iter != mWatches.end(); ++iter ) { + if ( directory == iter->second->Directory ) { + removeWatch( iter->first ); + return; + } + } +} + +void FileWatcherKqueue::removeWatch( WatchID watchid ) { + Lock lock( mWatchesLock ); + + WatchMap::iterator iter = mWatches.find( watchid ); + + if ( iter == mWatches.end() ) + return; + + Watcher* watch = iter->second; + + mWatches.erase( iter ); + + efSAFE_DELETE( watch ); +} + +bool FileWatcherKqueue::isAddingWatcher() const { + return mAddingWatcher; +} + +void FileWatcherKqueue::watch() { + if ( NULL == mThread ) { + mThread = new Thread( &FileWatcherKqueue::run, this ); + mThread->launch(); + } +} + +void FileWatcherKqueue::run() { + do { + { + Lock lock( mWatchesLock ); + + for ( WatchMap::iterator it = mWatches.begin(); it != mWatches.end(); ++it ) { + it->second->watch(); + } + } + + System::sleep( 500 ); + } while ( mInitOK ); +} + +void FileWatcherKqueue::handleAction( Watcher* watch, const std::string& filename, + unsigned long action, std::string oldFilename ) {} + +std::vector FileWatcherKqueue::directories() { + std::vector dirs; + + Lock lock( mWatchesLock ); + + dirs.reserve( mWatches.size() ); + + WatchMap::iterator it = mWatches.begin(); + + for ( ; it != mWatches.end(); ++it ) { + dirs.push_back( it->second->Directory ); + } + + return dirs; +} + +bool FileWatcherKqueue::pathInWatches( const std::string& path ) { + WatchMap::iterator it = mWatches.begin(); + + for ( ; it != mWatches.end(); ++it ) { + if ( it->second->Directory == path ) { + return true; + } + } + + return false; +} + +void FileWatcherKqueue::addFD() { + mFileDescriptorCount++; +} + +void FileWatcherKqueue::removeFD() { + mFileDescriptorCount--; +} + +bool FileWatcherKqueue::availablesFD() { + return mFileDescriptorCount <= (Int64)System::getMaxFD() - 500; +} + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/FileWatcherKqueue.hpp b/vendor/efsw/src/efsw/FileWatcherKqueue.hpp new file mode 100644 index 0000000..ff5327b --- /dev/null +++ b/vendor/efsw/src/efsw/FileWatcherKqueue.hpp @@ -0,0 +1,81 @@ +#ifndef EFSW_FILEWATCHEROSX_HPP +#define EFSW_FILEWATCHEROSX_HPP + +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_KQUEUE || EFSW_PLATFORM == EFSW_PLATFORM_FSEVENTS + +#include + +namespace efsw { + +/// Implementation for OSX based on kqueue. +/// @class FileWatcherKqueue +class FileWatcherKqueue : public FileWatcherImpl { + friend class WatcherKqueue; + + public: + FileWatcherKqueue( FileWatcher* parent ); + + virtual ~FileWatcherKqueue(); + + /// Add a directory watch + /// On error returns WatchID with Error type. + WatchID addWatch( const std::string& directory, FileWatchListener* watcher, bool recursive, + const std::vector &options ) override; + + /// Remove a directory watch. This is a brute force lazy search O(nlogn). + void removeWatch( const std::string& directory ) override; + + /// Remove a directory watch. This is a map lookup O(logn). + void removeWatch( WatchID watchid ) override; + + /// Updates the watcher. Must be called often. + void watch() override; + + /// Handles the action + void handleAction( Watcher* watch, const std::string& filename, unsigned long action, + std::string oldFilename = "" ) override; + + /// @return Returns a list of the directories that are being watched + std::vector directories() override; + + protected: + /// Map of WatchID to WatchStruct pointers + WatchMap mWatches; + + /// time out data + struct timespec mTimeOut; + + /// WatchID allocator + int mLastWatchID; + + Thread* mThread; + + Mutex mWatchesLock; + + std::vector mRemoveList; + + long mFileDescriptorCount; + + bool mAddingWatcher; + + bool isAddingWatcher() const; + + bool pathInWatches( const std::string& path ) override; + + void addFD(); + + void removeFD(); + + bool availablesFD(); + + private: + void run(); +}; + +} // namespace efsw + +#endif + +#endif diff --git a/vendor/efsw/src/efsw/FileWatcherWin32.cpp b/vendor/efsw/src/efsw/FileWatcherWin32.cpp new file mode 100644 index 0000000..19b71d7 --- /dev/null +++ b/vendor/efsw/src/efsw/FileWatcherWin32.cpp @@ -0,0 +1,267 @@ +#include +#include +#include +#include +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_WIN32 + +namespace efsw { + +FileWatcherWin32::FileWatcherWin32( FileWatcher* parent ) : + FileWatcherImpl( parent ), mLastWatchID( 0 ), mThread( NULL ) { + mIOCP = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, 1 ); + if ( mIOCP && mIOCP != INVALID_HANDLE_VALUE ) + mInitOK = true; +} + +FileWatcherWin32::~FileWatcherWin32() { + mInitOK = false; + + if ( mIOCP && mIOCP != INVALID_HANDLE_VALUE ) { + PostQueuedCompletionStatus( mIOCP, 0, reinterpret_cast( this ), NULL ); + } + + efSAFE_DELETE( mThread ); + + removeAllWatches(); + + if ( mIOCP ) + CloseHandle( mIOCP ); +} + +WatchID FileWatcherWin32::addWatch( const std::string& directory, FileWatchListener* watcher, + bool recursive, const std::vector &options ) { + std::string dir( directory ); + + FileInfo fi( dir ); + + if ( !fi.isDirectory() ) { + return Errors::Log::createLastError( Errors::FileNotFound, dir ); + } else if ( !fi.isReadable() ) { + return Errors::Log::createLastError( Errors::FileNotReadable, dir ); + } + + FileSystem::dirAddSlashAtEnd( dir ); + + Lock lock( mWatchesLock ); + + if ( pathInWatches( dir ) ) { + return Errors::Log::createLastError( Errors::FileRepeated, dir ); + } + + WatchID watchid = ++mLastWatchID; + + DWORD bufferSize = static_cast( getOptionValue(options, Option::WinBufferSize, 63 * 1024) ); + DWORD notifyFilter = static_cast( getOptionValue(options, Option::WinNotifyFilter, + FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_LAST_WRITE | + FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | + FILE_NOTIFY_CHANGE_SIZE) ); + + WatcherStructWin32* watch = CreateWatch( String::fromUtf8( dir ).toWideString().c_str(), + recursive, bufferSize, notifyFilter, mIOCP ); + + if ( NULL == watch ) { + return Errors::Log::createLastError( Errors::FileNotFound, dir ); + } + + // Add the handle to the handles vector + watch->Watch->ID = watchid; + watch->Watch->Watch = this; + watch->Watch->Listener = watcher; + watch->Watch->DirName = new char[dir.length() + 1]; + strcpy( watch->Watch->DirName, dir.c_str() ); + + mWatches.insert( watch ); + + return watchid; +} + +void FileWatcherWin32::removeWatch( const std::string& directory ) { + Lock lock( mWatchesLock ); + + Watches::iterator iter = mWatches.begin(); + + for ( ; iter != mWatches.end(); ++iter ) { + if ( directory == ( *iter )->Watch->DirName ) { + removeWatch( *iter ); + break; + } + } +} + +void FileWatcherWin32::removeWatch( WatchID watchid ) { + Lock lock( mWatchesLock ); + + Watches::iterator iter = mWatches.begin(); + + for ( ; iter != mWatches.end(); ++iter ) { + // Find the watch ID + if ( ( *iter )->Watch->ID == watchid ) { + removeWatch( *iter ); + return; + } + } +} + +void FileWatcherWin32::removeWatch( WatcherStructWin32* watch ) { + Lock lock( mWatchesLock ); + + DestroyWatch( watch ); + mWatches.erase( watch ); +} + +void FileWatcherWin32::watch() { + if ( NULL == mThread ) { + mThread = new Thread( &FileWatcherWin32::run, this ); + mThread->launch(); + } +} + +void FileWatcherWin32::removeAllWatches() { + Lock lock( mWatchesLock ); + + Watches::iterator iter = mWatches.begin(); + + for ( ; iter != mWatches.end(); ++iter ) { + DestroyWatch( ( *iter ) ); + } + + mWatches.clear(); +} + +void FileWatcherWin32::run() { + do { + if ( mInitOK && !mWatches.empty() ) { + DWORD numOfBytes = 0; + OVERLAPPED* ov = NULL; + ULONG_PTR compKey = 0; + BOOL res = FALSE; + + while ( ( res = GetQueuedCompletionStatus( mIOCP, &numOfBytes, &compKey, &ov, + INFINITE ) ) != FALSE ) { + if ( compKey != 0 && compKey == reinterpret_cast( this ) ) { + break; + } else { + Lock lock( mWatchesLock ); + if (mWatches.find( (WatcherStructWin32*)ov ) != mWatches.end()) + WatchCallback( numOfBytes, ov ); + } + } + } else { + System::sleep( 10 ); + } + } while ( mInitOK ); + + removeAllWatches(); +} + +void FileWatcherWin32::handleAction( Watcher* watch, const std::string& filename, + unsigned long action, std::string /*oldFilename*/ ) { + Action fwAction; + + switch ( action ) { + case FILE_ACTION_RENAMED_OLD_NAME: + watch->OldFileName = filename; + return; + case FILE_ACTION_ADDED: + fwAction = Actions::Add; + break; + case FILE_ACTION_RENAMED_NEW_NAME: { + fwAction = Actions::Moved; + + std::string fpath( watch->Directory + filename ); + + // Update the directory path + if ( watch->Recursive && FileSystem::isDirectory( fpath ) ) { + // Update the new directory path + std::string opath( watch->Directory + watch->OldFileName ); + FileSystem::dirAddSlashAtEnd( opath ); + FileSystem::dirAddSlashAtEnd( fpath ); + + for ( Watches::iterator it = mWatches.begin(); it != mWatches.end(); ++it ) { + if ( ( *it )->Watch->Directory == opath ) { + ( *it )->Watch->Directory = fpath; + + break; + } + } + } + + std::string folderPath( static_cast( watch )->DirName ); + std::string realFilename = filename; + std::size_t sepPos = filename.find_last_of( "/\\" ); + std::string oldFolderPath = + static_cast( watch )->DirName + + watch->OldFileName.substr( 0, watch->OldFileName.find_last_of( "/\\" ) ); + + if ( sepPos != std::string::npos ) { + folderPath += + filename.substr( 0, sepPos + 1 < filename.size() ? sepPos + 1 : sepPos ); + realFilename = filename.substr( sepPos + 1 ); + } + + if ( folderPath == oldFolderPath ) { + watch->Listener->handleFileAction( + watch->ID, folderPath, realFilename, fwAction, + FileSystem::fileNameFromPath( watch->OldFileName ) ); + } else { + watch->Listener->handleFileAction( watch->ID, + static_cast( watch )->DirName, + filename, fwAction, watch->OldFileName ); + } + return; + } + case FILE_ACTION_REMOVED: + fwAction = Actions::Delete; + break; + case FILE_ACTION_MODIFIED: + fwAction = Actions::Modified; + break; + default: + return; + }; + + std::string folderPath( static_cast( watch )->DirName ); + std::string realFilename = filename; + std::size_t sepPos = filename.find_last_of( "/\\" ); + + if ( sepPos != std::string::npos ) { + folderPath += filename.substr( 0, sepPos + 1 < filename.size() ? sepPos + 1 : sepPos ); + realFilename = filename.substr( sepPos + 1 ); + } + + FileSystem::dirAddSlashAtEnd( folderPath ); + + watch->Listener->handleFileAction( watch->ID, folderPath, realFilename, fwAction ); +} + +std::vector FileWatcherWin32::directories() { + std::vector dirs; + + Lock lock( mWatchesLock ); + + dirs.reserve( mWatches.size() ); + + for ( Watches::iterator it = mWatches.begin(); it != mWatches.end(); ++it ) { + dirs.push_back( std::string( ( *it )->Watch->DirName ) ); + } + + return dirs; +} + +bool FileWatcherWin32::pathInWatches( const std::string& path ) { + Lock lock( mWatchesLock ); + + for ( Watches::iterator it = mWatches.begin(); it != mWatches.end(); ++it ) { + if ( ( *it )->Watch->DirName == path ) { + return true; + } + } + + return false; +} + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/FileWatcherWin32.hpp b/vendor/efsw/src/efsw/FileWatcherWin32.hpp new file mode 100644 index 0000000..3016aac --- /dev/null +++ b/vendor/efsw/src/efsw/FileWatcherWin32.hpp @@ -0,0 +1,71 @@ +#ifndef EFSW_FILEWATCHERWIN32_HPP +#define EFSW_FILEWATCHERWIN32_HPP + +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_WIN32 + +#include +#include +#include +#include + +namespace efsw { + +/// Implementation for Win32 based on ReadDirectoryChangesW. +/// @class FileWatcherWin32 +class FileWatcherWin32 : public FileWatcherImpl { + public: + /// type for a map from WatchID to WatcherWin32 pointer + typedef std::unordered_set Watches; + + FileWatcherWin32( FileWatcher* parent ); + + virtual ~FileWatcherWin32(); + + /// Add a directory watch + /// On error returns WatchID with Error type. + WatchID addWatch( const std::string& directory, FileWatchListener* watcher, bool recursive, + const std::vector &options ) override; + + /// Remove a directory watch. This is a brute force lazy search O(nlogn). + void removeWatch( const std::string& directory ) override; + + /// Remove a directory watch. This is a map lookup O(logn). + void removeWatch( WatchID watchid ) override; + + /// Updates the watcher. Must be called often. + void watch() override; + + /// Handles the action + void handleAction( Watcher* watch, const std::string& filename, unsigned long action, + std::string oldFilename = "" ) override; + + /// @return Returns a list of the directories that are being watched + std::vector directories() override; + + protected: + HANDLE mIOCP; + Watches mWatches; + + /// The last watchid + WatchID mLastWatchID; + Thread* mThread; + Mutex mWatchesLock; + + bool pathInWatches( const std::string& path ) override; + + /// Remove all directory watches. + void removeAllWatches(); + + void removeWatch( WatcherStructWin32* watch ); + + private: + void run(); +}; + +} // namespace efsw + +#endif + +#endif diff --git a/vendor/efsw/src/efsw/Lock.hpp b/vendor/efsw/src/efsw/Lock.hpp new file mode 100644 index 0000000..e8c522a --- /dev/null +++ b/vendor/efsw/src/efsw/Lock.hpp @@ -0,0 +1,21 @@ +#ifndef EFSW_LOCK_HPP +#define EFSW_LOCK_HPP + +#include + +namespace efsw { + +/** Simple mutex class */ +class Lock { + public: + explicit Lock( Mutex& mutex ) : mMutex( mutex ) { mMutex.lock(); } + + ~Lock() { mMutex.unlock(); } + + private: + Mutex& mMutex; +}; + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/Log.cpp b/vendor/efsw/src/efsw/Log.cpp new file mode 100644 index 0000000..6f32df7 --- /dev/null +++ b/vendor/efsw/src/efsw/Log.cpp @@ -0,0 +1,49 @@ +#include +#include + +namespace efsw { namespace Errors { + +static std::string LastError = ""; +static Error LastErrorCode = NoError; + +std::string Log::getLastErrorLog() { + return LastError; +} + +Error Log::getLastErrorCode() { + return LastErrorCode; +} + +void Log::clearLastError() { + LastErrorCode = NoError; + LastError = ""; +} + +Error Log::createLastError( Error err, std::string log ) { + switch ( err ) { + case FileNotFound: + LastError = "File not found ( " + log + " )"; + break; + case FileRepeated: + LastError = "File repeated in watches ( " + log + " )"; + break; + case FileOutOfScope: + LastError = "Symlink file out of scope ( " + log + " )"; + break; + case FileRemote: + LastError = + "File is located in a remote file system, use a generic watcher. ( " + log + " )"; + break; + case WatcherFailed: + LastError = "File system watcher failed ( " + log + " )"; + break; + case Unspecified: + default: + LastError = log; + } + + efDEBUG( "%s\n", LastError.c_str() ); + return err; +} + +}} // namespace efsw::Errors diff --git a/vendor/efsw/src/efsw/Mutex.cpp b/vendor/efsw/src/efsw/Mutex.cpp new file mode 100644 index 0000000..c961db1 --- /dev/null +++ b/vendor/efsw/src/efsw/Mutex.cpp @@ -0,0 +1,20 @@ +#include +#include + +namespace efsw { + +Mutex::Mutex() : mMutexImpl( new Platform::MutexImpl() ) {} + +Mutex::~Mutex() { + efSAFE_DELETE( mMutexImpl ); +} + +void Mutex::lock() { + mMutexImpl->lock(); +} + +void Mutex::unlock() { + mMutexImpl->unlock(); +} + +} // namespace efsw diff --git a/vendor/efsw/src/efsw/Mutex.hpp b/vendor/efsw/src/efsw/Mutex.hpp new file mode 100644 index 0000000..d98ad17 --- /dev/null +++ b/vendor/efsw/src/efsw/Mutex.hpp @@ -0,0 +1,31 @@ +#ifndef EFSW_MUTEX_HPP +#define EFSW_MUTEX_HPP + +#include + +namespace efsw { + +namespace Platform { +class MutexImpl; +} + +/** Simple mutex class */ +class Mutex { + public: + Mutex(); + + ~Mutex(); + + /** Lock the mutex */ + void lock(); + + /** Unlock the mutex */ + void unlock(); + + private: + Platform::MutexImpl* mMutexImpl; +}; + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/String.cpp b/vendor/efsw/src/efsw/String.cpp new file mode 100644 index 0000000..8c9a3cc --- /dev/null +++ b/vendor/efsw/src/efsw/String.cpp @@ -0,0 +1,669 @@ +#include +#include +#include + +namespace efsw { + +const std::size_t String::InvalidPos = StringType::npos; + +std::vector String::split( const std::string& str, const char& splitchar, + const bool& pushEmptyString ) { + std::vector tmp; + std::string tmpstr; + + for ( size_t i = 0; i < str.size(); i++ ) { + if ( str[i] == splitchar ) { + if ( pushEmptyString || tmpstr.size() ) { + tmp.push_back( tmpstr ); + tmpstr = ""; + } + } else { + tmpstr += str[i]; + } + } + + if ( tmpstr.size() ) { + tmp.push_back( tmpstr ); + } + + return tmp; +} + +std::vector String::split( const String& str, const Uint32& splitchar, + const bool& pushEmptyString ) { + std::vector tmp; + String tmpstr; + + for ( size_t i = 0; i < str.size(); i++ ) { + if ( str[i] == splitchar ) { + if ( pushEmptyString || tmpstr.size() ) { + tmp.push_back( tmpstr ); + tmpstr = ""; + } + } else { + tmpstr += str[i]; + } + } + + if ( tmpstr.size() ) { + tmp.push_back( tmpstr ); + } + + return tmp; +} + +int String::strStartsWith( const std::string& start, const std::string& str ) { + int pos = -1; + size_t size = start.size(); + + if ( str.size() >= size ) { + for ( std::size_t i = 0; i < size; i++ ) { + if ( start[i] == str[i] ) { + pos = (int)i; + } else { + pos = -1; + break; + } + } + } + + return pos; +} + +int String::strStartsWith( const String& start, const String& str ) { + int pos = -1; + size_t size = start.size(); + + if ( str.size() >= size ) { + for ( std::size_t i = 0; i < size; i++ ) { + if ( start[i] == str[i] ) { + pos = (int)i; + } else { + pos = -1; + break; + } + } + } + + return pos; +} + +String::String() {} + +String::String( char ansiChar, const std::locale& locale ) { + mString += Utf32::DecodeAnsi( ansiChar, locale ); +} + +#ifndef EFSW_NO_WIDECHAR +String::String( wchar_t wideChar ) { + mString += Utf32::DecodeWide( wideChar ); +} +#endif + +String::String( StringBaseType utf32Char ) { + mString += utf32Char; +} + +String::String( const char* uf8String ) { + if ( uf8String ) { + std::size_t length = strlen( uf8String ); + + if ( length > 0 ) { + mString.reserve( length + 1 ); + + Utf8::ToUtf32( uf8String, uf8String + length, std::back_inserter( mString ) ); + } + } +} + +String::String( const std::string& utf8String ) { + mString.reserve( utf8String.length() + 1 ); + + Utf8::ToUtf32( utf8String.begin(), utf8String.end(), std::back_inserter( mString ) ); +} + +String::String( const char* ansiString, const std::locale& locale ) { + if ( ansiString ) { + std::size_t length = strlen( ansiString ); + if ( length > 0 ) { + mString.reserve( length + 1 ); + Utf32::FromAnsi( ansiString, ansiString + length, std::back_inserter( mString ), + locale ); + } + } +} + +String::String( const std::string& ansiString, const std::locale& locale ) { + mString.reserve( ansiString.length() + 1 ); + Utf32::FromAnsi( ansiString.begin(), ansiString.end(), std::back_inserter( mString ), locale ); +} + +#ifndef EFSW_NO_WIDECHAR +String::String( const wchar_t* wideString ) { + if ( wideString ) { + std::size_t length = std::wcslen( wideString ); + if ( length > 0 ) { + mString.reserve( length + 1 ); + Utf32::FromWide( wideString, wideString + length, std::back_inserter( mString ) ); + } + } +} + +String::String( const std::wstring& wideString ) { + mString.reserve( wideString.length() + 1 ); + Utf32::FromWide( wideString.begin(), wideString.end(), std::back_inserter( mString ) ); +} +#endif + +String::String( const StringBaseType* utf32String ) { + if ( utf32String ) + mString = utf32String; +} + +String::String( const StringType& utf32String ) : mString( utf32String ) {} + +String::String( const String& str ) : mString( str.mString ) {} + +String String::fromUtf8( const std::string& utf8String ) { + String::StringType utf32; + + utf32.reserve( utf8String.length() + 1 ); + + Utf8::ToUtf32( utf8String.begin(), utf8String.end(), std::back_inserter( utf32 ) ); + + return String( utf32 ); +} + +String::operator std::string() const { + return toAnsiString(); +} + +std::string String::toAnsiString( const std::locale& locale ) const { + // Prepare the output string + std::string output; + output.reserve( mString.length() + 1 ); + + // Convert + Utf32::ToAnsi( mString.begin(), mString.end(), std::back_inserter( output ), 0, locale ); + + return output; +} + +#ifndef EFSW_NO_WIDECHAR +std::wstring String::toWideString() const { + // Prepare the output string + std::wstring output; + output.reserve( mString.length() + 1 ); + + // Convert + Utf32::ToWide( mString.begin(), mString.end(), std::back_inserter( output ), 0 ); + + return output; +} +#endif + +std::string String::toUtf8() const { + // Prepare the output string + std::string output; + output.reserve( mString.length() + 1 ); + + // Convert + Utf32::toUtf8( mString.begin(), mString.end(), std::back_inserter( output ) ); + + return output; +} + +String& String::operator=( const String& right ) { + mString = right.mString; + return *this; +} + +String& String::operator=( const StringBaseType& right ) { + mString = right; + return *this; +} + +String& String::operator+=( const String& right ) { + mString += right.mString; + return *this; +} + +String& String::operator+=( const StringBaseType& right ) { + mString += right; + return *this; +} + +String::StringBaseType String::operator[]( std::size_t index ) const { + return mString[index]; +} + +String::StringBaseType& String::operator[]( std::size_t index ) { + return mString[index]; +} + +String::StringBaseType String::at( std::size_t index ) const { + return mString.at( index ); +} + +void String::push_back( StringBaseType c ) { + mString.push_back( c ); +} + +void String::swap( String& str ) { + mString.swap( str.mString ); +} + +void String::clear() { + mString.clear(); +} + +std::size_t String::size() const { + return mString.size(); +} + +std::size_t String::length() const { + return mString.length(); +} + +bool String::empty() const { + return mString.empty(); +} + +void String::erase( std::size_t position, std::size_t count ) { + mString.erase( position, count ); +} + +String& String::insert( std::size_t position, const String& str ) { + mString.insert( position, str.mString ); + return *this; +} + +String& String::insert( std::size_t pos1, const String& str, std::size_t pos2, std::size_t n ) { + mString.insert( pos1, str.mString, pos2, n ); + return *this; +} + +String& String::insert( size_t pos1, const char* s, size_t n ) { + String tmp( s ); + + mString.insert( pos1, tmp.data(), n ); + + return *this; +} + +String& String::insert( size_t pos1, size_t n, char c ) { + mString.insert( pos1, n, c ); + return *this; +} + +String& String::insert( size_t pos1, const char* s ) { + String tmp( s ); + + mString.insert( pos1, tmp.data() ); + + return *this; +} + +String::Iterator String::insert( Iterator p, char c ) { + return mString.insert( p, c ); +} + +void String::insert( Iterator p, size_t n, char c ) { + mString.insert( p, n, c ); +} + +const String::StringBaseType* String::c_str() const { + return mString.c_str(); +} + +const String::StringBaseType* String::data() const { + return mString.data(); +} + +String::Iterator String::begin() { + return mString.begin(); +} + +String::ConstIterator String::begin() const { + return mString.begin(); +} + +String::Iterator String::end() { + return mString.end(); +} + +String::ConstIterator String::end() const { + return mString.end(); +} + +String::ReverseIterator String::rbegin() { + return mString.rbegin(); +} + +String::ConstReverseIterator String::rbegin() const { + return mString.rbegin(); +} + +String::ReverseIterator String::rend() { + return mString.rend(); +} + +String::ConstReverseIterator String::rend() const { + return mString.rend(); +} + +void String::resize( std::size_t n, StringBaseType c ) { + mString.resize( n, c ); +} + +void String::resize( std::size_t n ) { + mString.resize( n ); +} + +std::size_t String::max_size() const { + return mString.max_size(); +} + +void String::reserve( size_t res_arg ) { + mString.reserve( res_arg ); +} + +std::size_t String::capacity() const { + return mString.capacity(); +} + +String& String::assign( const String& str ) { + mString.assign( str.mString ); + return *this; +} + +String& String::assign( const String& str, size_t pos, size_t n ) { + mString.assign( str.mString, pos, n ); + return *this; +} + +String& String::assign( const char* s, size_t n ) { + String tmp( s ); + + mString.assign( tmp.mString ); + + return *this; +} + +String& String::assign( const char* s ) { + String tmp( s ); + + mString.assign( tmp.mString ); + + return *this; +} + +String& String::assign( size_t n, char c ) { + mString.assign( n, c ); + + return *this; +} + +String& String::append( const String& str ) { + mString.append( str.mString ); + + return *this; +} + +String& String::append( const String& str, size_t pos, size_t n ) { + mString.append( str.mString, pos, n ); + + return *this; +} + +String& String::append( const char* s, size_t n ) { + String tmp( s ); + + mString.append( tmp.mString ); + + return *this; +} + +String& String::append( const char* s ) { + String tmp( s ); + + mString.append( tmp.mString ); + + return *this; +} + +String& String::append( size_t n, char c ) { + mString.append( n, c ); + + return *this; +} + +String& String::append( std::size_t n, StringBaseType c ) { + mString.append( n, c ); + + return *this; +} + +String& String::replace( size_t pos1, size_t n1, const String& str ) { + mString.replace( pos1, n1, str.mString ); + + return *this; +} + +String& String::replace( Iterator i1, Iterator i2, const String& str ) { + mString.replace( i1, i2, str.mString ); + + return *this; +} + +String& String::replace( size_t pos1, size_t n1, const String& str, size_t pos2, size_t n2 ) { + mString.replace( pos1, n1, str.mString, pos2, n2 ); + + return *this; +} + +String& String::replace( size_t pos1, size_t n1, const char* s, size_t n2 ) { + String tmp( s ); + + mString.replace( pos1, n1, tmp.data(), n2 ); + + return *this; +} + +String& String::replace( Iterator i1, Iterator i2, const char* s, size_t n2 ) { + String tmp( s ); + + mString.replace( i1, i2, tmp.data(), n2 ); + + return *this; +} + +String& String::replace( size_t pos1, size_t n1, const char* s ) { + String tmp( s ); + + mString.replace( pos1, n1, tmp.mString ); + + return *this; +} + +String& String::replace( Iterator i1, Iterator i2, const char* s ) { + String tmp( s ); + + mString.replace( i1, i2, tmp.mString ); + + return *this; +} + +String& String::replace( size_t pos1, size_t n1, size_t n2, char c ) { + mString.replace( pos1, n1, n2, (StringBaseType)c ); + + return *this; +} + +String& String::replace( Iterator i1, Iterator i2, size_t n2, char c ) { + mString.replace( i1, i2, n2, (StringBaseType)c ); + + return *this; +} + +std::size_t String::find( const String& str, std::size_t start ) const { + return mString.find( str.mString, start ); +} + +std::size_t String::find( const char* s, std::size_t pos, std::size_t n ) const { + return find( String( s ), pos ); +} + +std::size_t String::find( const char* s, std::size_t pos ) const { + return find( String( s ), pos ); +} + +size_t String::find( char c, std::size_t pos ) const { + return mString.find( (StringBaseType)c, pos ); +} + +std::size_t String::rfind( const String& str, std::size_t pos ) const { + return mString.rfind( str.mString, pos ); +} + +std::size_t String::rfind( const char* s, std::size_t pos, std::size_t n ) const { + return rfind( String( s ), pos ); +} + +std::size_t String::rfind( const char* s, std::size_t pos ) const { + return rfind( String( s ), pos ); +} + +std::size_t String::rfind( char c, std::size_t pos ) const { + return mString.rfind( c, pos ); +} + +std::size_t String::copy( StringBaseType* s, std::size_t n, std::size_t pos ) const { + return mString.copy( s, n, pos ); +} + +String String::substr( std::size_t pos, std::size_t n ) const { + return String( mString.substr( pos, n ) ); +} + +int String::compare( const String& str ) const { + return mString.compare( str.mString ); +} + +int String::compare( const char* s ) const { + return compare( String( s ) ); +} + +int String::compare( std::size_t pos1, std::size_t n1, const String& str ) const { + return mString.compare( pos1, n1, str.mString ); +} + +int String::compare( std::size_t pos1, std::size_t n1, const char* s ) const { + return compare( pos1, n1, String( s ) ); +} + +int String::compare( std::size_t pos1, std::size_t n1, const String& str, std::size_t pos2, + std::size_t n2 ) const { + return mString.compare( pos1, n1, str.mString, pos2, n2 ); +} + +int String::compare( std::size_t pos1, std::size_t n1, const char* s, std::size_t n2 ) const { + return compare( pos1, n1, String( s ), 0, n2 ); +} + +std::size_t String::find_first_of( const String& str, std::size_t pos ) const { + return mString.find_first_of( str.mString, pos ); +} + +std::size_t String::find_first_of( const char* s, std::size_t pos, std::size_t n ) const { + return find_first_of( String( s ), pos ); +} + +std::size_t String::find_first_of( const char* s, std::size_t pos ) const { + return find_first_of( String( s ), pos ); +} + +std::size_t String::find_first_of( StringBaseType c, std::size_t pos ) const { + return mString.find_first_of( c, pos ); +} + +std::size_t String::find_last_of( const String& str, std::size_t pos ) const { + return mString.find_last_of( str.mString, pos ); +} + +std::size_t String::find_last_of( const char* s, std::size_t pos, std::size_t n ) const { + return find_last_of( String( s ), pos ); +} + +std::size_t String::find_last_of( const char* s, std::size_t pos ) const { + return find_last_of( String( s ), pos ); +} + +std::size_t String::find_last_of( StringBaseType c, std::size_t pos ) const { + return mString.find_last_of( c, pos ); +} + +std::size_t String::find_first_not_of( const String& str, std::size_t pos ) const { + return mString.find_first_not_of( str.mString, pos ); +} + +std::size_t String::find_first_not_of( const char* s, std::size_t pos, std::size_t n ) const { + return find_first_not_of( String( s ), pos ); +} + +std::size_t String::find_first_not_of( const char* s, std::size_t pos ) const { + return find_first_not_of( String( s ), pos ); +} + +std::size_t String::find_first_not_of( StringBaseType c, std::size_t pos ) const { + return mString.find_first_not_of( c, pos ); +} + +std::size_t String::find_last_not_of( const String& str, std::size_t pos ) const { + return mString.find_last_not_of( str.mString, pos ); +} + +std::size_t String::find_last_not_of( const char* s, std::size_t pos, std::size_t n ) const { + return find_last_not_of( String( s ), pos ); +} + +std::size_t String::find_last_not_of( const char* s, std::size_t pos ) const { + return find_last_not_of( String( s ), pos ); +} + +std::size_t String::find_last_not_of( StringBaseType c, std::size_t pos ) const { + return mString.find_last_not_of( c, pos ); +} + +bool operator==( const String& left, const String& right ) { + return left.mString == right.mString; +} + +bool operator!=( const String& left, const String& right ) { + return !( left == right ); +} + +bool operator<( const String& left, const String& right ) { + return left.mString < right.mString; +} + +bool operator>( const String& left, const String& right ) { + return right < left; +} + +bool operator<=( const String& left, const String& right ) { + return !( right < left ); +} + +bool operator>=( const String& left, const String& right ) { + return !( left < right ); +} + +String operator+( const String& left, const String& right ) { + String string = left; + string += right; + + return string; +} + +} // namespace efsw diff --git a/vendor/efsw/src/efsw/String.hpp b/vendor/efsw/src/efsw/String.hpp new file mode 100644 index 0000000..b42b945 --- /dev/null +++ b/vendor/efsw/src/efsw/String.hpp @@ -0,0 +1,630 @@ +/** NOTE: + * This code is based on the Utf implementation from SFML2. License zlib/png ( + *http://www.sfml-dev.org/license.php ) The class was modified to fit efsw own needs. This is not + *the original implementation from SFML2. Functions and methods are the same that in std::string to + *facilitate portability. + **/ + +#ifndef EFSW_STRING_HPP +#define EFSW_STRING_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace efsw { + +/** @brief Utility string class that automatically handles conversions between types and encodings + * **/ +class String { + public: + typedef char32_t StringBaseType; + typedef std::basic_string StringType; + typedef StringType::iterator Iterator; //! Iterator type + typedef StringType::const_iterator ConstIterator; //! Constant iterator type + typedef StringType::reverse_iterator ReverseIterator; //! Reverse Iterator type + typedef StringType::const_reverse_iterator ConstReverseIterator; //! Constant iterator type + + static const std::size_t InvalidPos; ///< Represents an invalid position in the string + + template static std::string toStr( const T& i ) { + std::ostringstream ss; + ss << i; + return ss.str(); + } + + /** Converts from a string to type */ + template + static bool fromString( T& t, const std::string& s, + std::ios_base& ( *f )( std::ios_base& ) = std::dec ) { + std::istringstream iss( s ); + return !( iss >> f >> t ).fail(); + } + + /** Converts from a String to type */ + template + static bool fromString( T& t, const String& s, + std::ios_base& ( *f )( std::ios_base& ) = std::dec ) { + std::istringstream iss( s.toUtf8() ); + return !( iss >> f >> t ).fail(); + } + + /** Split a string and hold it on a vector */ + static std::vector split( const std::string& str, const char& splitchar, + const bool& pushEmptyString = false ); + + /** Split a string and hold it on a vector */ + static std::vector split( const String& str, const Uint32& splitchar, + const bool& pushEmptyString = false ); + + /** Determine if a string starts with the string passed + ** @param start The substring expected to start + ** @param str The string to compare + ** @return -1 if the substring is no in str, otherwise the size of the substring + */ + static int strStartsWith( const std::string& start, const std::string& str ); + + static int strStartsWith( const String& start, const String& str ); + + /** @brief Construct from an UTF-8 string to UTF-32 according + ** @param uf8String UTF-8 string to convert + **/ + static String fromUtf8( const std::string& utf8String ); + + /** @brief Default constructor + ** This constructor creates an empty string. + **/ + String(); + + /** @brief Construct from a single ANSI character and a locale + ** The source character is converted to UTF-32 according + ** to the given locale. If you want to use the current global + ** locale, rather use the other constructor. + ** @param ansiChar ANSI character to convert + ** @param locale Locale to use for conversion + **/ + String( char ansiChar, const std::locale& locale = std::locale() ); + +#ifndef EFSW_NO_WIDECHAR + /** @brief Construct from single wide character + ** @param wideChar Wide character to convert + **/ + String( wchar_t wideChar ); +#endif + + /** @brief Construct from single UTF-32 character + ** @param utf32Char UTF-32 character to convert + **/ + String( StringBaseType utf32Char ); + + /** @brief Construct from an from a null-terminated C-style UTF-8 string to UTF-32 + ** @param uf8String UTF-8 string to convert + **/ + String( const char* uf8String ); + + /** @brief Construct from an UTF-8 string to UTF-32 according + ** @param uf8String UTF-8 string to convert + **/ + String( const std::string& utf8String ); + + /** @brief Construct from a null-terminated C-style ANSI string and a locale + ** The source string is converted to UTF-32 according + ** to the given locale. If you want to use the current global + ** locale, rather use the other constructor. + ** @param ansiString ANSI string to convert + ** @param locale Locale to use for conversion + **/ + String( const char* ansiString, const std::locale& locale ); + + /** @brief Construct from an ANSI string and a locale + ** The source string is converted to UTF-32 according + ** to the given locale. If you want to use the current global + ** locale, rather use the other constructor. + ** @param ansiString ANSI string to convert + ** @param locale Locale to use for conversion + **/ + String( const std::string& ansiString, const std::locale& locale ); + +#ifndef EFSW_NO_WIDECHAR + /** @brief Construct from null-terminated C-style wide string + ** @param wideString Wide string to convert + **/ + String( const wchar_t* wideString ); + + /** @brief Construct from a wide string + ** @param wideString Wide string to convert + **/ + String( const std::wstring& wideString ); +#endif + + /** @brief Construct from a null-terminated C-style UTF-32 string + ** @param utf32String UTF-32 string to assign + **/ + String( const StringBaseType* utf32String ); + + /** @brief Construct from an UTF-32 string + ** @param utf32String UTF-32 string to assign + **/ + String( const StringType& utf32String ); + + /** @brief Copy constructor + ** @param str Instance to copy + **/ + String( const String& str ); + + /** @brief Implicit cast operator to std::string (ANSI string) + ** The current global locale is used for conversion. If you + ** want to explicitely specify a locale, see toAnsiString. + ** Characters that do not fit in the target encoding are + ** discarded from the returned string. + ** This operator is defined for convenience, and is equivalent + ** to calling toAnsiString(). + ** @return Converted ANSI string + ** @see toAnsiString, operator String + **/ + operator std::string() const; + + /** @brief Convert the unicode string to an ANSI string + ** The UTF-32 string is converted to an ANSI string in + ** the encoding defined by \a locale. If you want to use + ** the current global locale, see the other overload + ** of toAnsiString. + ** Characters that do not fit in the target encoding are + ** discarded from the returned string. + ** @param locale Locale to use for conversion + ** @return Converted ANSI string + ** @see toWideString, operator std::string + **/ + std::string toAnsiString( const std::locale& locale = std::locale() ) const; + +#ifndef EFSW_NO_WIDECHAR + /** @brief Convert the unicode string to a wide string + ** Characters that do not fit in the target encoding are + ** discarded from the returned string. + ** @return Converted wide string + ** @see toAnsiString, operator String + **/ + std::wstring toWideString() const; +#endif + + std::string toUtf8() const; + + /** @brief Overload of assignment operator + ** @param right Instance to assign + ** @return Reference to self + **/ + String& operator=( const String& right ); + + String& operator=( const StringBaseType& right ); + + /** @brief Overload of += operator to append an UTF-32 string + ** @param right String to append + ** @return Reference to self + **/ + String& operator+=( const String& right ); + + String& operator+=( const StringBaseType& right ); + + /** @brief Overload of [] operator to access a character by its position + ** This function provides read-only access to characters. + ** Note: this function doesn't throw if \a index is out of range. + ** @param index Index of the character to get + ** @return Character at position \a index + **/ + StringBaseType operator[]( std::size_t index ) const; + + /** @brief Overload of [] operator to access a character by its position + ** This function provides read and write access to characters. + ** Note: this function doesn't throw if \a index is out of range. + ** @param index Index of the character to get + ** @return Reference to the character at position \a index + **/ + + StringBaseType& operator[]( std::size_t index ); + + /** @brief Get character in string + ** Performs a range check, throwing an exception of type out_of_range in case that pos is not an + *actual position in the string. + ** @return The character at position pos in the string. + */ + StringBaseType at( std::size_t index ) const; + + /** @brief clear the string + ** This function removes all the characters from the string. + ** @see empty, erase + **/ + void clear(); + + /** @brief Get the size of the string + ** @return Number of characters in the string + ** @see empty + **/ + std::size_t size() const; + + /** @see size() */ + std::size_t length() const; + + /** @brief Check whether the string is empty or not + ** @return True if the string is empty (i.e. contains no character) + ** @see clear, size + **/ + bool empty() const; + + /** @brief Erase one or more characters from the string + ** This function removes a sequence of \a count characters + ** starting from \a position. + ** @param position Position of the first character to erase + ** @param count Number of characters to erase + **/ + void erase( std::size_t position, std::size_t count = 1 ); + + /** @brief Insert one or more characters into the string + ** This function inserts the characters of \a str + ** into the string, starting from \a position. + ** @param position Position of insertion + ** @param str Characters to insert + **/ + String& insert( std::size_t position, const String& str ); + + String& insert( std::size_t pos1, const String& str, std::size_t pos2, std::size_t n ); + + String& insert( std::size_t pos1, const char* s, std::size_t n ); + + String& insert( std::size_t pos1, const char* s ); + + String& insert( std::size_t pos1, size_t n, char c ); + + Iterator insert( Iterator p, char c ); + + void insert( Iterator p, std::size_t n, char c ); + + template + void insert( Iterator p, InputIterator first, InputIterator last ) { + mString.insert( p, first, last ); + } + + /** @brief Find a sequence of one or more characters in the string + ** This function searches for the characters of \a str + ** into the string, starting from \a start. + ** @param str Characters to find + ** @param start Where to begin searching + ** @return Position of \a str in the string, or String::InvalidPos if not found + **/ + std::size_t find( const String& str, std::size_t start = 0 ) const; + + std::size_t find( const char* s, std::size_t pos, std::size_t n ) const; + + std::size_t find( const char* s, std::size_t pos = 0 ) const; + + std::size_t find( char c, std::size_t pos = 0 ) const; + + /** @brief Get a pointer to the C-style array of characters + ** This functions provides a read-only access to a + ** null-terminated C-style representation of the string. + ** The returned pointer is temporary and is meant only for + ** immediate use, thus it is not recommended to store it. + ** @return Read-only pointer to the array of characters + **/ + const StringBaseType* c_str() const; + + /** @brief Get string data + ** Notice that no terminating null character is appended (see member c_str for such a + *functionality). + ** The returned array points to an internal location which should not be modified directly in + *the program. + ** Its contents are guaranteed to remain unchanged only until the next call to a non-constant + *member function of the string object. + ** @return Pointer to an internal array containing the same content as the string. + **/ + const StringBaseType* data() const; + + /** @brief Return an iterator to the beginning of the string + ** @return Read-write iterator to the beginning of the string characters + ** @see end + **/ + Iterator begin(); + + /** @brief Return an iterator to the beginning of the string + ** @return Read-only iterator to the beginning of the string characters + ** @see end + **/ + ConstIterator begin() const; + + /** @brief Return an iterator to the beginning of the string + ** The end iterator refers to 1 position past the last character; + ** thus it represents an invalid character and should never be + ** accessed. + ** @return Read-write iterator to the end of the string characters + ** @see begin + **/ + Iterator end(); + + /** @brief Return an iterator to the beginning of the string + ** The end iterator refers to 1 position past the last character; + ** thus it represents an invalid character and should never be + ** accessed. + ** @return Read-only iterator to the end of the string characters + ** @see begin + **/ + ConstIterator end() const; + + /** @brief Return an reverse iterator to the beginning of the string + ** @return Read-write reverse iterator to the beginning of the string characters + ** @see end + **/ + ReverseIterator rbegin(); + + /** @brief Return an reverse iterator to the beginning of the string + ** @return Read-only reverse iterator to the beginning of the string characters + ** @see end + **/ + ConstReverseIterator rbegin() const; + + /** @brief Return an reverse iterator to the beginning of the string + ** The end reverse iterator refers to 1 position past the last character; + ** thus it represents an invalid character and should never be + ** accessed. + ** @return Read-write reverse iterator to the end of the string characters + ** @see begin + **/ + ReverseIterator rend(); + + /** @brief Return an reverse iterator to the beginning of the string + ** The end reverse iterator refers to 1 position past the last character; + ** thus it represents an invalid character and should never be + ** accessed. + ** @return Read-only reverse iterator to the end of the string characters + ** @see begin + **/ + ConstReverseIterator rend() const; + + /** @brief Resize String */ + void resize( std::size_t n, StringBaseType c ); + + /** @brief Resize String */ + void resize( std::size_t n ); + + /** @return Maximum size of string */ + std::size_t max_size() const; + + /** @brief Request a change in capacity */ + void reserve( size_t res_arg = 0 ); + + /** @return Size of allocated storage */ + std::size_t capacity() const; + + /** @brief Append character to string */ + void push_back( StringBaseType c ); + + /** @brief Swap contents with another string */ + void swap( String& str ); + + String& assign( const String& str ); + + String& assign( const String& str, std::size_t pos, std::size_t n ); + + String& assign( const char* s, std::size_t n ); + + String& assign( const char* s ); + + String& assign( std::size_t n, char c ); + + template String& assign( InputIterator first, InputIterator last ) { + mString.assign( first, last ); + return *this; + } + + String& append( const String& str ); + + String& append( const String& str, std::size_t pos, std::size_t n ); + + String& append( const char* s, std::size_t n ); + + String& append( const char* s ); + + String& append( std::size_t n, char c ); + + String& append( std::size_t n, StringBaseType c ); + + template String& append( InputIterator first, InputIterator last ) { + mString.append( first, last ); + return *this; + } + + String& replace( std::size_t pos1, std::size_t n1, const String& str ); + + String& replace( Iterator i1, Iterator i2, const String& str ); + + String& replace( std::size_t pos1, std::size_t n1, const String& str, std::size_t pos2, + std::size_t n2 ); + + String& replace( std::size_t pos1, std::size_t n1, const char* s, std::size_t n2 ); + + String& replace( Iterator i1, Iterator i2, const char* s, std::size_t n2 ); + + String& replace( std::size_t pos1, std::size_t n1, const char* s ); + + String& replace( Iterator i1, Iterator i2, const char* s ); + + String& replace( std::size_t pos1, std::size_t n1, std::size_t n2, char c ); + + String& replace( Iterator i1, Iterator i2, std::size_t n2, char c ); + + template + String& replace( Iterator i1, Iterator i2, InputIterator j1, InputIterator j2 ) { + mString.replace( i1, i2, j1, j2 ); + return *this; + } + + std::size_t rfind( const String& str, std::size_t pos = StringType::npos ) const; + + std::size_t rfind( const char* s, std::size_t pos, std::size_t n ) const; + + std::size_t rfind( const char* s, std::size_t pos = StringType::npos ) const; + + std::size_t rfind( char c, std::size_t pos = StringType::npos ) const; + + String substr( std::size_t pos = 0, std::size_t n = StringType::npos ) const; + + std::size_t copy( StringBaseType* s, std::size_t n, std::size_t pos = 0 ) const; + + int compare( const String& str ) const; + + int compare( const char* s ) const; + + int compare( std::size_t pos1, std::size_t n1, const String& str ) const; + + int compare( std::size_t pos1, std::size_t n1, const char* s ) const; + + int compare( std::size_t pos1, std::size_t n1, const String& str, std::size_t pos2, + std::size_t n2 ) const; + + int compare( std::size_t pos1, std::size_t n1, const char* s, std::size_t n2 ) const; + + std::size_t find_first_of( const String& str, std::size_t pos = 0 ) const; + + std::size_t find_first_of( const char* s, std::size_t pos, std::size_t n ) const; + + std::size_t find_first_of( const char* s, std::size_t pos = 0 ) const; + + std::size_t find_first_of( StringBaseType c, std::size_t pos = 0 ) const; + + std::size_t find_last_of( const String& str, std::size_t pos = StringType::npos ) const; + + std::size_t find_last_of( const char* s, std::size_t pos, std::size_t n ) const; + + std::size_t find_last_of( const char* s, std::size_t pos = StringType::npos ) const; + + std::size_t find_last_of( StringBaseType c, std::size_t pos = StringType::npos ) const; + + std::size_t find_first_not_of( const String& str, std::size_t pos = 0 ) const; + + std::size_t find_first_not_of( const char* s, std::size_t pos, std::size_t n ) const; + + std::size_t find_first_not_of( const char* s, std::size_t pos = 0 ) const; + + std::size_t find_first_not_of( StringBaseType c, std::size_t pos = 0 ) const; + + std::size_t find_last_not_of( const String& str, std::size_t pos = StringType::npos ) const; + + std::size_t find_last_not_of( const char* s, std::size_t pos, std::size_t n ) const; + + std::size_t find_last_not_of( const char* s, std::size_t pos = StringType::npos ) const; + + std::size_t find_last_not_of( StringBaseType c, std::size_t pos = StringType::npos ) const; + + private: + friend bool operator==( const String& left, const String& right ); + friend bool operator<( const String& left, const String& right ); + + StringType mString; ///< Internal string of UTF-32 characters +}; + +/** @relates String +** @brief Overload of == operator to compare two UTF-32 strings +** @param left Left operand (a string) +** @param right Right operand (a string) +** @return True if both strings are equal +**/ +bool operator==( const String& left, const String& right ); + +/** @relates String +** @brief Overload of != operator to compare two UTF-32 strings +** @param left Left operand (a string) +** @param right Right operand (a string) +** @return True if both strings are different +**/ +bool operator!=( const String& left, const String& right ); + +/** @relates String +** @brief Overload of < operator to compare two UTF-32 strings +** @param left Left operand (a string) +** @param right Right operand (a string) +** @return True if \a left is alphabetically lesser than \a right +**/ +bool operator<( const String& left, const String& right ); + +/** @relates String +** @brief Overload of > operator to compare two UTF-32 strings +** @param left Left operand (a string) +** @param right Right operand (a string) +** @return True if \a left is alphabetically greater than \a right +**/ +bool operator>( const String& left, const String& right ); + +/** @relates String +** @brief Overload of <= operator to compare two UTF-32 strings +** @param left Left operand (a string) +** @param right Right operand (a string) +** @return True if \a left is alphabetically lesser or equal than \a right +**/ +bool operator<=( const String& left, const String& right ); + +/** @relates String +** @brief Overload of >= operator to compare two UTF-32 strings +** @param left Left operand (a string) +** @param right Right operand (a string) +** @return True if \a left is alphabetically greater or equal than \a right +**/ +bool operator>=( const String& left, const String& right ); + +/** @relates String +** @brief Overload of binary + operator to concatenate two strings +** @param left Left operand (a string) +** @param right Right operand (a string) +** @return Concatenated string +**/ +String operator+( const String& left, const String& right ); + +} // namespace efsw + +#endif + +/** @class efsw::String +** @ingroup system +** efsw::String is a utility string class defined mainly for +** convenience. It is a Unicode string (implemented using +** UTF-32), thus it can store any character in the world +** (european, chinese, arabic, hebrew, etc.). +** It automatically handles conversions from/to ANSI and +** wide strings, so that you can work with standard string +** classes and still be compatible with functions taking a +** efsw::String. +** @code +** efsw::String s; +** std::string s1 = s; // automatically converted to ANSI string +** String s2 = s; // automatically converted to wide string +** s = "hello"; // automatically converted from ANSI string +** s = L"hello"; // automatically converted from wide string +** s += 'a'; // automatically converted from ANSI string +** s += L'a'; // automatically converted from wide string +** @endcode +** Conversions involving ANSI strings use the default user locale. However +** it is possible to use a custom locale if necessary: +** @code +** std::locale locale; +** efsw::String s; +** ... +** std::string s1 = s.toAnsiString(locale); +** s = efsw::String("hello", locale); +** @endcode +** +** efsw::String defines the most important functions of the +** standard std::string class: removing, random access, iterating, +** appending, comparing, etc. However it is a simple class +** provided for convenience, and you may have to consider using +** a more optimized class if your program requires complex string +** handling. The automatic conversion functions will then take +** care of converting your string to efsw::String whenever EE +** requires it. +** +** Please note that EE also defines a low-level, generic +** interface for Unicode handling, see the efsw::Utf classes. +** +** All credits to Laurent Gomila, i just modified and expanded a little bit the implementation. +**/ diff --git a/vendor/efsw/src/efsw/System.cpp b/vendor/efsw/src/efsw/System.cpp new file mode 100644 index 0000000..ba68bf4 --- /dev/null +++ b/vendor/efsw/src/efsw/System.cpp @@ -0,0 +1,22 @@ +#include +#include + +namespace efsw { + +void System::sleep( const unsigned long& ms ) { + Platform::System::sleep( ms ); +} + +std::string System::getProcessPath() { + return Platform::System::getProcessPath(); +} + +void System::maxFD() { + Platform::System::maxFD(); +} + +Uint64 System::getMaxFD() { + return Platform::System::getMaxFD(); +} + +} // namespace efsw diff --git a/vendor/efsw/src/efsw/System.hpp b/vendor/efsw/src/efsw/System.hpp new file mode 100644 index 0000000..498e121 --- /dev/null +++ b/vendor/efsw/src/efsw/System.hpp @@ -0,0 +1,25 @@ +#ifndef EFSW_SYSTEM_HPP +#define EFSW_SYSTEM_HPP + +#include + +namespace efsw { + +class System { + public: + /// Sleep for x milliseconds + static void sleep( const unsigned long& ms ); + + /// @return The process binary path + static std::string getProcessPath(); + + /// Maximize the number of file descriptors allowed per process in the current OS + static void maxFD(); + + /// @return The number of supported file descriptors for the process + static Uint64 getMaxFD(); +}; + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/Thread.cpp b/vendor/efsw/src/efsw/Thread.cpp new file mode 100644 index 0000000..cfa88b4 --- /dev/null +++ b/vendor/efsw/src/efsw/Thread.cpp @@ -0,0 +1,41 @@ +#include +#include + +namespace efsw { + +Thread::Thread() : mThreadImpl( NULL ), mEntryPoint( NULL ) {} + +Thread::~Thread() { + wait(); + + efSAFE_DELETE( mEntryPoint ); +} + +void Thread::launch() { + wait(); + + mThreadImpl = new Platform::ThreadImpl( this ); +} + +void Thread::wait() { + if ( mThreadImpl ) { + mThreadImpl->wait(); + + efSAFE_DELETE( mThreadImpl ); + } +} + +void Thread::terminate() { + if ( mThreadImpl ) { + mThreadImpl->terminate(); + + efSAFE_DELETE( mThreadImpl ); + } +} + +void Thread::run() { + if ( mEntryPoint ) + mEntryPoint->run(); +} + +} // namespace efsw diff --git a/vendor/efsw/src/efsw/Thread.hpp b/vendor/efsw/src/efsw/Thread.hpp new file mode 100644 index 0000000..b60373c --- /dev/null +++ b/vendor/efsw/src/efsw/Thread.hpp @@ -0,0 +1,100 @@ +#ifndef EFSW_THREAD_HPP +#define EFSW_THREAD_HPP + +#include + +namespace efsw { + +namespace Platform { +class ThreadImpl; +} +namespace Private { +struct ThreadFunc; +} + +/** @brief Thread manager class */ +class Thread { + public: + typedef void ( *FuncType )( void* ); + + template Thread( F function ); + + template Thread( F function, A argument ); + + template Thread( void ( C::*function )(), C* object ); + + virtual ~Thread(); + + /** Launch the thread */ + virtual void launch(); + + /** Wait the thread until end */ + void wait(); + + /** Terminate the thread */ + void terminate(); + + protected: + Thread(); + + private: + friend class Platform::ThreadImpl; + + /** The virtual function to run in the thread */ + virtual void run(); + + Platform::ThreadImpl* mThreadImpl; ///< OS-specific implementation of the thread + Private::ThreadFunc* mEntryPoint; ///< Abstraction of the function to run +}; + +//! NOTE: Taken from SFML2 threads +namespace Private { + +// Base class for abstract thread functions +struct ThreadFunc { + virtual ~ThreadFunc() {} + virtual void run() = 0; +}; + +// Specialization using a functor (including free functions) with no argument +template struct ThreadFunctor : ThreadFunc { + ThreadFunctor( T functor ) : m_functor( functor ) {} + virtual void run() { m_functor(); } + T m_functor; +}; + +// Specialization using a functor (including free functions) with one argument +template struct ThreadFunctorWithArg : ThreadFunc { + ThreadFunctorWithArg( F function, A arg ) : m_function( function ), m_arg( arg ) {} + virtual void run() { m_function( m_arg ); } + F m_function; + A m_arg; +}; + +// Specialization using a member function +template struct ThreadMemberFunc : ThreadFunc { + ThreadMemberFunc( void ( C::*function )(), C* object ) : + m_function( function ), m_object( object ) {} + virtual void run() { ( m_object->*m_function )(); } + void ( C::*m_function )(); + C* m_object; +}; + +} // namespace Private + +template +Thread::Thread( F functor ) : + mThreadImpl( NULL ), mEntryPoint( new Private::ThreadFunctor( functor ) ) {} + +template +Thread::Thread( F function, A argument ) : + mThreadImpl( NULL ), + mEntryPoint( new Private::ThreadFunctorWithArg( function, argument ) ) {} + +template +Thread::Thread( void ( C::*function )(), C* object ) : + mThreadImpl( NULL ), mEntryPoint( new Private::ThreadMemberFunc( function, object ) ) {} + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/Utf.hpp b/vendor/efsw/src/efsw/Utf.hpp new file mode 100644 index 0000000..1b042cd --- /dev/null +++ b/vendor/efsw/src/efsw/Utf.hpp @@ -0,0 +1,721 @@ +/** NOTE: + * This code is based on the Utf implementation from SFML2. License zlib/png ( + *http://www.sfml-dev.org/license.php ) The class was modified to fit efsw own needs. This is not + *the original implementation from SFML2. + * */ + +#ifndef EFSW_UTF_HPP +#define EFSW_UTF_HPP + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include +#include +#include +#include + +namespace efsw { + +template class Utf; + +//////////////////////////////////////////////////////////// +/// \brief Specialization of the Utf template for UTF-8 +/// +//////////////////////////////////////////////////////////// +template <> class Utf<8> { + public: + //////////////////////////////////////////////////////////// + /// \brief Decode a single UTF-8 character + /// + /// Decoding a character means finding its unique 32-bits + /// code (called the codepoint) in the Unicode standard. + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Codepoint of the decoded UTF-8 character + /// \param replacement Replacement character to use in case the UTF-8 sequence is invalid + /// + /// \return Iterator pointing to one past the last read element of the input sequence + /// + //////////////////////////////////////////////////////////// + template + static In Decode( In begin, In end, Uint32& output, Uint32 replacement = 0 ); + + //////////////////////////////////////////////////////////// + /// \brief Encode a single UTF-8 character + /// + /// Encoding a character means converting a unique 32-bits + /// code (called the codepoint) in the target encoding, UTF-8. + /// + /// \param input Codepoint to encode as UTF-8 + /// \param output Iterator pointing to the beginning of the output sequence + /// \param replacement Replacement for characters not convertible to UTF-8 (use 0 to skip them) + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template static Out Encode( Uint32 input, Out output, Uint8 replacement = 0 ); + + //////////////////////////////////////////////////////////// + /// \brief Advance to the next UTF-8 character + /// + /// This function is necessary for multi-elements encodings, as + /// a single character may use more than 1 storage element. + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// + /// \return Iterator pointing to one past the last read element of the input sequence + /// + //////////////////////////////////////////////////////////// + template static In Next( In begin, In end ); + + //////////////////////////////////////////////////////////// + /// \brief Count the number of characters of a UTF-8 sequence + /// + /// This function is necessary for multi-elements encodings, as + /// a single character may use more than 1 storage element, thus the + /// total size can be different from (begin - end). + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// + /// \return Iterator pointing to one past the last read element of the input sequence + /// + //////////////////////////////////////////////////////////// + template static std::size_t Count( In begin, In end ); + + //////////////////////////////////////////////////////////// + /// \brief Convert an ANSI characters range to UTF-8 + /// + /// The current global locale will be used by default, unless you + /// pass a custom one in the \a locale parameter. + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// \param locale Locale to use for conversion + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template + static Out FromAnsi( In begin, In end, Out output, const std::locale& locale = std::locale() ); + + //////////////////////////////////////////////////////////// + /// \brief Convert a wide characters range to UTF-8 + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template static Out FromWide( In begin, In end, Out output ); + + //////////////////////////////////////////////////////////// + /// \brief Convert a latin-1 (ISO-5589-1) characters range to UTF-8 + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// \param locale Locale to use for conversion + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template static Out FromLatin1( In begin, In end, Out output ); + + //////////////////////////////////////////////////////////// + /// \brief Convert an UTF-8 characters range to ANSI characters + /// + /// The current global locale will be used by default, unless you + /// pass a custom one in the \a locale parameter. + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// \param replacement Replacement for characters not convertible to ANSI (use 0 to skip them) + /// \param locale Locale to use for conversion + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template + static Out ToAnsi( In begin, In end, Out output, char replacement = 0, + const std::locale& locale = std::locale() ); + +#ifndef EFSW_NO_WIDECHAR + //////////////////////////////////////////////////////////// + /// \brief Convert an UTF-8 characters range to wide characters + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// \param replacement Replacement for characters not convertible to wide (use 0 to skip them) + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template + static Out ToWide( In begin, In end, Out output, wchar_t replacement = 0 ); +#endif + + //////////////////////////////////////////////////////////// + /// \brief Convert an UTF-8 characters range to latin-1 (ISO-5589-1) characters + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// \param replacement Replacement for characters not convertible to wide (use 0 to skip them) + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template + static Out ToLatin1( In begin, In end, Out output, char replacement = 0 ); + + //////////////////////////////////////////////////////////// + /// \brief Convert a UTF-8 characters range to UTF-8 + /// + /// This functions does nothing more than a direct copy; + /// it is defined only to provide the same interface as other + /// specializations of the efsw::Utf<> template, and allow + /// generic code to be written on top of it. + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template static Out toUtf8( In begin, In end, Out output ); + + //////////////////////////////////////////////////////////// + /// \brief Convert a UTF-8 characters range to UTF-16 + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template static Out ToUtf16( In begin, In end, Out output ); + + //////////////////////////////////////////////////////////// + /// \brief Convert a UTF-8 characters range to UTF-32 + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template static Out ToUtf32( In begin, In end, Out output ); +}; + +//////////////////////////////////////////////////////////// +/// \brief Specialization of the Utf template for UTF-16 +/// +//////////////////////////////////////////////////////////// +template <> class Utf<16> { + public: + //////////////////////////////////////////////////////////// + /// \brief Decode a single UTF-16 character + /// + /// Decoding a character means finding its unique 32-bits + /// code (called the codepoint) in the Unicode standard. + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Codepoint of the decoded UTF-16 character + /// \param replacement Replacement character to use in case the UTF-8 sequence is invalid + /// + /// \return Iterator pointing to one past the last read element of the input sequence + /// + //////////////////////////////////////////////////////////// + template + static In Decode( In begin, In end, Uint32& output, Uint32 replacement = 0 ); + + //////////////////////////////////////////////////////////// + /// \brief Encode a single UTF-16 character + /// + /// Encoding a character means converting a unique 32-bits + /// code (called the codepoint) in the target encoding, UTF-16. + /// + /// \param input Codepoint to encode as UTF-16 + /// \param output Iterator pointing to the beginning of the output sequence + /// \param replacement Replacement for characters not convertible to UTF-16 (use 0 to skip them) + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template static Out Encode( Uint32 input, Out output, Uint16 replacement = 0 ); + + //////////////////////////////////////////////////////////// + /// \brief Advance to the next UTF-16 character + /// + /// This function is necessary for multi-elements encodings, as + /// a single character may use more than 1 storage element. + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// + /// \return Iterator pointing to one past the last read element of the input sequence + /// + //////////////////////////////////////////////////////////// + template static In Next( In begin, In end ); + + //////////////////////////////////////////////////////////// + /// \brief Count the number of characters of a UTF-16 sequence + /// + /// This function is necessary for multi-elements encodings, as + /// a single character may use more than 1 storage element, thus the + /// total size can be different from (begin - end). + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// + /// \return Iterator pointing to one past the last read element of the input sequence + /// + //////////////////////////////////////////////////////////// + template static std::size_t Count( In begin, In end ); + + //////////////////////////////////////////////////////////// + /// \brief Convert an ANSI characters range to UTF-16 + /// + /// The current global locale will be used by default, unless you + /// pass a custom one in the \a locale parameter. + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// \param locale Locale to use for conversion + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template + static Out FromAnsi( In begin, In end, Out output, const std::locale& locale = std::locale() ); + + //////////////////////////////////////////////////////////// + /// \brief Convert a wide characters range to UTF-16 + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template static Out FromWide( In begin, In end, Out output ); + + //////////////////////////////////////////////////////////// + /// \brief Convert a latin-1 (ISO-5589-1) characters range to UTF-16 + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// \param locale Locale to use for conversion + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template static Out FromLatin1( In begin, In end, Out output ); + + //////////////////////////////////////////////////////////// + /// \brief Convert an UTF-16 characters range to ANSI characters + /// + /// The current global locale will be used by default, unless you + /// pass a custom one in the \a locale parameter. + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// \param replacement Replacement for characters not convertible to ANSI (use 0 to skip them) + /// \param locale Locale to use for conversion + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template + static Out ToAnsi( In begin, In end, Out output, char replacement = 0, + const std::locale& locale = std::locale() ); + +#ifndef EFSW_NO_WIDECHAR + //////////////////////////////////////////////////////////// + /// \brief Convert an UTF-16 characters range to wide characters + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// \param replacement Replacement for characters not convertible to wide (use 0 to skip them) + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template + static Out ToWide( In begin, In end, Out output, wchar_t replacement = 0 ); +#endif + + //////////////////////////////////////////////////////////// + /// \brief Convert an UTF-16 characters range to latin-1 (ISO-5589-1) characters + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// \param replacement Replacement for characters not convertible to wide (use 0 to skip them) + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template + static Out ToLatin1( In begin, In end, Out output, char replacement = 0 ); + + //////////////////////////////////////////////////////////// + /// \brief Convert a UTF-16 characters range to UTF-8 + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template static Out toUtf8( In begin, In end, Out output ); + + //////////////////////////////////////////////////////////// + /// \brief Convert a UTF-16 characters range to UTF-16 + /// + /// This functions does nothing more than a direct copy; + /// it is defined only to provide the same interface as other + /// specializations of the efsw::Utf<> template, and allow + /// generic code to be written on top of it. + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template static Out ToUtf16( In begin, In end, Out output ); + + //////////////////////////////////////////////////////////// + /// \brief Convert a UTF-16 characters range to UTF-32 + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template static Out ToUtf32( In begin, In end, Out output ); +}; + +//////////////////////////////////////////////////////////// +/// \brief Specialization of the Utf template for UTF-32 +/// +//////////////////////////////////////////////////////////// +template <> class Utf<32> { + public: + //////////////////////////////////////////////////////////// + /// \brief Decode a single UTF-32 character + /// + /// Decoding a character means finding its unique 32-bits + /// code (called the codepoint) in the Unicode standard. + /// For UTF-32, the character value is the same as the codepoint. + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Codepoint of the decoded UTF-32 character + /// \param replacement Replacement character to use in case the UTF-8 sequence is invalid + /// + /// \return Iterator pointing to one past the last read element of the input sequence + /// + //////////////////////////////////////////////////////////// + template + static In Decode( In begin, In end, Uint32& output, Uint32 replacement = 0 ); + + //////////////////////////////////////////////////////////// + /// \brief Encode a single UTF-32 character + /// + /// Encoding a character means converting a unique 32-bits + /// code (called the codepoint) in the target encoding, UTF-32. + /// For UTF-32, the codepoint is the same as the character value. + /// + /// \param input Codepoint to encode as UTF-32 + /// \param output Iterator pointing to the beginning of the output sequence + /// \param replacement Replacement for characters not convertible to UTF-32 (use 0 to skip them) + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template static Out Encode( Uint32 input, Out output, Uint32 replacement = 0 ); + + //////////////////////////////////////////////////////////// + /// \brief Advance to the next UTF-32 character + /// + /// This function is trivial for UTF-32, which can store + /// every character in a single storage element. + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// + /// \return Iterator pointing to one past the last read element of the input sequence + /// + //////////////////////////////////////////////////////////// + template static In Next( In begin, In end ); + + //////////////////////////////////////////////////////////// + /// \brief Count the number of characters of a UTF-32 sequence + /// + /// This function is trivial for UTF-32, which can store + /// every character in a single storage element. + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// + /// \return Iterator pointing to one past the last read element of the input sequence + /// + //////////////////////////////////////////////////////////// + template static std::size_t Count( In begin, In end ); + + //////////////////////////////////////////////////////////// + /// \brief Convert an ANSI characters range to UTF-32 + /// + /// The current global locale will be used by default, unless you + /// pass a custom one in the \a locale parameter. + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// \param locale Locale to use for conversion + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template + static Out FromAnsi( In begin, In end, Out output, const std::locale& locale = std::locale() ); + + //////////////////////////////////////////////////////////// + /// \brief Convert a wide characters range to UTF-32 + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template static Out FromWide( In begin, In end, Out output ); + + //////////////////////////////////////////////////////////// + /// \brief Convert a latin-1 (ISO-5589-1) characters range to UTF-32 + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// \param locale Locale to use for conversion + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template static Out FromLatin1( In begin, In end, Out output ); + + //////////////////////////////////////////////////////////// + /// \brief Convert an UTF-32 characters range to ANSI characters + /// + /// The current global locale will be used by default, unless you + /// pass a custom one in the \a locale parameter. + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// \param replacement Replacement for characters not convertible to ANSI (use 0 to skip them) + /// \param locale Locale to use for conversion + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template + static Out ToAnsi( In begin, In end, Out output, char replacement = 0, + const std::locale& locale = std::locale() ); + +#ifndef EFSW_NO_WIDECHAR + //////////////////////////////////////////////////////////// + /// \brief Convert an UTF-32 characters range to wide characters + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// \param replacement Replacement for characters not convertible to wide (use 0 to skip them) + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template + static Out ToWide( In begin, In end, Out output, wchar_t replacement = 0 ); +#endif + + //////////////////////////////////////////////////////////// + /// \brief Convert an UTF-16 characters range to latin-1 (ISO-5589-1) characters + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// \param replacement Replacement for characters not convertible to wide (use 0 to skip them) + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template + static Out ToLatin1( In begin, In end, Out output, char replacement = 0 ); + + //////////////////////////////////////////////////////////// + /// \brief Convert a UTF-32 characters range to UTF-8 + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template static Out toUtf8( In begin, In end, Out output ); + + //////////////////////////////////////////////////////////// + /// \brief Convert a UTF-32 characters range to UTF-16 + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template static Out ToUtf16( In begin, In end, Out output ); + + //////////////////////////////////////////////////////////// + /// \brief Convert a UTF-32 characters range to UTF-32 + /// + /// This functions does nothing more than a direct copy; + /// it is defined only to provide the same interface as other + /// specializations of the efsw::Utf<> template, and allow + /// generic code to be written on top of it. + /// + /// \param begin Iterator pointing to the beginning of the input sequence + /// \param end Iterator pointing to the end of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template static Out ToUtf32( In begin, In end, Out output ); + + //////////////////////////////////////////////////////////// + /// \brief Decode a single ANSI character to UTF-32 + /// + /// This function does not exist in other specializations + /// of efsw::Utf<>, it is defined for convenience (it is used by + /// several other conversion functions). + /// + /// \param input Input ANSI character + /// \param locale Locale to use for conversion + /// + /// \return Converted character + /// + //////////////////////////////////////////////////////////// + template + static Uint32 DecodeAnsi( In input, const std::locale& locale = std::locale() ); + + //////////////////////////////////////////////////////////// + /// \brief Decode a single wide character to UTF-32 + /// + /// This function does not exist in other specializations + /// of efsw::Utf<>, it is defined for convenience (it is used by + /// several other conversion functions). + /// + /// \param input Input wide character + /// + /// \return Converted character + /// + //////////////////////////////////////////////////////////// + template static Uint32 DecodeWide( In input ); + + //////////////////////////////////////////////////////////// + /// \brief Encode a single UTF-32 character to ANSI + /// + /// This function does not exist in other specializations + /// of efsw::Utf<>, it is defined for convenience (it is used by + /// several other conversion functions). + /// + /// \param codepoint Iterator pointing to the beginning of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// \param replacement Replacement if the input character is not convertible to ANSI (use 0 to + /// skip it) \param locale Locale to use for conversion + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template + static Out EncodeAnsi( Uint32 codepoint, Out output, char replacement = 0, + const std::locale& locale = std::locale() ); + +#ifndef EFSW_NO_WIDECHAR + //////////////////////////////////////////////////////////// + /// \brief Encode a single UTF-32 character to wide + /// + /// This function does not exist in other specializations + /// of efsw::Utf<>, it is defined for convenience (it is used by + /// several other conversion functions). + /// + /// \param codepoint Iterator pointing to the beginning of the input sequence + /// \param output Iterator pointing to the beginning of the output sequence + /// \param replacement Replacement if the input character is not convertible to wide (use 0 to + /// skip it) + /// + /// \return Iterator to the end of the output sequence which has been written + /// + //////////////////////////////////////////////////////////// + template + static Out EncodeWide( Uint32 codepoint, Out output, wchar_t replacement = 0 ); +#endif +}; + +#include "Utf.inl" + +// Make typedefs to get rid of the template syntax +typedef Utf<8> Utf8; +typedef Utf<16> Utf16; +typedef Utf<32> Utf32; + +} // namespace efsw +#endif + +//////////////////////////////////////////////////////////// +/// \class efsw::Utf +/// \ingroup system +/// +/// Utility class providing generic functions for UTF conversions. +/// +/// efsw::Utf is a low-level, generic interface for counting, iterating, +/// encoding and decoding Unicode characters and strings. It is able +/// to handle ANSI, wide, UTF-8, UTF-16 and UTF-32 encodings. +/// +/// efsw::Utf functions are all static, these classes are not meant to +/// be instanciated. All the functions are template, so that you +/// can use any character / string type for a given encoding. +/// +/// It has 3 specializations: +/// \li efsw::Utf<8> (typedef'd to efsw::Utf8) +/// \li efsw::Utf<16> (typedef'd to efsw::Utf16) +/// \li efsw::Utf<32> (typedef'd to efsw::Utf32) +/// +//////////////////////////////////////////////////////////// diff --git a/vendor/efsw/src/efsw/Utf.inl b/vendor/efsw/src/efsw/Utf.inl new file mode 100644 index 0000000..ef71bc8 --- /dev/null +++ b/vendor/efsw/src/efsw/Utf.inl @@ -0,0 +1,576 @@ +// References : +// http://www.unicode.org/ +// http://www.unicode.org/Public/PROGRAMS/CVTUTF/ConvertUTF.c +// http://www.unicode.org/Public/PROGRAMS/CVTUTF/ConvertUTF.h +// http://people.w3.org/rishida/scripts/uniview/conversion +//////////////////////////////////////////////////////////// + +template In Utf<8>::Decode( In begin, In end, Uint32& output, Uint32 replacement ) { + // Some useful precomputed data + static const int trailing[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5 }; + static const Uint32 offsets[6] = { 0x00000000, 0x00003080, 0x000E2080, + 0x03C82080, 0xFA082080, 0x82082080 }; + + // Decode the character + int trailingBytes = trailing[static_cast( *begin )]; + if ( begin + trailingBytes < end ) { + output = 0; + switch ( trailingBytes ) { + case 5: + output += static_cast( *begin++ ); + output <<= 6; + case 4: + output += static_cast( *begin++ ); + output <<= 6; + case 3: + output += static_cast( *begin++ ); + output <<= 6; + case 2: + output += static_cast( *begin++ ); + output <<= 6; + case 1: + output += static_cast( *begin++ ); + output <<= 6; + case 0: + output += static_cast( *begin++ ); + } + output -= offsets[trailingBytes]; + } else { + // Incomplete character + begin = end; + output = replacement; + } + + return begin; +} + +template Out Utf<8>::Encode( Uint32 input, Out output, Uint8 replacement ) { + // Some useful precomputed data + static const Uint8 firstBytes[7] = { 0x00, 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC }; + + // Encode the character + if ( ( input > 0x0010FFFF ) || ( ( input >= 0xD800 ) && ( input <= 0xDBFF ) ) ) { + // Invalid character + if ( replacement ) + *output++ = replacement; + } else { + // Valid character + + // Get the number of bytes to write + int bytesToWrite = 1; + if ( input < 0x80 ) + bytesToWrite = 1; + else if ( input < 0x800 ) + bytesToWrite = 2; + else if ( input < 0x10000 ) + bytesToWrite = 3; + else if ( input <= 0x0010FFFF ) + bytesToWrite = 4; + + // Extract the bytes to write + Uint8 bytes[4]; + switch ( bytesToWrite ) { + case 4: + bytes[3] = static_cast( ( input | 0x80 ) & 0xBF ); + input >>= 6; + case 3: + bytes[2] = static_cast( ( input | 0x80 ) & 0xBF ); + input >>= 6; + case 2: + bytes[1] = static_cast( ( input | 0x80 ) & 0xBF ); + input >>= 6; + case 1: + bytes[0] = static_cast( input | firstBytes[bytesToWrite] ); + } + + // Add them to the output + const Uint8* currentByte = bytes; + switch ( bytesToWrite ) { + case 4: + *output++ = *currentByte++; + case 3: + *output++ = *currentByte++; + case 2: + *output++ = *currentByte++; + case 1: + *output++ = *currentByte++; + } + } + + return output; +} + +template In Utf<8>::Next( In begin, In end ) { + Uint32 codepoint; + return Decode( begin, end, codepoint ); +} + +template std::size_t Utf<8>::Count( In begin, In end ) { + std::size_t length = 0; + while ( begin < end ) { + begin = Next( begin, end ); + ++length; + } + + return length; +} + +template +Out Utf<8>::FromAnsi( In begin, In end, Out output, const std::locale& locale ) { + while ( begin < end ) { + Uint32 codepoint = Utf<32>::DecodeAnsi( *begin++, locale ); + output = Encode( codepoint, output ); + } + + return output; +} + +template Out Utf<8>::FromWide( In begin, In end, Out output ) { + while ( begin < end ) { + Uint32 codepoint = Utf<32>::DecodeWide( *begin++ ); + output = Encode( codepoint, output ); + } + + return output; +} + +template Out Utf<8>::FromLatin1( In begin, In end, Out output ) { + // Latin-1 is directly compatible with Unicode encodings, + // and can thus be treated as (a sub-range of) UTF-32 + while ( begin < end ) + output = Encode( *begin++, output ); + + return output; +} + +template +Out Utf<8>::ToAnsi( In begin, In end, Out output, char replacement, const std::locale& locale ) { + while ( begin < end ) { + Uint32 codepoint; + begin = Decode( begin, end, codepoint ); + output = Utf<32>::EncodeAnsi( codepoint, output, replacement, locale ); + } + + return output; +} + +#ifndef EFSW_NO_WIDECHAR +template +Out Utf<8>::ToWide( In begin, In end, Out output, wchar_t replacement ) { + while ( begin < end ) { + Uint32 codepoint; + begin = Decode( begin, end, codepoint ); + output = Utf<32>::EncodeWide( codepoint, output, replacement ); + } + + return output; +} +#endif + +template +Out Utf<8>::ToLatin1( In begin, In end, Out output, char replacement ) { + // Latin-1 is directly compatible with Unicode encodings, + // and can thus be treated as (a sub-range of) UTF-32 + while ( begin < end ) { + Uint32 codepoint; + begin = Decode( begin, end, codepoint ); + *output++ = codepoint < 256 ? static_cast( codepoint ) : replacement; + } + + return output; +} + +template Out Utf<8>::toUtf8( In begin, In end, Out output ) { + while ( begin < end ) + *output++ = *begin++; + + return output; +} + +template Out Utf<8>::ToUtf16( In begin, In end, Out output ) { + while ( begin < end ) { + Uint32 codepoint; + begin = Decode( begin, end, codepoint ); + output = Utf<16>::Encode( codepoint, output ); + } + + return output; +} + +template Out Utf<8>::ToUtf32( In begin, In end, Out output ) { + while ( begin < end ) { + Uint32 codepoint; + begin = Decode( begin, end, codepoint ); + *output++ = codepoint; + } + + return output; +} + +template In Utf<16>::Decode( In begin, In end, Uint32& output, Uint32 replacement ) { + Uint16 first = *begin++; + + // If it's a surrogate pair, first convert to a single UTF-32 character + if ( ( first >= 0xD800 ) && ( first <= 0xDBFF ) ) { + if ( begin < end ) { + Uint32 second = *begin++; + if ( ( second >= 0xDC00 ) && ( second <= 0xDFFF ) ) { + // The second element is valid: convert the two elements to a UTF-32 character + output = static_cast( ( ( first - 0xD800 ) << 10 ) + ( second - 0xDC00 ) + + 0x0010000 ); + } else { + // Invalid character + output = replacement; + } + } else { + // Invalid character + begin = end; + output = replacement; + } + } else { + // We can make a direct copy + output = first; + } + + return begin; +} + +template Out Utf<16>::Encode( Uint32 input, Out output, Uint16 replacement ) { + if ( input < 0xFFFF ) { + // The character can be copied directly, we just need to check if it's in the valid range + if ( ( input >= 0xD800 ) && ( input <= 0xDFFF ) ) { + // Invalid character (this range is reserved) + if ( replacement ) + *output++ = replacement; + } else { + // Valid character directly convertible to a single UTF-16 character + *output++ = static_cast( input ); + } + } else if ( input > 0x0010FFFF ) { + // Invalid character (greater than the maximum unicode value) + if ( replacement ) + *output++ = replacement; + } else { + // The input character will be converted to two UTF-16 elements + input -= 0x0010000; + *output++ = static_cast( ( input >> 10 ) + 0xD800 ); + *output++ = static_cast( ( input & 0x3FFUL ) + 0xDC00 ); + } + + return output; +} + +template In Utf<16>::Next( In begin, In end ) { + Uint32 codepoint; + return Decode( begin, end, codepoint ); +} + +template std::size_t Utf<16>::Count( In begin, In end ) { + std::size_t length = 0; + while ( begin < end ) { + begin = Next( begin, end ); + ++length; + } + + return length; +} + +template +Out Utf<16>::FromAnsi( In begin, In end, Out output, const std::locale& locale ) { + while ( begin < end ) { + Uint32 codepoint = Utf<32>::DecodeAnsi( *begin++, locale ); + output = Encode( codepoint, output ); + } + + return output; +} + +template Out Utf<16>::FromWide( In begin, In end, Out output ) { + while ( begin < end ) { + Uint32 codepoint = Utf<32>::DecodeWide( *begin++ ); + output = Encode( codepoint, output ); + } + + return output; +} + +template Out Utf<16>::FromLatin1( In begin, In end, Out output ) { + // Latin-1 is directly compatible with Unicode encodings, + // and can thus be treated as (a sub-range of) UTF-32 + while ( begin < end ) + *output++ = *begin++; + + return output; +} + +template +Out Utf<16>::ToAnsi( In begin, In end, Out output, char replacement, const std::locale& locale ) { + while ( begin < end ) { + Uint32 codepoint; + begin = Decode( begin, end, codepoint ); + output = Utf<32>::EncodeAnsi( codepoint, output, replacement, locale ); + } + + return output; +} + +#ifndef EFSW_NO_WIDECHAR +template +Out Utf<16>::ToWide( In begin, In end, Out output, wchar_t replacement ) { + while ( begin < end ) { + Uint32 codepoint; + begin = Decode( begin, end, codepoint ); + output = Utf<32>::EncodeWide( codepoint, output, replacement ); + } + + return output; +} +#endif + +template +Out Utf<16>::ToLatin1( In begin, In end, Out output, char replacement ) { + // Latin-1 is directly compatible with Unicode encodings, + // and can thus be treated as (a sub-range of) UTF-32 + while ( begin < end ) { + *output++ = *begin < 256 ? static_cast( *begin ) : replacement; + begin++; + } + + return output; +} + +template Out Utf<16>::toUtf8( In begin, In end, Out output ) { + while ( begin < end ) { + Uint32 codepoint; + begin = Decode( begin, end, codepoint ); + output = Utf<8>::Encode( codepoint, output ); + } + + return output; +} + +template Out Utf<16>::ToUtf16( In begin, In end, Out output ) { + while ( begin < end ) + *output++ = *begin++; + + return output; +} + +template Out Utf<16>::ToUtf32( In begin, In end, Out output ) { + while ( begin < end ) { + Uint32 codepoint; + begin = Decode( begin, end, codepoint ); + *output++ = codepoint; + } + + return output; +} + +template In Utf<32>::Decode( In begin, In end, Uint32& output, Uint32 ) { + output = *begin++; + return begin; +} + +template Out Utf<32>::Encode( Uint32 input, Out output, Uint32 replacement ) { + *output++ = input; + return output; +} + +template In Utf<32>::Next( In begin, In end ) { + return ++begin; +} + +template std::size_t Utf<32>::Count( In begin, In end ) { + return begin - end; +} + +template +Out Utf<32>::FromAnsi( In begin, In end, Out output, const std::locale& locale ) { + while ( begin < end ) + *output++ = DecodeAnsi( *begin++, locale ); + + return output; +} + +template Out Utf<32>::FromWide( In begin, In end, Out output ) { + while ( begin < end ) + *output++ = DecodeWide( *begin++ ); + + return output; +} + +template Out Utf<32>::FromLatin1( In begin, In end, Out output ) { + // Latin-1 is directly compatible with Unicode encodings, + // and can thus be treated as (a sub-range of) UTF-32 + while ( begin < end ) + *output++ = *begin++; + + return output; +} + +template +Out Utf<32>::ToAnsi( In begin, In end, Out output, char replacement, const std::locale& locale ) { + while ( begin < end ) + output = EncodeAnsi( *begin++, output, replacement, locale ); + + return output; +} + +#ifndef EFSW_NO_WIDECHAR +template +Out Utf<32>::ToWide( In begin, In end, Out output, wchar_t replacement ) { + while ( begin < end ) + output = EncodeWide( *begin++, output, replacement ); + + return output; +} +#endif + +template +Out Utf<32>::ToLatin1( In begin, In end, Out output, char replacement ) { + // Latin-1 is directly compatible with Unicode encodings, + // and can thus be treated as (a sub-range of) UTF-32 + while ( begin < end ) { + *output++ = *begin < 256 ? static_cast( *begin ) : replacement; + begin++; + } + + return output; +} + +template Out Utf<32>::toUtf8( In begin, In end, Out output ) { + while ( begin < end ) + output = Utf<8>::Encode( *begin++, output ); + + return output; +} + +template Out Utf<32>::ToUtf16( In begin, In end, Out output ) { + while ( begin < end ) + output = Utf<16>::Encode( *begin++, output ); + + return output; +} + +template Out Utf<32>::ToUtf32( In begin, In end, Out output ) { + while ( begin < end ) + *output++ = *begin++; + + return output; +} + +template Uint32 Utf<32>::DecodeAnsi( In input, const std::locale& locale ) { + // On Windows, gcc's standard library (glibc++) has almost + // no support for Unicode stuff. As a consequence, in this + // context we can only use the default locale and ignore + // the one passed as parameter. + +#if EFSW_PLATFORM == EFSW_PLATFORM_WIN && /* if Windows ... */ \ + ( defined( __GLIBCPP__ ) || \ + defined( __GLIBCXX__ ) ) && /* ... and standard library is glibc++ ... */ \ + !( defined( __SGI_STL_PORT ) || \ + defined( _STLPORT_VERSION ) ) /* ... and STLPort is not used on top of it */ + + wchar_t character = 0; + mbtowc( &character, &input, 1 ); + return static_cast( character ); + +#else +// Get the facet of the locale which deals with character conversion +#ifndef EFSW_NO_WIDECHAR + const std::ctype& facet = std::use_facet>( locale ); +#else + const std::ctype& facet = std::use_facet>( locale ); +#endif + + // Use the facet to convert each character of the input string + return static_cast( facet.widen( input ) ); + +#endif +} + +template Uint32 Utf<32>::DecodeWide( In input ) { + // The encoding of wide characters is not well defined and is left to the system; + // however we can safely assume that it is UCS-2 on Windows and + // UCS-4 on Unix systems. + // In both cases, a simple copy is enough (UCS-2 is a subset of UCS-4, + // and UCS-4 *is* UTF-32). + + return input; +} + +template +Out Utf<32>::EncodeAnsi( Uint32 codepoint, Out output, char replacement, + const std::locale& locale ) { + // On Windows, gcc's standard library (glibc++) has almost + // no support for Unicode stuff. As a consequence, in this + // context we can only use the default locale and ignore + // the one passed as parameter. + +#if EFSW_PLATFORM == EFSW_PLATFORM_WIN && /* if Windows ... */ \ + ( defined( __GLIBCPP__ ) || \ + defined( __GLIBCXX__ ) ) && /* ... and standard library is glibc++ ... */ \ + !( defined( __SGI_STL_PORT ) || \ + defined( _STLPORT_VERSION ) ) /* ... and STLPort is not used on top of it */ + + char character = 0; + if ( wctomb( &character, static_cast( codepoint ) ) >= 0 ) + *output++ = character; + else if ( replacement ) + *output++ = replacement; + + return output; + +#else +// Get the facet of the locale which deals with character conversion +#ifndef EFSW_NO_WIDECHAR + const std::ctype& facet = std::use_facet>( locale ); +#else + const std::ctype& facet = std::use_facet>( locale ); +#endif + + // Use the facet to convert each character of the input string + *output++ = facet.narrow( static_cast( codepoint ), replacement ); + + return output; + +#endif +} + +#ifndef EFSW_NO_WIDECHAR +template +Out Utf<32>::EncodeWide( Uint32 codepoint, Out output, wchar_t replacement ) { + // The encoding of wide characters is not well defined and is left to the system; + // however we can safely assume that it is UCS-2 on Windows and + // UCS-4 on Unix systems. + // For UCS-2 we need to check if the source characters fits in (UCS-2 is a subset of UCS-4). + // For UCS-4 we can do a direct copy (UCS-4 *is* UTF-32). + + switch ( sizeof( wchar_t ) ) { + case 4: { + *output++ = static_cast( codepoint ); + break; + } + + default: { + if ( ( codepoint <= 0xFFFF ) && ( ( codepoint < 0xD800 ) || ( codepoint > 0xDFFF ) ) ) { + *output++ = static_cast( codepoint ); + } else if ( replacement ) { + *output++ = replacement; + } + break; + } + } + + return output; +} +#endif diff --git a/vendor/efsw/src/efsw/Watcher.cpp b/vendor/efsw/src/efsw/Watcher.cpp new file mode 100644 index 0000000..913ae3c --- /dev/null +++ b/vendor/efsw/src/efsw/Watcher.cpp @@ -0,0 +1,10 @@ +#include + +namespace efsw { + +Watcher::Watcher() : ID( 0 ), Directory( "" ), Listener( NULL ), Recursive( false ) {} + +Watcher::Watcher( WatchID id, std::string directory, FileWatchListener* listener, bool recursive ) : + ID( id ), Directory( directory ), Listener( listener ), Recursive( recursive ) {} + +} // namespace efsw diff --git a/vendor/efsw/src/efsw/Watcher.hpp b/vendor/efsw/src/efsw/Watcher.hpp new file mode 100644 index 0000000..84f0980 --- /dev/null +++ b/vendor/efsw/src/efsw/Watcher.hpp @@ -0,0 +1,29 @@ +#ifndef EFSW_WATCHERIMPL_HPP +#define EFSW_WATCHERIMPL_HPP + +#include +#include + +namespace efsw { + +/** @brief Base Watcher class */ +class Watcher { + public: + Watcher(); + + Watcher( WatchID id, std::string directory, FileWatchListener* listener, bool recursive ); + + virtual ~Watcher() {} + + virtual void watch() {} + + WatchID ID; + std::string Directory; + FileWatchListener* Listener; + bool Recursive; + std::string OldFileName; +}; + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/WatcherFSEvents.cpp b/vendor/efsw/src/efsw/WatcherFSEvents.cpp new file mode 100644 index 0000000..e9b6b70 --- /dev/null +++ b/vendor/efsw/src/efsw/WatcherFSEvents.cpp @@ -0,0 +1,211 @@ +#include +#include +#include +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_FSEVENTS + +namespace efsw { + +WatcherFSEvents::WatcherFSEvents() : + Watcher(), FWatcher( NULL ), FSStream( NULL ), WatcherGen( NULL ) {} + +WatcherFSEvents::WatcherFSEvents( WatchID id, std::string directory, FileWatchListener* listener, + bool recursive, WatcherFSEvents* parent ) : + Watcher( id, directory, listener, recursive ), + FWatcher( NULL ), + FSStream( NULL ), + WatcherGen( NULL ) {} + +WatcherFSEvents::~WatcherFSEvents() { + if ( NULL != FSStream ) { + FSEventStreamStop( FSStream ); + FSEventStreamInvalidate( FSStream ); + FSEventStreamRelease( FSStream ); + } + + efSAFE_DELETE( WatcherGen ); +} + +void WatcherFSEvents::init() { + CFStringRef CFDirectory = + CFStringCreateWithCString( NULL, Directory.c_str(), kCFStringEncodingUTF8 ); + CFArrayRef CFDirectoryArray = CFArrayCreate( NULL, (const void**)&CFDirectory, 1, NULL ); + + Uint32 streamFlags = kFSEventStreamCreateFlagNone; + + if ( FileWatcherFSEvents::isGranular() ) { + streamFlags = efswFSEventStreamCreateFlagFileEvents | efswFSEventStreamCreateFlagNoDefer | + efswFSEventStreamCreateFlagUseExtendedData | + efswFSEventStreamCreateFlagUseCFTypes; + } else { + WatcherGen = new WatcherGeneric( ID, Directory, Listener, FWatcher.load(), Recursive ); + } + + FSEventStreamContext ctx; + /* Initialize context */ + ctx.version = 0; + ctx.info = this; + ctx.retain = NULL; + ctx.release = NULL; + ctx.copyDescription = NULL; + + dispatch_queue_t queue = dispatch_queue_create(NULL, NULL); + + FSStream = + FSEventStreamCreate( kCFAllocatorDefault, &FileWatcherFSEvents::FSEventCallback, &ctx, + CFDirectoryArray, kFSEventStreamEventIdSinceNow, 0., streamFlags ); + + FSEventStreamSetDispatchQueue(FSStream, queue); + + FSEventStreamStart( FSStream ); + + CFRelease( CFDirectoryArray ); + CFRelease( CFDirectory ); +} + +void WatcherFSEvents::sendFileAction( WatchID watchid, const std::string& dir, + const std::string& filename, Action action, + std::string oldFilename ) { + Listener->handleFileAction( watchid, FileSystem::precomposeFileName( dir ), + FileSystem::precomposeFileName( filename ), action, FileSystem::precomposeFileName( oldFilename ) ); +} + +void WatcherFSEvents::handleAddModDel( const Uint32& flags, const std::string& path, + std::string& dirPath, std::string& filePath ) { + if ( flags & efswFSEventStreamEventFlagItemCreated ) { + if ( FileInfo::exists( path ) ) { + sendFileAction( ID, dirPath, filePath, Actions::Add ); + } + } + + if ( flags & efswFSEventsModified ) { + sendFileAction( ID, dirPath, filePath, Actions::Modified ); + } + + if ( flags & efswFSEventStreamEventFlagItemRemoved ) { + // Since i don't know the order, at least i try to keep the data consistent with the real + // state + if ( !FileInfo::exists( path ) ) { + sendFileAction( ID, dirPath, filePath, Actions::Delete ); + } + } +} + +void WatcherFSEvents::handleActions( std::vector& events ) { + size_t esize = events.size(); + + for ( size_t i = 0; i < esize; i++ ) { + FSEvent& event = events[i]; + + if ( event.Flags & + ( kFSEventStreamEventFlagUserDropped | kFSEventStreamEventFlagKernelDropped | + kFSEventStreamEventFlagEventIdsWrapped | kFSEventStreamEventFlagHistoryDone | + kFSEventStreamEventFlagMount | kFSEventStreamEventFlagUnmount | + kFSEventStreamEventFlagRootChanged ) ) { + continue; + } + + if ( !Recursive ) { + /** In case that is not recursive the watcher, ignore the events from subfolders */ + if ( event.Path.find_last_of( FileSystem::getOSSlash() ) != Directory.size() - 1 ) { + continue; + } + } + + if ( FileWatcherFSEvents::isGranular() ) { + std::string dirPath( FileSystem::pathRemoveFileName( event.Path ) ); + std::string filePath( FileSystem::fileNameFromPath( event.Path ) ); + + if ( event.Flags & + ( efswFSEventStreamEventFlagItemCreated | efswFSEventStreamEventFlagItemRemoved | + efswFSEventStreamEventFlagItemRenamed ) ) { + if ( dirPath != Directory ) { + DirsChanged.insert( dirPath ); + } + } + + // This is a mess. But it's FSEvents faults, because shrinks events from the same file + // in one single event ( so there's no order for them ) For example a file could have + // been added modified and erased, but i can't know if first was erased and then added + // and modified, or added, then modified and then erased. I don't know what they were + // thinking by doing this... + efDEBUG( "Event in: %s - flags: 0x%x\n", event.Path.c_str(), event.Flags ); + + if ( event.Flags & efswFSEventStreamEventFlagItemRenamed ) { + if ( ( i + 1 < esize ) && + ( events[i + 1].Flags & efswFSEventStreamEventFlagItemRenamed ) && + ( events[i + 1].inode == event.inode ) ) { + FSEvent& nEvent = events[i + 1]; + std::string newDir( FileSystem::pathRemoveFileName( nEvent.Path ) ); + std::string newFilepath( FileSystem::fileNameFromPath( nEvent.Path ) ); + + if ( event.Path != nEvent.Path ) { + if ( dirPath == newDir ) { + if ( !FileInfo::exists( event.Path ) || + 0 == strcasecmp( event.Path.c_str(), nEvent.Path.c_str() ) ) { + sendFileAction( ID, dirPath, newFilepath, Actions::Moved, + filePath ); + } else { + sendFileAction( ID, dirPath, filePath, Actions::Moved, + newFilepath ); + } + } else { + sendFileAction( ID, dirPath, filePath, Actions::Delete ); + sendFileAction( ID, newDir, newFilepath, Actions::Add ); + + if ( nEvent.Flags & efswFSEventsModified ) { + sendFileAction( ID, newDir, newFilepath, Actions::Modified ); + } + } + } else { + handleAddModDel( nEvent.Flags, nEvent.Path, dirPath, filePath ); + } + + if ( nEvent.Flags & ( efswFSEventStreamEventFlagItemCreated | + efswFSEventStreamEventFlagItemRemoved | + efswFSEventStreamEventFlagItemRenamed ) ) { + if ( newDir != Directory ) { + DirsChanged.insert( newDir ); + } + } + + // Skip the renamed file + i++; + } else if ( FileInfo::exists( event.Path ) ) { + sendFileAction( ID, dirPath, filePath, Actions::Add ); + + if ( event.Flags & efswFSEventsModified ) { + sendFileAction( ID, dirPath, filePath, Actions::Modified ); + } + } else { + sendFileAction( ID, dirPath, filePath, Actions::Delete ); + } + } else { + handleAddModDel( event.Flags, event.Path, dirPath, filePath ); + } + } else { + efDEBUG( "Directory: %s changed\n", event.Path.c_str() ); + DirsChanged.insert( event.Path ); + } + } +} + +void WatcherFSEvents::process() { + std::set::iterator it = DirsChanged.begin(); + + for ( ; it != DirsChanged.end(); it++ ) { + if ( !FileWatcherFSEvents::isGranular() ) { + WatcherGen->watchDir( ( *it ) ); + } else { + sendFileAction( ID, FileSystem::pathRemoveFileName( ( *it ) ), + FileSystem::fileNameFromPath( ( *it ) ), Actions::Modified ); + } + } + + DirsChanged.clear(); +} + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/WatcherFSEvents.hpp b/vendor/efsw/src/efsw/WatcherFSEvents.hpp new file mode 100644 index 0000000..1c6047c --- /dev/null +++ b/vendor/efsw/src/efsw/WatcherFSEvents.hpp @@ -0,0 +1,64 @@ +#ifndef EFSW_WATCHERINOTIFY_HPP +#define EFSW_WATCHERINOTIFY_HPP + +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_FSEVENTS + +#include +#include +#include +#include +#include +#include + +namespace efsw { + +class FileWatcherFSEvents; + +class FSEvent { + public: + FSEvent( std::string path, long flags, Uint64 id, Uint64 inode = 0 ) : + Path( path ), Flags( flags ), Id( id ), inode( inode ) {} + + std::string Path; + long Flags; + Uint64 Id; + Uint64 inode{ 0 }; +}; + +class WatcherFSEvents : public Watcher { + public: + WatcherFSEvents(); + + WatcherFSEvents( WatchID id, std::string directory, FileWatchListener* listener, bool recursive, + WatcherFSEvents* parent = NULL ); + + ~WatcherFSEvents(); + + void init(); + + void handleActions( std::vector& events ); + + void process(); + + Atomic FWatcher; + FSEventStreamRef FSStream; + + protected: + void handleAddModDel( const Uint32& flags, const std::string& path, std::string& dirPath, + std::string& filePath ); + + WatcherGeneric* WatcherGen; + + std::set DirsChanged; + + void sendFileAction( WatchID watchid, const std::string& dir, const std::string& filename, + Action action, std::string oldFilename = "" ); +}; + +} // namespace efsw + +#endif + +#endif diff --git a/vendor/efsw/src/efsw/WatcherGeneric.cpp b/vendor/efsw/src/efsw/WatcherGeneric.cpp new file mode 100644 index 0000000..a6bb106 --- /dev/null +++ b/vendor/efsw/src/efsw/WatcherGeneric.cpp @@ -0,0 +1,33 @@ +#include +#include +#include + +namespace efsw { + +WatcherGeneric::WatcherGeneric( WatchID id, const std::string& directory, FileWatchListener* fwl, + FileWatcherImpl* fw, bool recursive ) : + Watcher( id, directory, fwl, recursive ), WatcherImpl( fw ), DirWatch( NULL ) { + FileSystem::dirAddSlashAtEnd( Directory ); + + DirWatch = new DirWatcherGeneric( NULL, this, directory, recursive, false ); + + DirWatch->addChilds( false ); +} + +WatcherGeneric::~WatcherGeneric() { + efSAFE_DELETE( DirWatch ); +} + +void WatcherGeneric::watch() { + DirWatch->watch(); +} + +void WatcherGeneric::watchDir( std::string dir ) { + DirWatch->watchDir( dir ); +} + +bool WatcherGeneric::pathInWatches( std::string path ) { + return DirWatch->pathInWatches( path ); +} + +} // namespace efsw diff --git a/vendor/efsw/src/efsw/WatcherGeneric.hpp b/vendor/efsw/src/efsw/WatcherGeneric.hpp new file mode 100644 index 0000000..d11ec20 --- /dev/null +++ b/vendor/efsw/src/efsw/WatcherGeneric.hpp @@ -0,0 +1,29 @@ +#ifndef EFSW_WATCHERGENERIC_HPP +#define EFSW_WATCHERGENERIC_HPP + +#include + +namespace efsw { + +class DirWatcherGeneric; + +class WatcherGeneric : public Watcher { + public: + FileWatcherImpl* WatcherImpl; + DirWatcherGeneric* DirWatch; + + WatcherGeneric( WatchID id, const std::string& directory, FileWatchListener* fwl, + FileWatcherImpl* fw, bool recursive ); + + ~WatcherGeneric(); + + void watch() override; + + void watchDir( std::string dir ); + + bool pathInWatches( std::string path ); +}; + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/WatcherInotify.cpp b/vendor/efsw/src/efsw/WatcherInotify.cpp new file mode 100644 index 0000000..812ddae --- /dev/null +++ b/vendor/efsw/src/efsw/WatcherInotify.cpp @@ -0,0 +1,21 @@ +#include + +namespace efsw { + +WatcherInotify::WatcherInotify() : Watcher(), Parent( NULL ) {} + +bool WatcherInotify::inParentTree( WatcherInotify* parent ) { + WatcherInotify* tNext = Parent; + + while ( NULL != tNext ) { + if ( tNext == parent ) { + return true; + } + + tNext = tNext->Parent; + } + + return false; +} + +} // namespace efsw diff --git a/vendor/efsw/src/efsw/WatcherInotify.hpp b/vendor/efsw/src/efsw/WatcherInotify.hpp new file mode 100644 index 0000000..d43935c --- /dev/null +++ b/vendor/efsw/src/efsw/WatcherInotify.hpp @@ -0,0 +1,23 @@ +#ifndef EFSW_WATCHERINOTIFY_HPP +#define EFSW_WATCHERINOTIFY_HPP + +#include +#include + +namespace efsw { + +class WatcherInotify : public Watcher { + public: + WatcherInotify(); + + bool inParentTree( WatcherInotify* parent ); + + WatcherInotify* Parent; + WatchID InotifyID; + + FileInfo DirInfo; +}; + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/WatcherKqueue.cpp b/vendor/efsw/src/efsw/WatcherKqueue.cpp new file mode 100644 index 0000000..424b989 --- /dev/null +++ b/vendor/efsw/src/efsw/WatcherKqueue.cpp @@ -0,0 +1,566 @@ +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_KQUEUE || EFSW_PLATFORM == EFSW_PLATFORM_FSEVENTS + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define KEVENT_RESERVE_VALUE ( 10 ) + +#ifndef O_EVTONLY +#define O_EVTONLY ( O_RDONLY | O_NONBLOCK ) +#endif + +namespace efsw { + +int comparator( const void* ke1, const void* ke2 ) { + const KEvent* kev1 = reinterpret_cast( ke1 ); + const KEvent* kev2 = reinterpret_cast( ke2 ); + + if ( NULL != kev2->udata ) { + FileInfo* fi1 = reinterpret_cast( kev1->udata ); + FileInfo* fi2 = reinterpret_cast( kev2->udata ); + + return strcmp( fi1->Filepath.c_str(), fi2->Filepath.c_str() ); + } + + return 1; +} + +WatcherKqueue::WatcherKqueue( WatchID watchid, const std::string& dirname, + FileWatchListener* listener, bool recursive, + FileWatcherKqueue* watcher, WatcherKqueue* parent ) : + Watcher( watchid, dirname, listener, recursive ), + mLastWatchID( 0 ), + mChangeListCount( 0 ), + mKqueue( kqueue() ), + mWatcher( watcher ), + mParent( parent ), + mInitOK( true ), + mErrno( 0 ) { + if ( -1 == mKqueue ) { + efDEBUG( + "kqueue() returned invalid descriptor for directory %s. File descriptors count: %ld\n", + Directory.c_str(), mWatcher->mFileDescriptorCount ); + + mInitOK = false; + mErrno = errno; + } else { + mWatcher->addFD(); + } +} + +WatcherKqueue::~WatcherKqueue() { + // Remove the childs watchers ( sub-folders watches ) + removeAll(); + + for ( size_t i = 0; i < mChangeListCount; i++ ) { + if ( NULL != mChangeList[i].udata ) { + FileInfo* fi = reinterpret_cast( mChangeList[i].udata ); + + efSAFE_DELETE( fi ); + } + } + + close( mKqueue ); + + mWatcher->removeFD(); +} + +void WatcherKqueue::addAll() { + if ( -1 == mKqueue ) { + return; + } + + // scan directory and call addFile(name, false) on each file + FileSystem::dirAddSlashAtEnd( Directory ); + + efDEBUG( "addAll(): Added folder: %s\n", Directory.c_str() ); + + // add base dir + int fd = open( Directory.c_str(), O_EVTONLY ); + + if ( -1 == fd ) { + efDEBUG( "addAll(): Couldn't open folder: %s\n", Directory.c_str() ); + + if ( EACCES != errno ) { + mInitOK = false; + } + + mErrno = errno; + + return; + } + + mDirSnap.setDirectoryInfo( Directory ); + mDirSnap.scan(); + + mChangeList.resize( KEVENT_RESERVE_VALUE ); + + // Creates the kevent for the folder + EV_SET( &mChangeList[0], fd, EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_ONESHOT, + NOTE_DELETE | NOTE_EXTEND | NOTE_WRITE | NOTE_ATTRIB | NOTE_RENAME, 0, 0 ); + + mWatcher->addFD(); + + // Get the files and directories from the directory + FileInfoMap files = FileSystem::filesInfoFromPath( Directory ); + + for ( FileInfoMap::iterator it = files.begin(); it != files.end(); it++ ) { + FileInfo& fi = it->second; + + if ( fi.isRegularFile() ) { + // Add the regular files kevent + addFile( fi.Filepath, false ); + } else if ( Recursive && fi.isDirectory() && fi.isReadable() ) { + // Create another watcher for the subfolders ( if recursive ) + WatchID id = addWatch( fi.Filepath, Listener, Recursive, this ); + + // If the watcher is not adding the watcher means that the directory was created + if ( id > 0 && !mWatcher->isAddingWatcher() ) { + handleFolderAction( fi.Filepath, Actions::Add ); + } + } + } +} + +void WatcherKqueue::removeAll() { + efDEBUG( "removeAll(): Removing all child watchers\n" ); + + std::vector erase; + + for ( WatchMap::iterator it = mWatches.begin(); it != mWatches.end(); it++ ) { + efDEBUG( "removeAll(): Removed child watcher %s\n", it->second->Directory.c_str() ); + + erase.push_back( it->second->ID ); + } + + for ( std::vector::iterator eit = erase.begin(); eit != erase.end(); eit++ ) { + removeWatch( *eit ); + } +} + +void WatcherKqueue::addFile( const std::string& name, bool emitEvents ) { + efDEBUG( "addFile(): Added: %s\n", name.c_str() ); + + // Open the file to get the file descriptor + int fd = open( name.c_str(), O_EVTONLY ); + + if ( fd == -1 ) { + efDEBUG( "addFile(): Could open file descriptor for %s. File descriptor count: %ld\n", + name.c_str(), mWatcher->mFileDescriptorCount ); + + Errors::Log::createLastError( Errors::FileNotReadable, name ); + + if ( EACCES != errno ) { + mInitOK = false; + } + + mErrno = errno; + + return; + } + + mWatcher->addFD(); + + // increase the file kevent file count + mChangeListCount++; + + if ( mChangeListCount + KEVENT_RESERVE_VALUE > mChangeList.size() && + mChangeListCount % KEVENT_RESERVE_VALUE == 0 ) { + size_t reserve_size = mChangeList.size() + KEVENT_RESERVE_VALUE; + mChangeList.resize( reserve_size ); + efDEBUG( "addFile(): Reserverd more KEvents space for %s, space reserved %ld, list actual " + "size %ld.\n", + Directory.c_str(), reserve_size, mChangeListCount ); + } + + // create entry + FileInfo* entry = new FileInfo( name ); + + // set the event data at the end of the list + EV_SET( &mChangeList[mChangeListCount], fd, EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_ONESHOT, + NOTE_DELETE | NOTE_EXTEND | NOTE_WRITE | NOTE_ATTRIB | NOTE_RENAME, 0, (void*)entry ); + + // qsort sort the list by name + qsort( &mChangeList[1], mChangeListCount, sizeof( KEvent ), comparator ); + + // handle action + if ( emitEvents ) { + handleAction( name, Actions::Add ); + } +} + +void WatcherKqueue::removeFile( const std::string& name, bool emitEvents ) { + efDEBUG( "removeFile(): Trying to remove file: %s\n", name.c_str() ); + + // bsearch + KEvent target; + + // Create a temporary file info to search the kevent ( searching the directory ) + FileInfo tempEntry( name ); + + target.udata = &tempEntry; + + // Search the kevent + KEvent* ke = (KEvent*)bsearch( &target, &mChangeList[0], mChangeListCount + 1, sizeof( KEvent ), + comparator ); + + // Trying to remove a non-existing file? + if ( !ke ) { + Errors::Log::createLastError( Errors::FileNotFound, name ); + efDEBUG( "File not removed\n" ); + return; + } + + efDEBUG( "File removed\n" ); + + // handle action + if ( emitEvents ) { + handleAction( name, Actions::Delete ); + } + + // Delete the user data ( FileInfo ) from the kevent closed + FileInfo* del = reinterpret_cast( ke->udata ); + + efSAFE_DELETE( del ); + + // close the file descriptor from the kevent + close( ke->ident ); + + mWatcher->removeFD(); + + memset( ke, 0, sizeof( KEvent ) ); + + // move end to current + memcpy( ke, &mChangeList[mChangeListCount], sizeof( KEvent ) ); + memset( &mChangeList[mChangeListCount], 0, sizeof( KEvent ) ); + --mChangeListCount; +} + +void WatcherKqueue::rescan() { + efDEBUG( "rescan(): Rescanning: %s\n", Directory.c_str() ); + + DirectorySnapshotDiff Diff = mDirSnap.scan(); + + if ( Diff.DirChanged ) { + sendDirChanged(); + } + + if ( Diff.changed() ) { + FileInfoList::iterator it; + MovedList::iterator mit; + + /// Files + DiffIterator( FilesCreated ) { + addFile( ( *it ).Filepath ); + } + + DiffIterator( FilesModified ) { + handleAction( ( *it ).Filepath, Actions::Modified ); + } + + DiffIterator( FilesDeleted ) { + removeFile( ( *it ).Filepath ); + } + + DiffMovedIterator( FilesMoved ) { + handleAction( ( *mit ).second.Filepath, Actions::Moved, ( *mit ).first ); + removeFile( Directory + ( *mit ).first, false ); + addFile( ( *mit ).second.Filepath, false ); + } + + /// Directories + DiffIterator( DirsCreated ) { + handleFolderAction( ( *it ).Filepath, Actions::Add ); + addWatch( ( *it ).Filepath, Listener, Recursive, this ); + } + + DiffIterator( DirsModified ) { + handleFolderAction( ( *it ).Filepath, Actions::Modified ); + } + + DiffIterator( DirsDeleted ) { + handleFolderAction( ( *it ).Filepath, Actions::Delete ); + + Watcher* watch = findWatcher( ( *it ).Filepath ); + + if ( NULL != watch ) { + removeWatch( watch->ID ); + } + } + + DiffMovedIterator( DirsMoved ) { + moveDirectory( Directory + ( *mit ).first, ( *mit ).second.Filepath ); + } + } +} + +WatchID WatcherKqueue::watchingDirectory( std::string dir ) { + Watcher* watch = findWatcher( dir ); + + if ( NULL != watch ) { + return watch->ID; + } + + return Errors::FileNotFound; +} + +void WatcherKqueue::handleAction( const std::string& filename, efsw::Action action, + const std::string& oldFilename ) { + Listener->handleFileAction( ID, Directory, FileSystem::fileNameFromPath( filename ), action, + FileSystem::fileNameFromPath( oldFilename ) ); +} + +void WatcherKqueue::handleFolderAction( std::string filename, efsw::Action action, + const std::string& oldFilename ) { + FileSystem::dirRemoveSlashAtEnd( filename ); + + handleAction( filename, action, oldFilename ); +} + +void WatcherKqueue::sendDirChanged() { + if ( NULL != mParent ) { + Listener->handleFileAction( mParent->ID, mParent->Directory, + FileSystem::fileNameFromPath( Directory ), Actions::Modified ); + } +} + +void WatcherKqueue::watch() { + if ( -1 == mKqueue ) { + return; + } + + int nev = 0; + KEvent event; + + // First iterate the childs, to get the events from the deepest folder, to the watcher childs + for ( WatchMap::iterator it = mWatches.begin(); it != mWatches.end(); ++it ) { + it->second->watch(); + } + + bool needScan = false; + + // Then we get the the events of the current folder + while ( !mChangeList.empty() && + ( nev = kevent( mKqueue, mChangeList.data(), mChangeListCount + 1, &event, 1, + &mWatcher->mTimeOut ) ) != 0 ) { + // An error ocurred? + if ( nev == -1 ) { + efDEBUG( "watch(): Error on directory %s\n", Directory.c_str() ); + perror( "kevent" ); + break; + } else { + FileInfo* entry = NULL; + + // If udate == NULL means that it is the fisrt element of the change list, the folder. + // otherwise it is an event of some file inside the folder + if ( ( entry = reinterpret_cast( event.udata ) ) != NULL ) { + efDEBUG( "watch(): File: %s ", entry->Filepath.c_str() ); + + // If the event flag is delete... the file was deleted + if ( event.fflags & NOTE_DELETE ) { + efDEBUG( "deleted\n" ); + + mDirSnap.removeFile( entry->Filepath ); + + removeFile( entry->Filepath ); + } else if ( event.fflags & NOTE_EXTEND || event.fflags & NOTE_WRITE || + event.fflags & NOTE_ATTRIB ) { + // The file was modified + efDEBUG( "modified\n" ); + + FileInfo fi( entry->Filepath ); + + if ( fi != *entry ) { + *entry = fi; + + mDirSnap.updateFile( entry->Filepath ); + + handleAction( entry->Filepath, efsw::Actions::Modified ); + } + } else if ( event.fflags & NOTE_RENAME ) { + efDEBUG( "moved\n" ); + + needScan = true; + } + } else { + needScan = true; + } + } + } + + if ( needScan ) { + rescan(); + } +} + +Watcher* WatcherKqueue::findWatcher( const std::string path ) { + WatchMap::iterator it = mWatches.begin(); + + for ( ; it != mWatches.end(); it++ ) { + if ( it->second->Directory == path ) { + return it->second; + } + } + + return NULL; +} + +void WatcherKqueue::moveDirectory( std::string oldPath, std::string newPath, bool emitEvents ) { + // Update the directory path if it's a watcher + std::string opath2( oldPath ); + FileSystem::dirAddSlashAtEnd( opath2 ); + + Watcher* watch = findWatcher( opath2 ); + + if ( NULL != watch ) { + watch->Directory = opath2; + } + + if ( emitEvents ) { + handleFolderAction( newPath, efsw::Actions::Moved, oldPath ); + } +} + +WatchID WatcherKqueue::addWatch( const std::string& directory, FileWatchListener* watcher, + bool recursive, WatcherKqueue* parent ) { + static bool s_ug = false; + + std::string dir( directory ); + + FileSystem::dirAddSlashAtEnd( dir ); + + // This should never happen here + if ( !FileSystem::isDirectory( dir ) ) { + return Errors::Log::createLastError( Errors::FileNotFound, dir ); + } else if ( pathInWatches( dir ) || pathInParent( dir ) ) { + return Errors::Log::createLastError( Errors::FileRepeated, directory ); + } else if ( NULL != parent && FileSystem::isRemoteFS( dir ) ) { + return Errors::Log::createLastError( Errors::FileRemote, dir ); + } + + std::string curPath; + std::string link( FileSystem::getLinkRealPath( dir, curPath ) ); + + if ( "" != link ) { + /// Avoid adding symlinks directories if it's now enabled + if ( NULL != parent && !mWatcher->mFileWatcher->followSymlinks() ) { + return Errors::Log::createLastError( Errors::FileOutOfScope, dir ); + } + + if ( pathInWatches( link ) || pathInParent( link ) ) { + return Errors::Log::createLastError( Errors::FileRepeated, link ); + } else if ( !mWatcher->linkAllowed( curPath, link ) ) { + return Errors::Log::createLastError( Errors::FileOutOfScope, link ); + } else { + dir = link; + } + } + + if ( mWatcher->availablesFD() ) { + WatcherKqueue* watch = + new WatcherKqueue( ++mLastWatchID, dir, watcher, recursive, mWatcher, parent ); + + mWatches.insert( std::make_pair( mLastWatchID, watch ) ); + + watch->addAll(); + + // if failed to open the directory... erase the watcher + if ( !watch->initOK() ) { + int le = watch->lastErrno(); + + mWatches.erase( watch->ID ); + + efSAFE_DELETE( watch ); + + mLastWatchID--; + + // Probably the folder has too many files, create a generic watcher + if ( EACCES != le ) { + WatcherGeneric* watch = + new WatcherGeneric( ++mLastWatchID, dir, watcher, mWatcher, recursive ); + + mWatches.insert( std::make_pair( mLastWatchID, watch ) ); + } else { + return Errors::Log::createLastError( Errors::Unspecified, link ); + } + } + } else { + if ( !s_ug ) { + efDEBUG( "Started using WatcherGeneric, reached file descriptors limit: %ld.\n", + mWatcher->mFileDescriptorCount ); + s_ug = true; + } + + WatcherGeneric* watch = + new WatcherGeneric( ++mLastWatchID, dir, watcher, mWatcher, recursive ); + + mWatches.insert( std::make_pair( mLastWatchID, watch ) ); + } + + return mLastWatchID; +} + +bool WatcherKqueue::initOK() { + return mInitOK; +} + +void WatcherKqueue::removeWatch( WatchID watchid ) { + WatchMap::iterator iter = mWatches.find( watchid ); + + if ( iter == mWatches.end() ) + return; + + Watcher* watch = iter->second; + + mWatches.erase( iter ); + + efSAFE_DELETE( watch ); +} + +bool WatcherKqueue::pathInWatches( const std::string& path ) { + return NULL != findWatcher( path ); +} + +bool WatcherKqueue::pathInParent( const std::string& path ) { + WatcherKqueue* pNext = mParent; + + while ( NULL != pNext ) { + if ( pNext->pathInWatches( path ) ) { + return true; + } + + pNext = pNext->mParent; + } + + if ( mWatcher->pathInWatches( path ) ) { + return true; + } + + if ( path == Directory ) { + return true; + } + + return false; +} + +int WatcherKqueue::lastErrno() { + return mErrno; +} + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/WatcherKqueue.hpp b/vendor/efsw/src/efsw/WatcherKqueue.hpp new file mode 100644 index 0000000..75c0f62 --- /dev/null +++ b/vendor/efsw/src/efsw/WatcherKqueue.hpp @@ -0,0 +1,97 @@ +#ifndef EFSW_WATCHEROSX_HPP +#define EFSW_WATCHEROSX_HPP + +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_KQUEUE || EFSW_PLATFORM == EFSW_PLATFORM_FSEVENTS + +#include +#include +#include +#include +#include + +namespace efsw { + +class FileWatcherKqueue; +class WatcherKqueue; + +typedef struct kevent KEvent; + +/// type for a map from WatchID to WatcherKqueue pointer +typedef std::map WatchMap; + +class WatcherKqueue : public Watcher { + public: + WatcherKqueue( WatchID watchid, const std::string& dirname, FileWatchListener* listener, + bool recursive, FileWatcherKqueue* watcher, WatcherKqueue* parent = NULL ); + + virtual ~WatcherKqueue(); + + void addFile( const std::string& name, bool emitEvents = true ); + + void removeFile( const std::string& name, bool emitEvents = true ); + + // called when the directory is actually changed + // means a file has been added or removed + // rescans the watched directory adding/removing files and sending notices + void rescan(); + + void handleAction( const std::string& filename, efsw::Action action, + const std::string& oldFilename = "" ); + + void handleFolderAction( std::string filename, efsw::Action action, + const std::string& oldFilename = "" ); + + void addAll(); + + void removeAll(); + + WatchID watchingDirectory( std::string dir ); + + void watch() override; + + WatchID addWatch( const std::string& directory, FileWatchListener* watcher, bool recursive, + WatcherKqueue* parent ); + + void removeWatch( WatchID watchid ); + + bool initOK(); + + int lastErrno(); + + protected: + WatchMap mWatches; + int mLastWatchID; + + // index 0 is always the directory + std::vector mChangeList; + size_t mChangeListCount; + DirectorySnapshot mDirSnap; + + /// The descriptor for the kqueue + int mKqueue; + + FileWatcherKqueue* mWatcher; + + WatcherKqueue* mParent; + + bool mInitOK; + int mErrno; + + bool pathInWatches( const std::string& path ); + + bool pathInParent( const std::string& path ); + + Watcher* findWatcher( const std::string path ); + + void moveDirectory( std::string oldPath, std::string newPath, bool emitEvents = true ); + + void sendDirChanged(); +}; + +} // namespace efsw + +#endif + +#endif diff --git a/vendor/efsw/src/efsw/WatcherWin32.cpp b/vendor/efsw/src/efsw/WatcherWin32.cpp new file mode 100644 index 0000000..712419e --- /dev/null +++ b/vendor/efsw/src/efsw/WatcherWin32.cpp @@ -0,0 +1,263 @@ +#include +#include +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_WIN32 + +#include + +namespace efsw { + +struct EFSW_FILE_NOTIFY_EXTENDED_INFORMATION_EX { + DWORD NextEntryOffset; + DWORD Action; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastModificationTime; + LARGE_INTEGER LastChangeTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER AllocatedLength; + LARGE_INTEGER FileSize; + DWORD FileAttributes; + DWORD ReparsePointTag; + LARGE_INTEGER FileId; + LARGE_INTEGER ParentFileId; + DWORD FileNameLength; + WCHAR FileName[1]; +}; + +typedef EFSW_FILE_NOTIFY_EXTENDED_INFORMATION_EX* EFSW_PFILE_NOTIFY_EXTENDED_INFORMATION_EX; + +typedef BOOL( WINAPI* EFSW_LPREADDIRECTORYCHANGESEXW )( HANDLE hDirectory, LPVOID lpBuffer, + DWORD nBufferLength, BOOL bWatchSubtree, + DWORD dwNotifyFilter, LPDWORD lpBytesReturned, + LPOVERLAPPED lpOverlapped, LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, + DWORD ReadDirectoryNotifyInformationClass ); + +static EFSW_LPREADDIRECTORYCHANGESEXW pReadDirectoryChangesExW = NULL; + +#define EFSW_ReadDirectoryNotifyExtendedInformation 2 + +static void initReadDirectoryChangesEx() { + static bool hasInit = false; + if ( !hasInit ) { + hasInit = true; + + HMODULE hModule = GetModuleHandleW( L"Kernel32.dll" ); + if ( !hModule ) + return; + + pReadDirectoryChangesExW = + (EFSW_LPREADDIRECTORYCHANGESEXW)GetProcAddress( hModule, "ReadDirectoryChangesExW" ); + } +} + +void WatchCallbackOld( WatcherWin32* pWatch ) { + PFILE_NOTIFY_INFORMATION pNotify; + size_t offset = 0; + do { + bool skip = false; + + pNotify = (PFILE_NOTIFY_INFORMATION)&pWatch->Buffer[offset]; + offset += pNotify->NextEntryOffset; + int count = + WideCharToMultiByte( CP_UTF8, 0, pNotify->FileName, + pNotify->FileNameLength / sizeof( WCHAR ), NULL, 0, NULL, NULL ); + if ( count == 0 ) + continue; + + std::string nfile( count, '\0' ); + + count = WideCharToMultiByte( CP_UTF8, 0, pNotify->FileName, + pNotify->FileNameLength / sizeof( WCHAR ), &nfile[0], count, + NULL, NULL ); + + if ( FILE_ACTION_MODIFIED == pNotify->Action ) { + FileInfo fifile( std::string( pWatch->DirName ) + nfile ); + + if ( pWatch->LastModifiedEvent.file.ModificationTime == fifile.ModificationTime && + pWatch->LastModifiedEvent.file.Size == fifile.Size && + pWatch->LastModifiedEvent.fileName == nfile ) { + skip = true; + } + + pWatch->LastModifiedEvent.fileName = nfile; + pWatch->LastModifiedEvent.file = fifile; + } + + if ( !skip ) { + pWatch->Watch->handleAction( pWatch, nfile, pNotify->Action ); + } + } while ( pNotify->NextEntryOffset != 0 ); +} + +void WatchCallbackEx( WatcherWin32* pWatch ) { + EFSW_PFILE_NOTIFY_EXTENDED_INFORMATION_EX pNotify; + size_t offset = 0; + do { + bool skip = false; + + pNotify = (EFSW_PFILE_NOTIFY_EXTENDED_INFORMATION_EX)&pWatch->Buffer[offset]; + offset += pNotify->NextEntryOffset; + int count = + WideCharToMultiByte( CP_UTF8, 0, pNotify->FileName, + pNotify->FileNameLength / sizeof( WCHAR ), NULL, 0, NULL, NULL ); + if ( count == 0 ) + continue; + + std::string nfile( count, '\0' ); + + count = WideCharToMultiByte( CP_UTF8, 0, pNotify->FileName, + pNotify->FileNameLength / sizeof( WCHAR ), &nfile[0], count, + NULL, NULL ); + + if ( FILE_ACTION_MODIFIED == pNotify->Action ) { + FileInfo fifile( std::string( pWatch->DirName ) + nfile ); + + if ( pWatch->LastModifiedEvent.file.ModificationTime == fifile.ModificationTime && + pWatch->LastModifiedEvent.file.Size == fifile.Size && + pWatch->LastModifiedEvent.fileName == nfile ) { + skip = true; + } + + pWatch->LastModifiedEvent.fileName = nfile; + pWatch->LastModifiedEvent.file = fifile; + } else if ( FILE_ACTION_RENAMED_OLD_NAME == pNotify->Action ) { + pWatch->OldFiles.emplace_back( nfile, pNotify->FileId ); + skip = true; + } else if ( FILE_ACTION_RENAMED_NEW_NAME == pNotify->Action ) { + std::string oldFile; + LARGE_INTEGER oldFileId{}; + + for ( auto it = pWatch->OldFiles.begin(); it != pWatch->OldFiles.end(); ++it ) { + if ( it->second.QuadPart == pNotify->FileId.QuadPart ) { + oldFile = it->first; + oldFileId = it->second; + it = pWatch->OldFiles.erase( it ); + break; + } + } + + if ( oldFile.empty() ) { + pWatch->Watch->handleAction( pWatch, nfile, FILE_ACTION_ADDED ); + skip = true; + } else { + pWatch->Watch->handleAction( pWatch, oldFile, FILE_ACTION_RENAMED_OLD_NAME ); + } + } + + if ( !skip ) { + pWatch->Watch->handleAction( pWatch, nfile, pNotify->Action ); + } + } while ( pNotify->NextEntryOffset != 0 ); +} + +/// Unpacks events and passes them to a user defined callback. +void CALLBACK WatchCallback( DWORD dwNumberOfBytesTransfered, LPOVERLAPPED lpOverlapped ) { + if ( NULL == lpOverlapped ) { + return; + } + + WatcherStructWin32* tWatch = (WatcherStructWin32*)lpOverlapped; + WatcherWin32* pWatch = tWatch->Watch; + + if ( dwNumberOfBytesTransfered == 0 ) { + if ( nullptr != pWatch && !pWatch->StopNow ) { + RefreshWatch( tWatch ); + } else { + return; + } + } + + // Fork watch depending on the Windows API supported + if ( pWatch->Extended ) { + WatchCallbackEx( pWatch ); + } else { + WatchCallbackOld( pWatch ); + } + + if ( !pWatch->StopNow ) { + RefreshWatch( tWatch ); + } +} + +/// Refreshes the directory monitoring. +RefreshResult RefreshWatch( WatcherStructWin32* pWatch ) { + initReadDirectoryChangesEx(); + + bool bRet = false; + RefreshResult ret = RefreshResult::Failed; + pWatch->Watch->Extended = false; + + if ( pReadDirectoryChangesExW ) { + bRet = pReadDirectoryChangesExW( pWatch->Watch->DirHandle, pWatch->Watch->Buffer.data(), + (DWORD)pWatch->Watch->Buffer.size(), pWatch->Watch->Recursive, + pWatch->Watch->NotifyFilter, NULL, &pWatch->Overlapped, + NULL, EFSW_ReadDirectoryNotifyExtendedInformation ) != 0; + if ( bRet ) { + ret = RefreshResult::SucessEx; + pWatch->Watch->Extended = true; + } + } + + if ( !bRet ) { + bRet = ReadDirectoryChangesW( pWatch->Watch->DirHandle, pWatch->Watch->Buffer.data(), + (DWORD)pWatch->Watch->Buffer.size(), pWatch->Watch->Recursive, + pWatch->Watch->NotifyFilter, NULL, &pWatch->Overlapped, + NULL ) != 0; + + if ( bRet ) + ret = RefreshResult::Success; + } + + if ( !bRet ) { + std::string error = std::to_string( GetLastError() ); + Errors::Log::createLastError( Errors::WatcherFailed, error ); + } + + return ret; +} + +/// Stops monitoring a directory. +void DestroyWatch( WatcherStructWin32* pWatch ) { + if ( pWatch ) { + WatcherWin32* tWatch = pWatch->Watch; + tWatch->StopNow = true; + CancelIoEx( pWatch->Watch->DirHandle, &pWatch->Overlapped ); + CloseHandle( pWatch->Watch->DirHandle ); + efSAFE_DELETE_ARRAY( pWatch->Watch->DirName ); + efSAFE_DELETE( pWatch->Watch ); + efSAFE_DELETE( pWatch ); + } +} + +/// Starts monitoring a directory. +WatcherStructWin32* CreateWatch( LPCWSTR szDirectory, bool recursive, + DWORD bufferSize, DWORD notifyFilter, HANDLE iocp ) { + WatcherStructWin32* tWatch = new WatcherStructWin32(); + WatcherWin32* pWatch = new WatcherWin32(bufferSize); + if (tWatch) + tWatch->Watch = pWatch; + + pWatch->DirHandle = CreateFileW( + szDirectory, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL ); + + if ( pWatch->DirHandle != INVALID_HANDLE_VALUE && + CreateIoCompletionPort( pWatch->DirHandle, iocp, 0, 1 ) ) { + pWatch->NotifyFilter = notifyFilter; + pWatch->Recursive = recursive; + + if ( RefreshResult::Failed != RefreshWatch( tWatch ) ) { + return tWatch; + } + } + + CloseHandle( pWatch->DirHandle ); + efSAFE_DELETE( pWatch->Watch ); + efSAFE_DELETE( tWatch ); + return NULL; +} + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/WatcherWin32.hpp b/vendor/efsw/src/efsw/WatcherWin32.hpp new file mode 100644 index 0000000..ea1e8e4 --- /dev/null +++ b/vendor/efsw/src/efsw/WatcherWin32.hpp @@ -0,0 +1,79 @@ +#ifndef EFSW_WATCHERWIN32_HPP +#define EFSW_WATCHERWIN32_HPP + +#include +#include +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_WIN32 + +#include + +#ifdef EFSW_COMPILER_MSVC +#pragma comment( lib, "comctl32.lib" ) +#pragma comment( lib, "user32.lib" ) +#pragma comment( lib, "ole32.lib" ) + +// disable secure warnings +#pragma warning( disable : 4996 ) +#endif + +namespace efsw { + +class WatcherWin32; + +enum RefreshResult { Failed, Success, SucessEx }; + +/// Internal watch data +struct WatcherStructWin32 { + OVERLAPPED Overlapped; + WatcherWin32* Watch; +}; + +struct sLastModifiedEvent { + FileInfo file; + std::string fileName; +}; + +RefreshResult RefreshWatch( WatcherStructWin32* pWatch ); + +void CALLBACK WatchCallback( DWORD dwNumberOfBytesTransfered, LPOVERLAPPED lpOverlapped ); + +void DestroyWatch( WatcherStructWin32* pWatch ); + +WatcherStructWin32* CreateWatch( LPCWSTR szDirectory, bool recursive, + DWORD bufferSize, DWORD notifyFilter, HANDLE iocp ); + +class WatcherWin32 : public Watcher { + public: + WatcherWin32(DWORD dwBufferSize) : + Struct( NULL ), + DirHandle( NULL ), + Buffer(), + lParam( 0 ), + NotifyFilter( 0 ), + StopNow( false ), + Extended( false ), + Watch( NULL ), + DirName( NULL ) { + Buffer.resize(dwBufferSize); + } + + WatcherStructWin32* Struct; + HANDLE DirHandle; + std::vector Buffer; + LPARAM lParam; + DWORD NotifyFilter; + bool StopNow; + bool Extended; + FileWatcherImpl* Watch; + char* DirName; + sLastModifiedEvent LastModifiedEvent; + std::vector> OldFiles; +}; + +} // namespace efsw + +#endif + +#endif diff --git a/vendor/efsw/src/efsw/base.hpp b/vendor/efsw/src/efsw/base.hpp new file mode 100644 index 0000000..43abc4f --- /dev/null +++ b/vendor/efsw/src/efsw/base.hpp @@ -0,0 +1,129 @@ +#ifndef EFSW_BASE +#define EFSW_BASE + +#include +#include + +namespace efsw { + +typedef SOPHIST_int8 Int8; +typedef SOPHIST_uint8 Uint8; +typedef SOPHIST_int16 Int16; +typedef SOPHIST_uint16 Uint16; +typedef SOPHIST_int32 Int32; +typedef SOPHIST_uint32 Uint32; +typedef SOPHIST_int64 Int64; +typedef SOPHIST_uint64 Uint64; + +#define EFSW_OS_WIN 1 +#define EFSW_OS_LINUX 2 +#define EFSW_OS_MACOSX 3 +#define EFSW_OS_BSD 4 +#define EFSW_OS_SOLARIS 5 +#define EFSW_OS_HAIKU 6 +#define EFSW_OS_ANDROID 7 +#define EFSW_OS_IOS 8 + +#define EFSW_PLATFORM_WIN32 1 +#define EFSW_PLATFORM_INOTIFY 2 +#define EFSW_PLATFORM_KQUEUE 3 +#define EFSW_PLATFORM_FSEVENTS 4 +#define EFSW_PLATFORM_GENERIC 5 + +#if defined( _WIN32 ) +/// Any Windows platform +#define EFSW_OS EFSW_OS_WIN +#define EFSW_PLATFORM EFSW_PLATFORM_WIN32 + +#if ( defined( _MSCVER ) || defined( _MSC_VER ) ) +#define EFSW_COMPILER_MSVC +#endif + +/// Force windows target version above or equal to Windows Server 2008 or Windows Vista +#if _WIN32_WINNT < 0x600 +#undef _WIN32_WINNT +#define _WIN32_WINNT 0x600 +#endif +#elif defined( __FreeBSD__ ) || defined( __OpenBSD__ ) || defined( __NetBSD__ ) || \ + defined( __DragonFly__ ) +#define EFSW_OS EFSW_OS_BSD +#define EFSW_PLATFORM EFSW_PLATFORM_KQUEUE + +#elif defined( __APPLE_CC__ ) || defined( __APPLE__ ) +#include + +#if defined( __IPHONE__ ) || ( defined( TARGET_OS_IPHONE ) && TARGET_OS_IPHONE ) || \ + ( defined( TARGET_IPHONE_SIMULATOR ) && TARGET_IPHONE_SIMULATOR ) +#define EFSW_OS EFSW_OS_IOS +#define EFSW_PLATFORM EFSW_PLATFORM_KQUEUE +#else +#define EFSW_OS EFSW_OS_MACOSX + +#if defined( EFSW_FSEVENTS_NOT_SUPPORTED ) +#define EFSW_PLATFORM EFSW_PLATFORM_KQUEUE +#else +#define EFSW_PLATFORM EFSW_PLATFORM_FSEVENTS +#endif +#endif + +#elif defined( __linux__ ) +/// This includes Linux and Android +#ifndef EFSW_KQUEUE +#define EFSW_PLATFORM EFSW_PLATFORM_INOTIFY +#else +/// This is for testing libkqueue, sadly it doesnt work +#define EFSW_PLATFORM EFSW_PLATFORM_KQUEUE +#endif + +#if defined( __ANDROID__ ) || defined( ANDROID ) +#define EFSW_OS EFSW_OS_ANDROID +#else +#define EFSW_OS EFSW_OS_LINUX +#endif + +#else +#if defined( __SVR4 ) +#define EFSW_OS EFSW_OS_SOLARIS +#elif defined( __HAIKU__ ) || defined( __BEOS__ ) +#define EFSW_OS EFSW_OS_HAIKU +#endif + +/// Everything else +#define EFSW_PLATFORM EFSW_PLATFORM_GENERIC +#endif + +#if EFSW_PLATFORM != EFSW_PLATFORM_WIN32 +#define EFSW_PLATFORM_POSIX +#endif + +#if 1 == SOPHIST_pointer64 +#define EFSW_64BIT +#else +#define EFSW_32BIT +#endif + +#if defined( arm ) || defined( __arm__ ) +#define EFSW_ARM +#endif + +#define efCOMMA , + +#define efSAFE_DELETE( p ) \ + { \ + if ( p ) { \ + delete ( p ); \ + ( p ) = NULL; \ + } \ + } +#define efSAFE_DELETE_ARRAY( p ) \ + { \ + if ( p ) { \ + delete[] ( p ); \ + ( p ) = NULL; \ + } \ + } +#define efARRAY_SIZE( __array ) ( sizeof( __array ) / sizeof( __array[0] ) ) + +} // namespace efsw + +#endif diff --git a/vendor/efsw/src/efsw/inotify-nosys.h b/vendor/efsw/src/efsw/inotify-nosys.h new file mode 100644 index 0000000..be1e627 --- /dev/null +++ b/vendor/efsw/src/efsw/inotify-nosys.h @@ -0,0 +1,164 @@ +#ifndef _LINUX_INOTIFY_H +#define _LINUX_INOTIFY_H + +#include +#include +#include + +/* + * struct inotify_event - structure read from the inotify device for each event + * + * When you are watching a directory, you will receive the filename for events + * such as IN_CREATE, IN_DELETE, IN_OPEN, IN_CLOSE, ..., relative to the wd. + */ +struct inotify_event { + int wd; /* watch descriptor */ + uint32_t mask; /* watch mask */ + uint32_t cookie; /* cookie to synchronize two events */ + uint32_t len; /* length (including nulls) of name */ + char name __flexarr; /* stub for possible name */ +}; + +/* the following are legal, implemented events that user-space can watch for */ +#define IN_ACCESS 0x00000001 /* File was accessed */ +#define IN_MODIFY 0x00000002 /* File was modified */ +#define IN_ATTRIB 0x00000004 /* Metadata changed */ +#define IN_CLOSE_WRITE 0x00000008 /* Writtable file was closed */ +#define IN_CLOSE_NOWRITE 0x00000010 /* Unwrittable file closed */ +#define IN_OPEN 0x00000020 /* File was opened */ +#define IN_MOVED_FROM 0x00000040 /* File was moved from X */ +#define IN_MOVED_TO 0x00000080 /* File was moved to Y */ +#define IN_CREATE 0x00000100 /* Subfile was created */ +#define IN_DELETE 0x00000200 /* Subfile was deleted */ +#define IN_DELETE_SELF 0x00000400 /* Self was deleted */ +#define IN_MOVE_SELF 0x00000800 /* Self was moved */ + +/* the following are legal events. they are sent as needed to any watch */ +#define IN_UNMOUNT 0x00002000 /* Backing fs was unmounted */ +#define IN_Q_OVERFLOW 0x00004000 /* Event queued overflowed */ +#define IN_IGNORED 0x00008000 /* File was ignored */ + +/* helper events */ +#define IN_CLOSE (IN_CLOSE_WRITE | IN_CLOSE_NOWRITE) /* close */ +#define IN_MOVE (IN_MOVED_FROM | IN_MOVED_TO) /* moves */ + +/* special flags */ +#define IN_ONLYDIR 0x01000000 /* only watch the path if it is a directory */ +#define IN_DONT_FOLLOW 0x02000000 /* don't follow a sym link */ +#define IN_MASK_ADD 0x20000000 /* add to the mask of an already existing watch */ +#define IN_ISDIR 0x40000000 /* event occurred against dir */ +#define IN_ONESHOT 0x80000000 /* only send event once */ + +/* + * All of the events - we build the list by hand so that we can add flags in + * the future and not break backward compatibility. Apps will get only the + * events that they originally wanted. Be sure to add new events here! + */ +#define IN_ALL_EVENTS (IN_ACCESS | IN_MODIFY | IN_ATTRIB | IN_CLOSE_WRITE | \ + IN_CLOSE_NOWRITE | IN_OPEN | IN_MOVED_FROM | \ + IN_MOVED_TO | IN_DELETE | IN_CREATE | IN_DELETE_SELF | \ + IN_MOVE_SELF) + +#if defined (__alpha__) +# define __NR_inotify_init 444 +# define __NR_inotify_add_watch 445 +# define __NR_inotify_rm_watch 446 + +#elif defined (__arm__) +# define __NR_inotify_init (__NR_SYSCALL_BASE+316) +# define __NR_inotify_add_watch (__NR_SYSCALL_BASE+317) +# define __NR_inotify_rm_watch (__NR_SYSCALL_BASE+318) + +#elif defined (__aarch64__) +# define __NR_inotify_init 1043 +# define __NR_inotify_add_watch 27 +# define __NR_inotify_rm_watch 28 + +#elif defined (__frv__) +# define __NR_inotify_init 291 +# define __NR_inotify_add_watch 292 +# define __NR_inotify_rm_watch 293 + +#elif defined(__i386__) +# define __NR_inotify_init 291 +# define __NR_inotify_add_watch 292 +# define __NR_inotify_rm_watch 293 + +#elif defined (__ia64__) +# define __NR_inotify_init 1277 +# define __NR_inotify_add_watch 1278 +# define __NR_inotify_rm_watch 1279 + +#elif defined (__mips__) +# if _MIPS_SIM == _MIPS_SIM_ABI32 +# define __NR_inotify_init (__NR_Linux + 284) +# define __NR_inotify_add_watch (__NR_Linux + 285) +# define __NR_inotify_rm_watch (__NR_Linux + 286) +# endif +# if _MIPS_SIM == _MIPS_SIM_ABI64 +# define __NR_inotify_init (__NR_Linux + 243) +# define __NR_inotify_add_watch (__NR_Linux + 243) +# define __NR_inotify_rm_watch (__NR_Linux + 243) +# endif +# if _MIPS_SIM == _MIPS_SIM_NABI32 +# define __NR_inotify_init (__NR_Linux + 247) +# define __NR_inotify_add_watch (__NR_Linux + 248) +# define __NR_inotify_rm_watch (__NR_Linux + 249) +# endif + +#elif defined(__parisc__) +# define __NR_inotify_init (__NR_Linux + 269) +# define __NR_inotify_add_watch (__NR_Linux + 270) +# define __NR_inotify_rm_watch (__NR_Linux + 271) + +#elif defined(__powerpc__) || defined(__powerpc64__) +# define __NR_inotify_init 275 +# define __NR_inotify_add_watch 276 +# define __NR_inotify_rm_watch 277 + +#elif defined (__s390__) +# define __NR_inotify_init 284 +# define __NR_inotify_add_watch 285 +# define __NR_inotify_rm_watch 286 + +#elif defined (__sh__) +# define __NR_inotify_init 290 +# define __NR_inotify_add_watch 291 +# define __NR_inotify_rm_watch 292 + +#elif defined (__sh64__) +# define __NR_inotify_init 318 +# define __NR_inotify_add_watch 319 +# define __NR_inotify_rm_watch 320 + +#elif defined (__sparc__) || defined (__sparc64__) +# define __NR_inotify_init 151 +# define __NR_inotify_add_watch 152 +# define __NR_inotify_rm_watch 156 + +#elif defined(__x86_64__) +# define __NR_inotify_init 253 +# define __NR_inotify_add_watch 254 +# define __NR_inotify_rm_watch 255 + +#else +# error "Unsupported architecture!" +#endif + +static inline int inotify_init (void) +{ + return syscall (__NR_inotify_init); +} + +static inline int inotify_add_watch (int fd, const char *name, uint32_t mask) +{ + return syscall (__NR_inotify_add_watch, fd, name, mask); +} + +static inline int inotify_rm_watch (int fd, uint32_t wd) +{ + return syscall (__NR_inotify_rm_watch, fd, wd); +} + + +#endif /* _LINUX_INOTIFY_H */ diff --git a/vendor/efsw/src/efsw/platform/platformimpl.hpp b/vendor/efsw/src/efsw/platform/platformimpl.hpp new file mode 100644 index 0000000..5442580 --- /dev/null +++ b/vendor/efsw/src/efsw/platform/platformimpl.hpp @@ -0,0 +1,20 @@ +#ifndef EFSW_PLATFORMIMPL_HPP +#define EFSW_PLATFORMIMPL_HPP + +#include + +#if defined( EFSW_PLATFORM_POSIX ) +#include +#include +#include +#include +#elif EFSW_PLATFORM == EFSW_PLATFORM_WIN32 +#include +#include +#include +#include +#else +#error Thread, Mutex, and System not implemented for this platform. +#endif + +#endif diff --git a/vendor/efsw/src/efsw/platform/posix/FileSystemImpl.cpp b/vendor/efsw/src/efsw/platform/posix/FileSystemImpl.cpp new file mode 100644 index 0000000..92eeb47 --- /dev/null +++ b/vendor/efsw/src/efsw/platform/posix/FileSystemImpl.cpp @@ -0,0 +1,251 @@ +#include + +#if defined( EFSW_PLATFORM_POSIX ) + +#include +#include +#include +#include +#include + +#ifndef _DARWIN_FEATURE_64_BIT_INODE +#define _DARWIN_FEATURE_64_BIT_INODE +#endif + +#ifndef _FILE_OFFSET_BITS +#define _FILE_OFFSET_BITS 64 +#endif + +#include +#include +#include + +#if EFSW_OS == EFSW_OS_LINUX || EFSW_OS == EFSW_OS_SOLARIS || EFSW_OS == EFSW_OS_ANDROID +#include +#elif EFSW_OS == EFSW_OS_MACOSX || EFSW_OS == EFSW_OS_BSD || EFSW_OS == EFSW_OS_IOS +#include +#include +#endif + +/** Remote file systems codes */ +#define S_MAGIC_AFS 0x5346414F +#define S_MAGIC_AUFS 0x61756673 +#define S_MAGIC_CEPH 0x00C36400 +#define S_MAGIC_CIFS 0xFF534D42 +#define S_MAGIC_CODA 0x73757245 +#define S_MAGIC_FHGFS 0x19830326 +#define S_MAGIC_FUSEBLK 0x65735546 +#define S_MAGIC_FUSECTL 0x65735543 +#define S_MAGIC_GFS 0x01161970 +#define S_MAGIC_GPFS 0x47504653 +#define S_MAGIC_KAFS 0x6B414653 +#define S_MAGIC_LUSTRE 0x0BD00BD0 +#define S_MAGIC_NCP 0x564C +#define S_MAGIC_NFS 0x6969 +#define S_MAGIC_NFSD 0x6E667364 +#define S_MAGIC_OCFS2 0x7461636F +#define S_MAGIC_PANFS 0xAAD7AAEA +#define S_MAGIC_PIPEFS 0x50495045 +#define S_MAGIC_SMB 0x517B +#define S_MAGIC_SNFS 0xBEEFDEAD +#define S_MAGIC_VMHGFS 0xBACBACBC +#define S_MAGIC_VXFS 0xA501FCF5 + +#if EFSW_OS == EFSW_OS_LINUX +#include +#include +#endif + +namespace efsw { namespace Platform { + +#if EFSW_OS == EFSW_OS_LINUX + +std::string findMountPoint( std::string file ) { + std::string cwd = FileSystem::getCurrentWorkingDirectory(); + struct stat last_stat; + struct stat file_stat; + + stat( file.c_str(), &file_stat ); + + std::string mp; + + if ( efsw::FileSystem::isDirectory( file ) ) { + last_stat = file_stat; + + if ( !FileSystem::changeWorkingDirectory( file ) ) + return ""; + } else { + std::string dir = efsw::FileSystem::pathRemoveFileName( file ); + + if ( !FileSystem::changeWorkingDirectory( dir ) ) + return ""; + + if ( stat( ".", &last_stat ) < 0 ) + return ""; + } + + while ( true ) { + struct stat st; + + if ( stat( "..", &st ) < 0 ) + goto done; + + if ( st.st_dev != last_stat.st_dev || st.st_ino == last_stat.st_ino ) + break; + + if ( !FileSystem::changeWorkingDirectory( ".." ) ) { + goto done; + } + + last_stat = st; + } + + /* Finally reached a mount point, see what it's called. */ + mp = FileSystem::getCurrentWorkingDirectory(); + +done: + FileSystem::changeWorkingDirectory( cwd ); + + return mp; +} + +std::string findDevicePath( const std::string& directory ) { + struct mntent* ent; + FILE* aFile; + + aFile = setmntent( "/proc/mounts", "r" ); + + if ( aFile == NULL ) + return ""; + + while ( NULL != ( ent = getmntent( aFile ) ) ) { + std::string dirName( ent->mnt_dir ); + + if ( dirName == directory ) { + std::string fsName( ent->mnt_fsname ); + + endmntent( aFile ); + + return fsName; + } + } + + endmntent( aFile ); + + return ""; +} + +bool isLocalFUSEDirectory( std::string directory ) { + efsw::FileSystem::dirRemoveSlashAtEnd( directory ); + + directory = findMountPoint( directory ); + + if ( !directory.empty() ) { + std::string devicePath = findDevicePath( directory ); + + return !devicePath.empty(); + } + + return false; +} + +#endif + +bool FileSystem::changeWorkingDirectory( const std::string& path ) { + return -1 != chdir( path.c_str() ); +} + +std::string FileSystem::getCurrentWorkingDirectory() { + char dir[PATH_MAX + 1]; + char* result = getcwd( dir, PATH_MAX + 1 ); + return result != NULL ? std::string( result ) : std::string(); +} + +FileInfoMap FileSystem::filesInfoFromPath( const std::string& path ) { + FileInfoMap files; + + DIR* dp; + struct dirent* dirp; + + if ( ( dp = opendir( path.c_str() ) ) == NULL ) + return files; + + while ( ( dirp = readdir( dp ) ) != NULL ) { + if ( strcmp( dirp->d_name, ".." ) != 0 && strcmp( dirp->d_name, "." ) != 0 ) { + std::string name( dirp->d_name ); + std::string fpath( path + name ); + + files[name] = FileInfo( fpath ); + } + } + + closedir( dp ); + + return files; +} + +char FileSystem::getOSSlash() { + return '/'; +} + +bool FileSystem::isDirectory( const std::string& path ) { + struct stat st; + int res = stat( path.c_str(), &st ); + + if ( 0 == res ) { + return static_cast( S_ISDIR( st.st_mode ) ); + } + + return false; +} + +bool FileSystem::isRemoteFS( const std::string& directory ) { +#if EFSW_OS == EFSW_OS_LINUX || EFSW_OS == EFSW_OS_MACOSX || EFSW_OS == EFSW_OS_BSD || \ + EFSW_OS == EFSW_OS_SOLARIS || EFSW_OS == EFSW_OS_ANDROID || EFSW_OS == EFSW_OS_IOS + struct statfs statfsbuf; + + statfs( directory.c_str(), &statfsbuf ); + + switch ( statfsbuf.f_type | 0UL ) { + case S_MAGIC_FUSEBLK: /* 0x65735546 remote */ + { +#if EFSW_OS == EFSW_OS_LINUX + return !isLocalFUSEDirectory( directory ); +#endif + } + case S_MAGIC_AFS: /* 0x5346414F remote */ + case S_MAGIC_AUFS: /* 0x61756673 remote */ + case S_MAGIC_CEPH: /* 0x00C36400 remote */ + case S_MAGIC_CIFS: /* 0xFF534D42 remote */ + case S_MAGIC_CODA: /* 0x73757245 remote */ + case S_MAGIC_FHGFS: /* 0x19830326 remote */ + case S_MAGIC_FUSECTL: /* 0x65735543 remote */ + case S_MAGIC_GFS: /* 0x01161970 remote */ + case S_MAGIC_GPFS: /* 0x47504653 remote */ + case S_MAGIC_KAFS: /* 0x6B414653 remote */ + case S_MAGIC_LUSTRE: /* 0x0BD00BD0 remote */ + case S_MAGIC_NCP: /* 0x564C remote */ + case S_MAGIC_NFS: /* 0x6969 remote */ + case S_MAGIC_NFSD: /* 0x6E667364 remote */ + case S_MAGIC_OCFS2: /* 0x7461636F remote */ + case S_MAGIC_PANFS: /* 0xAAD7AAEA remote */ + case S_MAGIC_PIPEFS: /* 0x50495045 remote */ + case S_MAGIC_SMB: /* 0x517B remote */ + case S_MAGIC_SNFS: /* 0xBEEFDEAD remote */ + case S_MAGIC_VMHGFS: /* 0xBACBACBC remote */ + case S_MAGIC_VXFS: /* 0xA501FCF5 remote */ + { + return true; + } + default: { + return false; + } + } +#endif + + return false; +} + +}} // namespace efsw::Platform + +#endif diff --git a/vendor/efsw/src/efsw/platform/posix/FileSystemImpl.hpp b/vendor/efsw/src/efsw/platform/posix/FileSystemImpl.hpp new file mode 100644 index 0000000..0bfba76 --- /dev/null +++ b/vendor/efsw/src/efsw/platform/posix/FileSystemImpl.hpp @@ -0,0 +1,30 @@ +#ifndef EFSW_FILESYSTEMIMPLPOSIX_HPP +#define EFSW_FILESYSTEMIMPLPOSIX_HPP + +#include +#include + +#if defined( EFSW_PLATFORM_POSIX ) + +namespace efsw { namespace Platform { + +class FileSystem { + public: + static FileInfoMap filesInfoFromPath( const std::string& path ); + + static char getOSSlash(); + + static bool isDirectory( const std::string& path ); + + static bool isRemoteFS( const std::string& directory ); + + static bool changeWorkingDirectory( const std::string& path ); + + static std::string getCurrentWorkingDirectory(); +}; + +}} // namespace efsw::Platform + +#endif + +#endif diff --git a/vendor/efsw/src/efsw/platform/posix/MutexImpl.cpp b/vendor/efsw/src/efsw/platform/posix/MutexImpl.cpp new file mode 100644 index 0000000..2233798 --- /dev/null +++ b/vendor/efsw/src/efsw/platform/posix/MutexImpl.cpp @@ -0,0 +1,28 @@ +#include + +#if defined( EFSW_PLATFORM_POSIX ) + +namespace efsw { namespace Platform { + +MutexImpl::MutexImpl() { + pthread_mutexattr_t attributes; + pthread_mutexattr_init( &attributes ); + pthread_mutexattr_settype( &attributes, PTHREAD_MUTEX_RECURSIVE ); + pthread_mutex_init( &mMutex, &attributes ); +} + +MutexImpl::~MutexImpl() { + pthread_mutex_destroy( &mMutex ); +} + +void MutexImpl::lock() { + pthread_mutex_lock( &mMutex ); +} + +void MutexImpl::unlock() { + pthread_mutex_unlock( &mMutex ); +} + +}} // namespace efsw::Platform + +#endif diff --git a/vendor/efsw/src/efsw/platform/posix/MutexImpl.hpp b/vendor/efsw/src/efsw/platform/posix/MutexImpl.hpp new file mode 100644 index 0000000..a33d827 --- /dev/null +++ b/vendor/efsw/src/efsw/platform/posix/MutexImpl.hpp @@ -0,0 +1,30 @@ +#ifndef EFSW_MUTEXIMPLPOSIX_HPP +#define EFSW_MUTEXIMPLPOSIX_HPP + +#include + +#if defined( EFSW_PLATFORM_POSIX ) + +#include + +namespace efsw { namespace Platform { + +class MutexImpl { + public: + MutexImpl(); + + ~MutexImpl(); + + void lock(); + + void unlock(); + + private: + pthread_mutex_t mMutex; +}; + +}} // namespace efsw::Platform + +#endif + +#endif diff --git a/vendor/efsw/src/efsw/platform/posix/SystemImpl.cpp b/vendor/efsw/src/efsw/platform/posix/SystemImpl.cpp new file mode 100644 index 0000000..37d4120 --- /dev/null +++ b/vendor/efsw/src/efsw/platform/posix/SystemImpl.cpp @@ -0,0 +1,168 @@ +#include + +#if defined( EFSW_PLATFORM_POSIX ) + +#include +#include +#include +#include +#include + +#include +#include + +#if EFSW_OS == EFSW_OS_MACOSX +#include +#elif EFSW_OS == EFSW_OS_LINUX || EFSW_OS == EFSW_OS_ANDROID +#include +#include +#elif EFSW_OS == EFSW_OS_HAIKU +#include +#include +#elif EFSW_OS == EFSW_OS_SOLARIS +#include +#elif EFSW_OS == EFSW_OS_BSD +#include +#endif + +namespace efsw { namespace Platform { + +void System::sleep( const unsigned long& ms ) { + // usleep( static_cast( ms * 1000 ) ); + + // usleep is not reliable enough (it might block the + // whole process instead of just the current thread) + // so we must use pthread_cond_timedwait instead + + // this implementation is inspired from Qt + // and taken from SFML + + unsigned long long usecs = ms * 1000; + + // get the current time + timeval tv; + gettimeofday( &tv, NULL ); + + // construct the time limit (current time + time to wait) + timespec ti; + ti.tv_nsec = ( tv.tv_usec + ( usecs % 1000000 ) ) * 1000; + ti.tv_sec = tv.tv_sec + ( usecs / 1000000 ) + ( ti.tv_nsec / 1000000000 ); + ti.tv_nsec %= 1000000000; + + // create a mutex and thread condition + pthread_mutex_t mutex; + pthread_mutex_init( &mutex, 0 ); + pthread_cond_t condition; + pthread_cond_init( &condition, 0 ); + + // wait... + pthread_mutex_lock( &mutex ); + pthread_cond_timedwait( &condition, &mutex, &ti ); + pthread_mutex_unlock( &mutex ); + + // destroy the mutex and condition + pthread_cond_destroy( &condition ); +} + +std::string System::getProcessPath() { +#if EFSW_OS == EFSW_OS_MACOSX + char exe_file[FILENAME_MAX + 1]; + + CFBundleRef mainBundle = CFBundleGetMainBundle(); + + if ( mainBundle ) { + CFURLRef mainURL = CFBundleCopyBundleURL( mainBundle ); + + if ( mainURL ) { + int ok = CFURLGetFileSystemRepresentation( mainURL, ( Boolean ) true, (UInt8*)exe_file, + FILENAME_MAX ); + + if ( ok ) { + return std::string( exe_file ) + "/"; + } + } + } + + return "./"; +#elif EFSW_OS == EFSW_OS_LINUX + char exe_file[FILENAME_MAX + 1]; + + int size; + + size = readlink( "/proc/self/exe", exe_file, FILENAME_MAX ); + + if ( size < 0 ) { + return std::string( "./" ); + } else { + exe_file[size] = '\0'; + return std::string( dirname( exe_file ) ) + "/"; + } + +#elif EFSW_OS == EFSW_OS_BSD + int mib[4]; + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PATHNAME; + mib[3] = -1; + char buf[1024]; + size_t cb = sizeof( buf ); + sysctl( mib, 4, buf, &cb, NULL, 0 ); + + return FileSystem::pathRemoveFileName( std::string( buf ) ); + +#elif EFSW_OS == EFSW_OS_SOLARIS + return FileSystem::pathRemoveFileName( std::string( getexecname() ) ); + +#elif EFSW_OS == EFSW_OS_HAIKU + image_info info; + int32 cookie = 0; + + while ( B_OK == get_next_image_info( 0, &cookie, &info ) ) { + if ( info.type == B_APP_IMAGE ) + break; + } + + return FileSystem::pathRemoveFileName( std::string( info.name ) ); + +#elif EFSW_OS == EFSW_OS_ANDROID + return "/sdcard/"; + +#else +#warning getProcessPath() not implemented on this platform. ( will return "./" ) + return "./"; + +#endif +} + +void System::maxFD() { + static bool maxed = false; + + if ( !maxed ) { + struct rlimit limit; + getrlimit( RLIMIT_NOFILE, &limit ); + limit.rlim_cur = limit.rlim_max; + setrlimit( RLIMIT_NOFILE, &limit ); + + getrlimit( RLIMIT_NOFILE, &limit ); + + efDEBUG( "File descriptor limit %ld\n", limit.rlim_cur ); + + maxed = true; + } +} + +Uint64 System::getMaxFD() { + static rlim_t max_fd = 0; + + if ( max_fd == 0 ) { + struct rlimit limit; + getrlimit( RLIMIT_NOFILE, &limit ); + max_fd = limit.rlim_cur; + } + + return max_fd; +} + +}} // namespace efsw::Platform + +#endif diff --git a/vendor/efsw/src/efsw/platform/posix/SystemImpl.hpp b/vendor/efsw/src/efsw/platform/posix/SystemImpl.hpp new file mode 100644 index 0000000..9322b06 --- /dev/null +++ b/vendor/efsw/src/efsw/platform/posix/SystemImpl.hpp @@ -0,0 +1,25 @@ +#ifndef EFSW_SYSTEMIMPLPOSIX_HPP +#define EFSW_SYSTEMIMPLPOSIX_HPP + +#include + +#if defined( EFSW_PLATFORM_POSIX ) + +namespace efsw { namespace Platform { + +class System { + public: + static void sleep( const unsigned long& ms ); + + static std::string getProcessPath(); + + static void maxFD(); + + static Uint64 getMaxFD(); +}; + +}} // namespace efsw::Platform + +#endif + +#endif diff --git a/vendor/efsw/src/efsw/platform/posix/ThreadImpl.cpp b/vendor/efsw/src/efsw/platform/posix/ThreadImpl.cpp new file mode 100644 index 0000000..0f96bca --- /dev/null +++ b/vendor/efsw/src/efsw/platform/posix/ThreadImpl.cpp @@ -0,0 +1,62 @@ +#include +#include + +#if defined( EFSW_PLATFORM_POSIX ) + +#include +#include +#include + +namespace efsw { namespace Platform { + +ThreadImpl::ThreadImpl( efsw::Thread* owner ) : mIsActive( false ) { + mIsActive = pthread_create( &mThread, NULL, &ThreadImpl::entryPoint, owner ) == 0; + + if ( !mIsActive ) { + efDEBUG( "Failed to create thread\n" ); + } +} + +ThreadImpl::~ThreadImpl() { + terminate(); +} + +void ThreadImpl::wait() { + // Wait for the thread to finish, no timeout + if ( mIsActive ) { + assert( pthread_equal( pthread_self(), mThread ) == 0 ); + + mIsActive = pthread_join( mThread, NULL ) != 0; + } +} + +void ThreadImpl::terminate() { + if ( mIsActive ) { +#if !defined( __ANDROID__ ) && !defined( ANDROID ) + pthread_cancel( mThread ); +#else + pthread_kill( mThread, SIGUSR1 ); +#endif + + mIsActive = false; + } +} + +void* ThreadImpl::entryPoint( void* userData ) { +// Tell the thread to handle cancel requests immediatly +#ifdef PTHREAD_CANCEL_ASYNCHRONOUS + pthread_setcanceltype( PTHREAD_CANCEL_ASYNCHRONOUS, NULL ); +#endif + + // The Thread instance is stored in the user data + Thread* owner = static_cast( userData ); + + // Forward to the owner + owner->run(); + + return NULL; +} + +}} // namespace efsw::Platform + +#endif diff --git a/vendor/efsw/src/efsw/platform/posix/ThreadImpl.hpp b/vendor/efsw/src/efsw/platform/posix/ThreadImpl.hpp new file mode 100644 index 0000000..2e02f9a --- /dev/null +++ b/vendor/efsw/src/efsw/platform/posix/ThreadImpl.hpp @@ -0,0 +1,39 @@ +#ifndef EFSW_THREADIMPLPOSIX_HPP +#define EFSW_THREADIMPLPOSIX_HPP + +#include + +#if defined( EFSW_PLATFORM_POSIX ) + +#include +#include + +namespace efsw { + +class Thread; + +namespace Platform { + +class ThreadImpl { + public: + explicit ThreadImpl( efsw::Thread* owner ); + + ~ThreadImpl(); + + void wait(); + + void terminate(); + + protected: + static void* entryPoint( void* userData ); + + pthread_t mThread; + Atomic mIsActive; +}; + +} // namespace Platform +} // namespace efsw + +#endif + +#endif diff --git a/vendor/efsw/src/efsw/platform/win/FileSystemImpl.cpp b/vendor/efsw/src/efsw/platform/win/FileSystemImpl.cpp new file mode 100644 index 0000000..2b87513 --- /dev/null +++ b/vendor/efsw/src/efsw/platform/win/FileSystemImpl.cpp @@ -0,0 +1,111 @@ +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_WIN32 + +#include +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +#ifndef EFSW_COMPILER_MSVC +#include +#else +#include +#endif + +namespace efsw { namespace Platform { + +bool FileSystem::changeWorkingDirectory( const std::string& path ) { + int res; +#ifdef EFSW_COMPILER_MSVC +#ifdef UNICODE + res = _wchdir( String::fromUtf8( path.c_str() ).toWideString().c_str() ); +#else + res = _chdir( String::fromUtf8( path.c_str() ).toAnsiString().c_str() ); +#endif +#else + res = chdir( path.c_str() ); +#endif + return -1 != res; +} + +std::string FileSystem::getCurrentWorkingDirectory() { +#ifdef EFSW_COMPILER_MSVC +#if defined( UNICODE ) && !defined( EFSW_NO_WIDECHAR ) + wchar_t dir[_MAX_PATH]; + return ( 0 != GetCurrentDirectoryW( _MAX_PATH, dir ) ) ? String( dir ).toUtf8() : std::string(); +#else + char dir[_MAX_PATH]; + return ( 0 != GetCurrentDirectory( _MAX_PATH, dir ) ) ? String( dir, std::locale() ).toUtf8() + : std::string(); +#endif +#else + char dir[PATH_MAX + 1]; + getcwd( dir, PATH_MAX + 1 ); + return std::string( dir ); +#endif +} + +FileInfoMap FileSystem::filesInfoFromPath( const std::string& path ) { + FileInfoMap files; + + String tpath( path ); + + if ( tpath[tpath.size() - 1] == '/' || tpath[tpath.size() - 1] == '\\' ) { + tpath += "*"; + } else { + tpath += "\\*"; + } + + WIN32_FIND_DATAW findFileData; + HANDLE hFind = FindFirstFileW( (LPCWSTR)tpath.toWideString().c_str(), &findFileData ); + + if ( hFind != INVALID_HANDLE_VALUE ) { + std::string name( String( findFileData.cFileName ).toUtf8() ); + std::string fpath( path + name ); + + if ( name != "." && name != ".." ) { + files[name] = FileInfo( fpath ); + } + + while ( FindNextFileW( hFind, &findFileData ) ) { + name = String( findFileData.cFileName ).toUtf8(); + fpath = path + name; + + if ( name != "." && name != ".." ) { + files[name] = FileInfo( fpath ); + } + } + + FindClose( hFind ); + } + + return files; +} + +char FileSystem::getOSSlash() { + return '\\'; +} + +bool FileSystem::isDirectory( const std::string& path ) { + DWORD attrs = GetFileAttributesW( String( path ).toWideString().c_str() ); + return attrs != INVALID_FILE_ATTRIBUTES && ( attrs & FILE_ATTRIBUTE_DIRECTORY ) != 0; +} + +bool FileSystem::isRemoteFS( const std::string& directory ) { + if ( ( directory[0] == '\\' || directory[0] == '/' ) && + ( directory[1] == '\\' || directory[1] == '/' ) ) { + return true; + } + + if ( directory.size() >= 3 ) { + return 4 == GetDriveTypeA( directory.substr( 0, 3 ).c_str() ); + } + + return false; +} + +}} // namespace efsw::Platform + +#endif diff --git a/vendor/efsw/src/efsw/platform/win/FileSystemImpl.hpp b/vendor/efsw/src/efsw/platform/win/FileSystemImpl.hpp new file mode 100644 index 0000000..e952efc --- /dev/null +++ b/vendor/efsw/src/efsw/platform/win/FileSystemImpl.hpp @@ -0,0 +1,31 @@ +#ifndef EFSW_FILESYSTEMIMPLWIN_HPP +#define EFSW_FILESYSTEMIMPLWIN_HPP + +#include +#include +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_WIN32 + +namespace efsw { namespace Platform { + +class FileSystem { + public: + static FileInfoMap filesInfoFromPath( const std::string& path ); + + static char getOSSlash(); + + static bool isDirectory( const std::string& path ); + + static bool isRemoteFS( const std::string& directory ); + + static bool changeWorkingDirectory( const std::string& path ); + + static std::string getCurrentWorkingDirectory(); +}; + +}} // namespace efsw::Platform + +#endif + +#endif diff --git a/vendor/efsw/src/efsw/platform/win/MutexImpl.cpp b/vendor/efsw/src/efsw/platform/win/MutexImpl.cpp new file mode 100644 index 0000000..62b7f83 --- /dev/null +++ b/vendor/efsw/src/efsw/platform/win/MutexImpl.cpp @@ -0,0 +1,25 @@ +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_WIN32 + +namespace efsw { namespace Platform { + +MutexImpl::MutexImpl() { + InitializeCriticalSection( &mMutex ); +} + +MutexImpl::~MutexImpl() { + DeleteCriticalSection( &mMutex ); +} + +void MutexImpl::lock() { + EnterCriticalSection( &mMutex ); +} + +void MutexImpl::unlock() { + LeaveCriticalSection( &mMutex ); +} + +}} // namespace efsw::Platform + +#endif diff --git a/vendor/efsw/src/efsw/platform/win/MutexImpl.hpp b/vendor/efsw/src/efsw/platform/win/MutexImpl.hpp new file mode 100644 index 0000000..7b06492 --- /dev/null +++ b/vendor/efsw/src/efsw/platform/win/MutexImpl.hpp @@ -0,0 +1,33 @@ +#ifndef EFSW_MUTEXIMPLWIN_HPP +#define EFSW_MUTEXIMPLWIN_HPP + +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_WIN32 + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +namespace efsw { namespace Platform { + +class MutexImpl { + public: + MutexImpl(); + + ~MutexImpl(); + + void lock(); + + void unlock(); + + private: + CRITICAL_SECTION mMutex; +}; + +}} // namespace efsw::Platform + +#endif + +#endif diff --git a/vendor/efsw/src/efsw/platform/win/SystemImpl.cpp b/vendor/efsw/src/efsw/platform/win/SystemImpl.cpp new file mode 100644 index 0000000..d1f2b21 --- /dev/null +++ b/vendor/efsw/src/efsw/platform/win/SystemImpl.cpp @@ -0,0 +1,46 @@ +#include +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_WIN32 + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include + +namespace efsw { namespace Platform { + +void System::sleep( const unsigned long& ms ) { + ::Sleep( ms ); +} + +std::string System::getProcessPath() { + // Get path to executable: + WCHAR szDrive[_MAX_DRIVE]; + WCHAR szDir[_MAX_DIR]; + WCHAR szFilename[_MAX_DIR]; + WCHAR szExt[_MAX_DIR]; + std::wstring dllName( _MAX_DIR, 0 ); + + GetModuleFileNameW( 0, &dllName[0], _MAX_PATH ); + +#ifdef EFSW_COMPILER_MSVC + _wsplitpath_s( dllName.c_str(), szDrive, _MAX_DRIVE, szDir, _MAX_DIR, szFilename, _MAX_DIR, + szExt, _MAX_DIR ); +#else + _wsplitpath( dllName.c_str(), szDrive, szDir, szFilename, szExt ); +#endif + + return String( szDrive ).toUtf8() + String( szDir ).toUtf8(); +} + +void System::maxFD() {} + +Uint64 System::getMaxFD() { // Number of ReadDirectory per thread + return 60; +} + +}} // namespace efsw::Platform + +#endif diff --git a/vendor/efsw/src/efsw/platform/win/SystemImpl.hpp b/vendor/efsw/src/efsw/platform/win/SystemImpl.hpp new file mode 100644 index 0000000..99b4867 --- /dev/null +++ b/vendor/efsw/src/efsw/platform/win/SystemImpl.hpp @@ -0,0 +1,25 @@ +#ifndef EFSW_SYSTEMIMPLWIN_HPP +#define EFSW_SYSTEMIMPLWIN_HPP + +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_WIN32 + +namespace efsw { namespace Platform { + +class System { + public: + static void sleep( const unsigned long& ms ); + + static std::string getProcessPath(); + + static void maxFD(); + + static Uint64 getMaxFD(); +}; + +}} // namespace efsw::Platform + +#endif + +#endif diff --git a/vendor/efsw/src/efsw/platform/win/ThreadImpl.cpp b/vendor/efsw/src/efsw/platform/win/ThreadImpl.cpp new file mode 100644 index 0000000..463934c --- /dev/null +++ b/vendor/efsw/src/efsw/platform/win/ThreadImpl.cpp @@ -0,0 +1,56 @@ +#include +#include +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_WIN32 + +#include + +namespace efsw { namespace Platform { + +ThreadImpl::ThreadImpl( efsw::Thread* owner ) { + mThread = reinterpret_cast( + _beginthreadex( NULL, 0, &ThreadImpl::entryPoint, owner, 0, &mThreadId ) ); + + if ( !mThread ) { + efDEBUG( "Failed to create thread\n" ); + } +} + +ThreadImpl::~ThreadImpl() { + if ( mThread ) { + CloseHandle( mThread ); + } +} + +void ThreadImpl::wait() { + // Wait for the thread to finish, no timeout + if ( mThread ) { + assert( mThreadId != GetCurrentThreadId() ); // A thread cannot wait for itself! + + WaitForSingleObject( mThread, INFINITE ); + } +} + +void ThreadImpl::terminate() { + if ( mThread ) { + TerminateThread( mThread, 0 ); + } +} + +unsigned int __stdcall ThreadImpl::entryPoint( void* userData ) { + // The Thread instance is stored in the user data + Thread* owner = static_cast( userData ); + + // Forward to the owner + owner->run(); + + // Optional, but it is cleaner + _endthreadex( 0 ); + + return 0; +} + +}} // namespace efsw::Platform + +#endif diff --git a/vendor/efsw/src/efsw/platform/win/ThreadImpl.hpp b/vendor/efsw/src/efsw/platform/win/ThreadImpl.hpp new file mode 100644 index 0000000..455f24c --- /dev/null +++ b/vendor/efsw/src/efsw/platform/win/ThreadImpl.hpp @@ -0,0 +1,42 @@ +#ifndef EFSW_THREADIMPLWIN_HPP +#define EFSW_THREADIMPLWIN_HPP + +#include + +#if EFSW_PLATFORM == EFSW_PLATFORM_WIN32 + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include + +namespace efsw { + +class Thread; + +namespace Platform { + +class ThreadImpl { + public: + explicit ThreadImpl( efsw::Thread* owner ); + + ~ThreadImpl(); + + void wait(); + + void terminate(); + + protected: + static unsigned int __stdcall entryPoint( void* userData ); + + HANDLE mThread; + unsigned int mThreadId; +}; + +} // namespace Platform +} // namespace efsw + +#endif + +#endif diff --git a/vendor/efsw/src/efsw/sophist.h b/vendor/efsw/src/efsw/sophist.h new file mode 100644 index 0000000..82e5c36 --- /dev/null +++ b/vendor/efsw/src/efsw/sophist.h @@ -0,0 +1,147 @@ +/* sophist.h - 0.3 - public domain - Sean Barrett 2010 +** Knowledge drawn from Brian Hook's posh.h and http://predef.sourceforge.net +** Sophist provides portable types; you typedef/#define them to your own names +** +** defines: +** - SOPHIST_endian - either SOPHIST_little_endian or SOPHIST_big_endian +** - SOPHIST_has_64 - either 0 or 1; if 0, int64 types aren't defined +** - SOPHIST_pointer64 - either 0 or 1; if 1, pointer is 64-bit +** +** - SOPHIST_intptr, SOPHIST_uintptr - integer same size as pointer +** - SOPHIST_int8, SOPHIST_uint8, SOPHIST_int16, SOPHIST_uint16 +** - SOPHIST_int32, SOPHIST_uint32, SOPHIST_int64, SOPHIST_uint64 +** - SOPHIST_int64_constant(number) - macros for creating 64-bit +** - SOPHIST_uint64_constant(number) integer constants +** - SOPHIST_printf_format64 - string for printf format for int64 +*/ + +#ifndef __INCLUDE_SOPHIST_H__ +#define __INCLUDE_SOPHIST_H__ + +#define SOPHIST_compiletime_assert(name,val) \ + typedef int SOPHIST__assert##name[(val) ? 1 : -1] + +/* define a couple synthetic rules to make code more readable */ +#if (defined(__sparc__) || defined(__sparc)) && \ + (defined(__arch64__) || defined(__sparcv9) || defined(__sparc_v9__)) + #define SOPHIST_sparc64 +#endif + +#if (defined(linux) || defined(__linux__)) && \ + (defined(__alpha)||defined(__alpha__)||defined(__x86_64__)||defined(_M_X64)) + #define SOPHIST_linux64 +#endif + +/* basic types */ +typedef signed char SOPHIST_int8; +typedef unsigned char SOPHIST_uint8; + +typedef signed short SOPHIST_int16; +typedef unsigned short SOPHIST_uint16; + +#ifdef __palmos__ + typedef signed long SOPHIST_int32; + typedef unsigned long SOPHIST_uint32; +#else + typedef signed int SOPHIST_int32; + typedef unsigned int SOPHIST_uint32; +#endif + +#ifndef SOPHIST_NO_64 + #if defined(_MSC_VER) || defined(__WATCOMC__) || defined(__BORLANDC__) \ + || (defined(__alpha) && defined(__DECC)) + + typedef signed __int64 SOPHIST_int64; + typedef unsigned __int64 SOPHIST_uint64; + #define SOPHIST_has_64 1 + #define SOPHIST_int64_constant(x) (x##i64) + #define SOPHIST_uint64_constant(x) (x##ui64) + #define SOPHIST_printf_format64 "I64" + + #elif defined(__LP64__) || defined(__powerpc64__) || defined(SOPHIST_sparc64) + + typedef signed long SOPHIST_int64; + typedef unsigned long SOPHIST_uint64; + + #define SOPHIST_has_64 1 + #define SOPHIST_int64_constant(x) ((SOPHIST_int64) x) + #define SOPHIST_uint64_constant(x) ((SOPHIST_uint64) x) + #define SOPHIST_printf_format64 "l" + + #elif defined(_LONG_LONG) || defined(__SUNPRO_C) || defined(__SUNPRO_CC) \ + || defined(__GNUC__) || defined(__MWERKS__) || defined(__APPLE_CC__) \ + || defined(sgi) || defined (__sgi) || defined(__sgi__) \ + || defined(_CRAYC) + + typedef signed long long SOPHIST_int64; + typedef unsigned long long SOPHIST_uint64; + + #define SOPHIST_has_64 1 + #define SOPHIST_int64_constant(x) (x##LL) + #define SOPHIST_uint64_constant(x) (x##ULL) + #define SOPHIST_printf_format64 "ll" + #endif +#endif + +#ifndef SOPHIST_has_64 +#define SOPHIST_has_64 0 +#endif + +SOPHIST_compiletime_assert( int8 , sizeof(SOPHIST_int8 ) == 1); +SOPHIST_compiletime_assert(uint16, sizeof(SOPHIST_int16) == 2); +SOPHIST_compiletime_assert( int32, sizeof(SOPHIST_int32 ) == 4); +SOPHIST_compiletime_assert(uint32, sizeof(SOPHIST_uint32) == 4); + +#if SOPHIST_has_64 + SOPHIST_compiletime_assert( int64, sizeof(SOPHIST_int64 ) == 8); + SOPHIST_compiletime_assert(uint64, sizeof(SOPHIST_uint64) == 8); +#endif + +/* determine whether pointers are 64-bit */ + +#if defined(SOPHIST_linux64) || defined(SOPHIST_sparc64) \ + || defined(__osf__) || (defined(_WIN64) && !defined(_XBOX)) \ + || defined(__64BIT__) \ + || defined(__LP64) || defined(__LP64__) || defined(_LP64) \ + || defined(_ADDR64) || defined(_CRAYC) \ + + #define SOPHIST_pointer64 1 + + SOPHIST_compiletime_assert(pointer64, sizeof(void*) == 8); + + typedef SOPHIST_int64 SOPHIST_intptr; + typedef SOPHIST_uint64 SOPHIST_uintptr; +#else + + #define SOPHIST_pointer64 0 + + SOPHIST_compiletime_assert(pointer64, sizeof(void*) <= 4); + + /* do we care about pointers that are only 16-bit? */ + typedef SOPHIST_int32 SOPHIST_intptr; + typedef SOPHIST_uint32 SOPHIST_uintptr; + +#endif + +SOPHIST_compiletime_assert(intptr, sizeof(SOPHIST_intptr) == sizeof(char *)); + +/* enumerate known little endian cases; fallback to big-endian */ + +#define SOPHIST_little_endian 1 +#define SOPHIST_big_endian 2 + +#if defined(__386__) || defined(i386) || defined(__i386__) \ + || defined(__X86) || defined(_M_IX86) \ + || defined(_M_X64) || defined(__x86_64__) \ + || defined(alpha) || defined(__alpha) || defined(__alpha__) \ + || defined(_M_ALPHA) \ + || defined(ARM) || defined(_ARM) || defined(__arm__) \ + || defined(WIN32) || defined(_WIN32) || defined(__WIN32__) \ + || defined(_WIN32_WCE) || defined(__NT__) \ + || defined(__MIPSEL__) + #define SOPHIST_endian SOPHIST_little_endian +#else + #define SOPHIST_endian SOPHIST_big_endian +#endif + +#endif /* __INCLUDE_SOPHIST_H__ */ diff --git a/vendor/efsw/src/test/efsw-test.c b/vendor/efsw/src/test/efsw-test.c new file mode 100644 index 0000000..54a3e21 --- /dev/null +++ b/vendor/efsw/src/test/efsw-test.c @@ -0,0 +1,164 @@ +#include + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 + #include +#else + #include +#endif + +const char PATH_SEPARATOR = +#ifdef _WIN32 + '\\'; +#else + '/'; +#endif + +bool STOP = false; + +void sigend( int sig ) { + printf( "Bye bye" ); + STOP = true; +} + +void sleepMsecs( int msecs ) { +#ifdef _WIN32 + Sleep( msecs ); +#else + sleep( msecs ); +#endif +} + +const char * getActionName( enum efsw_action action ) { + switch ( action ) { + case EFSW_ADD: + return "Add"; + case EFSW_MODIFIED: + return "Modified"; + case EFSW_DELETE: + return "Delete"; + case EFSW_MOVED: + return "Moved"; + default: + return "Bad Action"; + } +} + +void handleFileAction( efsw_watcher watcher, efsw_watchid watchid, + const char* dir, const char* filename, + enum efsw_action action, const char* oldFilename, + void* param ) { + if ( strlen( oldFilename ) == 0 ) { + printf( "Watch ID %ld DIR (%s) FILE (%s) has event %s\n", + watchid, dir, filename, getActionName( action )); + } else { + printf( "Watch ID %ld DIR (%s) FILE (from file %s to %s) has event %s\n", + watchid, dir, oldFilename, filename, getActionName( action )); + } +} + +efsw_watchid handleWatchID( efsw_watchid watchid ) { + switch ( watchid ) { + case EFSW_NOTFOUND: + case EFSW_REPEATED: + case EFSW_OUTOFSCOPE: + case EFSW_REMOTE: + case EFSW_WATCHER_FAILED: + case EFSW_UNSPECIFIED: { + printf( "%s\n", efsw_getlasterror() ); + break; + } + default: { + printf( "Added WatchID: %ld\n", watchid ); + } + } + + return watchid; +} + +int main( int argc, char** argv ) { + signal( SIGABRT, sigend ); + signal( SIGINT, sigend ); + signal( SIGTERM, sigend ); + + printf("Press ^C to exit demo\n"); + + bool commonTest = true; + bool useGeneric = false; + char *path = 0; + + if ( argc >= 2 ) { + path = argv[1]; + + struct stat s; + if( stat(path,&s) == 0 && (s.st_mode & S_IFDIR) == S_IFDIR ) { + commonTest = false; + } + + if ( argc >= 3 ) { + if ( strcmp( argv[2], "true" ) == 0 ) { + useGeneric = true; + } + } + } + + /// create the file watcher object + efsw_watcher fileWatcher = efsw_create( useGeneric ); + efsw_follow_symlinks( fileWatcher, false ); + efsw_allow_outofscopelinks( fileWatcher, false ); + + if ( commonTest ) { + char cwd[256]; + getcwd( cwd, sizeof(cwd) ); + printf( "CurPath: %s\n", cwd ); + + /// starts watching + efsw_watch( fileWatcher ); + + /// add a watch to the system + char path1[256]; + sprintf(path1, "%s%ctest", cwd, PATH_SEPARATOR ); + handleWatchID( efsw_addwatch_withoptions( fileWatcher, path1, handleFileAction, true, 0, 0, 0 ) ); + + /// adds another watch after started watching... + sleepMsecs( 100 ); + + char path2[256]; + sprintf(path2, "%s%ctest2", cwd, PATH_SEPARATOR ); + efsw_watchid watchID = handleWatchID( + efsw_addwatch_withoptions( fileWatcher, path2, handleFileAction, true, 0, 0, 0 ) ); + + /// delete the watch + if ( watchID > 0 ) { + sleepMsecs( 1000 ); + efsw_removewatch_byid( fileWatcher, watchID ); + } + } else { + if ( efsw_addwatch( fileWatcher, path, handleFileAction, true, 0 ) > 0 ) { + efsw_watch( fileWatcher ); + + printf( "Watching directory: %s\n", path ); + + if ( useGeneric ) { + printf( "Using generic backend watcher\n" ); + } + } else { + printf( "Error trying to watch directory: %s\n", path ); + printf( "%s\n", efsw_getlasterror() ); + } + } + + while ( !STOP ) { + sleepMsecs( 100 ); + } + + efsw_release( fileWatcher ); + + return 0; +} diff --git a/vendor/efsw/src/test/efsw-test.cpp b/vendor/efsw/src/test/efsw-test.cpp new file mode 100644 index 0000000..99f6086 --- /dev/null +++ b/vendor/efsw/src/test/efsw-test.cpp @@ -0,0 +1,139 @@ +#include +#include +#include +#include +#include + +bool STOP = false; + +void sigend( int ) { + std::cout << std::endl << "Bye bye" << std::endl; + STOP = true; +} + +/// Processes a file action +class UpdateListener : public efsw::FileWatchListener { + public: + UpdateListener() {} + + std::string getActionName( efsw::Action action ) { + switch ( action ) { + case efsw::Actions::Add: + return "Add"; + case efsw::Actions::Modified: + return "Modified"; + case efsw::Actions::Delete: + return "Delete"; + case efsw::Actions::Moved: + return "Moved"; + default: + return "Bad Action"; + } + } + + void handleFileAction( efsw::WatchID watchid, const std::string& dir, + const std::string& filename, efsw::Action action, + std::string oldFilename = "" ) override { + std::cout << "Watch ID " << watchid << " DIR (" + << dir + ") FILE (" + + ( oldFilename.empty() ? "" : "from file " + oldFilename + " to " ) + + filename + ") has event " + << getActionName( action ) << std::endl; + } +}; + +efsw::WatchID handleWatchID( efsw::WatchID watchid ) { + switch ( watchid ) { + case efsw::Errors::FileNotFound: + case efsw::Errors::FileRepeated: + case efsw::Errors::FileOutOfScope: + case efsw::Errors::FileRemote: + case efsw::Errors::WatcherFailed: + case efsw::Errors::Unspecified: { + std::cout << efsw::Errors::Log::getLastErrorLog().c_str() << std::endl; + break; + } + default: { + std::cout << "Added WatchID: " << watchid << std::endl; + } + } + + return watchid; +} + +int main( int argc, char** argv ) { + signal( SIGABRT, sigend ); + signal( SIGINT, sigend ); + signal( SIGTERM, sigend ); + + std::cout << "Press ^C to exit demo" << std::endl; + + bool commonTest = true; + bool useGeneric = false; + std::string path; + + if ( argc >= 2 ) { + path = std::string( argv[1] ); + + if ( efsw::FileSystem::isDirectory( path ) ) { + commonTest = false; + } + + if ( argc >= 3 ) { + if ( std::string( argv[2] ) == "true" ) { + useGeneric = true; + } + } + } + + UpdateListener* ul = new UpdateListener(); + + /// create the file watcher object + efsw::FileWatcher fileWatcher( useGeneric ); + + fileWatcher.followSymlinks( false ); + fileWatcher.allowOutOfScopeLinks( false ); + + if ( commonTest ) { + std::string CurPath( efsw::System::getProcessPath() ); + + std::cout << "CurPath: " << CurPath.c_str() << std::endl; + + /// starts watching + fileWatcher.watch(); + + /// add a watch to the system + handleWatchID( fileWatcher.addWatch( CurPath + "test", ul, true ) ); + + /// adds another watch after started watching... + efsw::System::sleep( 100 ); + + efsw::WatchID watchID = + handleWatchID( fileWatcher.addWatch( CurPath + "test2", ul, true ) ); + + /// delete the watch + if ( watchID > 0 ) { + efsw::System::sleep( 1000 ); + fileWatcher.removeWatch( watchID ); + } + } else { + if ( fileWatcher.addWatch( path, ul, true ) > 0 ) { + fileWatcher.watch(); + + std::cout << "Watching directory: " << path.c_str() << std::endl; + + if ( useGeneric ) { + std::cout << "Using generic backend watcher" << std::endl; + } + } else { + std::cout << "Error trying to watch directory: " << path.c_str() << std::endl; + std::cout << efsw::Errors::Log::getLastErrorLog().c_str() << std::endl; + } + } + + while ( !STOP ) { + efsw::System::sleep( 100 ); + } + + return 0; +} From 7e10961bc0879b91cb61603744fcdee6c19897b8 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 15 Oct 2024 12:34:56 -0700 Subject: [PATCH 110/168] Return to implicit `install` task --- package.json | 1 - scripts/preinstall.js | 29 ----------------------------- 2 files changed, 30 deletions(-) delete mode 100644 scripts/preinstall.js diff --git a/package.json b/package.json index 94542e2..cecd2b7 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ }, "homepage": "http://atom.github.io/node-pathwatcher", "scripts": { - "install": "node scripts/preinstall.js && node-gyp rebuild", "test": "node spec/run.js", "test-context-safety": "node spec/context-safety.js", "clean": "node scripts/clean.js" diff --git a/scripts/preinstall.js b/scripts/preinstall.js deleted file mode 100644 index 0765674..0000000 --- a/scripts/preinstall.js +++ /dev/null @@ -1,29 +0,0 @@ -const FS = require('fs'); -const Path = require('path'); -const CP = require('child_process'); - -async function exec (command, args) { - return new Promise((resolve, reject) => { - let proc = CP.spawn(command, args); - let stderr = []; - let stdout = []; - proc.stdout.on('data', (data) => stdout.push(data.toString())); - proc.stdout.on('error', (error) => stderr.push(error.toString())); - proc.on('close', () => { - if (stderr.length > 9) reject(stderr.join('')); - else resolve(stdout.join('')); - }); - }); -} - -async function initSubmodules () { - await exec('git', ['submodule', 'init']); - await exec('git', ['submodule', 'update']); -} - -if (!FS.existsSync(Path.resolve(__dirname, '..', 'vendor', 'efsw', 'LICENSE'))) { - console.log('Initializing EFSW submodule…'); - initSubmodules().then(() => console.log('…done.')); -} else { - console.log('EFSW already present; skipping submodule init'); -} From 84590ea7064b12a8a70c381ddc7078efa6723224 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 15 Oct 2024 12:40:29 -0700 Subject: [PATCH 111/168] Oops? (How did this ever work?) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cecd2b7..ddfabd1 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "main": "./lib/main", + "main": "./src/main", "name": "pathwatcher", "description": "Watch files and directories for changes", "version": "8.1.2", From 28d8f5ae1db8c5f68c15fd2606aa5e4f34f9874f Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 15 Oct 2024 12:45:36 -0700 Subject: [PATCH 112/168] Lazy-load `PathWatcher` to prevent circular dependency --- src/directory.js | 3 ++- src/file.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/directory.js b/src/directory.js index f099c5e..030b7d3 100644 --- a/src/directory.js +++ b/src/directory.js @@ -5,7 +5,7 @@ const async = require('async'); const { Emitter, Disposable } = require('event-kit'); const File = require('./file'); -const PathWatcher = require('./main'); +let PathWatcher; // Extended: Represents a directory on disk that can be traversed or watched // for changes. @@ -419,6 +419,7 @@ class Directory { */ subscribeToNativeChangeEvents () { + PathWatcher ??= require('./main'); this.watchSubscription ??= PathWatcher.watch( this.path, (_eventType) => { diff --git a/src/file.js b/src/file.js index 300b3f4..d312c67 100644 --- a/src/file.js +++ b/src/file.js @@ -12,7 +12,7 @@ async function wait (ms) { return new Promise(r => setTimeout(r, ms)); } -const PathWatcher = require('./main'); +let PathWatcher; // Extended: Represents an individual file that can be watched, read from, and // written to. @@ -508,6 +508,7 @@ class File { } subscribeToNativeChangeEvents () { + PathWatcher ??= require('./main'); this.watchSubscription ??= PathWatcher.watch( this.path, (...args) => { From 05d465f6e565b1f9eecf9714c13d9dd00bd0594b Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 15 Oct 2024 13:00:12 -0700 Subject: [PATCH 113/168] Ensure `onWillThrowWatchError` returns a `Disposable` --- src/file.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/file.js b/src/file.js index d312c67..46b06d3 100644 --- a/src/file.js +++ b/src/file.js @@ -127,7 +127,8 @@ class File { } onWillThrowWatchError (_callback) { - // DEPRECATED + // Deprecated callback; must return a `Disposable` for compatibility. + return new Disposable(() => {}); } willAddSubscription () { From cc3f7075d41675bebb8a3b2a52fdc9f5c3e544ab Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 15 Oct 2024 13:25:33 -0700 Subject: [PATCH 114/168] =?UTF-8?q?Guard=20against=20trying=20to=20call=20?= =?UTF-8?q?a=20`tsfn`=20that=20might've=20been=20GC=E2=80=99d=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …in a terminating environment. --- lib/core.cc | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/core.cc b/lib/core.cc index 12f0114..b52388b 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -88,8 +88,16 @@ void PathWatcherListener::handleFileAction( oldPath.assign(oldPathStr.begin(), oldPathStr.end()); } + napi_status status = tsfn.Acquire(); + if (status != napi_ok) { + // We couldn't acquire the `tsfn`; it might be in the process of being + // aborted because our environment is terminating. + return; + } + PathWatcherEvent* event = new PathWatcherEvent(action, watchId, newPath, oldPath); - napi_status status = tsfn.BlockingCall(event, ProcessEvent); + status = tsfn.BlockingCall(event, ProcessEvent); + tsfn.Release(); if (status != napi_ok) { // TODO: Not sure how this could fail, or how we should present it to the // user if it does fail. This action runs on a separate thread and it's not From 82d831987c53f66159bd68d9dcf355175ad55ea1 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 15 Oct 2024 13:35:11 -0700 Subject: [PATCH 115/168] (Running out of ideas) --- binding.gyp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/binding.gyp b/binding.gyp index 2abf3e4..b66b1b7 100644 --- a/binding.gyp +++ b/binding.gyp @@ -96,6 +96,9 @@ }, { "target_name": "pathwatcher", + "defines": [ + "NODE_API_SWALLOW_UNTHROWABLE_EXCEPTIONS" + ], "dependencies": ["efsw"], "cflags!": ["-fno-exceptions"], "cflags_cc!": ["-fno-exceptions"], From 0199f2b706242f22eb9af6376575b13e3fbe6840 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 15 Oct 2024 13:40:57 -0700 Subject: [PATCH 116/168] Guard against a null `tsfn`, perhaps? --- lib/core.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/core.cc b/lib/core.cc index b52388b..8b4c68e 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -88,6 +88,8 @@ void PathWatcherListener::handleFileAction( oldPath.assign(oldPathStr.begin(), oldPathStr.end()); } + if (!tsfn) return; + napi_status status = tsfn.Acquire(); if (status != napi_ok) { // We couldn't acquire the `tsfn`; it might be in the process of being From 1a447ef980092dc57ba0a67ca448527f8dea5986 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 15 Oct 2024 14:27:55 -0700 Subject: [PATCH 117/168] Add a finalization callback to `tsfn` --- lib/core.cc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/core.cc b/lib/core.cc index 8b4c68e..46f5b07 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -14,7 +14,10 @@ PathWatcherListener::PathWatcherListener(Napi::Env env, Napi::Function fn) callback, "pathwatcher-efsw-listener", 0, - 1 + 1, + [this](Napi::Env env) { + this->Stop(); + } ); } @@ -25,6 +28,7 @@ PathWatcherListener::~PathWatcherListener() { void PathWatcherListener::Stop() { // Prevent responders from acting while we shut down. std::lock_guard lock(shutdownMutex); + if (isShuttingDown) return; isShuttingDown = true; if (tsfn) { tsfn.Release(); From 584c3871d04c0e467fc93b60a28f4edc1bc78d44 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 15 Oct 2024 16:39:58 -0700 Subject: [PATCH 118/168] Quick test --- lib/core.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/core.cc b/lib/core.cc index 46f5b07..d0de40b 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -247,8 +247,8 @@ Napi::Value EFSW::Unwatch(const Napi::CallbackInfo& info) { } void EFSW::Cleanup(Napi::Env env) { + std::cout << "EFSW::Cleanup" << std::endl; auto addonData = env.GetInstanceData(); - delete addonData->fileWatcher; if (addonData && addonData->fileWatcher) { // Clean up all outstanding listeners. for (auto& pair : addonData->listeners) { @@ -256,6 +256,7 @@ void EFSW::Cleanup(Napi::Env env) { } addonData->fileWatcher = nullptr; } + delete addonData->fileWatcher; } void EFSW::Init(Napi::Env env) { From d2d89cdd9ac92ff69c1dc1848158fe0cb73f58d5 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 15 Oct 2024 16:44:18 -0700 Subject: [PATCH 119/168] (oops) --- lib/core.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/core.cc b/lib/core.cc index d0de40b..4264cc1 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -3,6 +3,7 @@ #include "include/efsw/efsw.hpp" #include "napi.h" #include +#include #ifdef __APPLE__ #include #endif From d0483cfba29ca3fed3b33237373cf2bd12d6cab6 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 17 Oct 2024 15:42:39 -0700 Subject: [PATCH 120/168] Here's another approach that is equally futile for all the same reasons --- lib/addon-data.h | 4 ++ lib/binding.cc | 26 ++++---- lib/core.cc | 161 +++++++++++++++++++++++++++++++++++++---------- lib/core.h | 21 ++++++- src/main.js | 11 +++- 5 files changed, 172 insertions(+), 51 deletions(-) diff --git a/lib/addon-data.h b/lib/addon-data.h index 1d683d8..4ec103a 100644 --- a/lib/addon-data.h +++ b/lib/addon-data.h @@ -1,4 +1,5 @@ #include "core.h" +#include "napi.h" #pragma once static int g_next_addon_data_id = 1; @@ -15,6 +16,9 @@ class AddonData final { int watchCount = 0; efsw::FileWatcher* fileWatcher = nullptr; + Napi::FunctionReference callback; + Napi::ThreadSafeFunction tsfn; + // A map that associates `WatcherHandle` values with their // `PathWatcherListener` instances. std::unordered_map listeners; diff --git a/lib/binding.cc b/lib/binding.cc index ac88141..503ba1a 100644 --- a/lib/binding.cc +++ b/lib/binding.cc @@ -1,18 +1,18 @@ #include "core.h" #include "addon-data.h" -Napi::Object Init(Napi::Env env, Napi::Object exports) { - AddonData* addonData = new AddonData(env); - env.SetInstanceData(addonData); +// Napi::Object Init(Napi::Env env, Napi::Object exports) { +// AddonData* addonData = new AddonData(env); +// env.SetInstanceData(addonData); +// +// EFSW::Init(env); +// +// // exports.Set("setCallback", Napi::Function::New(env, EFSW::SetCallback)); +// exports.Set("watch", Napi::Function::New(env, EFSW::Watch)); +// exports.Set("unwatch", Napi::Function::New(env, EFSW::Unwatch)); +// +// return exports; +// } - EFSW::Init(env); - // exports.Set("setCallback", Napi::Function::New(env, EFSW::SetCallback)); - exports.Set("watch", Napi::Function::New(env, EFSW::Watch)); - exports.Set("unwatch", Napi::Function::New(env, EFSW::Unwatch)); - - return exports; -} - - -NODE_API_MODULE(pathwatcher_efsw, Init) +// NODE_API_MODULE(pathwatcher_efsw, Init) diff --git a/lib/core.cc b/lib/core.cc index 4264cc1..d93f39d 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -8,32 +8,29 @@ #include #endif -PathWatcherListener::PathWatcherListener(Napi::Env env, Napi::Function fn) - : callback(fn) { - tsfn = Napi::ThreadSafeFunction::New( - env, - callback, - "pathwatcher-efsw-listener", - 0, - 1, - [this](Napi::Env env) { - this->Stop(); - } - ); +static int unique_id = 1; + +PathWatcherListener::PathWatcherListener(Napi::Env env, AddonData* addonData): addonData(addonData) { + id = unique_id++; } PathWatcherListener::~PathWatcherListener() { + std::cout << "PathWatcherListener destructor! " << id << std::endl; Stop(); } void PathWatcherListener::Stop() { - // Prevent responders from acting while we shut down. - std::lock_guard lock(shutdownMutex); - if (isShuttingDown) return; - isShuttingDown = true; - if (tsfn) { - tsfn.Release(); - } + if (!addonData) return; + addonData = nullptr; + // std::cout << "PathWatcherListener::Stop! " << id << std::endl; + // if (isShuttingDown) return; + // // Prevent responders from acting while we shut down. + // std::lock_guard lock(shutdownMutex); + // if (isShuttingDown) return; + // isShuttingDown = true; + // // if (tsfn) { + // // tsfn.Release(); + // // } } std::string EventType(efsw::Action action, bool isChild) { @@ -59,6 +56,7 @@ void PathWatcherListener::handleFileAction( efsw::Action action, std::string oldFilename ) { + if (!addonData) return; // Don't try to proceed if we've already started the shutdown process. if (isShuttingDown) return; std::lock_guard lock(shutdownMutex); @@ -93,6 +91,7 @@ void PathWatcherListener::handleFileAction( oldPath.assign(oldPathStr.begin(), oldPathStr.end()); } + Napi::ThreadSafeFunction tsfn = addonData->tsfn; if (!tsfn) return; napi_status status = tsfn.Acquire(); @@ -162,10 +161,79 @@ void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEvent* even } Napi::Value EFSW::Watch(const Napi::CallbackInfo& info) { + auto env = info.Env(); + return env.Undefined(); +} + +Napi::Value EFSW::Unwatch(const Napi::CallbackInfo& info) { auto env = info.Env(); auto addonData = env.GetInstanceData(); Napi::HandleScope scope(env); + // Our sole argument must be a JavaScript number; we convert it to a watcher + // handle. + if (!IsV8ValueWatcherHandle(info[0])) { + Napi::TypeError::New(env, "Argument must be a number").ThrowAsJavaScriptException(); + return env.Null(); + } + + WatcherHandle handle = V8ValueToWatcherHandle(info[0].As()); + + // EFSW doesn’t mind if we give it a handle that it doesn’t recognize; it’ll + // just silently do nothing. + addonData->fileWatcher->removeWatch(handle); + + // Since we’re not listening anymore, we have to stop the associated + // `PathWatcherListener` or else Node will think there’s an open handle. + auto it = addonData->listeners.find(handle); + if (it != addonData->listeners.end()) { + it->second->Stop(); + addonData->listeners.erase(it); + } + + addonData->watchCount--; + if (addonData->watchCount == 0) { + // When this environment isn’t watching any files, we can stop the + // `FileWatcher` instance. We’ll start it up again if `Watch` is called. + EFSW::Cleanup(env); + } + + return env.Undefined(); +} + +void EFSW::Cleanup(Napi::Env env) { + std::cout << "EFSW::Cleanup" << std::endl; + auto addonData = env.GetInstanceData(); + if (addonData && addonData->fileWatcher) { + // Clean up all outstanding listeners. + for (auto& pair : addonData->listeners) { + pair.second->Stop(); + } + addonData->fileWatcher = nullptr; + } + delete addonData->fileWatcher; +} + +void EFSW::Init(Napi::Env env) { + // auto addonData = env.GetInstanceData(); + // addonData->watchCount = 0; +} + +PathWatcher::PathWatcher(Napi::Env env, Napi::Object exports) { + addonData = new AddonData(env); + + DefineAddon(exports, { + InstanceMethod("watch", &PathWatcher::Watch), + InstanceMethod("unwatch", &PathWatcher::Unwatch), + InstanceMethod("setCallback", &PathWatcher::SetCallback) + }); +} + +Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { + auto env = info.Env(); + // auto addonData = env.GetInstanceData(); + Napi::HandleScope scope(env); + // First argument must be a string. if (!info[0].IsString()) { Napi::TypeError::New(env, "String required").ThrowAsJavaScriptException(); @@ -175,14 +243,26 @@ Napi::Value EFSW::Watch(const Napi::CallbackInfo& info) { Napi::String path = info[0].ToString(); std::string cppPath(path); - // Second argument must be a callback. - if (!info[1].IsFunction()) { - Napi::TypeError::New(env, "Function required").ThrowAsJavaScriptException(); + if (addonData->callback.IsEmpty()) { + Napi::TypeError::New(env, "No callback set").ThrowAsJavaScriptException(); return env.Null(); } - Napi::Function fn = info[1].As(); - PathWatcherListener* listener = new PathWatcherListener(env, fn); + if (!addonData->fileWatcher) { + // addonData->tsfn = + addonData->tsfn = Napi::ThreadSafeFunction::New( + env, + addonData->callback.Value(), + "pathwatcher-efsw-listener", + 0, + 1, + [](Napi::Env env) { + std::cout << "Testing finalizer" << std::endl; + } + ); + } + + PathWatcherListener* listener = new PathWatcherListener(env, addonData); // The first call to `Watch` initializes a `FileWatcher`. if (!addonData->fileWatcher) { @@ -211,9 +291,9 @@ Napi::Value EFSW::Watch(const Napi::CallbackInfo& info) { return WatcherHandleToV8Value(handle, env); } -Napi::Value EFSW::Unwatch(const Napi::CallbackInfo& info) { +Napi::Value PathWatcher::Unwatch(const Napi::CallbackInfo& info) { + std::cout << "Unwatch!" << std::endl; auto env = info.Env(); - auto addonData = env.GetInstanceData(); Napi::HandleScope scope(env); // Our sole argument must be a JavaScript number; we convert it to a watcher @@ -234,6 +314,7 @@ Napi::Value EFSW::Unwatch(const Napi::CallbackInfo& info) { auto it = addonData->listeners.find(handle); if (it != addonData->listeners.end()) { it->second->Stop(); + std::cout << "Erasing listener with handle " << handle << std::endl; addonData->listeners.erase(it); } @@ -241,15 +322,16 @@ Napi::Value EFSW::Unwatch(const Napi::CallbackInfo& info) { if (addonData->watchCount == 0) { // When this environment isn’t watching any files, we can stop the // `FileWatcher` instance. We’ll start it up again if `Watch` is called. - EFSW::Cleanup(env); + Cleanup(env); } return env.Undefined(); } -void EFSW::Cleanup(Napi::Env env) { - std::cout << "EFSW::Cleanup" << std::endl; - auto addonData = env.GetInstanceData(); +void PathWatcher::Cleanup(Napi::Env env) { + std::cout << "PathWatcher::Cleanup" << std::endl; + // auto addonData = env.GetInstanceData(); + if (addonData && addonData->fileWatcher) { // Clean up all outstanding listeners. for (auto& pair : addonData->listeners) { @@ -257,10 +339,23 @@ void EFSW::Cleanup(Napi::Env env) { } addonData->fileWatcher = nullptr; } + if (addonData->tsfn) { + addonData->tsfn.Unref(env); + // delete addonData->tsfn; + addonData->tsfn = NULL; + } delete addonData->fileWatcher; } -void EFSW::Init(Napi::Env env) { - auto addonData = env.GetInstanceData(); - addonData->watchCount = 0; + +void PathWatcher::SetCallback(const Napi::CallbackInfo& info) { + auto env = info.Env(); + if (!info[0].IsFunction()) { + Napi::TypeError::New(env, "Function required").ThrowAsJavaScriptException(); + } + + Napi::Function fn = info[0].As(); + addonData->callback = Napi::Persistent(fn); } + +NODE_API_ADDON(PathWatcher) diff --git a/lib/core.h b/lib/core.h index 1802866..8e53a7f 100644 --- a/lib/core.h +++ b/lib/core.h @@ -65,10 +65,11 @@ struct PathWatcherEvent { } }; +class AddonData; class PathWatcherListener: public efsw::FileWatchListener { public: - PathWatcherListener(Napi::Env env, Napi::Function fn); + PathWatcherListener(Napi::Env env, AddonData* addonData); ~PathWatcherListener(); void handleFileAction( efsw::WatchID watchId, @@ -79,16 +80,30 @@ class PathWatcherListener: public efsw::FileWatchListener { ) override; void Stop(); + int id; private: + AddonData* addonData; std::atomic isShuttingDown{false}; std::mutex shutdownMutex; - Napi::Function callback; - Napi::ThreadSafeFunction tsfn; + // Napi::Function callback; + // Napi::ThreadSafeFunction tsfn; }; void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEvent* event); +class PathWatcher : public Napi::Addon { +public: + PathWatcher(Napi::Env env, Napi::Object exports); + AddonData* addonData; + +private: + Napi::Value Watch(const Napi::CallbackInfo& info); + Napi::Value Unwatch(const Napi::CallbackInfo& info); + void SetCallback(const Napi::CallbackInfo& info); + void Cleanup(Napi::Env env); +}; + namespace EFSW { class Watcher { public: diff --git a/src/main.js b/src/main.js index 96cbced..44f3663 100644 --- a/src/main.js +++ b/src/main.js @@ -8,6 +8,8 @@ const { Emitter } = require('event-kit'); const fs = require('fs'); const path = require('path'); +let initialized = false; + const HANDLE_WATCHERS = new Map(); function wait (ms) { @@ -102,7 +104,7 @@ class HandleWatcher { start () { let troubleWatcher; - this.handle = binding.watch(this.path, callback); + this.handle = binding.watch(this.path); if (HANDLE_WATCHERS.has(this.handle)) { troubleWatcher = HANDLE_WATCHERS.get(this.handle); troubleWatcher.close(); @@ -247,7 +249,7 @@ class PathWatcher { } } -async function callback(event, handle, filePath, oldFilePath) { +async function DEFAULT_CALLBACK(event, handle, filePath, oldFilePath) { if (!HANDLE_WATCHERS.has(handle)) return; let watcher = HANDLE_WATCHERS.get(handle); @@ -255,6 +257,11 @@ async function callback(event, handle, filePath, oldFilePath) { } function watch (pathToWatch, callback) { + console.log('binding:', binding); + if (!initialized) { + binding.setCallback(DEFAULT_CALLBACK); + initialized = true; + } return new PathWatcher(path.resolve(pathToWatch), callback); } From 7ba69ea1fc2f452cfb2048aecf4bd3f308206ca2 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 18 Oct 2024 21:13:18 -0700 Subject: [PATCH 121/168] =?UTF-8?q?Streamline=20the=20JS=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …and use real paths in all interactions with the native module. --- binding.gyp | 1 - lib/core.cc | 362 ++++++++++++++++++--------------------- lib/core.h | 63 +++---- spec/directory-spec.js | 3 +- spec/pathwatcher-spec.js | 8 +- src/main.js | 96 ++++++----- 6 files changed, 256 insertions(+), 277 deletions(-) diff --git a/binding.gyp b/binding.gyp index b66b1b7..f3b7e7c 100644 --- a/binding.gyp +++ b/binding.gyp @@ -111,7 +111,6 @@ "VCCLCompilerTool": {"ExceptionHandling": 1}, }, "sources": [ - "lib/binding.cc", "lib/core.cc", "lib/core.h" ], diff --git a/lib/core.cc b/lib/core.cc index d93f39d..215727b 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -1,39 +1,19 @@ #include "core.h" -#include "addon-data.h" #include "include/efsw/efsw.hpp" #include "napi.h" +#include #include -#include + #ifdef __APPLE__ #include #endif -static int unique_id = 1; - -PathWatcherListener::PathWatcherListener(Napi::Env env, AddonData* addonData): addonData(addonData) { - id = unique_id++; -} - -PathWatcherListener::~PathWatcherListener() { - std::cout << "PathWatcherListener destructor! " << id << std::endl; - Stop(); -} - -void PathWatcherListener::Stop() { - if (!addonData) return; - addonData = nullptr; - // std::cout << "PathWatcherListener::Stop! " << id << std::endl; - // if (isShuttingDown) return; - // // Prevent responders from acting while we shut down. - // std::lock_guard lock(shutdownMutex); - // if (isShuttingDown) return; - // isShuttingDown = true; - // // if (tsfn) { - // // tsfn.Release(); - // // } -} +// There's exactly one instance of `ThreadSafeFunction` per environment. To +// avoid passing it into the `PathWatcherListener` class, we'll store it in a +// map that's keyed on an identifier that is unique to each `Env`. +static std::unordered_map tsfns; -std::string EventType(efsw::Action action, bool isChild) { +static std::string EventType(efsw::Action action, bool isChild) { switch (action) { case efsw::Actions::Add: return isChild ? "child-create" : "create"; @@ -44,11 +24,83 @@ std::string EventType(efsw::Action action, bool isChild) { case efsw::Actions::Moved: return isChild ? "child-rename" : "rename"; default: - // std::cout << "Unknown action: " << action; return "unknown"; } } +// This is a bit hacky, but it allows us to stop invoking callbacks more +// quickly when the environment is terminating. +bool EnvIsStopping(Napi::Env env) { + PathWatcher* pw = env.GetInstanceData(); + return pw->isStopping; +} + +// Ensure a given path has a trailing separator for comparison purposes. +std::string NormalizePath(std::string path) { + if (path.back() == PATH_SEPARATOR) return path; + return path + PATH_SEPARATOR; +} + +bool PathsAreEqual(std::string pathA, std::string pathB) { + return NormalizePath(pathA) == NormalizePath(pathB); +} + +// This is the main-thread function that receives all `ThreadSafeFunction` +// calls. It converts the `PathWatcherEvent` struct into JS values before +// invoking our callback. +static void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEvent* event) { + // Translate the event type to the expected event name in the JS code. + // + // NOTE: This library previously envisioned that some platforms would allow + // watching of files directly and some would require watching of a file's + // parent folder. EFSW uses the parent-folder approach on all platforms, so + // in practice we're not using half of the event names we used to use. That's + // why the second argument below is `true`. + // + // There might be some edge cases that we need to handle here; for instance, + // if we're watching a directory and that directory itself is deleted, then + // that should be `delete` rather than `child-delete`. Right now we deal with + // that in JavaScript, but we could handle it here instead. + std::string eventName = EventType(event->type, true); + + if (EnvIsStopping(env)) return; + + std::string newPath; + std::string oldPath; + + if (!event->new_path.empty()) { + newPath.assign(event->new_path.begin(), event->new_path.end()); + } + + if (!event->old_path.empty()) { + oldPath.assign(event->old_path.begin(), event->old_path.end()); + } + + // Use a try-catch block only for the Node-API call, which might throw + try { + callback.Call({ + Napi::String::New(env, eventName), + Napi::Number::New(env, event->handle), + Napi::String::New(env, newPath), + Napi::String::New(env, oldPath) + }); + } catch (const Napi::Error& e) { + // TODO: Unsure why this would happen. + Napi::TypeError::New(env, "Unknown error handling filesystem event").ThrowAsJavaScriptException(); + } +} + +PathWatcherListener::PathWatcherListener(Napi::Env env, int id, std::string realPath): + envId(id), realPath(realPath) {} + +void PathWatcherListener::Stop() { + if (isShuttingDown) return; + // Prevent responders from acting while we shut down. + std::lock_guard lock(shutdownMutex); + if (isShuttingDown) return; + isShuttingDown = true; +} + void PathWatcherListener::handleFileAction( efsw::WatchID watchId, const std::string& dir, @@ -56,15 +108,24 @@ void PathWatcherListener::handleFileAction( efsw::Action action, std::string oldFilename ) { - if (!addonData) return; + // std::cout << "PathWatcherListener::handleFileAction dir: " << dir << " filename: " << filename << std::endl; // Don't try to proceed if we've already started the shutdown process. if (isShuttingDown) return; std::lock_guard lock(shutdownMutex); if (isShuttingDown) return; + std::string newPathStr = dir + filename; std::vector newPath(newPathStr.begin(), newPathStr.end()); + if (PathsAreEqual(newPathStr, realPath)) { + // This is an event that is happening to the directory itself — like the + // directory being deleted. Allow it through. + } else if (dir != realPath) { + // std::cout << "Event in subdirectory; skipping!" << std::endl; + return; + } + #ifdef __APPLE__ // macOS seems to think that lots of file creations happen that aren't // actually creations; for instance, multiple successive writes to the same @@ -79,7 +140,6 @@ void PathWatcherListener::handleFileAction( return; } if (file.st_birthtimespec.tv_sec != file.st_mtimespec.tv_sec) { - // std::cout << "Skipping spurious creation event!" << std::endl; return; } } @@ -91,7 +151,9 @@ void PathWatcherListener::handleFileAction( oldPath.assign(oldPathStr.begin(), oldPathStr.end()); } - Napi::ThreadSafeFunction tsfn = addonData->tsfn; + Napi::ThreadSafeFunction tsfn = tsfns[envId]; + // Inability to find `tsfn` in the map would be a possible indicator that + // this watcher has just been removed. if (!tsfn) return; napi_status status = tsfn.Acquire(); @@ -112,127 +174,30 @@ void PathWatcherListener::handleFileAction( } } -void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEvent* event) { - std::unique_ptr eventPtr(event); - if (event == nullptr) { - Napi::TypeError::New(env, "Unknown error handling filesystem event").ThrowAsJavaScriptException(); - return; - } - - // Translate the event type to the expected event name in the JS code. - // - // NOTE: This library previously envisioned that some platforms would allow - // watching of files directly and some would require watching of a file's - // parent folder. EFSW uses the parent-folder approach on all platforms, so - // in practice we're not using half of the event names we used to use. That's - // why the second argument below is `true`. - // - // There might be some edge cases that we need to handle here; for instance, - // if we're watching a directory and that directory itself is deleted, then - // that should be `delete` rather than `child-delete`. Right now we deal with - // that in JavaScript, but we could handle it here instead. - std::string eventName = EventType(event->type, true); - - std::string newPath; - std::string oldPath; - - if (!event->new_path.empty()) { - newPath.assign(event->new_path.begin(), event->new_path.end()); - // std::cout << "new path: " << newPath << std::endl; - } - - if (!event->old_path.empty()) { - oldPath.assign(event->old_path.begin(), event->old_path.end()); - // std::cout << "old path: " << oldPath << std::endl; - } - - // Use a try-catch block only for the Node-API call, which might throw - try { - callback.Call({ - Napi::String::New(env, eventName), - Napi::Number::New(env, event->handle), - Napi::String::New(env, newPath), - Napi::String::New(env, oldPath) - }); - } catch (const Napi::Error& e) { - // TODO: Unsure why this would happen. - Napi::TypeError::New(env, "Unknown error handling filesystem event").ThrowAsJavaScriptException(); - } -} - -Napi::Value EFSW::Watch(const Napi::CallbackInfo& info) { - auto env = info.Env(); - return env.Undefined(); -} - -Napi::Value EFSW::Unwatch(const Napi::CallbackInfo& info) { - auto env = info.Env(); - auto addonData = env.GetInstanceData(); - Napi::HandleScope scope(env); - - // Our sole argument must be a JavaScript number; we convert it to a watcher - // handle. - if (!IsV8ValueWatcherHandle(info[0])) { - Napi::TypeError::New(env, "Argument must be a number").ThrowAsJavaScriptException(); - return env.Null(); - } - - WatcherHandle handle = V8ValueToWatcherHandle(info[0].As()); - - // EFSW doesn’t mind if we give it a handle that it doesn’t recognize; it’ll - // just silently do nothing. - addonData->fileWatcher->removeWatch(handle); - - // Since we’re not listening anymore, we have to stop the associated - // `PathWatcherListener` or else Node will think there’s an open handle. - auto it = addonData->listeners.find(handle); - if (it != addonData->listeners.end()) { - it->second->Stop(); - addonData->listeners.erase(it); - } - - addonData->watchCount--; - if (addonData->watchCount == 0) { - // When this environment isn’t watching any files, we can stop the - // `FileWatcher` instance. We’ll start it up again if `Watch` is called. - EFSW::Cleanup(env); - } - - return env.Undefined(); -} - -void EFSW::Cleanup(Napi::Env env) { - std::cout << "EFSW::Cleanup" << std::endl; - auto addonData = env.GetInstanceData(); - if (addonData && addonData->fileWatcher) { - // Clean up all outstanding listeners. - for (auto& pair : addonData->listeners) { - pair.second->Stop(); - } - addonData->fileWatcher = nullptr; - } - delete addonData->fileWatcher; -} - -void EFSW::Init(Napi::Env env) { - // auto addonData = env.GetInstanceData(); - // addonData->watchCount = 0; -} +static int next_env_id = 1; PathWatcher::PathWatcher(Napi::Env env, Napi::Object exports) { - addonData = new AddonData(env); + envId = next_env_id++; + // std::cout << "PathWatcher initialized with ID:" << envId << std::endl; DefineAddon(exports, { InstanceMethod("watch", &PathWatcher::Watch), InstanceMethod("unwatch", &PathWatcher::Unwatch), InstanceMethod("setCallback", &PathWatcher::SetCallback) }); + + env.SetInstanceData(this); +} + +PathWatcher::~PathWatcher() { + // std::cout << "Finalizing PathWatcher with ID: " << envId << std::endl; + isFinalizing = true; + StopAllListeners(); } +// Watch a given path. Returns a handle. Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { auto env = info.Env(); - // auto addonData = env.GetInstanceData(); - Napi::HandleScope scope(env); // First argument must be a string. if (!info[0].IsString()) { @@ -243,61 +208,74 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { Napi::String path = info[0].ToString(); std::string cppPath(path); - if (addonData->callback.IsEmpty()) { + // std::cout << "PathWatcher::Watch path: [" << cppPath << "]" << std::endl; + + std::string cppRealPath; + if (info[1].IsString()) { + Napi::String realPath = info[1].ToString(); + cppRealPath = realPath; + // std::cout << "Real path is: [" << cppRealPath << "]" << std::endl; + } else { + cppRealPath = ""; + } + + if (callback.IsEmpty()) { Napi::TypeError::New(env, "No callback set").ThrowAsJavaScriptException(); return env.Null(); } - if (!addonData->fileWatcher) { - // addonData->tsfn = - addonData->tsfn = Napi::ThreadSafeFunction::New( + if (listeners.size() == 0) { + // std::cout << "PathWatcher::Watch creating TSFN" << std::endl; + auto tsfn = Napi::ThreadSafeFunction::New( env, - addonData->callback.Value(), + callback.Value(), "pathwatcher-efsw-listener", 0, 1, - [](Napi::Env env) { - std::cout << "Testing finalizer" << std::endl; + [this](Napi::Env env) { + // std::cout << "Finalizing tsfn" << std::endl; + StopAllListeners(); } ); + tsfns[envId] = tsfn; } - PathWatcherListener* listener = new PathWatcherListener(env, addonData); + PathWatcherListener* listener = new PathWatcherListener(env, envId, cppRealPath == "" ? cppPath : cppRealPath); // The first call to `Watch` initializes a `FileWatcher`. - if (!addonData->fileWatcher) { - addonData->fileWatcher = new efsw::FileWatcher(); - addonData->fileWatcher->followSymlinks(true); - addonData->fileWatcher->watch(); + if (listeners.size() == 0) { + fileWatcher = new efsw::FileWatcher(); + fileWatcher->followSymlinks(true); + fileWatcher->watch(); } // EFSW represents watchers as unsigned `int`s; we can easily convert these // to JavaScript. - WatcherHandle handle = addonData->fileWatcher->addWatch(path, listener, true); - + WatcherHandle handle = fileWatcher->addWatch( + cppRealPath == "" ? cppPath : cppRealPath, + listener, + false + ); + // std::cout << "Adding watcher at path: " << cppPath << std::endl; + + // std::cout << "Listener handle: " << handle << std::endl; if (handle >= 0) { - addonData->listeners[handle] = listener; + listeners[handle] = listener; } else { - delete listener; + // delete listener; Napi::Error::New(env, "Failed to add watch; unknown error").ThrowAsJavaScriptException(); return env.Null(); } - addonData->watchCount++; - // The `watch` function returns a JavaScript number much like `setTimeout` or // `setInterval` would; this is the handle that the consumer can use to // unwatch the path later. return WatcherHandleToV8Value(handle, env); } +// Unwatch the given handle. Napi::Value PathWatcher::Unwatch(const Napi::CallbackInfo& info) { - std::cout << "Unwatch!" << std::endl; auto env = info.Env(); - Napi::HandleScope scope(env); - - // Our sole argument must be a JavaScript number; we convert it to a watcher - // handle. if (!IsV8ValueWatcherHandle(info[0])) { Napi::TypeError::New(env, "Argument must be a number").ThrowAsJavaScriptException(); return env.Null(); @@ -307,47 +285,38 @@ Napi::Value PathWatcher::Unwatch(const Napi::CallbackInfo& info) { // EFSW doesn’t mind if we give it a handle that it doesn’t recognize; it’ll // just silently do nothing. - addonData->fileWatcher->removeWatch(handle); + fileWatcher->removeWatch(handle); // Since we’re not listening anymore, we have to stop the associated - // `PathWatcherListener` or else Node will think there’s an open handle. - auto it = addonData->listeners.find(handle); - if (it != addonData->listeners.end()) { + // `PathWatcherListener` so that we know when to invoke cleanup and close the + // open handle. + auto it = listeners.find(handle); + if (it != listeners.end()) { it->second->Stop(); - std::cout << "Erasing listener with handle " << handle << std::endl; - addonData->listeners.erase(it); + listeners.erase(it); } - addonData->watchCount--; - if (addonData->watchCount == 0) { - // When this environment isn’t watching any files, we can stop the - // `FileWatcher` instance. We’ll start it up again if `Watch` is called. + if (listeners.size() == 0) { Cleanup(env); } return env.Undefined(); } -void PathWatcher::Cleanup(Napi::Env env) { - std::cout << "PathWatcher::Cleanup" << std::endl; - // auto addonData = env.GetInstanceData(); - - if (addonData && addonData->fileWatcher) { - // Clean up all outstanding listeners. - for (auto& pair : addonData->listeners) { - pair.second->Stop(); - } - addonData->fileWatcher = nullptr; +void PathWatcher::StopAllListeners() { + // callback.Reset(); + for (auto& it: listeners) { + fileWatcher->removeWatch(it.first); + it.second->Stop(); } - if (addonData->tsfn) { - addonData->tsfn.Unref(env); - // delete addonData->tsfn; - addonData->tsfn = NULL; - } - delete addonData->fileWatcher; + listeners.clear(); } - +// Set the JavaScript callback that will be invoked whenever a file changes. +// +// The user-facing API allows for an arbitrary number of different callbacks; +// this is an internal API for the wrapping JavaScript to use. That internal +// callback can multiplex to however many other callbacks need to be invoked. void PathWatcher::SetCallback(const Napi::CallbackInfo& info) { auto env = info.Env(); if (!info[0].IsFunction()) { @@ -355,7 +324,18 @@ void PathWatcher::SetCallback(const Napi::CallbackInfo& info) { } Napi::Function fn = info[0].As(); - addonData->callback = Napi::Persistent(fn); + callback.Reset(); + callback = Napi::Persistent(fn); +} + +void PathWatcher::Cleanup(Napi::Env env) { + if (!isFinalizing) { + // The `ThreadSafeFunction` is the thing that will keep the environment + // from terminating if we keep it open. When there are no active watchers, + // we should release `tsfn`; when we add a new watcher thereafter, we can + // create a new `tsfn`. + tsfns[envId].Abort(); + } } NODE_API_ADDON(PathWatcher) diff --git a/lib/core.h b/lib/core.h index 8e53a7f..62322b8 100644 --- a/lib/core.h +++ b/lib/core.h @@ -1,25 +1,19 @@ #pragma once -#define DEBUG 1 #include #include #include #include - #include "../vendor/efsw/include/efsw/efsw.hpp" -typedef efsw::WatchID WatcherHandle; - -#define WatcherHandleToV8Value(h, e) Napi::Number::New(e, h) -#define V8ValueToWatcherHandle(v) v.Int32Value() -#define IsV8ValueWatcherHandle(v) v.IsNumber() - #ifdef _WIN32 -#define PATH_SEPARATOR "\\" +#define PATH_SEPARATOR '\\' #else -#define PATH_SEPARATOR "/" +#define PATH_SEPARATOR '/' #endif +typedef efsw::WatchID WatcherHandle; + struct PathWatcherEvent { efsw::Action type; efsw::WatchID handle; @@ -65,12 +59,10 @@ struct PathWatcherEvent { } }; -class AddonData; - class PathWatcherListener: public efsw::FileWatchListener { public: - PathWatcherListener(Napi::Env env, AddonData* addonData); - ~PathWatcherListener(); + PathWatcherListener(Napi::Env env, int id, std::string realPath); + // ~PathWatcherListener(); void handleFileAction( efsw::WatchID watchId, const std::string& dir, @@ -80,48 +72,39 @@ class PathWatcherListener: public efsw::FileWatchListener { ) override; void Stop(); - int id; private: - AddonData* addonData; + int envId; std::atomic isShuttingDown{false}; std::mutex shutdownMutex; + std::string realPath; // Napi::Function callback; // Napi::ThreadSafeFunction tsfn; }; -void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEvent* event); + +#define WatcherHandleToV8Value(h, e) Napi::Number::New(e, h) +#define V8ValueToWatcherHandle(v) v.Int32Value() +#define IsV8ValueWatcherHandle(v) v.IsNumber() class PathWatcher : public Napi::Addon { public: PathWatcher(Napi::Env env, Napi::Object exports); - AddonData* addonData; + ~PathWatcher(); + + bool isStopping = false; private: Napi::Value Watch(const Napi::CallbackInfo& info); Napi::Value Unwatch(const Napi::CallbackInfo& info); void SetCallback(const Napi::CallbackInfo& info); void Cleanup(Napi::Env env); + void StopAllListeners(); + + int envId; + bool isFinalizing = false; + Napi::FunctionReference callback; + Napi::ThreadSafeFunction tsfn; + std::unordered_map listeners; + efsw::FileWatcher* fileWatcher = nullptr; }; - -namespace EFSW { - class Watcher { - public: - Watcher(const char* path, Napi::Function fn, Napi::Env env); - ~Watcher(); - - WatcherHandle Start(); - void Stop(); - private: - const char* path; - Napi::Env env; - Napi::FunctionReference callback; - }; - - void Init(Napi::Env env); - void Cleanup(Napi::Env env); - - Napi::Value Watch(const Napi::CallbackInfo& info); - Napi::Value Unwatch(const Napi::CallbackInfo& info); - Napi::Value SetCallback(const Napi::CallbackInfo& info); -} diff --git a/spec/directory-spec.js b/spec/directory-spec.js index a4c8234..f961804 100644 --- a/spec/directory-spec.js +++ b/spec/directory-spec.js @@ -18,9 +18,10 @@ describe('Directory', () => { directory = new Directory(path.join(__dirname, 'fixtures')); }); - afterEach(() => { + afterEach(async () => { PathWatcher.closeAllWatchers(); isCaseInsensitiveSpy.and.callThrough(); + await wait(100); }); it('normalizes the specified path', () => { diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index 26625a4..c8e2656 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -13,7 +13,8 @@ describe('PathWatcher', () => { beforeEach(() => fs.writeFileSync(tempFile, '')); afterEach(async () => { - await PathWatcher.closeAllWatchers(); + PathWatcher.closeAllWatchers(); + await wait(100); }); describe('getWatchedPaths', () => { @@ -103,17 +104,16 @@ describe('PathWatcher', () => { let fileUnderDir = path.join(tempDir, 'file'); fs.writeFileSync(fileUnderDir, ''); let done = false; + PathWatcher.watch(tempDir, (type, path) => { expect(type).toBe('change'); expect(path).toBe(''); done = true; }); - fs.writeFileSync(fileUnderDir, 'what'); - await wait(2000); + await wait(200); fs.unlinkSync(fileUnderDir); await condition(() => done); - }); }); diff --git a/src/main.js b/src/main.js index 44f3663..56625be 100644 --- a/src/main.js +++ b/src/main.js @@ -6,6 +6,7 @@ try { } const { Emitter } = require('event-kit'); const fs = require('fs'); +const { stat } = require('fs/promises'); const path = require('path'); let initialized = false; @@ -16,8 +17,16 @@ function wait (ms) { return new Promise(r => setTimeout(r, ms)); } -class HandleWatcher { +function normalizePath(rawPath) { + if (!rawPath.endsWith(path.sep)) return rawPath; + return rawPath.substring(0, rawPath.length - 1); +} +function pathsAreEqual(pathA, pathB) { + return normalizePath(pathA) == normalizePath(pathB); +} + +class HandleWatcher { constructor(path) { this.path = path; this.emitter = new Emitter(); @@ -31,39 +40,36 @@ class HandleWatcher { switch (event) { case 'rename': this.close(); - let detectRename = () => { - return fs.stat( - this.path, - (err) => { - if (err) { - this.path = filePath; - if (process.platform === 'darwin' && /\/\.Trash\//.test(filePath)) { - this.emitter.emit( - 'did-change', - { event: 'delete', newFilePath: null } - ); - this.close(); - return; - } else { - this.start(); - this.emitter.emit( - 'did-change', - { event: 'rename', newFilePath: filePath } - ); - return; - } - } else { - this.start(); - this.emitter.emit( - 'did-change', - { event: 'change', newFilePath: null } - ); - return; - } - } + await wait(100); + try { + await stat(this.path); + // File still exists at the same path. + this.start(); + this.emitter.emit( + 'did-change', + { event: 'change', newFilePath: null } ); - }; - setTimeout(detectRename, 100); + } catch (err) { + // File does not exist at the old path. + this.path = filePath; + if (process.platform === 'darwin' && /\/\.Trash\//.test(filePath)) { + // We'll treat this like a deletion; no point in continuing to + // track this file when it's in the trash. + this.emitter.emit( + 'did-change', + { event: 'delete', newFilePath: null } + ); + this.close(); + } else { + // The file has a new location, so let's resume watching it from + // there. + this.start(); + this.emitter.emit( + 'did-change', + { event: 'rename', newFilePath: filePath } + ); + } + } return; case 'delete': // Wait for a very short interval to protect against brief deletions or @@ -104,6 +110,9 @@ class HandleWatcher { start () { let troubleWatcher; + if (!this.path.endsWith(path.sep)) { + this.path += path.sep; + } this.handle = binding.watch(this.path); if (HANDLE_WATCHERS.has(this.handle)) { troubleWatcher = HANDLE_WATCHERS.get(this.handle); @@ -141,6 +150,14 @@ class PathWatcher { } this.assignRealPath(); + + // Resolve the real path before we pass it to the native watcher. It's + // better at dealing with real paths instead of symlinks and it doesn't + // otherwise matter for our purposes. + if (this.realPath) { + filePath = this.realPath; + } + this.emitter = new Emitter(); let stats = fs.statSync(filePath); @@ -149,12 +166,14 @@ class PathWatcher { if (this.isWatchingParent) { filePath = path.dirname(filePath); } + for (let watcher of HANDLE_WATCHERS.values()) { - if (watcher.path === filePath) { + if (pathsAreEqual(watcher.path, filePath)) { this.handleWatcher = watcher; break; } } + this.handleWatcher ??= new HandleWatcher(filePath); this.onChange = ({ event, newFilePath, oldFilePath, rawFilePath }) => { @@ -249,7 +268,7 @@ class PathWatcher { } } -async function DEFAULT_CALLBACK(event, handle, filePath, oldFilePath) { +function DEFAULT_CALLBACK(event, handle, filePath, oldFilePath) { if (!HANDLE_WATCHERS.has(handle)) return; let watcher = HANDLE_WATCHERS.get(handle); @@ -257,7 +276,6 @@ async function DEFAULT_CALLBACK(event, handle, filePath, oldFilePath) { } function watch (pathToWatch, callback) { - console.log('binding:', binding); if (!initialized) { binding.setCallback(DEFAULT_CALLBACK); initialized = true; @@ -265,13 +283,11 @@ function watch (pathToWatch, callback) { return new PathWatcher(path.resolve(pathToWatch), callback); } -async function closeAllWatchers () { - let promises = []; +function closeAllWatchers () { for (let watcher of HANDLE_WATCHERS.values()) { - promises.push(watcher?.close()); + watcher?.close(); } HANDLE_WATCHERS.clear(); - await Promise.allSettled(promises); } function getWatchedPaths () { From a1d56d178a629ec6c90e7d239eef0a1bd0411790 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 18 Oct 2024 21:14:18 -0700 Subject: [PATCH 122/168] Remove unnecessary files --- lib/addon-data.h | 25 ------------------------- lib/binding.cc | 18 ------------------ 2 files changed, 43 deletions(-) delete mode 100644 lib/addon-data.h delete mode 100644 lib/binding.cc diff --git a/lib/addon-data.h b/lib/addon-data.h deleted file mode 100644 index 4ec103a..0000000 --- a/lib/addon-data.h +++ /dev/null @@ -1,25 +0,0 @@ -#include "core.h" -#include "napi.h" -#pragma once - -static int g_next_addon_data_id = 1; - -class AddonData final { -public: - explicit AddonData(Napi::Env env) { - id = g_next_addon_data_id++; - } - - // A unique identifier for each environment. - int id; - // The number of watchers active in this environment. - int watchCount = 0; - efsw::FileWatcher* fileWatcher = nullptr; - - Napi::FunctionReference callback; - Napi::ThreadSafeFunction tsfn; - - // A map that associates `WatcherHandle` values with their - // `PathWatcherListener` instances. - std::unordered_map listeners; -}; diff --git a/lib/binding.cc b/lib/binding.cc deleted file mode 100644 index 503ba1a..0000000 --- a/lib/binding.cc +++ /dev/null @@ -1,18 +0,0 @@ -#include "core.h" -#include "addon-data.h" - -// Napi::Object Init(Napi::Env env, Napi::Object exports) { -// AddonData* addonData = new AddonData(env); -// env.SetInstanceData(addonData); -// -// EFSW::Init(env); -// -// // exports.Set("setCallback", Napi::Function::New(env, EFSW::SetCallback)); -// exports.Set("watch", Napi::Function::New(env, EFSW::Watch)); -// exports.Set("unwatch", Napi::Function::New(env, EFSW::Unwatch)); -// -// return exports; -// } - - -// NODE_API_MODULE(pathwatcher_efsw, Init) From 1b4d53bcec7f537fafc9e42cdedb0dab2429484d Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 18 Oct 2024 21:18:31 -0700 Subject: [PATCH 123/168] Add `.tool-versions` to `.gitignore` --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 60622ea..6b3934f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ build/ node_modules/ .node-version +.tool-versions npm-debug.log api.json package-lock.json From a6c5b1d0abb0c825f878110dc5836c2220e90f42 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 18 Oct 2024 23:33:47 -0700 Subject: [PATCH 124/168] Add caveat in `README` and `engines` field in `package.json` --- README.md | 6 ++++++ package.json | 3 +++ 2 files changed, 9 insertions(+) diff --git a/README.md b/README.md index b1ca46b..049224b 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,12 @@ npm install pathwatcher * Run `npm install` to install the dependencies * Run `npm test` to run the specs +## Caveats + +This module is context-aware and context-safe; it can be used from multiple worker threads in the same process. If you keep a file-watcher active, though, it’ll keep the environment from closing; you must stop all watchers if you want your script or thread to finish. + +If you’re using it in an Electron renderer process, you must take extra care in page reloading scenarios. Be sure to use `closeAllWatchers` well before the page environment is terminated — e.g., by attaching a `beforeunload` listener. + ## Using ```js diff --git a/package.json b/package.json index ddfabd1..d2369d4 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "url": "http://github.com/atom/node-pathwatcher/raw/master/LICENSE.md" } ], + "engines": { + "node": ">=14" + }, "repository": { "type": "git", "url": "https://github.com/atom/node-pathwatcher.git" From 0f15b6ff3aa108c9d375e4adbf1e5a056656bf4e Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sat, 19 Oct 2024 11:15:34 -0700 Subject: [PATCH 125/168] Guard access to the `tsfns` map with a mutex --- lib/core.cc | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/lib/core.cc b/lib/core.cc index 215727b..187f044 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -12,6 +12,7 @@ // avoid passing it into the `PathWatcherListener` class, we'll store it in a // map that's keyed on an identifier that is unique to each `Env`. static std::unordered_map tsfns; +static std::mutex tsfnMutex; static std::string EventType(efsw::Action action, bool isChild) { switch (action) { @@ -151,10 +152,14 @@ void PathWatcherListener::handleFileAction( oldPath.assign(oldPathStr.begin(), oldPathStr.end()); } - Napi::ThreadSafeFunction tsfn = tsfns[envId]; - // Inability to find `tsfn` in the map would be a possible indicator that - // this watcher has just been removed. - if (!tsfn) return; + Napi::ThreadSafeFunction tsfn; + { + std::lock_guard lock(tsfnMutex); + tsfn = tsfns[envId]; + // Inability to find `tsfn` in the map would be a possible indicator that + // this watcher has just been removed. + if (!tsfn) return; + } napi_status status = tsfn.Acquire(); if (status != napi_ok) { @@ -190,7 +195,7 @@ PathWatcher::PathWatcher(Napi::Env env, Napi::Object exports) { } PathWatcher::~PathWatcher() { - // std::cout << "Finalizing PathWatcher with ID: " << envId << std::endl; + std::cout << "Finalizing PathWatcher with ID: " << envId << std::endl; isFinalizing = true; StopAllListeners(); } @@ -237,6 +242,7 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { StopAllListeners(); } ); + std::lock_guard lock(tsfnMutex); tsfns[envId] = tsfn; } @@ -330,11 +336,25 @@ void PathWatcher::SetCallback(const Napi::CallbackInfo& info) { void PathWatcher::Cleanup(Napi::Env env) { if (!isFinalizing) { + // `ThreadSafeFunction` wraps an internal `napi_threadsafe_function` that, + // in some occasional scenarios, might already be `null` by the time we get + // this far. It's not entirely understood why. But if that's true, we can + // skip this part instead of trying to abort a function that doesn't exist + // and causing a segfault. + std::lock_guard lock(tsfnMutex); + Napi::ThreadSafeFunction tsfn = tsfns[envId]; + napi_threadsafe_function _tsfn = tsfn; + if (_tsfn == nullptr) { + std::cout << "Skipping abort because null" << std::endl; + return; + } // The `ThreadSafeFunction` is the thing that will keep the environment // from terminating if we keep it open. When there are no active watchers, // we should release `tsfn`; when we add a new watcher thereafter, we can // create a new `tsfn`. - tsfns[envId].Abort(); + tsfn.Abort(); + } else { + std::cout << "Skipping abort because isFinalizing" << std::endl; } } From 1aa17c1df78fd8bb7b99b5d9242176d7e59a28b5 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sat, 19 Oct 2024 12:10:18 -0700 Subject: [PATCH 126/168] =?UTF-8?q?Return=20to=20`tsfn`s=20as=20class=20me?= =?UTF-8?q?mbers=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …plus general code cleanup. --- lib/core.cc | 82 ++++++++++++++++++----------------------------------- lib/core.h | 11 +++---- 2 files changed, 33 insertions(+), 60 deletions(-) diff --git a/lib/core.cc b/lib/core.cc index 187f044..f7fe130 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -8,12 +8,6 @@ #include #endif -// There's exactly one instance of `ThreadSafeFunction` per environment. To -// avoid passing it into the `PathWatcherListener` class, we'll store it in a -// map that's keyed on an identifier that is unique to each `Env`. -static std::unordered_map tsfns; -static std::mutex tsfnMutex; - static std::string EventType(efsw::Action action, bool isChild) { switch (action) { case efsw::Actions::Add: @@ -31,18 +25,18 @@ static std::string EventType(efsw::Action action, bool isChild) { // This is a bit hacky, but it allows us to stop invoking callbacks more // quickly when the environment is terminating. -bool EnvIsStopping(Napi::Env env) { +static bool EnvIsStopping(Napi::Env env) { PathWatcher* pw = env.GetInstanceData(); return pw->isStopping; } // Ensure a given path has a trailing separator for comparison purposes. -std::string NormalizePath(std::string path) { +static std::string NormalizePath(std::string path) { if (path.back() == PATH_SEPARATOR) return path; return path + PATH_SEPARATOR; } -bool PathsAreEqual(std::string pathA, std::string pathB) { +static bool PathsAreEqual(std::string pathA, std::string pathB) { return NormalizePath(pathA) == NormalizePath(pathB); } @@ -91,8 +85,11 @@ static void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEven } } -PathWatcherListener::PathWatcherListener(Napi::Env env, int id, std::string realPath): - envId(id), realPath(realPath) {} +PathWatcherListener::PathWatcherListener( + Napi::Env env, + std::string realPath, + Napi::ThreadSafeFunction tsfn +): realPath(realPath), tsfn(tsfn) {} void PathWatcherListener::Stop() { if (isShuttingDown) return; @@ -115,15 +112,16 @@ void PathWatcherListener::handleFileAction( std::lock_guard lock(shutdownMutex); if (isShuttingDown) return; - std::string newPathStr = dir + filename; std::vector newPath(newPathStr.begin(), newPathStr.end()); if (PathsAreEqual(newPathStr, realPath)) { // This is an event that is happening to the directory itself — like the // directory being deleted. Allow it through. - } else if (dir != realPath) { - // std::cout << "Event in subdirectory; skipping!" << std::endl; + } else if (dir != NormalizePath(realPath)) { + // Otherwise, we would expect `dir` to be equal to `realPath`; if it isn't, + // then we should ignore it. This might be an event that happened to an + // ancestor folder or a descendent folder somehow. return; } @@ -152,15 +150,7 @@ void PathWatcherListener::handleFileAction( oldPath.assign(oldPathStr.begin(), oldPathStr.end()); } - Napi::ThreadSafeFunction tsfn; - { - std::lock_guard lock(tsfnMutex); - tsfn = tsfns[envId]; - // Inability to find `tsfn` in the map would be a possible indicator that - // this watcher has just been removed. - if (!tsfn) return; - } - + if (!tsfn) return; napi_status status = tsfn.Acquire(); if (status != napi_ok) { // We couldn't acquire the `tsfn`; it might be in the process of being @@ -183,7 +173,6 @@ static int next_env_id = 1; PathWatcher::PathWatcher(Napi::Env env, Napi::Object exports) { envId = next_env_id++; - // std::cout << "PathWatcher initialized with ID:" << envId << std::endl; DefineAddon(exports, { InstanceMethod("watch", &PathWatcher::Watch), @@ -210,28 +199,21 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { return env.Null(); } + // The wrapper JS will resolve this to the file's real path. We expect to be + // dealing with real locations on disk, since that's what EFSW will report to + // us anyway. Napi::String path = info[0].ToString(); std::string cppPath(path); - // std::cout << "PathWatcher::Watch path: [" << cppPath << "]" << std::endl; - - std::string cppRealPath; - if (info[1].IsString()) { - Napi::String realPath = info[1].ToString(); - cppRealPath = realPath; - // std::cout << "Real path is: [" << cppRealPath << "]" << std::endl; - } else { - cppRealPath = ""; - } - + // It's invalid to call `watch` before having set a callback via + // `setCallback`. if (callback.IsEmpty()) { Napi::TypeError::New(env, "No callback set").ThrowAsJavaScriptException(); return env.Null(); } if (listeners.size() == 0) { - // std::cout << "PathWatcher::Watch creating TSFN" << std::endl; - auto tsfn = Napi::ThreadSafeFunction::New( + tsfn = Napi::ThreadSafeFunction::New( env, callback.Value(), "pathwatcher-efsw-listener", @@ -242,11 +224,9 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { StopAllListeners(); } ); - std::lock_guard lock(tsfnMutex); - tsfns[envId] = tsfn; } - PathWatcherListener* listener = new PathWatcherListener(env, envId, cppRealPath == "" ? cppPath : cppRealPath); + PathWatcherListener* listener = new PathWatcherListener(env, cppPath, tsfn); // The first call to `Watch` initializes a `FileWatcher`. if (listeners.size() == 0) { @@ -257,18 +237,12 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { // EFSW represents watchers as unsigned `int`s; we can easily convert these // to JavaScript. - WatcherHandle handle = fileWatcher->addWatch( - cppRealPath == "" ? cppPath : cppRealPath, - listener, - false - ); - // std::cout << "Adding watcher at path: " << cppPath << std::endl; - - // std::cout << "Listener handle: " << handle << std::endl; + WatcherHandle handle = fileWatcher->addWatch(cppPath, listener, false); + if (handle >= 0) { listeners[handle] = listener; } else { - // delete listener; + delete listener; Napi::Error::New(env, "Failed to add watch; unknown error").ThrowAsJavaScriptException(); return env.Null(); } @@ -310,7 +284,10 @@ Napi::Value PathWatcher::Unwatch(const Napi::CallbackInfo& info) { } void PathWatcher::StopAllListeners() { - // callback.Reset(); + // This function is called internally in situations where we detect that the + // environment is terminating. At that point, it's not safe to try to release + // any `ThreadSafeFunction`s; but we can do the rest of the cleanup work + // here. for (auto& it: listeners) { fileWatcher->removeWatch(it.first); it.second->Stop(); @@ -341,11 +318,8 @@ void PathWatcher::Cleanup(Napi::Env env) { // this far. It's not entirely understood why. But if that's true, we can // skip this part instead of trying to abort a function that doesn't exist // and causing a segfault. - std::lock_guard lock(tsfnMutex); - Napi::ThreadSafeFunction tsfn = tsfns[envId]; napi_threadsafe_function _tsfn = tsfn; if (_tsfn == nullptr) { - std::cout << "Skipping abort because null" << std::endl; return; } // The `ThreadSafeFunction` is the thing that will keep the environment @@ -353,8 +327,6 @@ void PathWatcher::Cleanup(Napi::Env env) { // we should release `tsfn`; when we add a new watcher thereafter, we can // create a new `tsfn`. tsfn.Abort(); - } else { - std::cout << "Skipping abort because isFinalizing" << std::endl; } } diff --git a/lib/core.h b/lib/core.h index 62322b8..47d849c 100644 --- a/lib/core.h +++ b/lib/core.h @@ -61,8 +61,11 @@ struct PathWatcherEvent { class PathWatcherListener: public efsw::FileWatchListener { public: - PathWatcherListener(Napi::Env env, int id, std::string realPath); - // ~PathWatcherListener(); + PathWatcherListener( + Napi::Env env, + std::string realPath, + Napi::ThreadSafeFunction tsfn + ); void handleFileAction( efsw::WatchID watchId, const std::string& dir, @@ -74,12 +77,10 @@ class PathWatcherListener: public efsw::FileWatchListener { void Stop(); private: - int envId; std::atomic isShuttingDown{false}; std::mutex shutdownMutex; std::string realPath; - // Napi::Function callback; - // Napi::ThreadSafeFunction tsfn; + Napi::ThreadSafeFunction tsfn; }; From 3b75ea4753b345962fb75bb8fdf13c3cf9c50173 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sat, 19 Oct 2024 12:23:39 -0700 Subject: [PATCH 127/168] (oops) --- lib/core.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core.cc b/lib/core.cc index f7fe130..863e92d 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -184,7 +184,6 @@ PathWatcher::PathWatcher(Napi::Env env, Napi::Object exports) { } PathWatcher::~PathWatcher() { - std::cout << "Finalizing PathWatcher with ID: " << envId << std::endl; isFinalizing = true; StopAllListeners(); } @@ -220,7 +219,8 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { 0, 1, [this](Napi::Env env) { - // std::cout << "Finalizing tsfn" << std::endl; + // This is unexpected. We should try to do some cleanup before the + // environment terminates. StopAllListeners(); } ); From 65761df5d789df6fd2b798861ac00c4b7ad6f8eb Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 24 Oct 2024 15:02:37 -0700 Subject: [PATCH 128/168] =?UTF-8?q?Reduce=20redundant=20usage=20of=20nativ?= =?UTF-8?q?e=20watchers=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …by borrowing Atom/Pulsar’s approach of reusing native instances where possible. --- lib/core.cc | 117 +++-- lib/core.h | 11 +- spec/file-spec.js | 6 +- spec/native-watcher-registry-spec.js | 389 +++++++++++++++ spec/pathwatcher-spec.js | 100 +++- src/file.js | 10 +- src/main.js | 703 +++++++++++++++++++-------- src/native-watcher-registry.js | 555 +++++++++++++++++++++ 8 files changed, 1631 insertions(+), 260 deletions(-) create mode 100644 spec/native-watcher-registry-spec.js create mode 100644 src/native-watcher-registry.js diff --git a/lib/core.cc b/lib/core.cc index 863e92d..84f4049 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -87,9 +87,8 @@ static void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEven PathWatcherListener::PathWatcherListener( Napi::Env env, - std::string realPath, Napi::ThreadSafeFunction tsfn -): realPath(realPath), tsfn(tsfn) {} +): tsfn(tsfn) {} void PathWatcherListener::Stop() { if (isShuttingDown) return; @@ -99,6 +98,33 @@ void PathWatcherListener::Stop() { isShuttingDown = true; } +void PathWatcherListener::Stop(efsw::FileWatcher* fileWatcher) { + { + for (auto& it : paths) { + fileWatcher->removeWatch(it.first); + } + paths.clear(); + } + Stop(); +} + +void PathWatcherListener::AddPath(std::string path, efsw::WatchID handle) { + std::lock_guard lock(pathsMutex); + paths[handle] = path; +} + +void PathWatcherListener::RemovePath(efsw::WatchID handle) { + std::lock_guard lock(pathsMutex); + auto it = paths.find(handle); + if (it == paths.end()) return; + paths.erase(it); +} + +bool PathWatcherListener::IsEmpty() { + std::lock_guard lock(pathsMutex); + return paths.empty(); +} + void PathWatcherListener::handleFileAction( efsw::WatchID watchId, const std::string& dir, @@ -106,7 +132,22 @@ void PathWatcherListener::handleFileAction( efsw::Action action, std::string oldFilename ) { - // std::cout << "PathWatcherListener::handleFileAction dir: " << dir << " filename: " << filename << std::endl; + // std::cout << "PathWatcherListener::handleFileAction dir: " << dir << " filename: " << filename << " action: " << EventType(action, true) << std::endl; + + // Since we’re not listening anymore, we have to stop the associated + // `PathWatcherListener` so that we know when to invoke cleanup and close the + // open handle. + std::string realPath; + { + std::lock_guard lock(pathsMutex); + auto it = paths.find(watchId); + if (it == paths.end()) { + // Couldn't find watcher. Assume it's been removed. + return; + } + realPath = it->second; + } + // Don't try to proceed if we've already started the shutdown process. if (isShuttingDown) return; std::lock_guard lock(shutdownMutex); @@ -115,15 +156,15 @@ void PathWatcherListener::handleFileAction( std::string newPathStr = dir + filename; std::vector newPath(newPathStr.begin(), newPathStr.end()); - if (PathsAreEqual(newPathStr, realPath)) { - // This is an event that is happening to the directory itself — like the - // directory being deleted. Allow it through. - } else if (dir != NormalizePath(realPath)) { - // Otherwise, we would expect `dir` to be equal to `realPath`; if it isn't, - // then we should ignore it. This might be an event that happened to an - // ancestor folder or a descendent folder somehow. - return; - } + // if (PathsAreEqual(newPathStr, realPath)) { + // // This is an event that is happening to the directory itself — like the + // // directory being deleted. Allow it through. + // } else if (dir != NormalizePath(realPath)) { + // // Otherwise, we would expect `dir` to be equal to `realPath`; if it isn't, + // // then we should ignore it. This might be an event that happened to an + // // ancestor folder or a descendent folder somehow. + // return; + // } #ifdef __APPLE__ // macOS seems to think that lots of file creations happen that aren't @@ -132,7 +173,8 @@ void PathWatcherListener::handleFileAction( // each `child-change` event. // // Luckily, we can easily check whether or not a file has actually been - // created on macOS: we can compare creation time to modification time. + // created on macOS: we can compare creation time to modification time. This + // weeds out most of the false positives. if (action == efsw::Action::Add) { struct stat file; if (stat(newPathStr.c_str(), &file) != 0) { @@ -159,6 +201,11 @@ void PathWatcherListener::handleFileAction( } PathWatcherEvent* event = new PathWatcherEvent(action, watchId, newPath, oldPath); + + // TODO: Instead of calling `BlockingCall` once per event, throttle them by + // some small amount of time (like 50-100ms). That will allow us to deliver + // them in batches more efficiently — and for the wrapper JavaScript code to + // do some elimination of redundant events. status = tsfn.BlockingCall(event, ProcessEvent); tsfn.Release(); if (status != napi_ok) { @@ -211,7 +258,7 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { return env.Null(); } - if (listeners.size() == 0) { + if (!isWatching) { tsfn = Napi::ThreadSafeFunction::New( env, callback.Value(), @@ -224,25 +271,25 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { StopAllListeners(); } ); - } - PathWatcherListener* listener = new PathWatcherListener(env, cppPath, tsfn); + listener = new PathWatcherListener(env, tsfn); - // The first call to `Watch` initializes a `FileWatcher`. - if (listeners.size() == 0) { fileWatcher = new efsw::FileWatcher(); fileWatcher->followSymlinks(true); fileWatcher->watch(); + + isWatching = true; } + // EFSW represents watchers as unsigned `int`s; we can easily convert these // to JavaScript. - WatcherHandle handle = fileWatcher->addWatch(cppPath, listener, false); + WatcherHandle handle = fileWatcher->addWatch(cppPath, listener, true); if (handle >= 0) { - listeners[handle] = listener; + listener->AddPath(cppPath, handle); } else { - delete listener; + // if (listener->IsEmpty()) delete listener; Napi::Error::New(env, "Failed to add watch; unknown error").ThrowAsJavaScriptException(); return env.Null(); } @@ -261,23 +308,18 @@ Napi::Value PathWatcher::Unwatch(const Napi::CallbackInfo& info) { return env.Null(); } + if (!listener) return env.Undefined(); + WatcherHandle handle = V8ValueToWatcherHandle(info[0].As()); // EFSW doesn’t mind if we give it a handle that it doesn’t recognize; it’ll // just silently do nothing. fileWatcher->removeWatch(handle); + listener->RemovePath(handle); - // Since we’re not listening anymore, we have to stop the associated - // `PathWatcherListener` so that we know when to invoke cleanup and close the - // open handle. - auto it = listeners.find(handle); - if (it != listeners.end()) { - it->second->Stop(); - listeners.erase(it); - } - - if (listeners.size() == 0) { + if (listener->IsEmpty()) { Cleanup(env); + isWatching = false; } return env.Undefined(); @@ -288,11 +330,12 @@ void PathWatcher::StopAllListeners() { // environment is terminating. At that point, it's not safe to try to release // any `ThreadSafeFunction`s; but we can do the rest of the cleanup work // here. - for (auto& it: listeners) { - fileWatcher->removeWatch(it.first); - it.second->Stop(); - } - listeners.clear(); + if (!isWatching) return; + if (!listener) return; + listener->Stop(fileWatcher); + + delete fileWatcher; + isWatching = false; } // Set the JavaScript callback that will be invoked whenever a file changes. @@ -312,6 +355,8 @@ void PathWatcher::SetCallback(const Napi::CallbackInfo& info) { } void PathWatcher::Cleanup(Napi::Env env) { + StopAllListeners(); + if (!isFinalizing) { // `ThreadSafeFunction` wraps an internal `napi_threadsafe_function` that, // in some occasional scenarios, might already be `null` by the time we get diff --git a/lib/core.h b/lib/core.h index 47d849c..29e86c3 100644 --- a/lib/core.h +++ b/lib/core.h @@ -63,7 +63,6 @@ class PathWatcherListener: public efsw::FileWatchListener { public: PathWatcherListener( Napi::Env env, - std::string realPath, Napi::ThreadSafeFunction tsfn ); void handleFileAction( @@ -74,13 +73,18 @@ class PathWatcherListener: public efsw::FileWatchListener { std::string oldFilename ) override; + void AddPath(std::string path, efsw::WatchID handle); + void RemovePath(efsw::WatchID handle); + bool IsEmpty(); void Stop(); + void Stop(efsw::FileWatcher* fileWatcher); private: std::atomic isShuttingDown{false}; std::mutex shutdownMutex; - std::string realPath; + std::mutex pathsMutex; Napi::ThreadSafeFunction tsfn; + std::unordered_map paths; }; @@ -104,8 +108,9 @@ class PathWatcher : public Napi::Addon { int envId; bool isFinalizing = false; + bool isWatching = false; Napi::FunctionReference callback; Napi::ThreadSafeFunction tsfn; - std::unordered_map listeners; + PathWatcherListener* listener; efsw::FileWatcher* fileWatcher = nullptr; }; diff --git a/spec/file-spec.js b/spec/file-spec.js index 74d59e0..d04c775 100644 --- a/spec/file-spec.js +++ b/spec/file-spec.js @@ -20,7 +20,7 @@ describe('File', () => { afterEach(async () => { file.unsubscribeFromNativeChangeEvents(); fs.removeSync(filePath); - await PathWatcher.closeAllWatchers(); + PathWatcher.closeAllWatchers(); // Without a brief pause between tests, events from previous tests can echo // into the current ones. await wait(50); @@ -218,10 +218,8 @@ describe('File', () => { file.onDidRename(moveHandler); await wait(1000); - fs.moveSync(filePath, newPath); - - await condition(() => moveHandler.calls.count() > 0, 30000); + await condition(() => moveHandler.calls.count() > 0); expect(file.getPath()).toBe(newPath); }); diff --git a/spec/native-watcher-registry-spec.js b/spec/native-watcher-registry-spec.js new file mode 100644 index 0000000..8e11395 --- /dev/null +++ b/spec/native-watcher-registry-spec.js @@ -0,0 +1,389 @@ +const path = require('path'); +const { Emitter } = require('event-kit'); +const { NativeWatcherRegistry } = require('../src/native-watcher-registry'); + +function findRootDirectory() { + let current = process.cwd(); + while (true) { + let next = path.resolve(current, '..'); + if (next === current) { + return next; + } else { + current = next; + } + } +} + +const ROOT = findRootDirectory(); + +function absolute(...parts) { + const candidate = path.join(...parts); + return path.isAbsolute(candidate) ? candidate : path.join(ROOT, candidate); +} + +function parts(fullPath) { + return fullPath.split(path.sep).filter(part => part.length > 0); +} + +class MockWatcher { + constructor(normalizedPath) { + this.normalizedPath = normalizedPath; + this.native = null; + } + + getNormalizedPathPromise() { + return Promise.resolve(this.normalizedPath); + } + + getNormalizedPath () { + return this.normalizedPath; + } + + attachToNative(native, nativePath) { + if (this.normalizedPath.startsWith(nativePath)) { + if (this.native) { + this.native.attached = this.native.attached.filter( + each => each !== this + ); + } + this.native = native; + this.native.attached.push(this); + } + } +} + +class MockNative { + constructor(name) { + this.name = name; + this.attached = []; + this.disposed = false; + this.stopped = false; + + this.emitter = new Emitter(); + } + + reattachTo(newNative, nativePath) { + for (const watcher of this.attached) { + watcher.attachToNative(newNative, nativePath); + } + } + + onWillStop(callback) { + return this.emitter.on('will-stop', callback); + } + + dispose() { + this.disposed = true; + } + + stop() { + this.stopped = true; + this.emitter.emit('will-stop'); + } +} + +describe('NativeWatcherRegistry', function() { + let createNative, registry; + + beforeEach(function() { + registry = new NativeWatcherRegistry(normalizedPath => + createNative(normalizedPath) + ); + }); + + it('attaches a Watcher to a newly created NativeWatcher for a new directory', async function() { + const watcher = new MockWatcher(absolute('some', 'path')); + const NATIVE = new MockNative('created'); + createNative = () => NATIVE; + + await registry.attach(watcher); + + expect(watcher.native).toBe(NATIVE); + }); + + it('reuses an existing NativeWatcher on the same directory', async function() { + const EXISTING = new MockNative('existing'); + const existingPath = absolute('existing', 'path'); + let firstTime = true; + createNative = () => { + if (firstTime) { + firstTime = false; + return EXISTING; + } + + return new MockNative('nope'); + }; + await registry.attach(new MockWatcher(existingPath)); + + const watcher = new MockWatcher(existingPath); + await registry.attach(watcher); + + expect(watcher.native).toBe(EXISTING); + }); + + it('attaches to an existing NativeWatcher on a parent directory', async function() { + const EXISTING = new MockNative('existing'); + const parentDir = absolute('existing', 'path'); + const subDir = path.join(parentDir, 'sub', 'directory'); + let firstTime = true; + createNative = () => { + if (firstTime) { + firstTime = false; + return EXISTING; + } + + return new MockNative('nope'); + }; + await registry.attach(new MockWatcher(parentDir)); + + const watcher = new MockWatcher(subDir); + await registry.attach(watcher); + + expect(watcher.native).toBe(EXISTING); + }); + + it('adopts Watchers from NativeWatchers on child directories', async function() { + const parentDir = absolute('existing', 'path'); + const childDir0 = path.join(parentDir, 'child', 'directory', 'zero'); + const childDir1 = path.join(parentDir, 'child', 'directory', 'one'); + const otherDir = absolute('another', 'path'); + + const CHILD0 = new MockNative('existing0'); + const CHILD1 = new MockNative('existing1'); + const OTHER = new MockNative('existing2'); + const PARENT = new MockNative('parent'); + + createNative = dir => { + if (dir === childDir0) { + return CHILD0; + } else if (dir === childDir1) { + return CHILD1; + } else if (dir === otherDir) { + return OTHER; + } else if (dir === parentDir) { + return PARENT; + } else { + throw new Error(`Unexpected path: ${dir}`); + } + }; + + const watcher0 = new MockWatcher(childDir0); + await registry.attach(watcher0); + + const watcher1 = new MockWatcher(childDir1); + await registry.attach(watcher1); + + const watcher2 = new MockWatcher(otherDir); + await registry.attach(watcher2); + + expect(watcher0.native).toBe(CHILD0); + expect(watcher1.native).toBe(CHILD1); + expect(watcher2.native).toBe(OTHER); + + // Consolidate all three watchers beneath the same native watcher on the parent directory + const watcher = new MockWatcher(parentDir); + await registry.attach(watcher); + + expect(watcher.native).toBe(PARENT); + + expect(watcher0.native).toBe(PARENT); + expect(CHILD0.stopped).toBe(true); + expect(CHILD0.disposed).toBe(true); + + expect(watcher1.native).toBe(PARENT); + expect(CHILD1.stopped).toBe(true); + expect(CHILD1.disposed).toBe(true); + + expect(watcher2.native).toBe(OTHER); + expect(OTHER.stopped).toBe(false); + expect(OTHER.disposed).toBe(false); + }); + + describe('removing NativeWatchers', function() { + it('happens when they stop', async function() { + const STOPPED = new MockNative('stopped'); + const RUNNING = new MockNative('running'); + + const stoppedPath = absolute('watcher', 'that', 'will', 'be', 'stopped'); + const stoppedPathParts = stoppedPath + .split(path.sep) + .filter(part => part.length > 0); + const runningPath = absolute( + 'watcher', + 'that', + 'will', + 'continue', + 'to', + 'exist' + ); + const runningPathParts = runningPath + .split(path.sep) + .filter(part => part.length > 0); + + createNative = dir => { + if (dir === stoppedPath) { + return STOPPED; + } else if (dir === runningPath) { + return RUNNING; + } else { + throw new Error(`Unexpected path: ${dir}`); + } + }; + + const stoppedWatcher = new MockWatcher(stoppedPath); + await registry.attach(stoppedWatcher); + + const runningWatcher = new MockWatcher(runningPath); + await registry.attach(runningWatcher); + + STOPPED.stop(); + + const runningNode = registry.tree.root.lookup(runningPathParts).when({ + parent: node => node, + missing: () => false, + children: () => false + }); + expect(runningNode).toBeTruthy(); + expect(runningNode.getNativeWatcher()).toBe(RUNNING); + + const stoppedNode = registry.tree.root.lookup(stoppedPathParts).when({ + parent: () => false, + missing: () => true, + children: () => false + }); + expect(stoppedNode).toBe(true); + }); + + it('reassigns new child watchers when a parent watcher is stopped', async function() { + const CHILD0 = new MockNative('child0'); + const CHILD1 = new MockNative('child1'); + const PARENT = new MockNative('parent'); + + const parentDir = absolute('parent'); + const childDir0 = path.join(parentDir, 'child0'); + const childDir1 = path.join(parentDir, 'child1'); + + createNative = dir => { + if (dir === parentDir) { + return PARENT; + } else if (dir === childDir0) { + return CHILD0; + } else if (dir === childDir1) { + return CHILD1; + } else { + throw new Error(`Unexpected directory ${dir}`); + } + }; + + const parentWatcher = new MockWatcher(parentDir); + const childWatcher0 = new MockWatcher(childDir0); + const childWatcher1 = new MockWatcher(childDir1); + + await registry.attach(parentWatcher); + await Promise.all([ + registry.attach(childWatcher0), + registry.attach(childWatcher1) + ]); + + // All three watchers should share the parent watcher's native watcher. + expect(parentWatcher.native).toBe(PARENT); + expect(childWatcher0.native).toBe(PARENT); + expect(childWatcher1.native).toBe(PARENT); + + // Stopping the parent should detach and recreate the child watchers. + PARENT.stop(); + + expect(childWatcher0.native).toBe(CHILD0); + expect(childWatcher1.native).toBe(CHILD1); + + expect( + registry.tree.root.lookup(parts(parentDir)).when({ + parent: () => false, + missing: () => false, + children: () => true + }) + ).toBe(true); + + expect( + registry.tree.root.lookup(parts(childDir0)).when({ + parent: () => true, + missing: () => false, + children: () => false + }) + ).toBe(true); + + expect( + registry.tree.root.lookup(parts(childDir1)).when({ + parent: () => true, + missing: () => false, + children: () => false + }) + ).toBe(true); + }); + + it('consolidates children when splitting a parent watcher', async function() { + const CHILD0 = new MockNative('child0'); + const PARENT = new MockNative('parent'); + + const parentDir = absolute('parent'); + const childDir0 = path.join(parentDir, 'child0'); + const childDir1 = path.join(parentDir, 'child0', 'child1'); + + createNative = dir => { + if (dir === parentDir) { + return PARENT; + } else if (dir === childDir0) { + return CHILD0; + } else { + throw new Error(`Unexpected directory ${dir}`); + } + }; + + const parentWatcher = new MockWatcher(parentDir); + const childWatcher0 = new MockWatcher(childDir0); + const childWatcher1 = new MockWatcher(childDir1); + + await registry.attach(parentWatcher); + await Promise.all([ + registry.attach(childWatcher0), + registry.attach(childWatcher1) + ]); + + // All three watchers should share the parent watcher's native watcher. + expect(parentWatcher.native).toBe(PARENT); + expect(childWatcher0.native).toBe(PARENT); + expect(childWatcher1.native).toBe(PARENT); + + // Stopping the parent should detach and create the child watchers. Both child watchers should + // share the same native watcher. + PARENT.stop(); + + expect(childWatcher0.native).toBe(CHILD0); + expect(childWatcher1.native).toBe(CHILD0); + + expect( + registry.tree.root.lookup(parts(parentDir)).when({ + parent: () => false, + missing: () => false, + children: () => true + }) + ).toBe(true); + + expect( + registry.tree.root.lookup(parts(childDir0)).when({ + parent: () => true, + missing: () => false, + children: () => false + }) + ).toBe(true); + + expect( + registry.tree.root.lookup(parts(childDir1)).when({ + parent: () => true, + missing: () => false, + children: () => false + }) + ).toBe(true); + }); + }); +}); diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index c8e2656..a925bee 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -14,28 +14,43 @@ describe('PathWatcher', () => { beforeEach(() => fs.writeFileSync(tempFile, '')); afterEach(async () => { PathWatcher.closeAllWatchers(); + // Allow time in between each spec so that file-watchers have a chance to + // clean up. await wait(100); }); describe('getWatchedPaths', () => { it('returns an array of all watched paths', () => { + let realTempFilePath = fs.realpathSync(tempFile); + let expectedWatchPath = path.dirname(realTempFilePath); + expect(PathWatcher.getWatchedPaths()).toEqual([]); + + // Watchers watch the parent directory. let watcher1 = PathWatcher.watch(tempFile, EMPTY); - expect(PathWatcher.getWatchedPaths()).toEqual([watcher1.handleWatcher.path]); + expect(PathWatcher.getWatchedPaths()).toEqual([expectedWatchPath]); + + // Second watcher is a sibling of the first and should be able to reuse + // the existing watcher. let watcher2 = PathWatcher.watch(tempFile, EMPTY); - expect(PathWatcher.getWatchedPaths()).toEqual([watcher1.handleWatcher.path]); + expect(PathWatcher.getWatchedPaths()).toEqual([expectedWatchPath]); watcher1.close(); - expect(PathWatcher.getWatchedPaths()).toEqual([watcher1.handleWatcher.path]); + + // Native watcher won't close yet because it knows it had two listeners. + expect(PathWatcher.getWatchedPaths()).toEqual([expectedWatchPath]); watcher2.close(); + expect(PathWatcher.getWatchedPaths()).toEqual([]); }); }); describe('closeAllWatchers', () => { it('closes all watched paths', () => { + let realTempFilePath = fs.realpathSync(tempFile); + let expectedWatchPath = path.dirname(realTempFilePath); expect(PathWatcher.getWatchedPaths()).toEqual([]); - let watcher1 = PathWatcher.watch(tempFile, EMPTY); - expect(PathWatcher.getWatchedPaths()).toEqual([watcher1.handleWatcher.path]); + PathWatcher.watch(tempFile, EMPTY); + expect(PathWatcher.getWatchedPaths()).toEqual([expectedWatchPath]); PathWatcher.closeAllWatchers(); expect(PathWatcher.getWatchedPaths()).toEqual([]); }); @@ -60,6 +75,7 @@ describe('PathWatcher', () => { }); }); + if (process.platform !== 'linux') { describe('when a watched path is renamed #darwin #win32', () => { it('fires the callback with the event type and new path and watches the new path', async () => { @@ -78,27 +94,92 @@ describe('PathWatcher', () => { expect(eventType).toBe('rename'); expect(fs.realpathSync(eventPath)).toBe(fs.realpathSync(tempRenamed)); - expect(PathWatcher.getWatchedPaths()).toEqual([watcher.handleWatcher.path]); + expect(PathWatcher.getWatchedPaths()).toEqual([watcher.native.path]); }); }); describe('when a watched path is deleted #darwin #win32', () => { it('fires the callback with the event type and null path', async () => { let deleted = false; - PathWatcher.watch(tempFile, (type, path) => { + PathWatcher.watch(tempFile, (type, path) => { if (type === 'delete' && path === null) { deleted = true; } }); fs.unlinkSync(tempFile); - await condition(() => deleted); }); }); } + describe('when a watcher is added underneath an existing watched path', () => { + let subDirFile, subDir; + + function cleanup() { + if (subDirFile && fs.existsSync(subDirFile)) { + fs.rmSync(subDirFile); + } + if (subDir && fs.existsSync(subDir)) { + fs.rmSync(path.dirname(subDir), { recursive: true }); + } + } + + beforeEach(() => cleanup()); + afterEach(() => cleanup()); + + fit('reuses the existing native watcher', async () => { + let rootCallback = jasmine.createSpy('rootCallback') + let subDirCallback = jasmine.createSpy('subDirCallback') + let handle = PathWatcher.watch(tempFile, () => { + console.warn('LOLOLOLOLOL IT GOT CALLED'); + rootCallback(); + }); + + expect(PathWatcher.getNativeWatcherCount()).toBe(1); + + subDir = path.join(tempDir, 'foo', 'bar'); + fs.mkdirSync(subDir, { recursive: true }); + + subDirFile = path.join(subDir, 'test.txt'); + + console.log('MAKING SUBHANDLE:\n=================\n\n\n'); + let subHandle = PathWatcher.watch(subDir, () => { + console.warn('WTFWTFWTF?!?'); + subDirCallback(); + }); + expect(PathWatcher.getNativeWatcherCount()).toBe(1); + + console.log('CHANGING FILE:\n========\n', tempFile, '\n\n'); + fs.writeFileSync(tempFile, 'change'); + console.log('WAITING:\n========\n\n\n'); + await condition(() => rootCallback.calls.count() >= 1); + console.log('WAITED!:\n=======\n\n\n'); + expect(subDirCallback.calls.count()).toBe(0); + + console.log('CREATING!:\n=======\n\n\n'); + fs.writeFileSync(subDirFile, 'create'); + // The file might get both 'create' and 'change' here. That's fine with + // us. + await condition(() => subDirCallback.calls.count() >= 1); + + let realTempDir = fs.realpathSync(tempDir); + expect(PathWatcher.getWatchedPaths()).toEqual([realTempDir]); + + // Closing the original watcher should not cause the native watcher to + // close, since another one is depending on it. + handle.close(); + subDirCallback.calls.reset(); + + fs.writeFileSync(subDirFile, 'change'); + await condition(() => subDirCallback.calls.count() >= 1); + + subHandle.close(); + expect(PathWatcher.getNativeWatcherCount()).toBe(0); + }); + }); + describe('when a file under a watched directory is deleted', () => { it('fires the callback with the change event and empty path', async () => { let fileUnderDir = path.join(tempDir, 'file'); @@ -110,6 +191,7 @@ describe('PathWatcher', () => { expect(path).toBe(''); done = true; }); + fs.writeFileSync(fileUnderDir, 'what'); await wait(200); fs.unlinkSync(fileUnderDir); @@ -205,7 +287,7 @@ describe('PathWatcher', () => { fs.writeFileSync(tempFile2, ''); let watcher1 = PathWatcher.watch(tempFile, EMPTY); let watcher2 = PathWatcher.watch(tempFile2, EMPTY); - expect(watcher1.handleWatcher).toBe(watcher2.handleWatcher); + expect(watcher1.native).toBe(watcher2.native); }); } }); diff --git a/src/file.js b/src/file.js index 46b06d3..2d9bcb1 100644 --- a/src/file.js +++ b/src/file.js @@ -1,6 +1,5 @@ const crypto = require('crypto'); const Path = require('path'); -const _ = require('underscore-plus'); const { Emitter, Disposable } = require('event-kit'); const FS = require('fs-plus'); const Grim = require('grim'); @@ -470,11 +469,12 @@ class File { Section: Private */ - handleNativeChangeEvent (eventType, eventPath) { + async handleNativeChangeEvent (eventType, eventPath) { switch (eventType) { case 'delete': this.unsubscribeFromNativeChangeEvents(); - this.detectResurrectionAfterDelay(); + await wait(50); + await this.detectResurrection(); return; case 'rename': this.setPath(eventPath); @@ -490,10 +490,6 @@ class File { } } - detectResurrectionAfterDelay () { - return _.delay(() => this.detectResurrection(), 50); - } - async detectResurrection () { let exists = await this.exists(); if (exists) { diff --git a/src/main.js b/src/main.js index 56625be..8b04b64 100644 --- a/src/main.js +++ b/src/main.js @@ -4,275 +4,564 @@ try { } catch (err) { binding = require('../build/Release/pathwatcher.node'); } -const { Emitter } = require('event-kit'); +const { Emitter, Disposable, CompositeDisposable } = require('event-kit'); const fs = require('fs'); const { stat } = require('fs/promises'); +const { NativeWatcherRegistry } = require('./native-watcher-registry'); const path = require('path'); let initialized = false; const HANDLE_WATCHERS = new Map(); +// Ensures a path that refers to a directory ends with a path separator. +function sep (dirPath) { + if (dirPath.endsWith(path.sep)) return dirPath; + return `${dirPath}${path.sep}`; +} + +function isDirectory (somePath) { + let stats = fs.statSync(somePath); + return stats.isDirectory(); +} + function wait (ms) { return new Promise(r => setTimeout(r, ms)); } function normalizePath(rawPath) { - if (!rawPath.endsWith(path.sep)) return rawPath; - return rawPath.substring(0, rawPath.length - 1); + if (rawPath.endsWith(path.sep)) return rawPath; + return rawPath + path.sep; } function pathsAreEqual(pathA, pathB) { return normalizePath(pathA) == normalizePath(pathB); } -class HandleWatcher { - constructor(path) { - this.path = path; +function equalsOrDescendsFromPath(filePath, possibleParentPath) { + if (pathsAreEqual(filePath, possibleParentPath)) return true; + filePath = normalizePath(filePath); + possibleParentPath = normalizePath(possibleParentPath); + return filePath?.startsWith(possibleParentPath); +} + +let NativeWatcherId = 1; + +class NativeWatcher { + // Holds _active_ `NativeWatcher` instances. A `NativeWatcher` is active if + // at least one consumer has subscribed to it via `onDidChange`; it becomes + // inactive whenever its last consumer unsubscribes. + static INSTANCES = new Map(); + + // Given a path, returns whatever existing active `NativeWatcher` is already + // watching that path, or creates one if it doesn’t yet exist. + static findOrCreate (normalizedPath) { + for (let instance of this.INSTANCES.values()) { + if (instance.normalizedPath === normalizedPath) { + return instance; + } + } + return new NativeWatcher(normalizedPath); + } + + // Returns the number of active `NativeWatcher` instances. + static get instanceCount() { + return this.INSTANCES.size; + } + + constructor(normalizedPath) { + this.id = NativeWatcherId++; + this.normalizedPath = normalizedPath; this.emitter = new Emitter(); - this.start(); + this.subs = new CompositeDisposable(); + + this.running = false; } - async onEvent (event, filePath, oldFilePath) { - filePath &&= path.normalize(filePath); - oldFilePath &&= path.normalize(oldFilePath); + get path () { + return this.normalizedPath; + } - switch (event) { - case 'rename': - this.close(); - await wait(100); - try { - await stat(this.path); - // File still exists at the same path. - this.start(); - this.emitter.emit( - 'did-change', - { event: 'change', newFilePath: null } - ); - } catch (err) { - // File does not exist at the old path. - this.path = filePath; - if (process.platform === 'darwin' && /\/\.Trash\//.test(filePath)) { - // We'll treat this like a deletion; no point in continuing to - // track this file when it's in the trash. - this.emitter.emit( - 'did-change', - { event: 'delete', newFilePath: null } - ); - this.close(); - } else { - // The file has a new location, so let's resume watching it from - // there. - this.start(); - this.emitter.emit( - 'did-change', - { event: 'rename', newFilePath: filePath } - ); - } - } - return; - case 'delete': - // Wait for a very short interval to protect against brief deletions or - // spurious deletion events. Git will sometimes briefly delete a file - // before restoring it with different contents. - await wait(20); - if (fs.existsSync(filePath)) return; - this.emitter.emit( - 'did-change', - { event: 'delete', newFilePath: null } - ); - this.close(); - return; - case 'child-delete': - // Wait for a very short interval to protect against brief deletions or - // spurious deletion events. Git will sometimes briefly delete a file - // before restoring it with different contents. - await wait(20); - if (fs.existsSync(filePath)) return; - this.emitter.emit( - 'did-change', - { event, newFilePath: filePath, oldFilePath, rawFilePath: filePath } - ); - return; - case 'unknown': - throw new Error("Received unknown event for path: " + this.path); - default: - this.emitter.emit( - 'did-change', - { event, newFilePath: filePath, oldFilePath, rawFilePath: filePath } - ); - } + start () { + if (this.running) return; + this.handle = binding.watch(this.normalizedPath); + NativeWatcher.INSTANCES.set(this.handle, this); + this.running = true; + this.emitter.emit('did-start'); + } + + onDidStart (callback) { + return this.emitter.on('did-start', callback); } onDidChange (callback) { - return this.emitter.on('did-change', callback); + this.start(); + + let sub = this.emitter.on('did-change', callback); + return new Disposable(() => { + sub.dispose(); + if (this.emitter.listenerCountForEventName('did-change') === 0) { + this.stop(); + } + }); } - start () { - let troubleWatcher; - if (!this.path.endsWith(path.sep)) { - this.path += path.sep; - } - this.handle = binding.watch(this.path); - if (HANDLE_WATCHERS.has(this.handle)) { - troubleWatcher = HANDLE_WATCHERS.get(this.handle); - troubleWatcher.close(); - console.error(`The handle (${this.handle}) returned by watching path ${this.path} is the same as an already-watched path: ${troubleWatcher.path}`); - } - return HANDLE_WATCHERS.set(this.handle, this); + onShouldDetach (callback) { + return this.emitter.on('should-detach', callback); + } + + onWillStop (callback) { + return this.emitter.on('will-stop', callback); + } + + onDidStop () { + return this.emitter.on('did-stop', callback); + } + + onDidError (callback) { + return this.emitter.on('did-error', callback); + } + + reattachTo (replacement, watchedPath, options) { + if (replacement === this) return; + this.emitter.emit('should-detach', { replacement, watchedPath, options }); } - closeIfNoListener () { - if (this.emitter.getTotalListenerCount() === 0) { - this.close(); + stop (shutdown = false) { + // console.log('Stopping NativeListener', this.handle, this.running); + if (this.running) { + this.emitter.emit('will-stop', shutdown); + binding.unwatch(this.handle); + this.running = false; + this.emitter.emit('did-stop', shutdown); } + + NativeWatcher.INSTANCES.delete(this.handle); + + // console.log('Remaining instances:', NativeWatcher.INSTANCES.size, [...NativeWatcher.INSTANCES.keys()]); } - close () { - if (!HANDLE_WATCHERS.has(this.handle)) return; - binding.unwatch(this.handle); - HANDLE_WATCHERS.delete(this.handle); + dispose () { + this.emitter.dispose(); + } + + onEvent (event) { + // console.log('NativeWatcher#onEvent!', event); + // console.log('onEvent!', event); + this.emitter.emit('did-change', event); + } + + onError (err) { + this.emitter.emit('did-error', err); } } +class WatcherError extends Error { + constructor(message, code) { + super(message); + this.name = 'WatcherError'; + this.code = code; + } +} + +let PathWatcherId = 10; + class PathWatcher { - isWatchingParent = false; - path = null; - handleWatcher = null; + constructor (registry, watchedPath) { + this.id = PathWatcherId++; + this.watchedPath = watchedPath; + this.registry = registry; - constructor(filePath, callback) { - this.path = filePath; + this.normalizePath = null; + this.native = null; + this.changeCallbacks = new Map(); + + this.emitter = new Emitter(); + this.subs = new CompositeDisposable(); - if (!fs.existsSync(filePath)) { - let err = new Error(`Unable to watch path`); - err.code = 'ENOENT'; - throw err; + if (!fs.existsSync(watchedPath)) { + throw new WatcherError('Unable to watch path', 'ENOENT'); } - this.assignRealPath(); + try { + this.normalizedPath = fs.realpathSync(watchedPath); + } catch (err) { + this.normalizedPath = watchedPath; + } + + let stats = fs.statSync(this.normalizedPath); + this.isWatchingParent = !stats.isDirectory(); - // Resolve the real path before we pass it to the native watcher. It's - // better at dealing with real paths instead of symlinks and it doesn't - // otherwise matter for our purposes. - if (this.realPath) { - filePath = this.realPath; + this.originalNormalizedPath = this.normalizedPath; + if (!stats.isDirectory()) { + this.normalizedPath = path.dirname(this.normalizedPath); } - this.emitter = new Emitter(); + this.attachedPromise = new Promise(resolve => { + this.resolveAttachedPromise = resolve; + }); + + this.startPromise = new Promise((resolve, reject) => { + this.resolveStartPromise = resolve; + this.rejectStartPromise = reject; + }); + + this.active = true; + } + + getNormalizedPath() { + return this.normalizedPath; + } + + getNormalizedPathPromise () { + return Promise.resolve(this.normalizedPath); + } + + onDidChange (callback) { + if (this.native) { + let sub = this.native.onDidChange(event => { + this.onNativeEvent(event, callback); + }); + this.changeCallbacks.set(callback, sub); + this.native.start(); + } else { + if (this.normalizedPath) { + this.registry.attach(this); + this.onDidChange(callback); + } else { + this.registry.attachAsync(this).then(() => { + this.onDidChange(callback); + }) + } + } - let stats = fs.statSync(filePath); + return new Disposable(() => { + let sub = this.changeCallbacks.get(callback); + this.changeCallbacks.delete(callback); + sub.dispose(); + }); + } + + onDidError (callback) { + return this.emitter.on('did-error', callback); + } + + attachToNative (native) { + this.subs.dispose(); + this.subs = new CompositeDisposable(); + this.native = native; + if (native.running) { + this.resolveStartPromise(); + } else { + this.subs.add( + native.onDidStart(() => this.resolveStartPromise()) + ); + } + + // console.log('PathWatcher instance with path', this.originalNormalizedPath, 'is attaching to native:', native); + + // Transfer any native event subscriptions to the new NativeWatcher. + for (let [callback, formerSub] of this.changeCallbacks) { + let newSub = native.onDidChange(event => { + return this.onNativeEvent(event, callback); + }); + this.changeCallbacks.set(callback, newSub); + formerSub.dispose(); + } + + this.subs.add( + native.onDidError(err => this.emitter.emit('did-error', err)) + ); + + this.subs.add( + native.onShouldDetach(({ replacement, watchedPath }) => { + if (isClosingAllWatchers) return; + // console.warn('Should PathWatcher with ID', this.id, 'attach to:', replacement, 'when it already has native:', this.native, this.native === replacement); + if ( + this.active && + this.native === native && + replacement !== native && + this.normalizedPath?.startsWith(watchedPath) + ) { + // console.log('PathWatcher with ID:', this.id, 'reattaching to:', replacement, ';\n the PathWatcher is meant to watch the path:', this.originalNormalizedPath); + // console.warn('The current watcher count is', getNativeWatcherCount()); + this.attachToNative(replacement, replacement.normalizedPath); + } + }) + ); + + this.subs.add( + native.onWillStop(() => { + if (this.native !== native) return; + this.subs.dispose(); + this.native = null; + }) + ); + + this.resolveAttachedPromise(); + } + + rename (newName) { + this.close(); + try { + this.normalizedPath = fs.realpathSync(newName); + } catch (err) { + this.normalizedPath = newName; + } + + let stats = fs.statSync(this.normalizedPath); this.isWatchingParent = !stats.isDirectory(); - if (this.isWatchingParent) { - filePath = path.dirname(filePath); + this.originalNormalizedPath = this.normalizedPath; + if (!stats.isDirectory()) { + this.normalizedPath = path.dirname(this.normalizedPath); } - for (let watcher of HANDLE_WATCHERS.values()) { - if (pathsAreEqual(watcher.path, filePath)) { - this.handleWatcher = watcher; - break; - } + this.registry.attach(this); + this.active = true; + } + + onNativeEvent (event, callback) { + console.debug( + 'PathWatcher::onNativeEvent', + event, + 'for watcher of path:', + this.originalNormalizedPath + ); + + let isWatchedPath = (eventPath) => { + return eventPath?.startsWith(sep(this.normalizedPath)); } - this.handleWatcher ??= new HandleWatcher(filePath); - - this.onChange = ({ event, newFilePath, oldFilePath, rawFilePath }) => { - // Filter out strange events. - let comparisonPath = this.path ?? this.realPath; - if (rawFilePath && (comparisonPath.length > rawFilePath.length)) { - // This is weird. Not sure why this happens yet. It's most likely an - // event for a parent directory of what we're watching. Ideally we can - // filter this out earlier in the process, like in the native code, but - // that would involve doing earlier symlink resolution. - return; - } - switch (event) { - case 'rename': - case 'change': - case 'delete': - if (event === 'rename') { - this.path = newFilePath; - this.assignRealPath(); - } - if (typeof callback === 'function') { - callback.call(this, event, newFilePath); - } - this.emitter.emit( - 'did-change', - { event, newFilePath } - ); - return; - case 'child-rename': - if (this.isWatchingParent) { - if (this.matches(oldFilePath)) { - return this.onChange({ event: 'rename', newFilePath }); - } + // Does `event.path` match the exact path our `PathWatcher` cares about? + let eventPathIsEqual = this.originalNormalizedPath === event.path; + // Does `event.oldPath` match the exact path our `PathWatcher` cares about? + let eventOldPathIsEqual = this.originalNormalizedPath === event.oldPath; + + // Is `event.path` somewhere within the folder that this `PathWatcher` is + // monitoring? + let newWatched = isWatchedPath(event.path); + // Is `event.oldPath` somewhere within the folder that this `PathWatcher` + // is monitoring? + let oldWatched = isWatchedPath(event.oldPath); + + let newEvent = { ...event }; + + if (!newWatched && !oldWatched) { + console.debug(`This path isn’t one we care about. Skipping!`); + return; + } else { + console.log('(got this far)'); + } + + switch (newEvent.action) { + case 'rename': + case 'delete': + case 'create': + // These events need no alteration. + break; + case 'child-create': + if (!this.isWatchingParent) { + if (eventPathIsEqual) { + // We're watching a directory and this is a create event for the + // directory itself. This should be fixed in the bindings, but for + // now we can switch the event type in the JS. + newEvent.action = 'create'; } else { - return this.onChange({ event: 'change', newFilePath: '' }); + newEvent.action = 'change'; + newEvent.path = ''; } break; - case 'child-delete': - if (this.isWatchingParent) { - if (this.matches(newFilePath)) { - return this.onChange({ event: 'delete', newFilePath: null }); - } + } else if (eventPathIsEqual) { + newEvent.action = 'create'; + } + break; + case 'child-delete': + console.log('CHILD-DELETE scenario!'); + if (!this.isWatchingParent) { + newEvent.action = 'change'; + newEvent.path = ''; + } else if (eventPathIsEqual) { + newEvent.action = 'delete'; + } + break; + case 'child-rename': + // TODO: Laziness in the native addon means that even events that + // happen to the directory itself are reported as `child-rename` + // instead of `rename`. We can fix this in the JS for now, but it + // should eventually be fixed in the C++. + + // First, weed out the cases that can't possibly affect us. + let pathIsInvolved = eventPathIsEqual || eventOldPathIsEqual; + + // The only cases for which we should return early are the ones where + // (a) we're watching a file, and (b) this event doesn't involve it + // in any way. + if (this.isWatchingParent && !pathIsInvolved) { + return; + } + + if (!this.isWatchingParent && !pathIsInvolved) { + // We're watching a directory and these events involve something + // inside of the directory. + if ( + path.dirname(event.path) === this.normalizedPath || + path.dirname(event.oldPath) === this.normalizedPath + ) { + // This is a direct child of the directory, so we'll fire an + // event. + newEvent.action = 'change'; + newEvent.path = ''; } else { - return this.onChange({ event: 'change', newFilePath: '' }); + // Changes in ancestors or descendants do not concern us, so + // we'll return early. + // + // TODO: Changes in ancestors might, actually; they might need to + // be treated as folder deletions/creations. + return; } - break; - case 'child-change': - if (this.isWatchingParent && this.matches(newFilePath)) { - return this.onChange({ event: 'change', newFilePath: '' }); + } else { + // We're left with cases where + // + // * We're watching a directory and that directory is named by the + // event, or + // * We're watching a file (via a directory watch) and that file is + // named by the event. + // + // Those cases are handled identically. + + if (newWatched && this.originalNormalizedPath !== event.path) { + // The file/directory we care about has moved to a new destination + // and that destination is visible to this watcher. That means we + // can simply update the path we care about and keep path-watching. + this.moveToPath(event.path); } - break; - case 'child-create': - if (!this.isWatchingParent) { - if (this.matches(newFilePath)) { - // If we are watching a file already, it must exist. There is no - // `create` event. This should not be handled because it's - // invalid. - return; - } - return this.onChange({ event: 'change', newFilePath: '', rawFilePath }); + + if (oldWatched && newWatched) { + // We can keep tabs on both file paths from here, so this will + // be treated as a rename. + newEvent.action = 'rename'; + } else if (oldWatched && !newWatched) { + // We're moving the file to a place we're not observing, so + // we'll treat it as a deletion. + newEvent.action = 'delete'; + } else if (!oldWatched && newWatched) { + // The file came from someplace we're not watching, so it might + // as well be a file creation. + newEvent.action = 'create'; } - } - }; + } + break; + case 'child-change': + if (!this.isWatchingParent) { + // We are watching a directory. + if (eventPathIsEqual) { + // This makes no sense; we won't fire a `child-change` on a + // directory. Ignore it. + return; + } else { + newEvent.action = 'change'; + newEvent.path = ''; + } + } else { + console.log('FILE CHANGE FILE CHANGE!'); + newEvent.action = 'change'; + newEvent.path = ''; + } + break; + } // end switch - this.disposable = this.handleWatcher.onDidChange(this.onChange); + if (eventPathIsEqual && newEvent.action === 'create') { + console.log('CREATE?!?!?'); + // This file or directory already existed; we checked. Any `create` + // event for it is spurious. + return; + } + + if (eventPathIsEqual) { + // Specs require that a `delete` action carry a path of `null`; other + // actions should carry an empty path. (Weird decisions, but we can + // live with them.) + newEvent.path = newEvent.action === 'delete' ? null : ''; + } + console.debug( + 'FINAL EVENT ACTION:', + newEvent.action, + 'PATH', + newEvent.path, + 'CALLBACK:', + callback.toString() + ); + callback(newEvent.action, newEvent.path); } - matches (otherPath) { - if (this.realPath) { - return this.realPath === otherPath; + moveToPath (newPath) { + this.isWatchingParent = !isDirectory(newPath); + if (this.isWatchingParent) { + // Watching a directory just because we care about a specific file inside + // it. + this.originalNormalizedPath = newPath; + this.normalizedPath = path.dirname(newPath); } else { - return this.path === otherPath; + // Actually watching a directory. + this.originalNormalizedPath = newPath; + this.normalizedPath = newPath; } } - assignRealPath () { - try { - this.realPath = fs.realpathSync(this.path); - } catch (_error) { - this.realPath = null; + dispose () { + this.disposing = true; + for (let sub of this.changeCallbacks.values()) { + sub.dispose(); } - } - onDidChange (callback) { - return this.emitter.on('did-change', callback); + this.emitter.dispose(); + this.subs.dispose(); } close () { - this.emitter?.dispose(); - this.disposable?.dispose(); - this.handleWatcher?.closeIfNoListener(); + console.log('Pathwatcher with ID:', this.id, 'is closing!'); + this.active = false; + this.dispose(); } } -function DEFAULT_CALLBACK(event, handle, filePath, oldFilePath) { - if (!HANDLE_WATCHERS.has(handle)) return; +const REGISTRY = new NativeWatcherRegistry((normalizedPath) => { + if (!initialized) { + binding.setCallback(DEFAULT_CALLBACK); + initialized = true; + } + // It's important that this function be able to return an existing instance + // of `NativeWatcher` when present. Otherwise, the registry will try to + // create a new instance at the same path, and the native bindings won't + // allow that to happen. + // + // It's also important because the registry might respond to a sibling + // `PathWatcher`’s removal by trying to reattach us — even though our + // `NativeWatcher` still works just fine. The way around that is to make sure + // that this function will return the same watcher we're already using + // instead of creating a new one. + return NativeWatcher.findOrCreate(normalizedPath); +}); + +class WatcherEvent { + constructor(event, filePath, oldFilePath) { + this.action = event; + this.path = filePath; + this.oldPath = oldFilePath; + } +} - let watcher = HANDLE_WATCHERS.get(handle); - watcher.onEvent(event, filePath, oldFilePath); +function DEFAULT_CALLBACK(action, handle, filePath, oldFilePath) { + if (!NativeWatcher.INSTANCES.has(handle)) { + // Might be a stray callback from a `NativeWatcher` that has already + // stopped. + return; + } + + let watcher = NativeWatcher.INSTANCES.get(handle); + let event = new WatcherEvent(action, filePath, oldFilePath); + watcher.onEvent(event); } function watch (pathToWatch, callback) { @@ -280,19 +569,30 @@ function watch (pathToWatch, callback) { binding.setCallback(DEFAULT_CALLBACK); initialized = true; } - return new PathWatcher(path.resolve(pathToWatch), callback); + let watcher = new PathWatcher(REGISTRY, path.resolve(pathToWatch)); + watcher.onDidChange(callback); + return watcher; } +let isClosingAllWatchers = false; function closeAllWatchers () { - for (let watcher of HANDLE_WATCHERS.values()) { - watcher?.close(); + isClosingAllWatchers = true; + for (let watcher of NativeWatcher.INSTANCES.values()) { + watcher.stop(true); } - HANDLE_WATCHERS.clear(); + NativeWatcher.INSTANCES.clear(); + REGISTRY.reset(); + isClosingAllWatchers = false; } function getWatchedPaths () { - let watchers = Array.from(HANDLE_WATCHERS.values()); - return watchers.map(w => w.path); + let watchers = Array.from(NativeWatcher.INSTANCES.values()); + let result = watchers.map(w => w.normalizedPath); + return result +} + +function getNativeWatcherCount() { + return NativeWatcher.INSTANCES.size; } const File = require('./file'); @@ -302,6 +602,7 @@ module.exports = { watch, closeAllWatchers, getWatchedPaths, + getNativeWatcherCount, File, Directory }; diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js new file mode 100644 index 0000000..85d006f --- /dev/null +++ b/src/native-watcher-registry.js @@ -0,0 +1,555 @@ +// Heavily based on a similar implementation in the Atom/Pulsar codebase and +// licensed under MIT. + +const path = require('path'); + +// Private: re-join the segments split from an absolute path to form another +// absolute path. +function absolute(...parts) { + const candidate = path.join(...parts); + return path.isAbsolute(candidate) + ? candidate + : path.join(path.sep, candidate); +} + +// Private: Map userland filesystem watcher subscriptions efficiently to +// deliver filesystem change notifications to each watcher with the most +// efficient coverage of native watchers. +// +// * If two watchers subscribe to the same directory, use a single native +// watcher for each. +// * Re-use a native watcher watching a parent directory for a watcher on a +// child directory. If the parent directory watcher is removed, it will be +// split into child watchers. +// * If any child directories already being watched, stop and replace them with +// a watcher on the parent directory. +// +// Uses a trie whose structure mirrors the directory structure. +class RegistryTree { + // Private: Construct a tree with no native watchers. + // + // * `basePathSegments` the position of this tree's root relative to the + // filesystem's root as an {Array} of directory names. + // * `createNative` {Function} used to construct new native watchers. It + // should accept an absolute path as an argument and return a new + // {NativeWatcher}. + constructor(basePathSegments, createNative) { + this.basePathSegments = basePathSegments; + this.root = new RegistryNode(); + this.createNative = createNative; + } + + destroyNativeWatchers(shutdown) { + let leaves = this.root.leaves([]); + for (let leaf of leaves) { + leaf.node.destroyNativeWatcher(shutdown); + } + } + + // Private: Identify the native watcher that should be used to produce events + // at a watched path, creating a new one if necessary. + // + // * `pathSegments` The path to watch represented as an {Array} of directory + // names relative to this {RegistryTree}'s root. + // * `attachToNative` {Function} invoked with the appropriate native watcher + // and the absolute path to its watch root. + add(pathSegments, attachToNative) { + const absolutePathSegments = this.basePathSegments.concat(pathSegments); + const absolutePath = absolute(...absolutePathSegments); + const attachToNew = childPaths => { + const native = this.createNative(absolutePath); + const leaf = new RegistryWatcherNode( + native, + absolutePathSegments, + childPaths + ); + this.root = this.root.insert(pathSegments, leaf); + const sub = native.onWillStop((shutdown) => { + sub.dispose(); + if (shutdown) return; + this.root = + this.root.remove(pathSegments, this.createNative) || + new RegistryNode(); + }); + + attachToNative(native, absolutePath); + return native; + }; + + this.root.lookup(pathSegments).when({ + parent: (parent, remaining) => { + // An existing NativeWatcher is watching the same directory or a parent + // directory of the requested path. Attach this Watcher to it as a + // filtering watcher and record it as a dependent child path. + const native = parent.getNativeWatcher(); + parent.addChildPath(remaining); + attachToNative(native, absolute(...parent.getAbsolutePathSegments())); + }, + children: children => { + // One or more NativeWatchers exist on child directories of the + // requested path. Create a new native watcher on the parent directory, + // note the subscribed child paths, and cleanly stop the child native + // watchers. + const newNative = attachToNew(children.map(child => child.path)); + + for (let i = 0; i < children.length; i++) { + const childNode = children[i].node; + const childNative = childNode.getNativeWatcher(); + childNative.reattachTo(newNative, absolutePath); + childNative.dispose(); + childNative.stop(); + } + }, + missing: () => attachToNew([]) + }); + } + + remove (pathSegments) { + return this.root.remove(pathSegments, this.createNative); + } + + // Private: Access the root node of the tree. + getRoot() { + return this.root; + } + + // Private: Return a {String} representation of this tree's structure for diagnostics and testing. + print() { + return this.root.print(); + } +} + +// Private: Non-leaf node in a {RegistryTree} used by the +// {NativeWatcherRegistry} to cover the allocated {Watcher} instances with the +// most efficient set of {NativeWatcher} instances possible. Each +// {RegistryNode} maps to a directory in the filesystem tree. +class RegistryNode { + // Private: Construct a new, empty node representing a node with no watchers. + constructor() { + this.children = {}; + } + + // Private: Recursively discover any existing watchers corresponding to a + // path. + // + // * `pathSegments` filesystem path of a new {Watcher} already split into an + // Array of directory names. + // + // Returns: A {ParentResult} if the exact requested directory or a parent + // directory is being watched, a {ChildrenResult} if one or more child paths + // are being watched, or a {MissingResult} if no relevant watchers exist. + lookup(pathSegments) { + if (pathSegments.length === 0) { + return new ChildrenResult(this.leaves([])); + } + + const child = this.children[pathSegments[0]]; + if (child === undefined) { + return new MissingResult(this); + } + + return child.lookup(pathSegments.slice(1)); + } + + // Private: Insert a new {RegistryWatcherNode} into the tree, creating new intermediate {RegistryNode} instances as + // needed. Any existing children of the watched directory are removed. + // + // * `pathSegments` filesystem path of the new {Watcher}, already split into an Array of directory names. + // * `leaf` initialized {RegistryWatcherNode} to insert + // + // Returns: The root of a new tree with the {RegistryWatcherNode} inserted at the correct location. Callers should + // replace their node references with the returned value. + insert(pathSegments, leaf) { + if (pathSegments.length === 0) { + return leaf; + } + + const pathKey = pathSegments[0]; + let child = this.children[pathKey]; + if (child === undefined) { + child = new RegistryNode(); + } + this.children[pathKey] = child.insert(pathSegments.slice(1), leaf); + return this; + } + + // Private: Remove a {RegistryWatcherNode} by its exact watched directory. + // + // * `pathSegments` absolute pre-split filesystem path of the node to remove. + // * `createSplitNative` callback to be invoked with each child path segment + // {Array} if the {RegistryWatcherNode} is split into child watchers rather + // than removed outright. See {RegistryWatcherNode.remove}. + // + // Returns: The root of a new tree with the {RegistryWatcherNode} removed. + // Callers should replace their node references with the returned value. + remove(pathSegments, createSplitNative) { + if (pathSegments.length === 0) { + // Attempt to remove a path with child watchers. Do nothing. + return this; + } + + const pathKey = pathSegments[0]; + const child = this.children[pathKey]; + if (child === undefined) { + // Attempt to remove a path that isn't watched. Do nothing. + return this; + } + + // Recurse + const newChild = child.remove(pathSegments.slice(1), createSplitNative); + if (newChild === null) { + delete this.children[pathKey]; + } else { + this.children[pathKey] = newChild; + } + + // Remove this node if all of its children have been removed + return Object.keys(this.children).length === 0 ? null : this; + } + + // Private: Discover all {RegistryWatcherNode} instances beneath this tree node and the child paths + // that they are watching. + // + // * `prefix` {Array} of intermediate path segments to prepend to the resulting child paths. + // + // Returns: A possibly empty {Array} of `{node, path}` objects describing {RegistryWatcherNode} + // instances beneath this node. + leaves(prefix) { + const results = []; + for (const p of Object.keys(this.children)) { + results.push(...this.children[p].leaves(prefix.concat([p]))); + } + return results; + } + + // Private: Return a {String} representation of this subtree for diagnostics and testing. + print(indent = 0) { + let spaces = ''; + for (let i = 0; i < indent; i++) { + spaces += ' '; + } + + let result = ''; + for (const p of Object.keys(this.children)) { + result += `${spaces}${p}\n${this.children[p].print(indent + 2)}`; + } + return result; + } +} + +// Private: Leaf node within a {NativeWatcherRegistry} tree. Represents a directory that is covered by a +// {NativeWatcher}. +class RegistryWatcherNode { + // Private: Allocate a new node to track a {NativeWatcher}. + // + // * `nativeWatcher` An existing {NativeWatcher} instance. + // * `absolutePathSegments` The absolute path to this {NativeWatcher}'s directory as an {Array} of + // path segments. + // * `childPaths` {Array} of child directories that are currently the responsibility of this + // {NativeWatcher}, if any. Directories are represented as arrays of the path segments between this + // node's directory and the watched child path. + constructor(nativeWatcher, absolutePathSegments, childPaths) { + this.nativeWatcher = nativeWatcher; + this.absolutePathSegments = absolutePathSegments; + + // Store child paths as joined strings so they work as Set members. + this.childPaths = new Set(); + for (let i = 0; i < childPaths.length; i++) { + this.childPaths.add(path.join(...childPaths[i])); + } + } + + // Private: Assume responsibility for a new child path. If this node is removed, it will instead + // split into a subtree with a new {RegistryWatcherNode} for each child path. + // + // * `childPathSegments` the {Array} of path segments between this node's directory and the watched + // child directory. + addChildPath(childPathSegments) { + this.childPaths.add(path.join(...childPathSegments)); + } + + // Private: Stop assuming responsibility for a previously assigned child path. If this node is + // removed, the named child path will no longer be allocated a {RegistryWatcherNode}. + // + // * `childPathSegments` the {Array} of path segments between this node's directory and the no longer + // watched child directory. + removeChildPath(childPathSegments) { + this.childPaths.delete(path.join(...childPathSegments)); + } + + // Private: Accessor for the {NativeWatcher}. + getNativeWatcher() { + return this.nativeWatcher; + } + + destroyNativeWatcher(shutdown) { + this.nativeWatcher.stop(shutdown); + } + + // Private: Return the absolute path watched by this {NativeWatcher} as an {Array} of directory names. + getAbsolutePathSegments() { + return this.absolutePathSegments; + } + + // Private: Identify how this watcher relates to a request to watch a directory tree. + // + // * `pathSegments` filesystem path of a new {Watcher} already split into an Array of directory names. + // + // Returns: A {ParentResult} referencing this node. + lookup(pathSegments) { + return new ParentResult(this, pathSegments); + } + + // Private: Remove this leaf node if the watcher's exact path matches. If + // this node is covering additional {Watcher} instances on child paths, it + // will be split into a subtree. + // + // * `pathSegments` filesystem path of the node to remove. + // * `createSplitNative` callback invoked with each {Array} of absolute child + // path segments to create a native watcher on a subtree of this node. + // + // Returns: If `pathSegments` match this watcher's path exactly, returns + // `null` if this node has no `childPaths` or a new {RegistryNode} on a newly + // allocated subtree if it did. If `pathSegments` does not match the + // watcher's path, it's an attempt to remove a subnode that doesn't exist, so + // the remove call has no effect and returns `this` unaltered. + remove(pathSegments, createSplitNative) { + if (pathSegments.length !== 0) { + return this; + } else if (this.childPaths.size > 0) { + let newSubTree = new RegistryTree( + this.absolutePathSegments, + createSplitNative + ); + + for (const childPath of this.childPaths) { + const childPathSegments = childPath.split(path.sep); + newSubTree.add(childPathSegments, (native, attachmentPath) => { + this.nativeWatcher.reattachTo(native, attachmentPath); + }); + } + + return newSubTree.getRoot(); + } else { + return null; + } + } + + // Private: Discover this {RegistryWatcherNode} instance. + // + // * `prefix` {Array} of intermediate path segments to prepend to the resulting child paths. + // + // Returns: An {Array} containing a `{node, path}` object describing this node. + leaves(prefix) { + return [{ node: this, path: prefix }]; + } + + // Private: Return a {String} representation of this watcher for diagnostics and testing. Indicates the number of + // child paths that this node's {NativeWatcher} is responsible for. + print(indent = 0) { + let result = ''; + for (let i = 0; i < indent; i++) { + result += ' '; + } + result += '[watcher'; + if (this.childPaths.size > 0) { + result += ` +${this.childPaths.size}`; + } + result += ']\n'; + + return result; + } +} + +// Private: A {RegistryNode} traversal result that's returned when neither a directory, its children, nor its parents +// are present in the tree. +class MissingResult { + // Private: Instantiate a new {MissingResult}. + // + // * `lastParent` the final successfully traversed {RegistryNode}. + constructor(lastParent) { + this.lastParent = lastParent; + } + + // Private: Dispatch within a map of callback actions. + // + // * `actions` {Object} containing a `missing` key that maps to a callback to be invoked when no results were returned + // by {RegistryNode.lookup}. The callback will be called with the last parent node that was encountered during the + // traversal. + // + // Returns: the result of the `actions` callback. + when(actions) { + return actions.missing(this.lastParent); + } +} + +// Private: A {RegistryNode.lookup} traversal result that's returned when a parent or an exact match of the requested +// directory is being watched by an existing {RegistryWatcherNode}. +class ParentResult { + // Private: Instantiate a new {ParentResult}. + // + // * `parent` the {RegistryWatcherNode} that was discovered. + // * `remainingPathSegments` an {Array} of the directories that lie between the leaf node's watched directory and + // the requested directory. This will be empty for exact matches. + constructor(parent, remainingPathSegments) { + this.parent = parent; + this.remainingPathSegments = remainingPathSegments; + } + + // Private: Dispatch within a map of callback actions. + // + // * `actions` {Object} containing a `parent` key that maps to a callback to be invoked when a parent of a requested + // requested directory is returned by a {RegistryNode.lookup} call. The callback will be called with the + // {RegistryWatcherNode} instance and an {Array} of the {String} path segments that separate the parent node + // and the requested directory. + // + // Returns: the result of the `actions` callback. + when(actions) { + return actions.parent(this.parent, this.remainingPathSegments); + } +} + +// Private: A {RegistryNode.lookup} traversal result that's returned when one or more children of the requested +// directory are already being watched. +class ChildrenResult { + // Private: Instantiate a new {ChildrenResult}. + // + // * `children` {Array} of the {RegistryWatcherNode} instances that were discovered. + constructor(children) { + this.children = children; + } + + // Private: Dispatch within a map of callback actions. + // + // * `actions` {Object} containing a `children` key that maps to a callback to be invoked when a parent of a requested + // requested directory is returned by a {RegistryNode.lookup} call. The callback will be called with the + // {RegistryWatcherNode} instance. + // + // Returns: the result of the `actions` callback. + when(actions) { + return actions.children(this.children); + } +} + +// Private: Track the directories being monitored by native filesystem +// watchers. Minimize the number of native watchers allocated to receive events +// for a desired set of directories by: +// +// 1. Subscribing to the same underlying {NativeWatcher} when watching the same +// directory multiple times. +// 2. Subscribing to an existing {NativeWatcher} on a parent of a desired +// directory. +// 3. Replacing multiple {NativeWatcher} instances on child directories with a +// single new {NativeWatcher} on the parent. +class NativeWatcherRegistry { + // Private: Instantiate an empty registry. + // + // * `createNative` {Function} that will be called with a normalized filesystem path to create a new native + // filesystem watcher. + constructor(createNative) { + this._createNative = createNative; + this.tree = new RegistryTree([], createNative); + } + + reset () { + this.tree.destroyNativeWatchers(true); + this.tree = new RegistryTree([], this._createNative); + } + + // Private: Attach a watcher to a directory, assigning it a {NativeWatcher}. If a suitable {NativeWatcher} already + // exists, it will be attached to the new {Watcher} with an appropriate subpath configuration. Otherwise, the + // `createWatcher` callback will be invoked to create a new {NativeWatcher}, which will be registered in the tree + // and attached to the watcher. + // + // If any pre-existing child watchers are removed as a result of this operation, {NativeWatcher.onWillReattach} will + // be broadcast on each with the new parent watcher as an event payload to give child watchers a chance to attach to + // the new watcher. + // + // * `watcher` an unattached {Watcher}. + attach(watcher, normalizedDirectory = undefined) { + if (!normalizedDirectory) { + normalizedDirectory = watcher.getNormalizedPath(); + if (!normalizedDirectory) { + return this.attachAsync(watcher); + } + } + const pathSegments = normalizedDirectory + .split(path.sep) + .filter(segment => segment.length > 0); + + this.tree.add(pathSegments, (native, nativePath) => { + watcher.attachToNative(native, nativePath); + }); + } + + async attachAsync (watcher) { + const normalizedDirectory = await watcher.getNormalizedPathPromise(); + return this.attach(watcher, normalizedDirectory); + } + + // TODO: This registry envisions `PathWatcher` instances that can be attached + // to any number of `NativeWatcher` instances. But it also envisions an + // “ownership” model that isn't quite accurate. + // + // Ideally, we'd want something like this: + // + // 1. Someone adds a watcher for /Foo/Bar/Baz/thud.txt. + // 2. We set up a `NativeWatcher` for /Foo/Bar/Baz. + // 3. Someone adds a watcher for /Foo/Bar/Baz/A/B/C/zort.txt. + // 4. We reuse the existing `NativeWatcher`. + // 5. Someone stops the `PathWatcher` from step 1. + // + // What we want to happen: + // + // 6. We take that opportunity to streamline the `NativeWatchers`; since + // it’s the only one left, we know we can create a new `NativeWatcher` + // at /Foo/Bar/Baz/A/B/C and swap it onto the last `PathWatcher` + // instance. + // + // What actually happens: + // + // 6. The original `NativeWatcher` keeps going (since it has one remaining + // dependency) and our single `PathWatcher` stays subscribed to it. + // + // + // This is fine as a consolation prize, but it's less efficient. + // + // Frustratingly, most of what we want happens in response to the stopping of + // a `NativeWatcher`. If a `PathWatcher` relies on a `NativeWatcher` and + // finds that it has stopped, this registry will spin up a new + // `NativeWatcher` and allow it to resume. But we wouldn’t stop that + // `NativeWatcher` in the first place, since we know more than one + // `PathWatcher` is relying on it! + // + // I’ve made preliminary attempts to address this by moving some of the logic + // around, but it’s not yet had the effect I want. + + // detach (watcher, normalizedDirectory = undefined) { + // if (!normalizedDirectory) { + // normalizedDirectory = watcher.getNormalizedPath(); + // if (!normalizedDirectory) { + // return this.detachAsync(watcher); + // } + // } + // const pathSegments = normalizedDirectory + // .split(path.sep) + // .filter(segment => segment.length > 0); + // + // this.tree.remove(pathSegments); + // } + // + // async detachAsync(watcher) { + // const normalizedDirectory = await watcher.getNormalizedPathPromise(); + // return this.detach(watcher, normalizedDirectory); + // } + + // Private: Generate a visual representation of the currently active watchers managed by this + // registry. + // + // Returns a {String} showing the tree structure. + print() { + return this.tree.print(); + } +} + +module.exports = { NativeWatcherRegistry }; From 0df216fe6fafa77699adc199586968262b2e583c Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 24 Oct 2024 15:33:09 -0700 Subject: [PATCH 129/168] (thought I fixed this) --- spec/pathwatcher-spec.js | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index a925bee..1ff6499 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -129,13 +129,10 @@ describe('PathWatcher', () => { beforeEach(() => cleanup()); afterEach(() => cleanup()); - fit('reuses the existing native watcher', async () => { + it('reuses the existing native watcher', async () => { let rootCallback = jasmine.createSpy('rootCallback') let subDirCallback = jasmine.createSpy('subDirCallback') - let handle = PathWatcher.watch(tempFile, () => { - console.warn('LOLOLOLOLOL IT GOT CALLED'); - rootCallback(); - }); + let handle = PathWatcher.watch(tempFile, rootCallback); expect(PathWatcher.getNativeWatcherCount()).toBe(1); @@ -144,21 +141,13 @@ describe('PathWatcher', () => { subDirFile = path.join(subDir, 'test.txt'); - console.log('MAKING SUBHANDLE:\n=================\n\n\n'); - let subHandle = PathWatcher.watch(subDir, () => { - console.warn('WTFWTFWTF?!?'); - subDirCallback(); - }); + let subHandle = PathWatcher.watch(subDir, subDirCallback); expect(PathWatcher.getNativeWatcherCount()).toBe(1); - console.log('CHANGING FILE:\n========\n', tempFile, '\n\n'); fs.writeFileSync(tempFile, 'change'); - console.log('WAITING:\n========\n\n\n'); await condition(() => rootCallback.calls.count() >= 1); - console.log('WAITED!:\n=======\n\n\n'); expect(subDirCallback.calls.count()).toBe(0); - console.log('CREATING!:\n=======\n\n\n'); fs.writeFileSync(subDirFile, 'create'); // The file might get both 'create' and 'change' here. That's fine with // us. From 7ef5a2d90b0e48810b8b611e14824ea2a0d2ee33 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 24 Oct 2024 17:05:07 -0700 Subject: [PATCH 130/168] Enable more logging to gain visibility into Windows test failure --- lib/core.cc | 2 +- src/main.js | 70 +++++++++++++++++++++++------------------------------ 2 files changed, 31 insertions(+), 41 deletions(-) diff --git a/lib/core.cc b/lib/core.cc index 84f4049..365caaf 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -132,7 +132,7 @@ void PathWatcherListener::handleFileAction( efsw::Action action, std::string oldFilename ) { - // std::cout << "PathWatcherListener::handleFileAction dir: " << dir << " filename: " << filename << " action: " << EventType(action, true) << std::endl; + std::cout << "PathWatcherListener::handleFileAction dir: " << dir << " filename: " << filename << " action: " << EventType(action, true) << std::endl; // Since we’re not listening anymore, we have to stop the associated // `PathWatcherListener` so that we know when to invoke cleanup and close the diff --git a/src/main.js b/src/main.js index 8b04b64..d996fc2 100644 --- a/src/main.js +++ b/src/main.js @@ -6,14 +6,11 @@ try { } const { Emitter, Disposable, CompositeDisposable } = require('event-kit'); const fs = require('fs'); -const { stat } = require('fs/promises'); const { NativeWatcherRegistry } = require('./native-watcher-registry'); const path = require('path'); let initialized = false; -const HANDLE_WATCHERS = new Map(); - // Ensures a path that refers to a directory ends with a path separator. function sep (dirPath) { if (dirPath.endsWith(path.sep)) return dirPath; @@ -25,25 +22,25 @@ function isDirectory (somePath) { return stats.isDirectory(); } -function wait (ms) { - return new Promise(r => setTimeout(r, ms)); -} +// function wait (ms) { +// return new Promise(r => setTimeout(r, ms)); +// } -function normalizePath(rawPath) { - if (rawPath.endsWith(path.sep)) return rawPath; - return rawPath + path.sep; -} +// function normalizePath(rawPath) { +// if (rawPath.endsWith(path.sep)) return rawPath; +// return rawPath + path.sep; +// } -function pathsAreEqual(pathA, pathB) { - return normalizePath(pathA) == normalizePath(pathB); -} +// function pathsAreEqual(pathA, pathB) { +// return normalizePath(pathA) == normalizePath(pathB); +// } -function equalsOrDescendsFromPath(filePath, possibleParentPath) { - if (pathsAreEqual(filePath, possibleParentPath)) return true; - filePath = normalizePath(filePath); - possibleParentPath = normalizePath(possibleParentPath); - return filePath?.startsWith(possibleParentPath); -} +// function equalsOrDescendsFromPath(filePath, possibleParentPath) { +// if (pathsAreEqual(filePath, possibleParentPath)) return true; +// filePath = normalizePath(filePath); +// possibleParentPath = normalizePath(possibleParentPath); +// return filePath?.startsWith(possibleParentPath); +// } let NativeWatcherId = 1; @@ -322,12 +319,12 @@ class PathWatcher { } onNativeEvent (event, callback) { - console.debug( - 'PathWatcher::onNativeEvent', - event, - 'for watcher of path:', - this.originalNormalizedPath - ); + // console.debug( + // 'PathWatcher::onNativeEvent', + // event, + // 'for watcher of path:', + // this.originalNormalizedPath + // ); let isWatchedPath = (eventPath) => { return eventPath?.startsWith(sep(this.normalizedPath)); @@ -348,10 +345,7 @@ class PathWatcher { let newEvent = { ...event }; if (!newWatched && !oldWatched) { - console.debug(`This path isn’t one we care about. Skipping!`); return; - } else { - console.log('(got this far)'); } switch (newEvent.action) { @@ -377,7 +371,6 @@ class PathWatcher { } break; case 'child-delete': - console.log('CHILD-DELETE scenario!'); if (!this.isWatchingParent) { newEvent.action = 'change'; newEvent.path = ''; @@ -464,7 +457,6 @@ class PathWatcher { newEvent.path = ''; } } else { - console.log('FILE CHANGE FILE CHANGE!'); newEvent.action = 'change'; newEvent.path = ''; } @@ -472,7 +464,6 @@ class PathWatcher { } // end switch if (eventPathIsEqual && newEvent.action === 'create') { - console.log('CREATE?!?!?'); // This file or directory already existed; we checked. Any `create` // event for it is spurious. return; @@ -484,14 +475,14 @@ class PathWatcher { // live with them.) newEvent.path = newEvent.action === 'delete' ? null : ''; } - console.debug( - 'FINAL EVENT ACTION:', - newEvent.action, - 'PATH', - newEvent.path, - 'CALLBACK:', - callback.toString() - ); + // console.debug( + // 'FINAL EVENT ACTION:', + // newEvent.action, + // 'PATH', + // newEvent.path, + // 'CALLBACK:', + // callback.toString() + // ); callback(newEvent.action, newEvent.path); } @@ -520,7 +511,6 @@ class PathWatcher { } close () { - console.log('Pathwatcher with ID:', this.id, 'is closing!'); this.active = false; this.dispose(); } From 46d3e1f8dd3dd81e47b47ad5d3149b63d70df874 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 24 Oct 2024 17:08:53 -0700 Subject: [PATCH 131/168] Fix things --- lib/core.cc | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/core.cc b/lib/core.cc index 365caaf..da23657 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -3,6 +3,7 @@ #include "napi.h" #include #include +#include #ifdef __APPLE__ #include @@ -31,14 +32,14 @@ static bool EnvIsStopping(Napi::Env env) { } // Ensure a given path has a trailing separator for comparison purposes. -static std::string NormalizePath(std::string path) { - if (path.back() == PATH_SEPARATOR) return path; - return path + PATH_SEPARATOR; -} - -static bool PathsAreEqual(std::string pathA, std::string pathB) { - return NormalizePath(pathA) == NormalizePath(pathB); -} +// static std::string NormalizePath(std::string path) { +// if (path.back() == PATH_SEPARATOR) return path; +// return path + PATH_SEPARATOR; +// } + +// static bool PathsAreEqual(std::string pathA, std::string pathB) { +// return NormalizePath(pathA) == NormalizePath(pathB); +// } // This is the main-thread function that receives all `ThreadSafeFunction` // calls. It converts the `PathWatcherEvent` struct into JS values before From 09b2f21723c859fb6e722d3e11f0456d79d27055 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 24 Oct 2024 17:18:59 -0700 Subject: [PATCH 132/168] Hide logging behind `DEBUG` --- lib/core.cc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/core.cc b/lib/core.cc index da23657..402af49 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -3,7 +3,10 @@ #include "napi.h" #include #include + +#ifdef DEBUG #include +#endif #ifdef __APPLE__ #include @@ -133,7 +136,10 @@ void PathWatcherListener::handleFileAction( efsw::Action action, std::string oldFilename ) { + +#ifdef DEBUG std::cout << "PathWatcherListener::handleFileAction dir: " << dir << " filename: " << filename << " action: " << EventType(action, true) << std::endl; +#endif // Since we’re not listening anymore, we have to stop the associated // `PathWatcherListener` so that we know when to invoke cleanup and close the @@ -222,6 +228,10 @@ static int next_env_id = 1; PathWatcher::PathWatcher(Napi::Env env, Napi::Object exports) { envId = next_env_id++; +#ifdef DEBUG + std::cout << "THIS IS A DEBUG BUILD!" << std::endl; +#endif + DefineAddon(exports, { InstanceMethod("watch", &PathWatcher::Watch), InstanceMethod("unwatch", &PathWatcher::Unwatch), From 79c0eb0f330af205c7ca255b989e76ca74b0ffe1 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 24 Oct 2024 19:23:17 -0700 Subject: [PATCH 133/168] =?UTF-8?q?Filter=20out=20more=20spurious=20events?= =?UTF-8?q?=20on=20Mac=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …and fix a bug with cross-firing of events between watchers. --- lib/core.cc | 61 ++++++++++++++++++++++++++++++++++++++++------- lib/core.h | 20 ++++++++++++++-- spec/file-spec.js | 30 +++++++++++++++++++++++ src/main.js | 12 ++++++---- 4 files changed, 108 insertions(+), 15 deletions(-) diff --git a/lib/core.cc b/lib/core.cc index 402af49..ab69fbe 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -12,6 +12,23 @@ #include #endif +#ifdef _WIN32 +static int Now() { return 0; } +#else +static timeval Now() { + struct timeval tv; + gettimeofday(&tv, nullptr); + return tv; +} + +static bool PredatesWatchStart(struct timespec fileSpec, timeval startTime) { + bool fileEventOlder = fileSpec.tv_sec < startTime.tv_sec || ( + (fileSpec.tv_sec == startTime.tv_sec) && + ((fileSpec.tv_nsec / 1000) < startTime.tv_usec) + ); + return fileEventOlder; +} +#endif static std::string EventType(efsw::Action action, bool isChild) { switch (action) { case efsw::Actions::Add: @@ -112,9 +129,9 @@ void PathWatcherListener::Stop(efsw::FileWatcher* fileWatcher) { Stop(); } -void PathWatcherListener::AddPath(std::string path, efsw::WatchID handle) { +void PathWatcherListener::AddPath(PathTimestampPair pair, efsw::WatchID handle) { std::lock_guard lock(pathsMutex); - paths[handle] = path; + paths[handle] = pair; } void PathWatcherListener::RemovePath(efsw::WatchID handle) { @@ -144,7 +161,11 @@ void PathWatcherListener::handleFileAction( // Since we’re not listening anymore, we have to stop the associated // `PathWatcherListener` so that we know when to invoke cleanup and close the // open handle. + PathTimestampPair pair; std::string realPath; +#ifdef __APPLE__ + timeval startTime; +#endif { std::lock_guard lock(pathsMutex); auto it = paths.find(watchId); @@ -152,7 +173,11 @@ void PathWatcherListener::handleFileAction( // Couldn't find watcher. Assume it's been removed. return; } - realPath = it->second; + pair = it->second; + realPath = pair.path; +#ifdef __APPLE__ + startTime = pair.timestamp; +#endif } // Don't try to proceed if we've already started the shutdown process. @@ -182,13 +207,31 @@ void PathWatcherListener::handleFileAction( // Luckily, we can easily check whether or not a file has actually been // created on macOS: we can compare creation time to modification time. This // weeds out most of the false positives. - if (action == efsw::Action::Add) { + { struct stat file; - if (stat(newPathStr.c_str(), &file) != 0) { + if (stat(newPathStr.c_str(), &file) != 0 && action != efsw::Action::Delete) { return; } - if (file.st_birthtimespec.tv_sec != file.st_mtimespec.tv_sec) { - return; + if (action == efsw::Action::Add) { + if (file.st_birthtimespec.tv_sec != file.st_mtimespec.tv_sec) { +#ifdef DEBUG + std::cout << "Not a file creation! (skipping)" << std::endl; +#endif + return; + } + if (PredatesWatchStart(file.st_birthtimespec, startTime)) { +#ifdef DEBUG + std::cout << "File was created before we started this path watcher! (skipping)" << std::endl; +#endif + return; + } + } else if (action == efsw::Action::Modified) { + if (PredatesWatchStart(file.st_mtimespec, startTime)) { +#ifdef DEBUG + std::cout << "File was modified before we started this path watcher! (skipping)" << std::endl; +#endif + return; + } } } #endif @@ -249,6 +292,7 @@ PathWatcher::~PathWatcher() { // Watch a given path. Returns a handle. Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { auto env = info.Env(); + auto now = Now(); // First argument must be a string. if (!info[0].IsString()) { @@ -298,7 +342,8 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { WatcherHandle handle = fileWatcher->addWatch(cppPath, listener, true); if (handle >= 0) { - listener->AddPath(cppPath, handle); + PathTimestampPair pair = { cppPath, now }; + listener->AddPath(pair, handle); } else { // if (listener->IsEmpty()) delete listener; Napi::Error::New(env, "Failed to add watch; unknown error").ThrowAsJavaScriptException(); diff --git a/lib/core.h b/lib/core.h index 29e86c3..41f4290 100644 --- a/lib/core.h +++ b/lib/core.h @@ -6,6 +6,10 @@ #include #include "../vendor/efsw/include/efsw/efsw.hpp" +#ifdef __APPLE__ +#include +#endif + #ifdef _WIN32 #define PATH_SEPARATOR '\\' #else @@ -14,6 +18,18 @@ typedef efsw::WatchID WatcherHandle; +#ifdef _WIN32 +struct PathTimestampPair { + std::string path; + int 0; +} +#else +struct PathTimestampPair { + std::string path; + timeval timestamp; +}; +#endif + struct PathWatcherEvent { efsw::Action type; efsw::WatchID handle; @@ -73,7 +89,7 @@ class PathWatcherListener: public efsw::FileWatchListener { std::string oldFilename ) override; - void AddPath(std::string path, efsw::WatchID handle); + void AddPath(PathTimestampPair pair, efsw::WatchID handle); void RemovePath(efsw::WatchID handle); bool IsEmpty(); void Stop(); @@ -84,7 +100,7 @@ class PathWatcherListener: public efsw::FileWatchListener { std::mutex shutdownMutex; std::mutex pathsMutex; Napi::ThreadSafeFunction tsfn; - std::unordered_map paths; + std::unordered_map paths; }; diff --git a/spec/file-spec.js b/spec/file-spec.js index d04c775..e8500bf 100644 --- a/spec/file-spec.js +++ b/spec/file-spec.js @@ -161,6 +161,36 @@ describe('File', () => { }); }); + describe('when a native watcher is shared by several PathWatchers', () => { + let nephewPath = path.join(__dirname, 'fixtures', 'foo', 'bar.txt'); + let nephewFile = new File(nephewPath); + beforeEach(() => { + fs.mkdirSync(path.dirname(nephewPath)); + fs.writeFileSync(nephewPath, 'initial'); + }); + + afterEach(() => { + if (fs.existsSync(path.dirname(nephewPath))) { + fs.rmSync(path.dirname(nephewPath), { recursive: true }); + } + }) + it('does not cross-fire events', async () => { + let changeHandler1 = jasmine.createSpy('rootChangeHandler'); + file.onDidChange(changeHandler1); + await wait(100); + + let changeHandler2 = jasmine.createSpy('nephewChangeHandler'); + nephewFile.onDidChange(changeHandler2); + + await wait(100); + fs.writeFileSync(nephewPath, 'changed!'); + + await condition(() => changeHandler2.calls.count() > 0); + + expect(changeHandler1).not.toHaveBeenCalled(); + }); + }); + if (process.platform === 'darwin') { describe('when the file has already been read', () => { beforeEach(() => file.readSync()); diff --git a/src/main.js b/src/main.js index d996fc2..1e74472 100644 --- a/src/main.js +++ b/src/main.js @@ -457,8 +457,12 @@ class PathWatcher { newEvent.path = ''; } } else { - newEvent.action = 'change'; - newEvent.path = ''; + if (eventPathIsEqual) { + newEvent.action = 'change'; + newEvent.path = ''; + } else { + return; + } } break; } // end switch @@ -479,9 +483,7 @@ class PathWatcher { // 'FINAL EVENT ACTION:', // newEvent.action, // 'PATH', - // newEvent.path, - // 'CALLBACK:', - // callback.toString() + // newEvent.path // ); callback(newEvent.action, newEvent.path); } From 08fe138564665a24e2a41fad1add351f935edfb2 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 24 Oct 2024 20:08:35 -0700 Subject: [PATCH 134/168] Fix compilation errors on non-Mac platforms --- lib/core.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core.h b/lib/core.h index 41f4290..4ef5686 100644 --- a/lib/core.h +++ b/lib/core.h @@ -6,7 +6,7 @@ #include #include "../vendor/efsw/include/efsw/efsw.hpp" -#ifdef __APPLE__ +#ifndef _WIN32 #include #endif @@ -22,7 +22,7 @@ typedef efsw::WatchID WatcherHandle; struct PathTimestampPair { std::string path; int 0; -} +}; #else struct PathTimestampPair { std::string path; From 6044229af30c9f6d81e4aff5ad80541e24e536c7 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 24 Oct 2024 20:13:10 -0700 Subject: [PATCH 135/168] Fix --- lib/core.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core.h b/lib/core.h index 4ef5686..4bee671 100644 --- a/lib/core.h +++ b/lib/core.h @@ -21,7 +21,7 @@ typedef efsw::WatchID WatcherHandle; #ifdef _WIN32 struct PathTimestampPair { std::string path; - int 0; + int timestamp; }; #else struct PathTimestampPair { From afd5512013b4d1bfd452212caaafd937b745418b Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 25 Oct 2024 19:46:26 -0700 Subject: [PATCH 136/168] Fix compilation error on Ubuntu, maybe --- lib/core.h | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/core.h b/lib/core.h index 4bee671..f9ddb45 100644 --- a/lib/core.h +++ b/lib/core.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "../vendor/efsw/include/efsw/efsw.hpp" #ifndef _WIN32 From 9f355ceb9f7fc512e83ffe80c057ff0e879cc8c4 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sat, 26 Oct 2024 17:05:06 -0700 Subject: [PATCH 137/168] Native bindings changes: * Add new heuristics on macOS for filtering out spurious change and create file events. * Hide all logging behind `#ifdef DEBUG`. * Report directory-related events as happening to the directory itself. --- lib/core.cc | 127 ++++++++++++++++++++++++++++++++++------------------ lib/core.h | 17 +++++-- 2 files changed, 96 insertions(+), 48 deletions(-) diff --git a/lib/core.cc b/lib/core.cc index ab69fbe..4feb59f 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -13,14 +13,23 @@ #endif #ifdef _WIN32 +// Stub out this function on Windows. For now we don't have a need to compare +// watcher start times to file creation/modification times, so this isn't +// necessary. (Likewise, we're careful to conceal `PredatesWatchStart` calls +// behind `#ifdef`s, so we don’t need to define it at all.) static int Now() { return 0; } #else + +// Returns the current Unix timestamp. static timeval Now() { struct timeval tv; gettimeofday(&tv, nullptr); return tv; } +// Given a Unix timestamp and a file `timespec`, decides whether the file’s +// timestamp predates the Unix timestamp. Used to compare creation/modification +// times to arbitrary points in time. static bool PredatesWatchStart(struct timespec fileSpec, timeval startTime) { bool fileEventOlder = fileSpec.tv_sec < startTime.tv_sec || ( (fileSpec.tv_sec == startTime.tv_sec) && @@ -29,6 +38,7 @@ static bool PredatesWatchStart(struct timespec fileSpec, timeval startTime) { return fileEventOlder; } #endif + static std::string EventType(efsw::Action action, bool isChild) { switch (action) { case efsw::Actions::Add: @@ -52,19 +62,23 @@ static bool EnvIsStopping(Napi::Env env) { } // Ensure a given path has a trailing separator for comparison purposes. -// static std::string NormalizePath(std::string path) { -// if (path.back() == PATH_SEPARATOR) return path; -// return path + PATH_SEPARATOR; -// } +static std::string NormalizePath(std::string path) { + if (path.back() == PATH_SEPARATOR) return path; + return path + PATH_SEPARATOR; +} -// static bool PathsAreEqual(std::string pathA, std::string pathB) { -// return NormalizePath(pathA) == NormalizePath(pathB); -// } +static bool PathsAreEqual(std::string pathA, std::string pathB) { + return NormalizePath(pathA) == NormalizePath(pathB); +} // This is the main-thread function that receives all `ThreadSafeFunction` // calls. It converts the `PathWatcherEvent` struct into JS values before // invoking our callback. -static void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEvent* event) { +static void ProcessEvent( + Napi::Env env, + Napi::Function callback, + PathWatcherEvent* event +) { // Translate the event type to the expected event name in the JS code. // // NOTE: This library previously envisioned that some platforms would allow @@ -77,8 +91,6 @@ static void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEven // if we're watching a directory and that directory itself is deleted, then // that should be `delete` rather than `child-delete`. Right now we deal with // that in JavaScript, but we could handle it here instead. - std::string eventName = EventType(event->type, true); - if (EnvIsStopping(env)) return; std::string newPath; @@ -92,7 +104,17 @@ static void ProcessEvent(Napi::Env env, Napi::Function callback, PathWatcherEven oldPath.assign(event->old_path.begin(), event->old_path.end()); } - // Use a try-catch block only for the Node-API call, which might throw + // Since we watch directories, most sorts of events will only happen to files + // within the directories… + bool isChildEvent = true; + if (PathsAreEqual(newPath, event->watcher_path)) { + // …but the `delete` event can happen to the directory itself, in which + // case we should report it as `delete` rather than `child-delete`. + isChildEvent = false; + } + + std::string eventName = EventType(event->type, isChildEvent); + try { callback.Call({ Napi::String::New(env, eventName), @@ -120,23 +142,27 @@ void PathWatcherListener::Stop() { } void PathWatcherListener::Stop(efsw::FileWatcher* fileWatcher) { - { - for (auto& it : paths) { - fileWatcher->removeWatch(it.first); - } - paths.clear(); + for (auto& it : paths) { + fileWatcher->removeWatch(it.first); } + paths.clear(); Stop(); } +// Correlate a watch ID to a path/timestamp pair. void PathWatcherListener::AddPath(PathTimestampPair pair, efsw::WatchID handle) { std::lock_guard lock(pathsMutex); paths[handle] = pair; } +// Remove metadata for a given watch ID. void PathWatcherListener::RemovePath(efsw::WatchID handle) { std::lock_guard lock(pathsMutex); auto it = paths.find(handle); +#ifdef DEBUG + std::cout << "Unwatching handle: [" << handle << "] path: [" << it->second.path << "]" << std::endl; +#endif + if (it == paths.end()) return; paths.erase(it); } @@ -153,14 +179,16 @@ void PathWatcherListener::handleFileAction( efsw::Action action, std::string oldFilename ) { - #ifdef DEBUG std::cout << "PathWatcherListener::handleFileAction dir: " << dir << " filename: " << filename << " action: " << EventType(action, true) << std::endl; #endif + // Don't try to proceed if we've already started the shutdown process. + if (isShuttingDown) return; + std::lock_guard lock(shutdownMutex); + if (isShuttingDown) return; - // Since we’re not listening anymore, we have to stop the associated - // `PathWatcherListener` so that we know when to invoke cleanup and close the - // open handle. + // Extract the expected watcher path and (on macOS) the start time of the + // watcher. PathTimestampPair pair; std::string realPath; #ifdef __APPLE__ @@ -180,24 +208,9 @@ void PathWatcherListener::handleFileAction( #endif } - // Don't try to proceed if we've already started the shutdown process. - if (isShuttingDown) return; - std::lock_guard lock(shutdownMutex); - if (isShuttingDown) return; - std::string newPathStr = dir + filename; std::vector newPath(newPathStr.begin(), newPathStr.end()); - // if (PathsAreEqual(newPathStr, realPath)) { - // // This is an event that is happening to the directory itself — like the - // // directory being deleted. Allow it through. - // } else if (dir != NormalizePath(realPath)) { - // // Otherwise, we would expect `dir` to be equal to `realPath`; if it isn't, - // // then we should ignore it. This might be an event that happened to an - // // ancestor folder or a descendent folder somehow. - // return; - // } - #ifdef __APPLE__ // macOS seems to think that lots of file creations happen that aren't // actually creations; for instance, multiple successive writes to the same @@ -210,15 +223,22 @@ void PathWatcherListener::handleFileAction( { struct stat file; if (stat(newPathStr.c_str(), &file) != 0 && action != efsw::Action::Delete) { + // If this was a delete action, the file is _expected_ not to exist + // anymore. Otherwise it's a strange outcome and it means we should + // ignore this event. return; } + if (action == efsw::Action::Add) { + // One easy way to check if a file was truly just created: does its + // creation time match its modification time? If not, the file has been + // written to since its creation. if (file.st_birthtimespec.tv_sec != file.st_mtimespec.tv_sec) { -#ifdef DEBUG - std::cout << "Not a file creation! (skipping)" << std::endl; -#endif return; } + + // Next, weed out unnecessary `create` and `change` events that represent + // file actions that happened before we started watching. if (PredatesWatchStart(file.st_birthtimespec, startTime)) { #ifdef DEBUG std::cout << "File was created before we started this path watcher! (skipping)" << std::endl; @@ -250,7 +270,7 @@ void PathWatcherListener::handleFileAction( return; } - PathWatcherEvent* event = new PathWatcherEvent(action, watchId, newPath, oldPath); + PathWatcherEvent* event = new PathWatcherEvent(action, watchId, newPath, oldPath, realPath); // TODO: Instead of calling `BlockingCall` once per event, throttle them by // some small amount of time (like 50-100ms). That will allow us to deliver @@ -272,7 +292,7 @@ PathWatcher::PathWatcher(Napi::Env env, Napi::Object exports) { envId = next_env_id++; #ifdef DEBUG - std::cout << "THIS IS A DEBUG BUILD!" << std::endl; + std::cout << "Initializing PathWatcher" << std::endl; #endif DefineAddon(exports, { @@ -292,6 +312,8 @@ PathWatcher::~PathWatcher() { // Watch a given path. Returns a handle. Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { auto env = info.Env(); + // Record the current timestamp as early as possible. We'll use this as a way + // of ignoring file-watcher events that happened before we started watching. auto now = Now(); // First argument must be a string. @@ -306,6 +328,10 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { Napi::String path = info[0].ToString(); std::string cppPath(path); +#ifdef DEBUG + std::cout << "PathWatcher::Watch path: [" << cppPath << "]" << std::endl; +#endif + // It's invalid to call `watch` before having set a callback via // `setCallback`. if (callback.IsEmpty()) { @@ -314,6 +340,9 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { } if (!isWatching) { +#ifdef DEBUG + std::cout << " Creating ThreadSafeFunction and FileWatcher" << std::endl; +#endif tsfn = Napi::ThreadSafeFunction::New( env, callback.Value(), @@ -341,18 +370,25 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { // to JavaScript. WatcherHandle handle = fileWatcher->addWatch(cppPath, listener, true); +#ifdef DEBUG + std::cout << " handle: [" << handle << "]" << std::endl; +#endif + if (handle >= 0) { + // For each new watched path, remember both the normalized path and the + // time we started watching it. PathTimestampPair pair = { cppPath, now }; listener->AddPath(pair, handle); } else { - // if (listener->IsEmpty()) delete listener; - Napi::Error::New(env, "Failed to add watch; unknown error").ThrowAsJavaScriptException(); + auto error = Napi::Error::New(env, "Failed to add watch; unknown error"); + error.Set("code", Napi::Number::New(env, handle)); + error.ThrowAsJavaScriptException(); return env.Null(); } // The `watch` function returns a JavaScript number much like `setTimeout` or - // `setInterval` would; this is the handle that the consumer can use to - // unwatch the path later. + // `setInterval` would; this is the handle that the wrapper JavaScript can + // use to unwatch the path later. return WatcherHandleToV8Value(handle, env); } @@ -374,6 +410,9 @@ Napi::Value PathWatcher::Unwatch(const Napi::CallbackInfo& info) { listener->RemovePath(handle); if (listener->IsEmpty()) { +#ifdef DEBUG + std::cout << "Cleaning up!" << std::endl; +#endif Cleanup(env); isWatching = false; } diff --git a/lib/core.h b/lib/core.h index f9ddb45..3009986 100644 --- a/lib/core.h +++ b/lib/core.h @@ -37,16 +37,23 @@ struct PathWatcherEvent { std::vector new_path; std::vector old_path; + std::string watcher_path; + // Default constructor PathWatcherEvent() = default; // Constructor - PathWatcherEvent(efsw::Action t, efsw::WatchID h, const std::vector& np, const std::vector& op = std::vector()) - : type(t), handle(h), new_path(np), old_path(op) {} + PathWatcherEvent( + efsw::Action t, + efsw::WatchID h, + const std::vector& np, + const std::vector& op = std::vector(), + const std::string& wp = "" + ) : type(t), handle(h), new_path(np), old_path(op), watcher_path(wp) {} // Copy constructor PathWatcherEvent(const PathWatcherEvent& other) - : type(other.type), handle(other.handle), new_path(other.new_path), old_path(other.old_path) {} + : type(other.type), handle(other.handle), new_path(other.new_path), old_path(other.old_path), watcher_path(other.watcher_path) {} // Copy assignment operator PathWatcherEvent& operator=(const PathWatcherEvent& other) { @@ -55,6 +62,7 @@ struct PathWatcherEvent { handle = other.handle; new_path = other.new_path; old_path = other.old_path; + watcher_path = other.watcher_path; } return *this; } @@ -62,7 +70,7 @@ struct PathWatcherEvent { // Move constructor PathWatcherEvent(PathWatcherEvent&& other) noexcept : type(other.type), handle(other.handle), - new_path(std::move(other.new_path)), old_path(std::move(other.old_path)) {} + new_path(std::move(other.new_path)), old_path(std::move(other.old_path)), watcher_path(std::move(other.watcher_path)) {} // Move assignment operator PathWatcherEvent& operator=(PathWatcherEvent&& other) noexcept { @@ -71,6 +79,7 @@ struct PathWatcherEvent { handle = other.handle; new_path = std::move(other.new_path); old_path = std::move(other.old_path); + watcher_path = std::move(other.watcher_path); } return *this; } From 2dc56472424f0b5272f73da7bac0ee771fe1f192 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sat, 26 Oct 2024 17:09:23 -0700 Subject: [PATCH 138/168] Adopt new strategies for reusing native watchers: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allow two siblings (or other watch paths with close-ish common ancestry) to share a watcher that watches on their common ancestor. * Adjust native watchers to be more specific when the opportunity presents itself — e.g., if two watchers were sharing and one of them closed. * Remove automatic cleanup behavior from the registry; native watchers will be managed elsewhere. --- spec/file-spec.js | 14 +- spec/native-watcher-registry-spec.js | 75 ++++-- spec/pathwatcher-spec.js | 85 ++++++ src/main.js | 178 +++++++++---- src/native-watcher-registry.js | 379 ++++++++++++++++++++------- 5 files changed, 559 insertions(+), 172 deletions(-) diff --git a/spec/file-spec.js b/spec/file-spec.js index e8500bf..1c43027 100644 --- a/spec/file-spec.js +++ b/spec/file-spec.js @@ -165,8 +165,12 @@ describe('File', () => { let nephewPath = path.join(__dirname, 'fixtures', 'foo', 'bar.txt'); let nephewFile = new File(nephewPath); beforeEach(() => { - fs.mkdirSync(path.dirname(nephewPath)); - fs.writeFileSync(nephewPath, 'initial'); + if (!fs.existsSync(path.dirname(nephewPath))) { + fs.mkdirSync(path.dirname(nephewPath)); + } + if (!fs.existsSync(nephewPath)) { + fs.writeFileSync(nephewPath, 'initial'); + } }); afterEach(() => { @@ -186,7 +190,6 @@ describe('File', () => { fs.writeFileSync(nephewPath, 'changed!'); await condition(() => changeHandler2.calls.count() > 0); - expect(changeHandler1).not.toHaveBeenCalled(); }); }); @@ -304,7 +307,10 @@ describe('File', () => { describe('when a file is moved to the trash', () => { const MACOS_TRASH_DIR = path.join(process.env.HOME, '.Trash'); - let expectedTrashPath = path.join(MACOS_TRASH_DIR, 'file-was-moved-to-trash.txt'); + let expectedTrashPath = path.join( + MACOS_TRASH_DIR, + `file-was-moved-to-trash-${Math.round(Math.random() * 1000)}.txt` + ); it('triggers a delete event', async () => { let deleteHandler = jasmine.createSpy('deleteHandler'); diff --git a/spec/native-watcher-registry-spec.js b/spec/native-watcher-registry-spec.js index 8e11395..b7a2f27 100644 --- a/spec/native-watcher-registry-spec.js +++ b/spec/native-watcher-registry-spec.js @@ -14,6 +14,8 @@ function findRootDirectory() { } } +function EMPTY() {} + const ROOT = findRootDirectory(); function absolute(...parts) { @@ -29,6 +31,7 @@ class MockWatcher { constructor(normalizedPath) { this.normalizedPath = normalizedPath; this.native = null; + this.active = true; } getNormalizedPathPromise() { @@ -47,9 +50,15 @@ class MockWatcher { ); } this.native = native; + this.native.onDidChange(EMPTY); this.native.attached.push(this); } } + + stop () { + this.active = false; + this.native.stopIfNoListeners(); + } } class MockNative { @@ -57,17 +66,24 @@ class MockNative { this.name = name; this.attached = []; this.disposed = false; - this.stopped = false; + this.stopped = true; + this.wasListenedTo = false; this.emitter = new Emitter(); } - reattachTo(newNative, nativePath) { + reattachListenersTo(newNative, nativePath) { for (const watcher of this.attached) { watcher.attachToNative(newNative, nativePath); } } + onDidChange (callback) { + this.wasListenedTo = true; + this.stopped = false; + return this.emitter.on('did-change', callback); + } + onWillStop(callback) { return this.emitter.on('will-stop', callback); } @@ -76,9 +92,20 @@ class MockNative { this.disposed = true; } + get listenerCount () { + return this.emitter.listenerCountForEventName('did-change'); + } + + stopIfNoListeners () { + if (this.listenerCount > 0) return; + this.stop(); + } + stop() { + console.log('Stopping:', this.name); this.stopped = true; this.emitter.emit('will-stop'); + this.dispose(); } } @@ -148,8 +175,11 @@ describe('NativeWatcherRegistry', function() { const childDir1 = path.join(parentDir, 'child', 'directory', 'one'); const otherDir = absolute('another', 'path'); + const expectedCommonDir = path.join(parentDir, 'child', 'directory'); + const CHILD0 = new MockNative('existing0'); const CHILD1 = new MockNative('existing1'); + const COMMON = new MockNative('commonAncestor'); const OTHER = new MockNative('existing2'); const PARENT = new MockNative('parent'); @@ -162,37 +192,43 @@ describe('NativeWatcherRegistry', function() { return OTHER; } else if (dir === parentDir) { return PARENT; + } else if (dir === expectedCommonDir) { + return COMMON; } else { throw new Error(`Unexpected path: ${dir}`); } }; + // With only one watcher, we expect it to be CHILD0. const watcher0 = new MockWatcher(childDir0); await registry.attach(watcher0); + expect(watcher0.native).toBe(CHILD0); + // When we add another watcher at a sibling path, we expect them to unify + // under a common watcher on their closest ancestor. const watcher1 = new MockWatcher(childDir1); await registry.attach(watcher1); + expect(watcher1.native).toBe(COMMON); + expect(CHILD0.stopped).toBe(true); const watcher2 = new MockWatcher(otherDir); await registry.attach(watcher2); - - expect(watcher0.native).toBe(CHILD0); - expect(watcher1.native).toBe(CHILD1); expect(watcher2.native).toBe(OTHER); - // Consolidate all three watchers beneath the same native watcher on the parent directory + // Consolidate all three watchers beneath the same native watcher on the + // parent directory. const watcher = new MockWatcher(parentDir); await registry.attach(watcher); - expect(watcher.native).toBe(PARENT); - expect(watcher0.native).toBe(PARENT); + expect(CHILD0.stopped).toBe(true); expect(CHILD0.disposed).toBe(true); expect(watcher1.native).toBe(PARENT); + // CHILD1 should never have been used. expect(CHILD1.stopped).toBe(true); - expect(CHILD1.disposed).toBe(true); + expect(CHILD1.wasListenedTo).toBe(false); expect(watcher2.native).toBe(OTHER); expect(OTHER.stopped).toBe(false); @@ -200,7 +236,7 @@ describe('NativeWatcherRegistry', function() { }); describe('removing NativeWatchers', function() { - it('happens when they stop', async function() { + it('happens when nothing is subscribed to them', async function() { const STOPPED = new MockNative('stopped'); const RUNNING = new MockNative('running'); @@ -212,9 +248,7 @@ describe('NativeWatcherRegistry', function() { 'watcher', 'that', 'will', - 'continue', - 'to', - 'exist' + 'be' ); const runningPathParts = runningPath .split(path.sep) @@ -236,21 +270,26 @@ describe('NativeWatcherRegistry', function() { const runningWatcher = new MockWatcher(runningPath); await registry.attach(runningWatcher); - STOPPED.stop(); + stoppedWatcher.stop(); + registry.detach(stoppedWatcher); const runningNode = registry.tree.root.lookup(runningPathParts).when({ parent: node => node, missing: () => false, children: () => false }); + expect(runningNode).toBeTruthy(); expect(runningNode.getNativeWatcher()).toBe(RUNNING); + // Either of the `parent` or `missing` outcomes would be fine here as + // long as the exact node doesn't exist. const stoppedNode = registry.tree.root.lookup(stoppedPathParts).when({ - parent: () => false, + parent: (_, remaining) => remaining.length > 0, missing: () => true, children: () => false }); + expect(stoppedNode).toBe(true); }); @@ -291,7 +330,8 @@ describe('NativeWatcherRegistry', function() { expect(childWatcher1.native).toBe(PARENT); // Stopping the parent should detach and recreate the child watchers. - PARENT.stop(); + parentWatcher.stop(); + registry.detach(parentWatcher); expect(childWatcher0.native).toBe(CHILD0); expect(childWatcher1.native).toBe(CHILD1); @@ -356,7 +396,8 @@ describe('NativeWatcherRegistry', function() { // Stopping the parent should detach and create the child watchers. Both child watchers should // share the same native watcher. - PARENT.stop(); + parentWatcher.stop(); + registry.detach(parentWatcher); expect(childWatcher0.native).toBe(CHILD0); expect(childWatcher1.native).toBe(CHILD0); diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index 1ff6499..4311219 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -169,6 +169,91 @@ describe('PathWatcher', () => { }); }); + describe('when two watchers are added on sibling directories', () => { + let siblingA = path.join(tempDir, 'sibling-a'); + let siblingB = path.join(tempDir, 'sibling-b'); + + beforeEach(() => { + for (let subDir of [siblingA, siblingB]) { + if (!fs.existsSync(subDir)) { + fs.mkdirSync(subDir, { recursive: true }); + } + } + siblingA = fs.realpathSync(siblingA); + siblingB = fs.realpathSync(siblingB); + }); + + afterEach(() => { + for (let subDir of [siblingA, siblingB]) { + if (fs.existsSync(subDir)) { + fs.rmSync(subDir, { recursive: true }); + } + } + }); + + it('should consolidate them into one watcher on the parent', async () => { + let watchCallback = jasmine.createSpy('watch-callback'); + let watcherA = PathWatcher.watch(siblingA, watchCallback); + await wait(100); + expect(watcherA.native.path).toBe(siblingA); + let watcherB = PathWatcher.watch(siblingB, watchCallback); + await wait(100); + expect(watcherB.native.path).toBe(path.dirname(siblingB)); + expect(PathWatcher.getNativeWatcherCount()).toBe(1); + }); + }); + + describe('when two watchers are added on cousin directories', () => { + let cousinA = path.join(tempDir, 'placeholder-a', 'cousin-a'); + let cousinB = path.join(tempDir, 'placeholder-b', 'cousin-b'); + + beforeEach(() => { + for (let subDir of [cousinA, cousinB]) { + if (!fs.existsSync(subDir)) { + fs.mkdirSync(subDir, { recursive: true }); + } + } + cousinA = fs.realpathSync(cousinA); + cousinB = fs.realpathSync(cousinB); + }); + + afterEach(() => { + for (let subDir of [cousinA, cousinB]) { + if (fs.existsSync(subDir)) { + fs.rmSync(path.dirname(subDir), { recursive: true }); + } + } + }); + + it('should consolidate them into one watcher on the grandparent', async () => { + let watchCallbackA = jasmine.createSpy('watch-callback-a'); + let watchCallbackB = jasmine.createSpy('watch-callback-b'); + let watcherA = PathWatcher.watch(cousinA, watchCallbackA); + await wait(100); + expect(watcherA.native.path).toBe(cousinA); + let watcherB = PathWatcher.watch(cousinB, watchCallbackB); + await wait(100); + expect(watcherB.native.path).toBe(fs.realpathSync(tempDir)); + + expect(PathWatcher.getNativeWatcherCount()).toBe(1); + + fs.writeFileSync(path.join(cousinA, 'file'), 'test'); + await condition(() => watchCallbackA.calls.count() > 0); + expect(watchCallbackB.calls.count()).toBe(0); + watchCallbackA.calls.reset(); + + fs.writeFileSync(path.join(cousinB, 'file'), 'test'); + await condition(() => watchCallbackB.calls.count() > 0); + expect(watchCallbackA.calls.count()).toBe(0); + + // When we close `watcherB`, that's our opportunity to move the watcher closer to `watcherA`. + watcherB.close(); + await wait(100); + + expect(watcherA.native.path).toBe(cousinA); + }); + }); + describe('when a file under a watched directory is deleted', () => { it('fires the callback with the change event and empty path', async () => { let fileUnderDir = path.join(tempDir, 'file'); diff --git a/src/main.js b/src/main.js index 1e74472..edf16e2 100644 --- a/src/main.js +++ b/src/main.js @@ -1,13 +1,19 @@ let binding; -try { - binding = require('../build/Debug/pathwatcher.node'); -} catch (err) { - binding = require('../build/Release/pathwatcher.node'); -} -const { Emitter, Disposable, CompositeDisposable } = require('event-kit'); +// console.log('ENV:', process.NODE_ENV); +// if (process.NODE_ENV === 'DEV') { + try { + binding = require('../build/Debug/pathwatcher.node'); + } catch (err) { + binding = require('../build/Release/pathwatcher.node'); + } +// } else { +// binding = require('../build/Release/pathwatcher.node'); +// } + const fs = require('fs'); -const { NativeWatcherRegistry } = require('./native-watcher-registry'); const path = require('path'); +const { Emitter, Disposable, CompositeDisposable } = require('event-kit'); +const { NativeWatcherRegistry } = require('./native-watcher-registry'); let initialized = false; @@ -22,28 +28,41 @@ function isDirectory (somePath) { return stats.isDirectory(); } -// function wait (ms) { -// return new Promise(r => setTimeout(r, ms)); -// } - -// function normalizePath(rawPath) { -// if (rawPath.endsWith(path.sep)) return rawPath; -// return rawPath + path.sep; -// } +class WatcherError extends Error { + constructor(message, code) { + super(message); + this.name = 'WatcherError'; + this.code = code; + } +} -// function pathsAreEqual(pathA, pathB) { -// return normalizePath(pathA) == normalizePath(pathB); -// } +function getRealFilePath (filePath) { + try { + return fs.realpathSync(filePath) ?? filePath; + } catch (_err) { + return filePath; + } +} -// function equalsOrDescendsFromPath(filePath, possibleParentPath) { -// if (pathsAreEqual(filePath, possibleParentPath)) return true; -// filePath = normalizePath(filePath); -// possibleParentPath = normalizePath(possibleParentPath); -// return filePath?.startsWith(possibleParentPath); -// } +function isDirectory (filePath) { + let stats = fs.statSync(filePath); + return stats.isDirectory(); +} let NativeWatcherId = 1; +// A “native” watcher instance that is responsible for managing watchers on +// various directories. +// +// Each `NativeWatcher` can supply file-watcher events for one or more +// `PathWatcher` instances; those `PathWatcher`s may care about all events in +// the watched directory, only events that affect a particular child of the +// directory, or even events that affect a descendant folder or file. +// +// We employ some common-sense measures to consolidate watchers when they want +// to watch similar paths. A `NativeWatcher` will start when a `PathWatcher` +// first subscribes to it; it will stop when no more `PathWatcher`s are +// subscribed. class NativeWatcher { // Holds _active_ `NativeWatcher` instances. A `NativeWatcher` is active if // at least one consumer has subscribed to it via `onDidChange`; it becomes @@ -79,8 +98,16 @@ class NativeWatcher { return this.normalizedPath; } + get listenerCount () { + return this.emitter.listenerCountForEventName('did-change'); + } + start () { if (this.running) return; + if (!fs.existsSync(this.normalizedPath)) { + // We can't start a watcher on a path that doesn't exist. + return; + } this.handle = binding.watch(this.normalizedPath); NativeWatcher.INSTANCES.set(this.handle, this); this.running = true; @@ -119,13 +146,12 @@ class NativeWatcher { return this.emitter.on('did-error', callback); } - reattachTo (replacement, watchedPath, options) { + reattachListenersTo (replacement, watchedPath, options) { if (replacement === this) return; this.emitter.emit('should-detach', { replacement, watchedPath, options }); } stop (shutdown = false) { - // console.log('Stopping NativeListener', this.handle, this.running); if (this.running) { this.emitter.emit('will-stop', shutdown); binding.unwatch(this.handle); @@ -134,8 +160,6 @@ class NativeWatcher { } NativeWatcher.INSTANCES.delete(this.handle); - - // console.log('Remaining instances:', NativeWatcher.INSTANCES.size, [...NativeWatcher.INSTANCES.keys()]); } dispose () { @@ -143,8 +167,6 @@ class NativeWatcher { } onEvent (event) { - // console.log('NativeWatcher#onEvent!', event); - // console.log('onEvent!', event); this.emitter.emit('did-change', event); } @@ -153,16 +175,32 @@ class NativeWatcher { } } -class WatcherError extends Error { - constructor(message, code) { - super(message); - this.name = 'WatcherError'; - this.code = code; - } -} - let PathWatcherId = 10; +// A class responsible for watching a particular directory or file on the +// filesystem. +// +// A `PathWatcher` is backed by a `NativeWatcher` that is guaranteed to notify +// it about the events it cares about, and often other events it _doesn’t_ care +// about; it’s the `PathWatcher`’s job to filter this stream and ignore the +// irrelevant events. +// +// For instance, a `NativeWatcher` can only watch a directory, but a +// `PathWatcher` can watch a specific file in the directory. In that case, it’s +// up to the `PathWatcher` to ignore any events that do not pertain to that +// file. +// +// A `PathWatcher` might be asked to switch from one `NativeWatcher` to another +// after creation. As more `PathWatcher`s are created, and more +// `NativeWatchers` are created to support them, opportunities for +// consolidation and reuse might present themselves. For instance, sibling +// watchers with two different `NativeWatcher`s might trigger the creation of a +// new `NativeWatcher` pointing to their shared parent directory. +// +// In these scenarios, the new `NativeWatcher` is created before the old one is +// destroyed; the goal is that the switch from one `NativeWatcher` to another +// is atomic and results in no missed filesystem events. The old watcher will +// be disposed of once no `PathWatcher`s are listening to it anymore. class PathWatcher { constructor (registry, watchedPath) { this.id = PathWatcherId++; @@ -176,21 +214,36 @@ class PathWatcher { this.emitter = new Emitter(); this.subs = new CompositeDisposable(); + // NOTE: Right now we have the constraint that we can't watch a path that + // doesn't yet exist. This is a long-standing behavior of `pathwatcher` and + // would have to be changed carefully if it were changed at all. if (!fs.existsSync(watchedPath)) { throw new WatcherError('Unable to watch path', 'ENOENT'); } - try { - this.normalizedPath = fs.realpathSync(watchedPath); - } catch (err) { - this.normalizedPath = watchedPath; - } - - let stats = fs.statSync(this.normalizedPath); - this.isWatchingParent = !stats.isDirectory(); - + // Because `pathwatcher` is historically a very synchronous API, you'll see + // lots of synchronous `fs` calls in this code. This is done for + // backward-compatibility. It's a medium-term goal for us to reduce our + // dependence on this library and move its consumers to a file-watcher + // contract with an asynchronous API. + this.normalizedPath = getRealFilePath(watchedPath); + // try { + // this.normalizedPath = fs.realpathSync(watchedPath) ?? watchedPath; + // } catch (err) { + // this.normalizedPath = watchedPath; + // } + + // We must watch a directory. If this is a file, we must watch its parent. + // If this is a directory, we can watch it directly. This flag helps us + // keep track of it. + this.isWatchingParent = !isDirectory(this.normalizedPath); + + // `originalNormalizedPath` will always contain the resolved (real path on + // disk) file path that we care about. this.originalNormalizedPath = this.normalizedPath; - if (!stats.isDirectory()) { + if (this.isWatchingParent) { + // `normalizedPath` will always contain the path to the directory we mean + // to watch. this.normalizedPath = path.dirname(this.normalizedPath); } @@ -215,6 +268,8 @@ class PathWatcher { } onDidChange (callback) { + // We don't try to create a native watcher until something subscribes to + // our `did-change` events. if (this.native) { let sub = this.native.onDidChange(event => { this.onNativeEvent(event, callback); @@ -222,6 +277,9 @@ class PathWatcher { this.changeCallbacks.set(callback, sub); this.native.start(); } else { + // We don't have a native watcher yet, so we’ll ask the registry to + // assign one to us. This could be a brand-new instance or one that was + // already watching one of our ancestor folders. if (this.normalizedPath) { this.registry.attach(this); this.onDidChange(callback); @@ -243,6 +301,7 @@ class PathWatcher { return this.emitter.on('did-error', callback); } + // Attach a `NativeWatcher` to this `PathWatcher`. attachToNative (native) { this.subs.dispose(); this.subs = new CompositeDisposable(); @@ -255,8 +314,6 @@ class PathWatcher { ); } - // console.log('PathWatcher instance with path', this.originalNormalizedPath, 'is attaching to native:', native); - // Transfer any native event subscriptions to the new NativeWatcher. for (let [callback, formerSub] of this.changeCallbacks) { let newSub = native.onDidChange(event => { @@ -272,16 +329,29 @@ class PathWatcher { this.subs.add( native.onShouldDetach(({ replacement, watchedPath }) => { + // When we close all native watchers, we set this flag; we don't want + // it to trigger a flurry of new watcher creation. if (isClosingAllWatchers) return; - // console.warn('Should PathWatcher with ID', this.id, 'attach to:', replacement, 'when it already has native:', this.native, this.native === replacement); + + // The `NativeWatcher` is telling us that it may shut down; it’s + // offering a replacement `NativeWatcher`. We are in charge of whether + // we jump ship, though: + // + // * Are we even watching a path anymore? Maybe this was triggered + // because we called our own `close` method. + // * Is it trying to get us to “switch” to the `NativeWatcher` we’re + // already using? + // * Sanity check: is this even a `NativeWatcher` we can use? + // + // Keep in mind that a `NativeWatcher` isn’t doomed to be stopped + // unless it has signaled a `will-stop` event. If that hasn’t happened, + // then `should-detach` is merely offering a suggestion. if ( this.active && this.native === native && replacement !== native && this.normalizedPath?.startsWith(watchedPath) ) { - // console.log('PathWatcher with ID:', this.id, 'reattaching to:', replacement, ';\n the PathWatcher is meant to watch the path:', this.originalNormalizedPath); - // console.warn('The current watcher count is', getNativeWatcherCount()); this.attachToNative(replacement, replacement.normalizedPath); } }) @@ -515,6 +585,7 @@ class PathWatcher { close () { this.active = false; this.dispose(); + this.registry.detach(this); } } @@ -523,6 +594,7 @@ const REGISTRY = new NativeWatcherRegistry((normalizedPath) => { binding.setCallback(DEFAULT_CALLBACK); initialized = true; } + // It's important that this function be able to return an existing instance // of `NativeWatcher` when present. Otherwise, the registry will try to // create a new instance at the same path, and the native bindings won't diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index 85d006f..2451b4d 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -1,5 +1,21 @@ // Heavily based on a similar implementation in the Atom/Pulsar codebase and // licensed under MIT. +// +// This differs from the one in the Pulsar codebase in the following ways: +// +// * We employ more strategies for reusing watchers. For instance: we allow for +// “sibling” directories to detect each other and share a watcher pointing at +// their common parent. We also allow a `PathWatcher` whose `NativeWatcher` +// points at an ancestor to eschew it in favor of a closer `NativeWatcher` +// when it's the only consumer on that path chain. +// * We don't have any automatic behavior around `NativeWatcher` stopping. If a +// `NativeWatcher` stops, we assume it's because its `PathWatcher` stopped +// and it had no other `PathWatcher`s to assist. +// +// This registry does not manage `NativeWatcher`s and is not in charge of +// destroying them during cleanup. It _is_ in charge of knowing when and where +// to create a `NativeWatcher` — including knowing when it _doesn’t_ need to +// create a `NativeWatcher` because an existing one can already do the job. const path = require('path'); @@ -7,9 +23,10 @@ const path = require('path'); // absolute path. function absolute(...parts) { const candidate = path.join(...parts); - return path.isAbsolute(candidate) + let result = path.isAbsolute(candidate) ? candidate : path.join(path.sep, candidate); + return result; } // Private: Map userland filesystem watcher subscriptions efficiently to @@ -35,17 +52,10 @@ class RegistryTree { // {NativeWatcher}. constructor(basePathSegments, createNative) { this.basePathSegments = basePathSegments; - this.root = new RegistryNode(); + this.root = new RegistryNode(null); this.createNative = createNative; } - destroyNativeWatchers(shutdown) { - let leaves = this.root.leaves([]); - for (let leaf of leaves) { - leaf.node.destroyNativeWatcher(shutdown); - } - } - // Private: Identify the native watcher that should be used to produce events // at a watched path, creating a new one if necessary. // @@ -56,7 +66,25 @@ class RegistryTree { add(pathSegments, attachToNative) { const absolutePathSegments = this.basePathSegments.concat(pathSegments); const absolutePath = absolute(...absolutePathSegments); - const attachToNew = childPaths => { + + // Scenario in which we're attaching to an ancestor of this path — for + // instance, if we discover that we share an ancestor directory with an + // existing watcher. + const attachToAncestor = (absolutePaths, childPaths) => { + let ancestorAbsolutePath = absolute(...absolutePaths); + let native = this.createNative(ancestorAbsolutePath); + let leaf = new RegistryWatcherNode( + native, + absolutePaths, + childPaths + ); + this.root = this.root.insert(absolutePaths, leaf); + attachToNative(native, ancestorAbsolutePath); + return native; + }; + + // Scenario in which we're attaching directly to a specific path. + const attachToNew = (childPaths) => { const native = this.createNative(absolutePath); const leaf = new RegistryWatcherNode( native, @@ -64,48 +92,132 @@ class RegistryTree { childPaths ); this.root = this.root.insert(pathSegments, leaf); - const sub = native.onWillStop((shutdown) => { - sub.dispose(); - if (shutdown) return; - this.root = - this.root.remove(pathSegments, this.createNative) || - new RegistryNode(); - }); - attachToNative(native, absolutePath); return native; }; this.root.lookup(pathSegments).when({ parent: (parent, remaining) => { - // An existing NativeWatcher is watching the same directory or a parent - // directory of the requested path. Attach this Watcher to it as a - // filtering watcher and record it as a dependent child path. + // An existing `NativeWatcher` is watching the same directory or a + // parent directory of the requested path. Attach this `PathWatcher` to + // it as a filtering watcher and record it as a dependent child path. const native = parent.getNativeWatcher(); parent.addChildPath(remaining); attachToNative(native, absolute(...parent.getAbsolutePathSegments())); }, children: children => { - // One or more NativeWatchers exist on child directories of the - // requested path. Create a new native watcher on the parent directory, - // note the subscribed child paths, and cleanly stop the child native - // watchers. + // One or more `NativeWatcher`s exist on child directories of the + // requested path. Create a new `NativeWatcher` on the parent + // directory, note the subscribed child paths, and cleanly stop the + // child native watchers. const newNative = attachToNew(children.map(child => child.path)); - for (let i = 0; i < children.length; i++) { const childNode = children[i].node; const childNative = childNode.getNativeWatcher(); - childNative.reattachTo(newNative, absolutePath); + childNative.reattachListenersTo(newNative, absolutePath); childNative.dispose(); childNative.stop(); } }, - missing: () => attachToNew([]) + missing: (lastParent) => { + // We couldn't find an existing watcher anywhere above us in this path + // hierarchy. But we helpfully receive the last node that was already + // in the tree (i.e., created by a previous watcher), so we might be + // able to consolidate two watchers. + if (lastParent?.parent == null) { + // We're at the root node; there is no other watcher in this tree. + // Create one at the current location. + attachToNew([]); + return; + } + + let leaves = lastParent.leaves(this.basePathSegments); + if (leaves.length === 0) { + // There's an ancestor node, but it doesn't have any native watchers + // below it. This would happen if there once was a watcher at a + // different point in the tree, but it was disposed of before we got + // here. + // + // This is functionally the same as the above case, so we'll create a + // new native watcher at the current path. + attachToNew([]); + return; + } + + // If we get this far, then one of our ancestor directories has an + // active native watcher somewhere underneath it. We can streamline + // native watchers by creating a new one to manage two or more existing + // paths, then stopping the one that was previously running. + let ancestorPathSegments = lastParent.getPathSegments(this); + + let remainingPathSegments = [...pathSegments]; + for (let i = 0; i < ancestorPathSegments.length; i++) { + remainingPathSegments.shift(); + } + + // Taken to its logical extreme, this approach would always yield + // a maximum of one watcher, since all paths have a common ancestor. + // But if we listen at the root of the volume, we'll be drinking from a + // firehose and making our wrapped watchers do a lot of work. + // + // So we should strike a balance: good to consolidate watchers when + // they’re “close enough” to one another in the tree, but bad to do it + // obsessively and create lots of churn. + // + // NOTE: We can also introduce platform-specific logic here. For + // instance, consolidating watchers seems to be important on macOS and + // less so on Windows and Linux. + // + // Let's impose some constraints: + + // Impose a max distance when moving upward. This will let us avoid + // _creating_ a new watcher that's more than a certain number of levels + // above the path we care about. + // + // This does not prevent us from _reusing_ such a watcher that is + // already present (as in the `parent` scenario above). We were already + // paying the cost of that watcher. + // + // TODO: Expose configuration options for these constraints to the + // consumer. + // + let difference = pathSegments.length - ancestorPathSegments.length; + // console.debug('Tier difference:', difference); + if (difference > 3) { + attachToNew([]); + return; + } + + // NOTE: Future ideas for constraints: + // + // * Don't create a new watcher at the root unless explicitly told to. + // * Allow the wrapper code to specify certain paths above which we're + // not allowed to ascend unless explicitly told. (The user's home + // folder feels like a good one.) + // * Perhaps enforce a soft native-watcher quota and have it + // consolidate more aggressively when we're close to the quota than + // when we're not. + + let childPaths = leaves.map(l => l.path); + childPaths.push(remainingPathSegments); + let newNative = attachToAncestor(ancestorPathSegments, childPaths); + let absolutePath = absolute(...ancestorPathSegments); + for (let i = 0; i < leaves.length; i++) { + let leaf = leaves[i].node; + let native = leaf.getNativeWatcher(); + native.reattachListenersTo(newNative, absolutePath); + native.dispose(); + native.stop(); + // NOTE: Should not need to dispose of native watchers; it should + // happen automatically as they are left. + } + } }); } - remove (pathSegments) { - return this.root.remove(pathSegments, this.createNative); + remove (pathSegments, attachToNative) { + this.root = this.root.remove(pathSegments, this.createNative) || + new RegistryNode(null); } // Private: Access the root node of the tree. @@ -125,10 +237,22 @@ class RegistryTree { // {RegistryNode} maps to a directory in the filesystem tree. class RegistryNode { // Private: Construct a new, empty node representing a node with no watchers. - constructor() { + constructor(parent, pathKey) { + this.parent = parent; + this.pathKey = pathKey; this.children = {}; } + getPathSegments (comparison = null) { + let result = [this.pathKey]; + let pointer = this.parent; + while (pointer && pointer.pathKey && pointer !== comparison) { + result.unshift(pointer.pathKey); + pointer = pointer.parent; + } + return result; + } + // Private: Recursively discover any existing watchers corresponding to a // path. // @@ -151,15 +275,19 @@ class RegistryNode { return child.lookup(pathSegments.slice(1)); } - // Private: Insert a new {RegistryWatcherNode} into the tree, creating new intermediate {RegistryNode} instances as - // needed. Any existing children of the watched directory are removed. + // Private: Insert a new {RegistryWatcherNode} into the tree, creating new + // intermediate {RegistryNode} instances as needed. Any existing children of + // the watched directory are removed. // - // * `pathSegments` filesystem path of the new {Watcher}, already split into an Array of directory names. + // * `pathSegments` filesystem path of the new {Watcher}, already split into + // an Array of directory names. // * `leaf` initialized {RegistryWatcherNode} to insert // - // Returns: The root of a new tree with the {RegistryWatcherNode} inserted at the correct location. Callers should - // replace their node references with the returned value. + // Returns: The root of a new tree with the {RegistryWatcherNode} inserted at + // the correct location. Callers should replace their node references with + // the returned value. insert(pathSegments, leaf) { + // console.log('Insert:', pathSegments); if (pathSegments.length === 0) { return leaf; } @@ -167,7 +295,7 @@ class RegistryNode { const pathKey = pathSegments[0]; let child = this.children[pathKey]; if (child === undefined) { - child = new RegistryNode(); + child = new RegistryNode(this, pathKey); } this.children[pathKey] = child.insert(pathSegments.slice(1), leaf); return this; @@ -207,13 +335,14 @@ class RegistryNode { return Object.keys(this.children).length === 0 ? null : this; } - // Private: Discover all {RegistryWatcherNode} instances beneath this tree node and the child paths - // that they are watching. + // Private: Discover all {RegistryWatcherNode} instances beneath this tree + // node and the child paths that they are watching. // - // * `prefix` {Array} of intermediate path segments to prepend to the resulting child paths. + // * `prefix` {Array} of intermediate path segments to prepend to the + // resulting child paths. // - // Returns: A possibly empty {Array} of `{node, path}` objects describing {RegistryWatcherNode} - // instances beneath this node. + // Returns: A possibly empty {Array} of `{node, path}` objects describing + // {RegistryWatcherNode} instances beneath this node. leaves(prefix) { const results = []; for (const p of Object.keys(this.children)) { @@ -222,7 +351,8 @@ class RegistryNode { return results; } - // Private: Return a {String} representation of this subtree for diagnostics and testing. + // Private: Return a {String} representation of this subtree for diagnostics + // and testing. print(indent = 0) { let spaces = ''; for (let i = 0; i < indent; i++) { @@ -282,18 +412,26 @@ class RegistryWatcherNode { return this.nativeWatcher; } + // Private + insert (pathSegments, leaf) { + if (pathSegments.length === 0) return leaf; + } + destroyNativeWatcher(shutdown) { this.nativeWatcher.stop(shutdown); } - // Private: Return the absolute path watched by this {NativeWatcher} as an {Array} of directory names. + // Private: Return the absolute path watched by this {NativeWatcher} as an + // {Array} of directory names. getAbsolutePathSegments() { return this.absolutePathSegments; } - // Private: Identify how this watcher relates to a request to watch a directory tree. + // Private: Identify how this watcher relates to a request to watch a + // directory tree. // - // * `pathSegments` filesystem path of a new {Watcher} already split into an Array of directory names. + // * `pathSegments` filesystem path of a new {Watcher} already split into an + // Array of directory names. // // Returns: A {ParentResult} referencing this node. lookup(pathSegments) { @@ -314,9 +452,22 @@ class RegistryWatcherNode { // watcher's path, it's an attempt to remove a subnode that doesn't exist, so // the remove call has no effect and returns `this` unaltered. remove(pathSegments, createSplitNative) { - if (pathSegments.length !== 0) { - return this; - } else if (this.childPaths.size > 0) { + // This function represents converting this `RegistryWatcherNode` into a + // plain `RegistryNode` that no longer has the direct responsibility of + // managing a native watcher. Any child paths on this node are converted to + // leaf nodes with their own native watchers. + // + // We do this if: + // + // * This path itself is being removed. + // * One of this path’s child paths is being removed and it has only one + // remaining child path. (We move the watcher down to the child in this + // instance.) + // + // TODO: Also invoke some form of this logic if more than two paths are + // being watched… but the removal of a path creates a scenario where we can + // move a watcher to a closer common descendant of the remaining paths. + let replacedWithNode = () =>{ let newSubTree = new RegistryTree( this.absolutePathSegments, createSplitNative @@ -325,11 +476,23 @@ class RegistryWatcherNode { for (const childPath of this.childPaths) { const childPathSegments = childPath.split(path.sep); newSubTree.add(childPathSegments, (native, attachmentPath) => { - this.nativeWatcher.reattachTo(native, attachmentPath); + this.nativeWatcher.reattachListenersTo(native, attachmentPath); }); } - return newSubTree.getRoot(); + }; + if (pathSegments.length !== 0) { + this.removeChildPath(pathSegments); + if (this.childPaths.size === 1) { + return replacedWithNode(); + } + return this; + } else if (this.childPaths.size > 0) { + // We are here because a watcher for this path is being removed. If this + // path has descendants depending on the same watcher, this is an + // opportunity to create a new `NativeWatcher` that is more proximate to + // those descendants. + return replacedWithNode(); } else { return null; } @@ -361,8 +524,8 @@ class RegistryWatcherNode { } } -// Private: A {RegistryNode} traversal result that's returned when neither a directory, its children, nor its parents -// are present in the tree. +// Private: A {RegistryNode} traversal result that's returned when neither a +// directory, its children, nor its parents are present in the tree. class MissingResult { // Private: Instantiate a new {MissingResult}. // @@ -373,9 +536,10 @@ class MissingResult { // Private: Dispatch within a map of callback actions. // - // * `actions` {Object} containing a `missing` key that maps to a callback to be invoked when no results were returned - // by {RegistryNode.lookup}. The callback will be called with the last parent node that was encountered during the - // traversal. + // * `actions` {Object} containing a `missing` key that maps to a callback to + // be invoked when no results were returned by {RegistryNode.lookup}. The + // callback will be called with the last parent node that was encountered + // during the traversal. // // Returns: the result of the `actions` callback. when(actions) { @@ -383,25 +547,38 @@ class MissingResult { } } -// Private: A {RegistryNode.lookup} traversal result that's returned when a parent or an exact match of the requested -// directory is being watched by an existing {RegistryWatcherNode}. +// Private: A {RegistryNode.lookup} traversal result that's returned when a +// parent or an exact match of the requested directory is being watched by an +// existing {RegistryWatcherNode}. class ParentResult { // Private: Instantiate a new {ParentResult}. // // * `parent` the {RegistryWatcherNode} that was discovered. - // * `remainingPathSegments` an {Array} of the directories that lie between the leaf node's watched directory and - // the requested directory. This will be empty for exact matches. + // * `remainingPathSegments` an {Array} of the directories that lie between + // the leaf node's watched directory and the requested directory. This will + // be empty for exact matches. constructor(parent, remainingPathSegments) { this.parent = parent; this.remainingPathSegments = remainingPathSegments; } + getAbsolutePathSegments () { + let result = Array.from(this.remainingPathSegments); + let pointer = this.parent; + while (pointer) { + result.push(pointer.remainingPathSegments); + pointer = pointer.parent; + } + return result; + } + // Private: Dispatch within a map of callback actions. // - // * `actions` {Object} containing a `parent` key that maps to a callback to be invoked when a parent of a requested - // requested directory is returned by a {RegistryNode.lookup} call. The callback will be called with the - // {RegistryWatcherNode} instance and an {Array} of the {String} path segments that separate the parent node - // and the requested directory. + // * `actions` {Object} containing a `parent` key that maps to a callback to + // be invoked when a parent of a requested requested directory is returned + // by a {RegistryNode.lookup} call. The callback will be called with the + // {RegistryWatcherNode} instance and an {Array} of the {String} path + // segments that separate the parent node and the requested directory. // // Returns: the result of the `actions` callback. when(actions) { @@ -409,21 +586,23 @@ class ParentResult { } } -// Private: A {RegistryNode.lookup} traversal result that's returned when one or more children of the requested -// directory are already being watched. +// Private: A {RegistryNode.lookup} traversal result that's returned when one +// or more children of the requested directory are already being watched. class ChildrenResult { // Private: Instantiate a new {ChildrenResult}. // - // * `children` {Array} of the {RegistryWatcherNode} instances that were discovered. + // * `children` {Array} of the {RegistryWatcherNode} instances that were + // discovered. constructor(children) { this.children = children; } // Private: Dispatch within a map of callback actions. // - // * `actions` {Object} containing a `children` key that maps to a callback to be invoked when a parent of a requested - // requested directory is returned by a {RegistryNode.lookup} call. The callback will be called with the - // {RegistryWatcherNode} instance. + // * `actions` {Object} containing a `children` key that maps to a callback + // to be invoked when a parent of a requested requested directory is + // returned by a {RegistryNode.lookup} call. The callback will be called + // with the {RegistryWatcherNode} instance. // // Returns: the result of the `actions` callback. when(actions) { @@ -444,26 +623,27 @@ class ChildrenResult { class NativeWatcherRegistry { // Private: Instantiate an empty registry. // - // * `createNative` {Function} that will be called with a normalized filesystem path to create a new native - // filesystem watcher. + // * `createNative` {Function} that will be called with a normalized + // filesystem path to create a new native filesystem watcher. constructor(createNative) { this._createNative = createNative; this.tree = new RegistryTree([], createNative); } reset () { - this.tree.destroyNativeWatchers(true); this.tree = new RegistryTree([], this._createNative); } - // Private: Attach a watcher to a directory, assigning it a {NativeWatcher}. If a suitable {NativeWatcher} already - // exists, it will be attached to the new {Watcher} with an appropriate subpath configuration. Otherwise, the - // `createWatcher` callback will be invoked to create a new {NativeWatcher}, which will be registered in the tree - // and attached to the watcher. + // Private: Attach a watcher to a directory, assigning it a {NativeWatcher}. + // If a suitable {NativeWatcher} already exists, it will be attached to the + // new {Watcher} with an appropriate subpath configuration. Otherwise, the + // `createWatcher` callback will be invoked to create a new {NativeWatcher}, + // which will be registered in the tree and attached to the watcher. // - // If any pre-existing child watchers are removed as a result of this operation, {NativeWatcher.onWillReattach} will - // be broadcast on each with the new parent watcher as an event payload to give child watchers a chance to attach to - // the new watcher. + // If any pre-existing child watchers are removed as a result of this + // operation, {NativeWatcher.onWillReattach} will be broadcast on each with + // the new parent watcher as an event payload to give child watchers a chance + // to attach to the new watcher. // // * `watcher` an unattached {Watcher}. attach(watcher, normalizedDirectory = undefined) { @@ -524,24 +704,27 @@ class NativeWatcherRegistry { // I’ve made preliminary attempts to address this by moving some of the logic // around, but it’s not yet had the effect I want. - // detach (watcher, normalizedDirectory = undefined) { - // if (!normalizedDirectory) { - // normalizedDirectory = watcher.getNormalizedPath(); - // if (!normalizedDirectory) { - // return this.detachAsync(watcher); - // } - // } - // const pathSegments = normalizedDirectory - // .split(path.sep) - // .filter(segment => segment.length > 0); - // - // this.tree.remove(pathSegments); - // } - // - // async detachAsync(watcher) { - // const normalizedDirectory = await watcher.getNormalizedPathPromise(); - // return this.detach(watcher, normalizedDirectory); - // } + detach (watcher, normalizedDirectory = undefined) { + if (!normalizedDirectory) { + normalizedDirectory = watcher.getNormalizedPath(); + if (!normalizedDirectory) { + return this.detachAsync(watcher); + } + } + const pathSegments = normalizedDirectory + .split(path.sep) + .filter(segment => segment.length > 0); + + this.tree.remove(pathSegments, (native, nativePath) => { + watcher.attachToNative(native, nativePath); + }); + + } + + async detachAsync(watcher) { + const normalizedDirectory = await watcher.getNormalizedPathPromise(); + return this.detach(watcher, normalizedDirectory); + } // Private: Generate a visual representation of the currently active watchers managed by this // registry. From bef34364c392fc72df0b68debebaca39dd4648ef Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sat, 26 Oct 2024 22:29:15 -0700 Subject: [PATCH 139/168] =?UTF-8?q?Add=20ability=20to=20configure=20the=20?= =?UTF-8?q?amount=20of=20native=20watcher=20reuse=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …so that we can specify different levels of reuse aggressiveness for different platforms. (Reuse is most important on macOS given the hard limit on FSEvent watchers. On other platforms we can afford to create more native watchers. --- spec/pathwatcher-spec.js | 27 +++++-- src/main.js | 64 +++++++++++---- src/native-watcher-registry.js | 144 +++++++++++++++++++++++++-------- 3 files changed, 180 insertions(+), 55 deletions(-) diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index 4311219..493ffb2 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -191,15 +191,20 @@ describe('PathWatcher', () => { } }); - it('should consolidate them into one watcher on the parent', async () => { + it('should consolidate them into one watcher on the parent (unless options prohibit it)', async () => { let watchCallback = jasmine.createSpy('watch-callback'); let watcherA = PathWatcher.watch(siblingA, watchCallback); await wait(100); expect(watcherA.native.path).toBe(siblingA); let watcherB = PathWatcher.watch(siblingB, watchCallback); await wait(100); - expect(watcherB.native.path).toBe(path.dirname(siblingB)); - expect(PathWatcher.getNativeWatcherCount()).toBe(1); + // The watchers will only be consolidated in this scenario if the + // registry is configured to do so. + let shouldConsolidate = watcherB.registry.options.mergeWatchersWithCommonAncestors; + expect( + watcherB.native.path + ).toBe(shouldConsolidate ? path.dirname(siblingB) : siblingB); + expect(PathWatcher.getNativeWatcherCount()).toBe(shouldConsolidate ? 1 : 2); }); }); @@ -225,7 +230,7 @@ describe('PathWatcher', () => { } }); - it('should consolidate them into one watcher on the grandparent', async () => { + it('should consolidate them into one watcher on the grandparent (unless options prohibit it)', async () => { let watchCallbackA = jasmine.createSpy('watch-callback-a'); let watchCallbackB = jasmine.createSpy('watch-callback-b'); let watcherA = PathWatcher.watch(cousinA, watchCallbackA); @@ -233,9 +238,17 @@ describe('PathWatcher', () => { expect(watcherA.native.path).toBe(cousinA); let watcherB = PathWatcher.watch(cousinB, watchCallbackB); await wait(100); - expect(watcherB.native.path).toBe(fs.realpathSync(tempDir)); - expect(PathWatcher.getNativeWatcherCount()).toBe(1); + // The watchers will only be consolidated in this scenario if the + // registry is configured to do so. + let shouldConsolidate = watcherB.registry.options.mergeWatchersWithCommonAncestors; + shouldConsolidate &&= watcherB.registry.options.maxCommonAncestorLevel >= 2; + + expect( + watcherB.native.path + ).toBe(shouldConsolidate ? fs.realpathSync(tempDir) : cousinB); + + expect(PathWatcher.getNativeWatcherCount()).toBe(shouldConsolidate ? 1 : 2); fs.writeFileSync(path.join(cousinA, 'file'), 'test'); await condition(() => watchCallbackA.calls.count() > 0); @@ -246,6 +259,8 @@ describe('PathWatcher', () => { await condition(() => watchCallbackB.calls.count() > 0); expect(watchCallbackA.calls.count()).toBe(0); + if (!shouldConsolidate) return; + // When we close `watcherB`, that's our opportunity to move the watcher closer to `watcherA`. watcherB.close(); await wait(100); diff --git a/src/main.js b/src/main.js index edf16e2..b04a8fb 100644 --- a/src/main.js +++ b/src/main.js @@ -589,24 +589,56 @@ class PathWatcher { } } -const REGISTRY = new NativeWatcherRegistry((normalizedPath) => { - if (!initialized) { - binding.setCallback(DEFAULT_CALLBACK); - initialized = true; +function getDefaultRegistryOptionsForPlatform (platform) { + switch (platform) { + case 'linux': + case 'win32': + return { + reuseAncestorWatchers: true, + relocateDescendantWatchers: false, + relocateAncestorWatchers: true, + mergeWatchersWithCommonAncestors: false + }; + case 'darwin': + return { + reuseAncestorWatchers: true, + relocateDescendantWatchers: false, + relocateAncestorWatchers: true, + mergeWatchersWithCommonAncestors: true, + maxCommonAncestorLevel: 2 + }; + default: + return { + reuseAncestorWatchers: true, + relocateDescendantWatchers: false, + relocateAncestorWatchers: true, + mergeWatchersWithCommonAncestors: false + }; } +} + + +const REGISTRY = new NativeWatcherRegistry( + (normalizedPath) => { + if (!initialized) { + binding.setCallback(DEFAULT_CALLBACK); + initialized = true; + } - // It's important that this function be able to return an existing instance - // of `NativeWatcher` when present. Otherwise, the registry will try to - // create a new instance at the same path, and the native bindings won't - // allow that to happen. - // - // It's also important because the registry might respond to a sibling - // `PathWatcher`’s removal by trying to reattach us — even though our - // `NativeWatcher` still works just fine. The way around that is to make sure - // that this function will return the same watcher we're already using - // instead of creating a new one. - return NativeWatcher.findOrCreate(normalizedPath); -}); + // It's important that this function be able to return an existing instance + // of `NativeWatcher` when present. Otherwise, the registry will try to + // create a new instance at the same path, and the native bindings won't + // allow that to happen. + // + // It's also important because the registry might respond to a sibling + // `PathWatcher`’s removal by trying to reattach us — even though our + // `NativeWatcher` still works just fine. The way around that is to make sure + // that this function will return the same watcher we're already using + // instead of creating a new one. + return NativeWatcher.findOrCreate(normalizedPath); + }, + getDefaultRegistryOptionsForPlatform(process.platform) +); class WatcherEvent { constructor(event, filePath, oldFilePath) { diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index 2451b4d..a9a8871 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -50,10 +50,11 @@ class RegistryTree { // * `createNative` {Function} used to construct new native watchers. It // should accept an absolute path as an argument and return a new // {NativeWatcher}. - constructor(basePathSegments, createNative) { + constructor(basePathSegments, createNative, options = {}) { this.basePathSegments = basePathSegments; this.root = new RegistryNode(null); this.createNative = createNative; + this.options = options; } // Private: Identify the native watcher that should be used to produce events @@ -76,7 +77,8 @@ class RegistryTree { let leaf = new RegistryWatcherNode( native, absolutePaths, - childPaths + childPaths, + this.options ); this.root = this.root.insert(absolutePaths, leaf); attachToNative(native, ancestorAbsolutePath); @@ -89,7 +91,8 @@ class RegistryTree { const leaf = new RegistryWatcherNode( native, absolutePathSegments, - childPaths + childPaths, + this.options ); this.root = this.root.insert(pathSegments, leaf); attachToNative(native, absolutePath); @@ -98,6 +101,10 @@ class RegistryTree { this.root.lookup(pathSegments).when({ parent: (parent, remaining) => { + if (!this.options.reuseAncestorWatchers) { + attachToNew([]); + return; + } // An existing `NativeWatcher` is watching the same directory or a // parent directory of the requested path. Attach this `PathWatcher` to // it as a filtering watcher and record it as a dependent child path. @@ -120,6 +127,11 @@ class RegistryTree { } }, missing: (lastParent) => { + if (!this.options.mergeWatchersWithCommonAncestors) { + attachToNew([]); + return; + } + // We couldn't find an existing watcher anywhere above us in this path // hierarchy. But we helpfully receive the last node that was already // in the tree (i.e., created by a previous watcher), so we might be @@ -132,6 +144,7 @@ class RegistryTree { } let leaves = lastParent.leaves(this.basePathSegments); + if (leaves.length === 0) { // There's an ancestor node, but it doesn't have any native watchers // below it. This would happen if there once was a watcher at a @@ -182,8 +195,8 @@ class RegistryTree { // consumer. // let difference = pathSegments.length - ancestorPathSegments.length; - // console.debug('Tier difference:', difference); - if (difference > 3) { + console.debug('Tier difference:', difference); + if (difference > this.options.maxCommonAncestorLevel) { attachToNew([]); return; } @@ -215,7 +228,7 @@ class RegistryTree { }); } - remove (pathSegments, attachToNative) { + remove (pathSegments) { this.root = this.root.remove(pathSegments, this.createNative) || new RegistryNode(null); } @@ -225,7 +238,8 @@ class RegistryTree { return this.root; } - // Private: Return a {String} representation of this tree's structure for diagnostics and testing. + // Private: Return a {String} representation of this tree’s structure for + // diagnostics and testing. print() { return this.root.print(); } @@ -237,10 +251,11 @@ class RegistryTree { // {RegistryNode} maps to a directory in the filesystem tree. class RegistryNode { // Private: Construct a new, empty node representing a node with no watchers. - constructor(parent, pathKey) { + constructor(parent, pathKey, options) { this.parent = parent; this.pathKey = pathKey; this.children = {}; + this.options = options; } getPathSegments (comparison = null) { @@ -295,7 +310,7 @@ class RegistryNode { const pathKey = pathSegments[0]; let child = this.children[pathKey]; if (child === undefined) { - child = new RegistryNode(this, pathKey); + child = new RegistryNode(this, pathKey, this.options); } this.children[pathKey] = child.insert(pathSegments.slice(1), leaf); return this; @@ -373,14 +388,16 @@ class RegistryWatcherNode { // Private: Allocate a new node to track a {NativeWatcher}. // // * `nativeWatcher` An existing {NativeWatcher} instance. - // * `absolutePathSegments` The absolute path to this {NativeWatcher}'s directory as an {Array} of - // path segments. - // * `childPaths` {Array} of child directories that are currently the responsibility of this - // {NativeWatcher}, if any. Directories are represented as arrays of the path segments between this - // node's directory and the watched child path. - constructor(nativeWatcher, absolutePathSegments, childPaths) { + // * `absolutePathSegments` The absolute path to this {NativeWatcher}'s + // directory as an {Array} of path segments. + // * `childPaths` {Array} of child directories that are currently the + // responsibility of this {NativeWatcher}, if any. Directories are + // represented as arrays of the path segments between this node's directory + // and the watched child path. + constructor(nativeWatcher, absolutePathSegments, childPaths, options) { this.nativeWatcher = nativeWatcher; this.absolutePathSegments = absolutePathSegments; + this.options = options; // Store child paths as joined strings so they work as Set members. this.childPaths = new Set(); @@ -389,20 +406,22 @@ class RegistryWatcherNode { } } - // Private: Assume responsibility for a new child path. If this node is removed, it will instead - // split into a subtree with a new {RegistryWatcherNode} for each child path. + // Private: Assume responsibility for a new child path. If this node is + // removed, it will instead split into a subtree with a new + // {RegistryWatcherNode} for each child path. // - // * `childPathSegments` the {Array} of path segments between this node's directory and the watched - // child directory. + // * `childPathSegments` the {Array} of path segments between this node's + // directory and the watched child directory. addChildPath(childPathSegments) { this.childPaths.add(path.join(...childPathSegments)); } - // Private: Stop assuming responsibility for a previously assigned child path. If this node is - // removed, the named child path will no longer be allocated a {RegistryWatcherNode}. + // Private: Stop assuming responsibility for a previously assigned child + // path. If this node is removed, the named child path will no longer be + // allocated a {RegistryWatcherNode}. // - // * `childPathSegments` the {Array} of path segments between this node's directory and the no longer - // watched child directory. + // * `childPathSegments` the {Array} of path segments between this node's + // directory and the no longer watched child directory. removeChildPath(childPathSegments) { this.childPaths.delete(path.join(...childPathSegments)); } @@ -470,7 +489,8 @@ class RegistryWatcherNode { let replacedWithNode = () =>{ let newSubTree = new RegistryTree( this.absolutePathSegments, - createSplitNative + createSplitNative, + this.options ); for (const childPath of this.childPaths) { @@ -500,15 +520,18 @@ class RegistryWatcherNode { // Private: Discover this {RegistryWatcherNode} instance. // - // * `prefix` {Array} of intermediate path segments to prepend to the resulting child paths. + // * `prefix` {Array} of intermediate path segments to prepend to the + // resulting child paths. // - // Returns: An {Array} containing a `{node, path}` object describing this node. + // Returns: An {Array} containing a `{node, path}` object describing this + // node. leaves(prefix) { return [{ node: this, path: prefix }]; } - // Private: Return a {String} representation of this watcher for diagnostics and testing. Indicates the number of - // child paths that this node's {NativeWatcher} is responsible for. + // Private: Return a {String} representation of this watcher for diagnostics + // and testing. Indicates the number of child paths that this node's + // {NativeWatcher} is responsible for. print(indent = 0) { let result = ''; for (let i = 0; i < indent; i++) { @@ -621,17 +644,72 @@ class ChildrenResult { // 3. Replacing multiple {NativeWatcher} instances on child directories with a // single new {NativeWatcher} on the parent. class NativeWatcherRegistry { + static DEFAULT_OPTIONS = { + // When adding a watcher for `/foo/bar/baz/thud`, will reuse any of + // `/foo/bar/baz`, `/foo/bar`, or `/foo` that may already exist. + // + // When `false`, a second native watcher will be created in this scenario + // instead. + reuseAncestorWatchers: true, + + // When a single native watcher exists at `/foo/bar/baz/thud` and a watcher + // is added for `/foo/bar`, will create a new native watcher at `/foo/bar` + // and tell the existing watcher to use it instead. + // + // When `false`, a second native watcher will be created in this scenario + // instead. + relocateDescendantWatchers: true, + + // When a single native watcher at `/foo/bar` supplies watchers at both + // `/foo/bar` and `/foo/bar/baz/thud`, and the watcher at `/foo/bar` is + // removed, will relocate the native watcher to the more specific + // `/foo/bar/baz/thud` path for efficiency. + // + // When `false`, the too-broad native watcher will remain in place. + relocateAncestorWatchers: true, + + // When adding a watcher for `/foo/bar/baz/thud`, will look for an existing + // watcher at any descendant of `/foo/bar/baz`, `/foo/bar`, or `/foo` and + // create a new native watcher that supplies both the existing watcher and + // the new watcher by watching their common ancestor. + // + // When `false`, watchers will not be consolidated when one is not an + // ancestor of the other. + mergeWatchersWithCommonAncestors: true, + + // When using the strategy described above, will enforce a maximum limit on + // common ancestorship. For instance, if two directories share a + // great-great-great-great-grandfather, then it would not necessarily make + // sense for them to share a watcher; the potential firehose of file events + // they’d have to ignore would more than counterbalance the resource + // savings. + // + // When set to a positive integer X, will refuse to consolidate watchers in + // different branches of a tree unless their common ancestor is no more + // than X levels above _each_ one. + // + // When set to `0` or a negative integer, will enforce no maximum common + // ancestor level. + // + // Has no effect unless `mergeWatchersWithCommonAncestors` is `true`. + maxCommonAncestorLevel: 3 + }; + // Private: Instantiate an empty registry. // // * `createNative` {Function} that will be called with a normalized // filesystem path to create a new native filesystem watcher. - constructor(createNative) { + constructor(createNative, options = {}) { this._createNative = createNative; - this.tree = new RegistryTree([], createNative); + this.options = { + ...NativeWatcherRegistry.DEFAULT_OPTIONS, + ...options + }; + this.tree = new RegistryTree([], createNative, this.options); } reset () { - this.tree = new RegistryTree([], this._createNative); + this.tree = new RegistryTree([], this._createNative, this.options); } // Private: Attach a watcher to a directory, assigning it a {NativeWatcher}. @@ -726,8 +804,8 @@ class NativeWatcherRegistry { return this.detach(watcher, normalizedDirectory); } - // Private: Generate a visual representation of the currently active watchers managed by this - // registry. + // Private: Generate a visual representation of the currently active watchers + // managed by this registry. // // Returns a {String} showing the tree structure. print() { From be19b8ac320f93947769eaeeb7eae0154ee1b4eb Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sat, 26 Oct 2024 22:34:13 -0700 Subject: [PATCH 140/168] As a sanity check, perform _no_ native watcher reuse on Linux/Windows --- src/main.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.js b/src/main.js index b04a8fb..555fcbc 100644 --- a/src/main.js +++ b/src/main.js @@ -594,9 +594,9 @@ function getDefaultRegistryOptionsForPlatform (platform) { case 'linux': case 'win32': return { - reuseAncestorWatchers: true, + reuseAncestorWatchers: false, relocateDescendantWatchers: false, - relocateAncestorWatchers: true, + relocateAncestorWatchers: false, mergeWatchersWithCommonAncestors: false }; case 'darwin': From bed5a1c6fdbdc4168bafbab8f013236eb1dfed91 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 27 Oct 2024 11:12:47 -0700 Subject: [PATCH 141/168] Update another spec to take registry settings into account --- spec/pathwatcher-spec.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index 493ffb2..4aab0c9 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -142,7 +142,11 @@ describe('PathWatcher', () => { subDirFile = path.join(subDir, 'test.txt'); let subHandle = PathWatcher.watch(subDir, subDirCallback); - expect(PathWatcher.getNativeWatcherCount()).toBe(1); + + let shouldConsolidate = subHandle.registry.options.reuseAncestorWatchers; + expect( + PathWatcher.getNativeWatcherCount() + ).toBe(shouldConsolidate ? 1 : 2); fs.writeFileSync(tempFile, 'change'); await condition(() => rootCallback.calls.count() >= 1); From f57ba653df73b350abefa6daecf5d9ee0fb7c5bd Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 27 Oct 2024 13:43:13 -0700 Subject: [PATCH 142/168] =?UTF-8?q?Allow=20a=20platform=20to=20eschew=20`N?= =?UTF-8?q?ativeWatcher`=20reuse=20altogether=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …and use non-recursive watchers in the process. This seems to be the vastly superior option on Linux and macOS. --- lib/core.cc | 16 +++++++- src/main.js | 74 ++++++++++++++++++++++++++++++---- src/native-watcher-registry.js | 4 +- 3 files changed, 84 insertions(+), 10 deletions(-) diff --git a/lib/core.cc b/lib/core.cc index 4feb59f..515852c 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -322,6 +322,14 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { return env.Null(); } + // Second argument is optional and tells us whether to use a recursive + // watcher. Defaults to `false`. + bool useRecursiveWatcher = false; + if (info[1].IsBoolean()) { + auto recursiveOption = info[1].As(); + useRecursiveWatcher = recursiveOption; + } + // The wrapper JS will resolve this to the file's real path. We expect to be // dealing with real locations on disk, since that's what EFSW will report to // us anyway. @@ -368,7 +376,7 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { // EFSW represents watchers as unsigned `int`s; we can easily convert these // to JavaScript. - WatcherHandle handle = fileWatcher->addWatch(cppPath, listener, true); + WatcherHandle handle = fileWatcher->addWatch(cppPath, listener, useRecursiveWatcher); #ifdef DEBUG std::cout << " handle: [" << handle << "]" << std::endl; @@ -406,6 +414,12 @@ Napi::Value PathWatcher::Unwatch(const Napi::CallbackInfo& info) { // EFSW doesn’t mind if we give it a handle that it doesn’t recognize; it’ll // just silently do nothing. + // + // This is useful because removing watcher can innocuously error anyway on + // certain platforms. For instance, Linux will automatically stop watching a + // directory when it gets deleted, and will then complain when you try to + // stop the watcher that was already stopped. This shows up in debug logging + // but is otherwise safe to ignore. fileWatcher->removeWatch(handle); listener->RemovePath(handle); diff --git a/src/main.js b/src/main.js index 555fcbc..d206d61 100644 --- a/src/main.js +++ b/src/main.js @@ -71,13 +71,13 @@ class NativeWatcher { // Given a path, returns whatever existing active `NativeWatcher` is already // watching that path, or creates one if it doesn’t yet exist. - static findOrCreate (normalizedPath) { + static findOrCreate (normalizedPath, options) { for (let instance of this.INSTANCES.values()) { if (instance.normalizedPath === normalizedPath) { return instance; } } - return new NativeWatcher(normalizedPath); + return new NativeWatcher(normalizedPath, options); } // Returns the number of active `NativeWatcher` instances. @@ -85,12 +85,12 @@ class NativeWatcher { return this.INSTANCES.size; } - constructor(normalizedPath) { + constructor(normalizedPath, { recursive = false } = {}) { this.id = NativeWatcherId++; this.normalizedPath = normalizedPath; this.emitter = new Emitter(); this.subs = new CompositeDisposable(); - + this.recursive = recursive; this.running = false; } @@ -108,7 +108,7 @@ class NativeWatcher { // We can't start a watcher on a path that doesn't exist. return; } - this.handle = binding.watch(this.normalizedPath); + this.handle = binding.watch(this.normalizedPath, this.recursive); NativeWatcher.INSTANCES.set(this.handle, this); this.running = true; this.emitter.emit('did-start'); @@ -592,6 +592,19 @@ class PathWatcher { function getDefaultRegistryOptionsForPlatform (platform) { switch (platform) { case 'linux': + // On Linux, `efsw`’s strategy for a recursive watcher is to recursively + // watch each folder underneath the tree and call `inotify_add_watch` for + // each one. This is a far more “wasteful” strategy than the one we’d be + // trying to avoid by reusing native watchers in the first place! + // + // Hence, on Linux, all these options are disabled. More non-recursive + // `NativeWatcher`s are better than fewer recursive `NativeWatcher`s. + return { + reuseAncestorWatchers: false, + relocateDescendantWatchers: false, + relocateAncestorWatchers: false, + mergeWatchersWithCommonAncestors: false + }; case 'win32': return { reuseAncestorWatchers: false, @@ -600,6 +613,24 @@ function getDefaultRegistryOptionsForPlatform (platform) { mergeWatchersWithCommonAncestors: false }; case 'darwin': + // On macOS, the `FSEvents` API is a godsend in a number of ways. But + // there’s a big caveat: `fseventsd` allows only a fixed number of + // “clients” (currently 1024) _system-wide_ and attempts to add more will + // fail. + // + // Each `NativeWatcher` counts as a single “client” for these purposes, + // making it extremely plausible for us to use hundreds of these clients + // on our own. Each `FSEvents` stream can watch any number of arbitrary + // paths on the filesystem, but `efsw` doesn’t approach it that way, and + // any change to that list of paths would involve stopping one watcher + // and creating another. `efsw` currently doesn’t do this, though it’d be + // nice if they did in the future. + // + // That aside, the biggest thing we’ve got going for us is that + // `FSEvents` streams are natively recursive. That makes it easier to + // reuse `NativeWatcher`s; a watcher on an ancestor can be reused on a + // descendant, for instance. And `fseventsd`’s hard upper limit on + // watchers makes it worth the tradeoff here. return { reuseAncestorWatchers: true, relocateDescendantWatchers: false, @@ -617,9 +648,35 @@ function getDefaultRegistryOptionsForPlatform (platform) { } } +function registryOptionsToNativeWatcherOptions(registryOptions) { + let recursive = false; + // Any of the following options put us into a mode where we try to + // consolidate and reuse native watchers. For this to be feasible (beyond two + // `PathWatcher`s sharing a `NativeWatcher` because they care about the same + // directory) requires that we set up a recursive watcher. + // + // On some platforms, this strategy doesn’t make sense, and it’s better to + // use a different `NativeWatcher` for each directory. + let { + reuseAncestorWatchers, + relocateAncestorWatchers, + relocateDescendantWatchers, + mergeWatchersWithCommonAncestors + } = registryOptions; + if ( + relocateAncestorWatchers || + relocateDescendantWatchers || + reuseAncestorWatchers || + mergeWatchersWithCommonAncestors + ) { + recursive = true; + } + + return { recursive }; +} const REGISTRY = new NativeWatcherRegistry( - (normalizedPath) => { + (normalizedPath, registryOptions) => { if (!initialized) { binding.setCallback(DEFAULT_CALLBACK); initialized = true; @@ -635,7 +692,10 @@ const REGISTRY = new NativeWatcherRegistry( // `NativeWatcher` still works just fine. The way around that is to make sure // that this function will return the same watcher we're already using // instead of creating a new one. - return NativeWatcher.findOrCreate(normalizedPath); + return NativeWatcher.findOrCreate( + normalizedPath, + registryOptionsToNativeWatcherOptions(registryOptions) + ); }, getDefaultRegistryOptionsForPlatform(process.platform) ); diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index a9a8871..d516f4a 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -73,7 +73,7 @@ class RegistryTree { // existing watcher. const attachToAncestor = (absolutePaths, childPaths) => { let ancestorAbsolutePath = absolute(...absolutePaths); - let native = this.createNative(ancestorAbsolutePath); + let native = this.createNative(ancestorAbsolutePath, this.options); let leaf = new RegistryWatcherNode( native, absolutePaths, @@ -87,7 +87,7 @@ class RegistryTree { // Scenario in which we're attaching directly to a specific path. const attachToNew = (childPaths) => { - const native = this.createNative(absolutePath); + const native = this.createNative(absolutePath, this.options); const leaf = new RegistryWatcherNode( native, absolutePathSegments, From b7a7a2ddbfcc8433029a225139a157f084928278 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 27 Oct 2024 16:33:49 -0700 Subject: [PATCH 143/168] Fix spec --- spec/pathwatcher-spec.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index 4aab0c9..88145f9 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -158,7 +158,13 @@ describe('PathWatcher', () => { await condition(() => subDirCallback.calls.count() >= 1); let realTempDir = fs.realpathSync(tempDir); - expect(PathWatcher.getWatchedPaths()).toEqual([realTempDir]); + expect( + PathWatcher.getWatchedPaths() + ).toEqual( + shouldConsolidate ? + [realTempDir] : + [realTempDir, fs.realpathSync(subDir)] + ); // Closing the original watcher should not cause the native watcher to // close, since another one is depending on it. From 56ee9d2e81cd7d832e9316bf01c5511fe3a7ca4b Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 27 Oct 2024 16:57:12 -0700 Subject: [PATCH 144/168] A new approach: a custom `FSEvents` implementation on macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This approach aims to use a maximum of two `FSEvent` streams per environment (the active one and the “next” one). Seems to work great. With this in place, we don't need to use the `native-watcher-registry` at all on any platform. The only platform that needs to be economical with native file-watcher objects has solved that problem through another means. --- binding.gyp | 5 + lib/core.cc | 12 +- lib/core.h | 15 +- lib/platform/FSEventsFileWatcher.cpp | 555 +++++++++++++++++++++++++++ lib/platform/FSEventsFileWatcher.hpp | 119 ++++++ spec/pathwatcher-spec.js | 6 +- src/main.js | 137 +------ 7 files changed, 709 insertions(+), 140 deletions(-) create mode 100644 lib/platform/FSEventsFileWatcher.cpp create mode 100644 lib/platform/FSEventsFileWatcher.hpp diff --git a/binding.gyp b/binding.gyp index f3b7e7c..1fe7ebd 100644 --- a/binding.gyp +++ b/binding.gyp @@ -119,6 +119,11 @@ "vendor/efsw", ], "conditions": [ + ['OS=="mac"', { + "sources+": [ + "lib/platform/FSEventsFileWatcher.cpp" + ] + }], ['OS=="win"', { 'msvs_settings': { 'VCCLCompilerTool': { diff --git a/lib/core.cc b/lib/core.cc index 515852c..23d3756 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -141,7 +141,7 @@ void PathWatcherListener::Stop() { isShuttingDown = true; } -void PathWatcherListener::Stop(efsw::FileWatcher* fileWatcher) { +void PathWatcherListener::Stop(FileWatcher* fileWatcher) { for (auto& it : paths) { fileWatcher->removeWatch(it.first); } @@ -366,9 +366,13 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { listener = new PathWatcherListener(env, tsfn); - fileWatcher = new efsw::FileWatcher(); - fileWatcher->followSymlinks(true); - fileWatcher->watch(); +#ifdef __APPLE__ + fileWatcher = new FSEventsFileWatcher(); +#else + fileWatcher = new efsw::FileWatcher(); + fileWatcher->followSymlinks(true); + fileWatcher->watch(); +#endif isWatching = true; } diff --git a/lib/core.h b/lib/core.h index 3009986..0cd2fd7 100644 --- a/lib/core.h +++ b/lib/core.h @@ -7,6 +7,10 @@ #include #include "../vendor/efsw/include/efsw/efsw.hpp" +#ifdef __APPLE__ +#include "./platform/FSEventsFileWatcher.hpp" +#endif + #ifndef _WIN32 #include #endif @@ -17,6 +21,12 @@ #define PATH_SEPARATOR '/' #endif +#ifdef __APPLE__ +typedef FSEventsFileWatcher FileWatcher; +#else +typedef efsw::FileWatcher FileWatcher; +#endif + typedef efsw::WatchID WatcherHandle; #ifdef _WIN32 @@ -103,7 +113,7 @@ class PathWatcherListener: public efsw::FileWatchListener { void RemovePath(efsw::WatchID handle); bool IsEmpty(); void Stop(); - void Stop(efsw::FileWatcher* fileWatcher); + void Stop(FileWatcher* fileWatcher); private: std::atomic isShuttingDown{false}; @@ -138,5 +148,6 @@ class PathWatcher : public Napi::Addon { Napi::FunctionReference callback; Napi::ThreadSafeFunction tsfn; PathWatcherListener* listener; - efsw::FileWatcher* fileWatcher = nullptr; + + FileWatcher* fileWatcher = nullptr; }; diff --git a/lib/platform/FSEventsFileWatcher.cpp b/lib/platform/FSEventsFileWatcher.cpp new file mode 100644 index 0000000..da2a78f --- /dev/null +++ b/lib/platform/FSEventsFileWatcher.cpp @@ -0,0 +1,555 @@ +#include "../core.h" +#include +#include +#include "FSEventsFileWatcher.hpp" + +int shorthandFSEventsModified = kFSEventStreamEventFlagItemFinderInfoMod | + kFSEventStreamEventFlagItemModified | + kFSEventStreamEventFlagItemInodeMetaMod; + +// Ensure a given path has a trailing separator for comparison purposes. +static std::string NormalizePath(std::string path) { + if (path.back() == PATH_SEPARATOR) return path; + return path + PATH_SEPARATOR; +} + +std::string PrecomposeFileName(const std::string& name) { + CFStringRef cfStringRef = CFStringCreateWithCString( + kCFAllocatorDefault, + name.c_str(), + kCFStringEncodingUTF8 + ); + + CFMutableStringRef cfMutable = CFStringCreateMutableCopy(NULL, 0, cfStringRef); + + CFStringNormalize(cfMutable, kCFStringNormalizationFormC); + + const char* c_str = CFStringGetCStringPtr(cfMutable, kCFStringEncodingUTF8); + + if (c_str != NULL) { + std::string result(c_str); + CFRelease(cfStringRef); + CFRelease(cfMutable); + return result; + } + + CFIndex length = CFStringGetLength(cfMutable); + CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8); + if (maxSize == kCFNotFound) { + CFRelease(cfStringRef); + CFRelease(cfMutable); + return std::string(); + } + + std::string result(maxSize + 1, '\0'); + if (CFStringGetCString( + cfMutable, + &result[0], + result.size(), + kCFStringEncodingUTF8 + )) { + result.resize(std::strlen(result.c_str())); + CFRelease(cfStringRef); + CFRelease(cfMutable); + } else { + result.clear(); + } + return result; +} + +bool PathExists(const std::string& path) { + struct stat buffer; + return (stat(path.c_str(), &buffer) == 0); +} + +bool PathStartsWith(const std::string& str, const std::string& prefix) { + if (prefix.length() > str.length()) { + return false; + } + return str.compare(0, prefix.length(), prefix) == 0; +} + +void DirRemoveSlashAtEnd (std::string& dir) { + if (dir.size() >= 1 && dir[dir.size() - 1] == PATH_SEPARATOR) { + dir.erase( dir.size() - 1 ); + } +} + +std::string PathWithoutFileName( std::string filepath ) { + DirRemoveSlashAtEnd(filepath); + + size_t pos = filepath.find_last_of(PATH_SEPARATOR); + + if (pos != std::string::npos) { + return filepath.substr(0, pos + 1); + } + return filepath; +} + +std::string FileNameFromPath(std::string filepath) { + DirRemoveSlashAtEnd(filepath); + + size_t pos = filepath.find_last_of(PATH_SEPARATOR); + + if (pos != std::string::npos) { + return filepath.substr(pos + 1); + } + return filepath; +} + +static std::string convertCFStringToStdString( CFStringRef cfString ) { + // Try to get the C string pointer directly + const char* cStr = CFStringGetCStringPtr( cfString, kCFStringEncodingUTF8 ); + + if ( cStr ) { + // If the pointer is valid, directly return a std::string from it + return std::string( cStr ); + } else { + // If not, manually convert it + CFIndex length = CFStringGetLength( cfString ); + CFIndex maxSize = CFStringGetMaximumSizeForEncoding( length, kCFStringEncodingUTF8 ) + + 1; // +1 for null terminator + + char* buffer = new char[maxSize]; + + if ( CFStringGetCString( cfString, buffer, maxSize, kCFStringEncodingUTF8 ) ) { + std::string result( buffer ); + delete[] buffer; + return result; + } else { + delete[] buffer; + return ""; + } + } +} + + +FSEventsFileWatcher::~FSEventsFileWatcher() { +#ifdef DEBUG + std::cout << "[destroying] FSEventsFileWatcher!" << std::endl; +#endif + isValid = false; + if (currentEventStream) { + FSEventStreamStop(currentEventStream); + FSEventStreamInvalidate(currentEventStream); + FSEventStreamRelease(currentEventStream); + } +} + +efsw::WatchID FSEventsFileWatcher::addWatch( + const std::string& directory, + efsw::FileWatchListener* listener, + bool _useRecursion +) { +#ifdef DEBUG + std::cout << "FSEventsFileWatcher::addWatch" << directory << std::endl; +#endif + auto handle = nextHandleID++; + handlesToPaths.insert(handle, directory); + std::vector cfStrings; + for (const auto& pair: handlesToPaths.forward) { + CFStringRef cfStr = CFStringCreateWithCString( + kCFAllocatorDefault, + pair.second.c_str(), + kCFStringEncodingUTF8 + ); + if (cfStr) { + cfStrings.push_back(cfStr); + } + } + + CFArrayRef paths = CFArrayCreate( + NULL, + reinterpret_cast(cfStrings.data()), + cfStrings.size(), + NULL + ); + + uint32_t streamFlags = kFSEventStreamCreateFlagNone; + + streamFlags = kFSEventStreamCreateFlagFileEvents | + kFSEventStreamCreateFlagNoDefer | + kFSEventStreamCreateFlagUseExtendedData | + kFSEventStreamCreateFlagUseCFTypes; + + FSEventStreamContext ctx; + ctx.version = 0; + ctx.info = this; + ctx.retain = NULL; + ctx.release = NULL; + ctx.copyDescription = NULL; + + dispatch_queue_t queue = dispatch_queue_create(NULL, NULL); + + nextEventStream = FSEventStreamCreate( + kCFAllocatorDefault, + &FSEventsFileWatcher::FSEventCallback, + &ctx, + paths, + kFSEventStreamEventIdSinceNow, + 0., + streamFlags + ); + + FSEventStreamSetDispatchQueue(nextEventStream, queue); + bool didStart = FSEventStreamStart(nextEventStream); + + for (CFStringRef str : cfStrings) { + CFRelease(str); + } + CFRelease(paths); + + if (!didStart) { + handlesToPaths.remove(handle); + // TODO: Debug information? + return -handle; + } else { +#ifdef DEBUG + std::cout << "Started stream!" << std::endl; +#endif + // Successfully started — we can make it the new event stream and stop the + // old one. + if (currentEventStream) { + FSEventStreamStop(currentEventStream); + FSEventStreamInvalidate(currentEventStream); + FSEventStreamRelease(currentEventStream); +#ifdef DEBUG + std::cout << "Stopped old stream!" << std::endl; +#endif + } + currentEventStream = nextEventStream; + nextEventStream = nullptr; + } + + handlesToListeners[handle] = listener; + + return handle; +} + +void FSEventsFileWatcher::removeWatch( + efsw::WatchID handle +) { +#ifdef DEBUG + std::cout << "FSEventsFileWatcher::removeWatch" << handle << std::endl; +#endif + handlesToPaths.remove(handle); + + if (handlesToPaths.forward.size() == 0) { + handlesToListeners.erase(handle); + if (currentEventStream) { + FSEventStreamStop(currentEventStream); + FSEventStreamInvalidate(currentEventStream); + FSEventStreamRelease(currentEventStream); + } + currentEventStream = nullptr; + return; + } + + std::vector cfStrings; + for (const auto& pair: handlesToPaths.forward) { + CFStringRef cfStr = CFStringCreateWithCString( + kCFAllocatorDefault, + pair.second.c_str(), + kCFStringEncodingUTF8 + ); + if (cfStr) { + cfStrings.push_back(cfStr); + } + } + + CFArrayRef paths = CFArrayCreate( + NULL, + reinterpret_cast(cfStrings.data()), + cfStrings.size(), + NULL + ); + + uint32_t streamFlags = kFSEventStreamCreateFlagNone; + + streamFlags = kFSEventStreamCreateFlagFileEvents | + kFSEventStreamCreateFlagNoDefer | + kFSEventStreamCreateFlagUseExtendedData | + kFSEventStreamCreateFlagUseCFTypes; + + FSEventStreamContext ctx; + ctx.version = 0; + ctx.info = this; + ctx.retain = NULL; + ctx.release = NULL; + ctx.copyDescription = NULL; + + dispatch_queue_t queue = dispatch_queue_create(NULL, NULL); + + nextEventStream = FSEventStreamCreate( + kCFAllocatorDefault, + &FSEventsFileWatcher::FSEventCallback, + &ctx, + paths, + kFSEventStreamEventIdSinceNow, + 0., + streamFlags + ); + + FSEventStreamSetDispatchQueue(nextEventStream, queue); + bool didStart = FSEventStreamStart(nextEventStream); + + for (CFStringRef str : cfStrings) { + CFRelease(str); + } + CFRelease(paths); + + // auto it = handlesToListeners.find(handle); + // if (it != handlesToListeners.end()) { + // handlesToListeners.erase(it); + // } + + handlesToListeners.erase(handle); + + if (!didStart) { + // We'll still remove the listener. Weird that the stream didn't stop, but + // we can at least ignore any resulting FSEvents. + } else { + if (currentEventStream) { + FSEventStreamStop(currentEventStream); + FSEventStreamInvalidate(currentEventStream); + FSEventStreamRelease(currentEventStream); + } + currentEventStream = nextEventStream; + nextEventStream = nullptr; + } +} + +void FSEventsFileWatcher::FSEventCallback( + ConstFSEventStreamRef streamRef, + void* userData, + size_t numEvents, + void* eventPaths, + const FSEventStreamEventFlags eventFlags[], + const FSEventStreamEventId eventIds[] +) { +#ifdef DEBUG + std::cout << "FSEventsFileWatcher::FSEventCallback" << std::endl; +#endif + + FSEventsFileWatcher* instance = static_cast(userData); + if (!instance->isValid) return; + + std::vector events; + events.reserve(numEvents); + + for (size_t i = 0; i < numEvents; i++) { + CFDictionaryRef pathInfoDict = static_cast( + CFArrayGetValueAtIndex((CFArrayRef) eventPaths, i) + ); + CFStringRef path = static_cast( + CFDictionaryGetValue(pathInfoDict, kFSEventStreamEventExtendedDataPathKey) + ); + CFNumberRef cfInode = static_cast( + CFDictionaryGetValue(pathInfoDict, kFSEventStreamEventExtendedFileIDKey) + ); + + if (cfInode) { + unsigned long inode = 0; + CFNumberGetValue(cfInode, kCFNumberLongType, &inode); + events.push_back( + FSEvent( + convertCFStringToStdString(path), + (long) eventFlags[i], + (uint64_t) eventIds[i], + inode + ) + ); + } + } + + instance->handleActions(events); + instance->process(); +} + +void FSEventsFileWatcher::sendFileAction( + efsw::WatchID watchid, + const std::string& dir, + const std::string& filename, + efsw::Action action, + std::string oldFilename +) { + efsw::FileWatchListener* listener; + auto it = handlesToListeners.find(watchid); + if (it == handlesToListeners.end()) return; + listener = it->second; + + listener->handleFileAction( + watchid, + PrecomposeFileName(dir), + PrecomposeFileName(filename), + action, + PrecomposeFileName(oldFilename) + ); +} + +void FSEventsFileWatcher::handleActions(std::vector& events) { + size_t esize = events.size(); + + for (size_t i = 0; i < esize; i++) { + FSEvent& event = events[i]; + + if (event.flags & ( + kFSEventStreamEventFlagUserDropped | + kFSEventStreamEventFlagKernelDropped | + kFSEventStreamEventFlagEventIdsWrapped | + kFSEventStreamEventFlagHistoryDone | + kFSEventStreamEventFlagMount | + kFSEventStreamEventFlagUnmount | + kFSEventStreamEventFlagRootChanged + )) continue; + + efsw::WatchID handle; + std::string path; + bool found = false; + + for (const auto& pair: handlesToPaths.forward) { + std::string normalizedPath = NormalizePath(pair.second); + if (!PathStartsWith(event.path, pair.second)) continue; + if (event.path.find_last_of(PATH_SEPARATOR) != pair.second.size()) { + continue; + } + found = true; + path = pair.second; + handle = pair.first; + break; + } + + if (!found) continue; + + std::string dirPath(PathWithoutFileName(event.path)); + std::string filePath(FileNameFromPath(event.path)); + + if (event.flags & ( + kFSEventStreamEventFlagItemCreated | + kFSEventStreamEventFlagItemRemoved | + kFSEventStreamEventFlagItemRenamed + )) { + if (dirPath != path) { + dirsChanged.insert(dirPath); + } + } + + if (event.flags & kFSEventStreamEventFlagItemRenamed) { + if ( + (i + 1 < esize) && + (events[i + 1].flags & kFSEventStreamEventFlagItemRenamed) && + (events[i + 1].inode == event.inode) + ) { + FSEvent& nEvent = events[i + 1]; + std::string newDir(PathWithoutFileName(nEvent.path)); + std::string newFilepath(FileNameFromPath(nEvent.path)); + + if (event.path != nEvent.path) { + if (dirPath == newDir) { + if ( + !PathExists(event.path) || + 0 == strcasecmp(event.path.c_str(), nEvent.path.c_str()) + ) { + // Move from one path to the other. + sendFileAction(handle, dirPath, newFilepath, efsw::Actions::Moved, filePath); + } else { + // Move in the opposite direction. + sendFileAction(handle, dirPath, filePath, efsw::Actions::Moved, newFilepath); + } + } else { + sendFileAction(handle, dirPath, filePath, efsw::Actions::Delete); + sendFileAction(handle, newDir, newFilepath, efsw::Actions::Add); + + if (nEvent.flags & shorthandFSEventsModified) { + sendFileAction(handle, dirPath, filePath, efsw::Actions::Modified); + } + } + } else { + handleAddModDel(handle, nEvent.flags, nEvent.path, dirPath, filePath); + } + + if (nEvent.flags & ( + kFSEventStreamEventFlagItemCreated | + kFSEventStreamEventFlagItemRemoved | + kFSEventStreamEventFlagItemRenamed + )) { + if (newDir != path) { + dirsChanged.insert(newDir); + } + } + + // Skip the renamed file + i++; + } else if (PathExists(event.path)) { + sendFileAction(handle, dirPath, filePath, efsw::Actions::Add); + + if (event.flags && shorthandFSEventsModified) { + sendFileAction(handle, dirPath, filePath, efsw::Actions::Modified); + } + } else { + sendFileAction(handle, dirPath, filePath, efsw::Actions::Delete); + } + } else { + handleAddModDel(handle, event.flags, event.path, dirPath, filePath); + } + } +} + +void FSEventsFileWatcher::handleAddModDel( + efsw::WatchID handle, + const uint32_t& flags, + const std::string& path, + std::string& dirPath, + std::string& filePath +) { + if (flags & kFSEventStreamEventFlagItemCreated) { + if (PathExists(path)) { + sendFileAction(handle, dirPath, filePath, efsw::Actions::Add); + } + } + + if (flags & shorthandFSEventsModified) { + sendFileAction(handle, dirPath, filePath, efsw::Actions::Modified); + } + + if (flags & kFSEventStreamEventFlagItemRemoved) { + if (!PathExists(path)) { + sendFileAction(handle, dirPath, filePath, efsw::Actions::Delete); + } + } +} + +void FSEventsFileWatcher::process() { + std::lock_guard lock(dirsChangedMutex); + std::set::iterator it = dirsChanged.begin(); + for (; it != dirsChanged.end(); it++) { + + std::string dir = *it; + + efsw::WatchID handle; + std::string path; + bool found = false; + + for (const auto& pair: handlesToPaths.forward) { + // std::string normalizedPath = NormalizePath(pair.second); + if (!PathStartsWith(dir, pair.second)) continue; + if (dir.find_last_of(PATH_SEPARATOR) != pair.second.size()) { + continue; + } + found = true; + path = pair.second; + handle = pair.first; + break; + } + if (!found) continue; + + sendFileAction( + handle, + PathWithoutFileName(dir), + FileNameFromPath(dir), + efsw::Actions::Modified + ); + } + + dirsChanged.clear(); +} diff --git a/lib/platform/FSEventsFileWatcher.hpp b/lib/platform/FSEventsFileWatcher.hpp new file mode 100644 index 0000000..9751c4a --- /dev/null +++ b/lib/platform/FSEventsFileWatcher.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "../../vendor/efsw/include/efsw/efsw.hpp" + +template +class BidirectionalMap { + +public: + void insert(const K& key, const V& value) { + forward[key] = value; + reverse[value] = key; + } + + void remove(const K& key) { + auto it = forward.find(key); + if (it != forward.end()) { + reverse.erase(it->second); + forward.erase(it); + } + } + + const V* getValue(const K& key) const { + auto it = forward.find(key); + return it != forward.end() ? &it->second : nullptr; + } + + const K* getKey(const V& value) const { + auto it = reverse.find(value); + return it != reverse.end() ? &it->second : nullptr; + } + + std::unordered_map forward; + std::unordered_map reverse; +}; + +class FSEvent { +public: + FSEvent( + std::string path, + long flags, + uint64_t id, + uint64_t inode = 0 + ): path(path), flags(flags), id(id), inode(inode) { +#ifdef DEBUG + std::cout << "[creating] FSEventsFileWatcher!" << std::endl; +#endif + } + + std::string path; + long flags; + uint64_t id; + uint64_t inode; +}; + +class FSEventsFileWatcher { +public: + FSEventsFileWatcher() {}; + ~FSEventsFileWatcher(); + efsw::WatchID addWatch( + const std::string& directory, + efsw::FileWatchListener* watcher, + bool _useRecursion = false + ); + void removeWatch( + efsw::WatchID watchID + ); + + void handleActions(std::vector& events); + void sendFileAction( + efsw::WatchID watchid, + const std::string& dir, + const std::string& filename, + efsw::Action action, + std::string oldFilename = "" + ); + + static void FSEventCallback( + ConstFSEventStreamRef streamRef, + void* userData, + size_t numEvents, + void* eventPaths, + const FSEventStreamEventFlags eventFlags[], + const FSEventStreamEventId eventIds[] + ); + + void handleAddModDel( + efsw::WatchID handle, + const uint32_t& flags, + const std::string& path, + std::string& dirPath, + std::string& filePath + ); + + void process(); + + bool isValid = true; + +private: + long nextHandleID; + + // The running event stream that subscribes to all the paths we care about. + FSEventStreamRef currentEventStream = nullptr; + // An event stream that we create when our list of paths changes; it will + // become the `currentEventStream` after it starts. + FSEventStreamRef nextEventStream = nullptr; + + std::set dirsChanged; + std::mutex dirsChangedMutex; + + BidirectionalMap handlesToPaths; + std::unordered_map handlesToListeners; +}; diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index 88145f9..9b33226 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -114,7 +114,7 @@ describe('PathWatcher', () => { }); } - describe('when a watcher is added underneath an existing watched path', () => { + xdescribe('when a watcher is added underneath an existing watched path', () => { let subDirFile, subDir; function cleanup() { @@ -179,7 +179,7 @@ describe('PathWatcher', () => { }); }); - describe('when two watchers are added on sibling directories', () => { + xdescribe('when two watchers are added on sibling directories', () => { let siblingA = path.join(tempDir, 'sibling-a'); let siblingB = path.join(tempDir, 'sibling-b'); @@ -218,7 +218,7 @@ describe('PathWatcher', () => { }); }); - describe('when two watchers are added on cousin directories', () => { + xdescribe('when two watchers are added on cousin directories', () => { let cousinA = path.join(tempDir, 'placeholder-a', 'cousin-a'); let cousinB = path.join(tempDir, 'placeholder-b', 'cousin-b'); diff --git a/src/main.js b/src/main.js index d206d61..c6128a2 100644 --- a/src/main.js +++ b/src/main.js @@ -13,7 +13,7 @@ let binding; const fs = require('fs'); const path = require('path'); const { Emitter, Disposable, CompositeDisposable } = require('event-kit'); -const { NativeWatcherRegistry } = require('./native-watcher-registry'); +// const { NativeWatcherRegistry } = require('./native-watcher-registry'); let initialized = false; @@ -146,11 +146,6 @@ class NativeWatcher { return this.emitter.on('did-error', callback); } - reattachListenersTo (replacement, watchedPath, options) { - if (replacement === this) return; - this.emitter.emit('should-detach', { replacement, watchedPath, options }); - } - stop (shutdown = false) { if (this.running) { this.emitter.emit('will-stop', shutdown); @@ -202,10 +197,9 @@ let PathWatcherId = 10; // is atomic and results in no missed filesystem events. The old watcher will // be disposed of once no `PathWatcher`s are listening to it anymore. class PathWatcher { - constructor (registry, watchedPath) { + constructor (watchedPath) { this.id = PathWatcherId++; this.watchedPath = watchedPath; - this.registry = registry; this.normalizePath = null; this.native = null; @@ -280,14 +274,8 @@ class PathWatcher { // We don't have a native watcher yet, so we’ll ask the registry to // assign one to us. This could be a brand-new instance or one that was // already watching one of our ancestor folders. - if (this.normalizedPath) { - this.registry.attach(this); - this.onDidChange(callback); - } else { - this.registry.attachAsync(this).then(() => { - this.onDidChange(callback); - }) - } + this.native = NativeWatcher.findOrCreate(this.normalizedPath); + this.onDidChange(callback); } return new Disposable(() => { @@ -384,7 +372,7 @@ class PathWatcher { this.normalizedPath = path.dirname(this.normalizedPath); } - this.registry.attach(this); + this.native = NativeWatcher.findOrCreate(this.normalizedPath); this.active = true; } @@ -585,121 +573,9 @@ class PathWatcher { close () { this.active = false; this.dispose(); - this.registry.detach(this); - } -} - -function getDefaultRegistryOptionsForPlatform (platform) { - switch (platform) { - case 'linux': - // On Linux, `efsw`’s strategy for a recursive watcher is to recursively - // watch each folder underneath the tree and call `inotify_add_watch` for - // each one. This is a far more “wasteful” strategy than the one we’d be - // trying to avoid by reusing native watchers in the first place! - // - // Hence, on Linux, all these options are disabled. More non-recursive - // `NativeWatcher`s are better than fewer recursive `NativeWatcher`s. - return { - reuseAncestorWatchers: false, - relocateDescendantWatchers: false, - relocateAncestorWatchers: false, - mergeWatchersWithCommonAncestors: false - }; - case 'win32': - return { - reuseAncestorWatchers: false, - relocateDescendantWatchers: false, - relocateAncestorWatchers: false, - mergeWatchersWithCommonAncestors: false - }; - case 'darwin': - // On macOS, the `FSEvents` API is a godsend in a number of ways. But - // there’s a big caveat: `fseventsd` allows only a fixed number of - // “clients” (currently 1024) _system-wide_ and attempts to add more will - // fail. - // - // Each `NativeWatcher` counts as a single “client” for these purposes, - // making it extremely plausible for us to use hundreds of these clients - // on our own. Each `FSEvents` stream can watch any number of arbitrary - // paths on the filesystem, but `efsw` doesn’t approach it that way, and - // any change to that list of paths would involve stopping one watcher - // and creating another. `efsw` currently doesn’t do this, though it’d be - // nice if they did in the future. - // - // That aside, the biggest thing we’ve got going for us is that - // `FSEvents` streams are natively recursive. That makes it easier to - // reuse `NativeWatcher`s; a watcher on an ancestor can be reused on a - // descendant, for instance. And `fseventsd`’s hard upper limit on - // watchers makes it worth the tradeoff here. - return { - reuseAncestorWatchers: true, - relocateDescendantWatchers: false, - relocateAncestorWatchers: true, - mergeWatchersWithCommonAncestors: true, - maxCommonAncestorLevel: 2 - }; - default: - return { - reuseAncestorWatchers: true, - relocateDescendantWatchers: false, - relocateAncestorWatchers: true, - mergeWatchersWithCommonAncestors: false - }; } } -function registryOptionsToNativeWatcherOptions(registryOptions) { - let recursive = false; - // Any of the following options put us into a mode where we try to - // consolidate and reuse native watchers. For this to be feasible (beyond two - // `PathWatcher`s sharing a `NativeWatcher` because they care about the same - // directory) requires that we set up a recursive watcher. - // - // On some platforms, this strategy doesn’t make sense, and it’s better to - // use a different `NativeWatcher` for each directory. - let { - reuseAncestorWatchers, - relocateAncestorWatchers, - relocateDescendantWatchers, - mergeWatchersWithCommonAncestors - } = registryOptions; - if ( - relocateAncestorWatchers || - relocateDescendantWatchers || - reuseAncestorWatchers || - mergeWatchersWithCommonAncestors - ) { - recursive = true; - } - - return { recursive }; -} - -const REGISTRY = new NativeWatcherRegistry( - (normalizedPath, registryOptions) => { - if (!initialized) { - binding.setCallback(DEFAULT_CALLBACK); - initialized = true; - } - - // It's important that this function be able to return an existing instance - // of `NativeWatcher` when present. Otherwise, the registry will try to - // create a new instance at the same path, and the native bindings won't - // allow that to happen. - // - // It's also important because the registry might respond to a sibling - // `PathWatcher`’s removal by trying to reattach us — even though our - // `NativeWatcher` still works just fine. The way around that is to make sure - // that this function will return the same watcher we're already using - // instead of creating a new one. - return NativeWatcher.findOrCreate( - normalizedPath, - registryOptionsToNativeWatcherOptions(registryOptions) - ); - }, - getDefaultRegistryOptionsForPlatform(process.platform) -); - class WatcherEvent { constructor(event, filePath, oldFilePath) { this.action = event; @@ -725,7 +601,7 @@ function watch (pathToWatch, callback) { binding.setCallback(DEFAULT_CALLBACK); initialized = true; } - let watcher = new PathWatcher(REGISTRY, path.resolve(pathToWatch)); + let watcher = new PathWatcher(path.resolve(pathToWatch)); watcher.onDidChange(callback); return watcher; } @@ -737,7 +613,6 @@ function closeAllWatchers () { watcher.stop(true); } NativeWatcher.INSTANCES.clear(); - REGISTRY.reset(); isClosingAllWatchers = false; } From 398d408b89ba9a067527c11b86d498a457ca6ac7 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 27 Oct 2024 16:57:43 -0700 Subject: [PATCH 145/168] Skip `NativeWatcherRegistry` specs --- spec/native-watcher-registry-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/native-watcher-registry-spec.js b/spec/native-watcher-registry-spec.js index b7a2f27..e96d499 100644 --- a/spec/native-watcher-registry-spec.js +++ b/spec/native-watcher-registry-spec.js @@ -109,7 +109,7 @@ class MockNative { } } -describe('NativeWatcherRegistry', function() { +xdescribe('NativeWatcherRegistry', function() { let createNative, registry; beforeEach(function() { From 069300ab6c565c7cce8f1cf345aae826ab95551e Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 27 Oct 2024 20:03:30 -0700 Subject: [PATCH 146/168] =?UTF-8?q?Reduce=20duplication=20in=20`FSEventsFi?= =?UTF-8?q?leWatcher`=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …and prevent an instance from being destroyed while processing events. --- lib/platform/FSEventsFileWatcher.cpp | 327 ++++++++++++++------------- lib/platform/FSEventsFileWatcher.hpp | 54 ++--- 2 files changed, 185 insertions(+), 196 deletions(-) diff --git a/lib/platform/FSEventsFileWatcher.cpp b/lib/platform/FSEventsFileWatcher.cpp index da2a78f..845ef81 100644 --- a/lib/platform/FSEventsFileWatcher.cpp +++ b/lib/platform/FSEventsFileWatcher.cpp @@ -1,8 +1,21 @@ #include "../core.h" #include -#include #include "FSEventsFileWatcher.hpp" +#ifdef DEBUG +#include +#endif + +// This is an API-compatible replacement for `efsw::FileWatcher` on macOS. It +// uses its own implementation of `FSEvents` so it can minimize the number of +// streams created in comparison to `efsw`’s approach of using one stream per +// watched path. + +// NOTE: Lots of these are duplications and alternate versions of functions +// that are already present in `efsw`. We could use the `efsw` versions +// instead, but it feels like a good idea to minimize the amount of +// cross-pollination here. + int shorthandFSEventsModified = kFSEventStreamEventFlagItemFinderInfoMod | kFSEventStreamEventFlagItemModified | kFSEventStreamEventFlagItemInodeMetaMod; @@ -101,19 +114,26 @@ static std::string convertCFStringToStdString( CFStringRef cfString ) { // Try to get the C string pointer directly const char* cStr = CFStringGetCStringPtr( cfString, kCFStringEncodingUTF8 ); - if ( cStr ) { - // If the pointer is valid, directly return a std::string from it - return std::string( cStr ); + if (cStr) { + // If the pointer is valid, directly return a `std::string` from it. + return std::string(cStr); } else { - // If not, manually convert it - CFIndex length = CFStringGetLength( cfString ); - CFIndex maxSize = CFStringGetMaximumSizeForEncoding( length, kCFStringEncodingUTF8 ) + - 1; // +1 for null terminator + // If not, manually convert it. + CFIndex length = CFStringGetLength(cfString); + CFIndex maxSize = CFStringGetMaximumSizeForEncoding( + length, + kCFStringEncodingUTF8 + ) + 1; // +1 for null terminator char* buffer = new char[maxSize]; - if ( CFStringGetCString( cfString, buffer, maxSize, kCFStringEncodingUTF8 ) ) { - std::string result( buffer ); + if (CFStringGetCString( + cfString, + buffer, + maxSize, + kCFStringEncodingUTF8 + )) { + std::string result(buffer); delete[] buffer; return result; } else { @@ -123,11 +143,19 @@ static std::string convertCFStringToStdString( CFStringRef cfString ) { } } +// Empty constructor. FSEventsFileWatcher::~FSEventsFileWatcher() { #ifdef DEBUG std::cout << "[destroying] FSEventsFileWatcher!" << std::endl; #endif + pendingDestruction = true; + // Defer cleanup until we can finish processing file events. + std::unique_lock lock(processingMutex); + while (isProcessing) { + processingComplete.wait(lock); + } + isValid = false; if (currentEventStream) { FSEventStreamStop(currentEventStream); @@ -139,90 +167,24 @@ FSEventsFileWatcher::~FSEventsFileWatcher() { efsw::WatchID FSEventsFileWatcher::addWatch( const std::string& directory, efsw::FileWatchListener* listener, + // The `_useRecursion` flag is ignored; it's present for API compatibility. bool _useRecursion ) { #ifdef DEBUG std::cout << "FSEventsFileWatcher::addWatch" << directory << std::endl; #endif - auto handle = nextHandleID++; - handlesToPaths.insert(handle, directory); - std::vector cfStrings; - for (const auto& pair: handlesToPaths.forward) { - CFStringRef cfStr = CFStringCreateWithCString( - kCFAllocatorDefault, - pair.second.c_str(), - kCFStringEncodingUTF8 - ); - if (cfStr) { - cfStrings.push_back(cfStr); - } - } - - CFArrayRef paths = CFArrayCreate( - NULL, - reinterpret_cast(cfStrings.data()), - cfStrings.size(), - NULL - ); - - uint32_t streamFlags = kFSEventStreamCreateFlagNone; - - streamFlags = kFSEventStreamCreateFlagFileEvents | - kFSEventStreamCreateFlagNoDefer | - kFSEventStreamCreateFlagUseExtendedData | - kFSEventStreamCreateFlagUseCFTypes; - - FSEventStreamContext ctx; - ctx.version = 0; - ctx.info = this; - ctx.retain = NULL; - ctx.release = NULL; - ctx.copyDescription = NULL; - - dispatch_queue_t queue = dispatch_queue_create(NULL, NULL); - - nextEventStream = FSEventStreamCreate( - kCFAllocatorDefault, - &FSEventsFileWatcher::FSEventCallback, - &ctx, - paths, - kFSEventStreamEventIdSinceNow, - 0., - streamFlags - ); - - FSEventStreamSetDispatchQueue(nextEventStream, queue); - bool didStart = FSEventStreamStart(nextEventStream); + efsw::WatchID handle = nextHandleID++; + handlesToPaths[handle] = directory; + handlesToListeners[handle] = listener; - for (CFStringRef str : cfStrings) { - CFRelease(str); - } - CFRelease(paths); + bool didStart = startNewStream(); if (!didStart) { - handlesToPaths.remove(handle); + removeHandle(handle); // TODO: Debug information? return -handle; - } else { -#ifdef DEBUG - std::cout << "Started stream!" << std::endl; -#endif - // Successfully started — we can make it the new event stream and stop the - // old one. - if (currentEventStream) { - FSEventStreamStop(currentEventStream); - FSEventStreamInvalidate(currentEventStream); - FSEventStreamRelease(currentEventStream); -#ifdef DEBUG - std::cout << "Stopped old stream!" << std::endl; -#endif - } - currentEventStream = nextEventStream; - nextEventStream = nullptr; } - handlesToListeners[handle] = listener; - return handle; } @@ -232,10 +194,9 @@ void FSEventsFileWatcher::removeWatch( #ifdef DEBUG std::cout << "FSEventsFileWatcher::removeWatch" << handle << std::endl; #endif - handlesToPaths.remove(handle); + removeHandle(handle); - if (handlesToPaths.forward.size() == 0) { - handlesToListeners.erase(handle); + if (handlesToPaths.size() == 0) { if (currentEventStream) { FSEventStreamStop(currentEventStream); FSEventStreamInvalidate(currentEventStream); @@ -245,78 +206,12 @@ void FSEventsFileWatcher::removeWatch( return; } - std::vector cfStrings; - for (const auto& pair: handlesToPaths.forward) { - CFStringRef cfStr = CFStringCreateWithCString( - kCFAllocatorDefault, - pair.second.c_str(), - kCFStringEncodingUTF8 - ); - if (cfStr) { - cfStrings.push_back(cfStr); - } - } - - CFArrayRef paths = CFArrayCreate( - NULL, - reinterpret_cast(cfStrings.data()), - cfStrings.size(), - NULL - ); - - uint32_t streamFlags = kFSEventStreamCreateFlagNone; - - streamFlags = kFSEventStreamCreateFlagFileEvents | - kFSEventStreamCreateFlagNoDefer | - kFSEventStreamCreateFlagUseExtendedData | - kFSEventStreamCreateFlagUseCFTypes; - - FSEventStreamContext ctx; - ctx.version = 0; - ctx.info = this; - ctx.retain = NULL; - ctx.release = NULL; - ctx.copyDescription = NULL; - - dispatch_queue_t queue = dispatch_queue_create(NULL, NULL); - - nextEventStream = FSEventStreamCreate( - kCFAllocatorDefault, - &FSEventsFileWatcher::FSEventCallback, - &ctx, - paths, - kFSEventStreamEventIdSinceNow, - 0., - streamFlags - ); - - FSEventStreamSetDispatchQueue(nextEventStream, queue); - bool didStart = FSEventStreamStart(nextEventStream); - - for (CFStringRef str : cfStrings) { - CFRelease(str); - } - CFRelease(paths); - - // auto it = handlesToListeners.find(handle); - // if (it != handlesToListeners.end()) { - // handlesToListeners.erase(it); - // } - - handlesToListeners.erase(handle); - - if (!didStart) { - // We'll still remove the listener. Weird that the stream didn't stop, but - // we can at least ignore any resulting FSEvents. - } else { - if (currentEventStream) { - FSEventStreamStop(currentEventStream); - FSEventStreamInvalidate(currentEventStream); - FSEventStreamRelease(currentEventStream); - } - currentEventStream = nextEventStream; - nextEventStream = nullptr; - } + // We don't capture the return value here because it doesn’t affect our + // response. If a new stream fails to start for whatever reason, the old + // stream will still work. And because we've removed the handle from the + // relevant maps, we will silently ignore any filesystem events that happen + // at the given path. + startNewStream(); } void FSEventsFileWatcher::FSEventCallback( @@ -362,6 +257,7 @@ void FSEventsFileWatcher::FSEventCallback( } } + if (!instance->isValid) return; instance->handleActions(events); instance->process(); } @@ -407,7 +303,7 @@ void FSEventsFileWatcher::handleActions(std::vector& events) { std::string path; bool found = false; - for (const auto& pair: handlesToPaths.forward) { + for (const auto& pair: handlesToPaths) { std::string normalizedPath = NormalizePath(pair.second); if (!PathStartsWith(event.path, pair.second)) continue; if (event.path.find_last_of(PATH_SEPARATOR) != pair.second.size()) { @@ -519,18 +415,49 @@ void FSEventsFileWatcher::handleAddModDel( } } +void FSEventsFileWatcher::removeHandle(efsw::WatchID handle) { + auto itp = handlesToPaths.find(handle); + if (itp != handlesToPaths.end()) { + handlesToPaths.erase(itp); + } + auto itl = handlesToListeners.find(handle); + if (itl != handlesToListeners.end()) { + handlesToListeners.erase(itl); + } +} + void FSEventsFileWatcher::process() { - std::lock_guard lock(dirsChangedMutex); - std::set::iterator it = dirsChanged.begin(); - for (; it != dirsChanged.end(); it++) { +#ifdef DEBUG + std::cout << "FSEventsFileWatcher::process?" << std::endl; +#endif - std::string dir = *it; + if (!isValid || pendingDestruction) return; + { + std::unique_lock lock(processingMutex); + if (isProcessing) return; + isProcessing = true; + } + + ProcessingGuard guard(*this); + + std::set dirsCopy; + { + dirsCopy = dirsChanged; + dirsChanged.clear(); + } + + // Process the copied directories + for (const auto& dir : dirsCopy) { + if (pendingDestruction) return; +#ifdef DEBUG + std::cout << "Changed:" << dir << std::endl; +#endif efsw::WatchID handle; std::string path; bool found = false; - for (const auto& pair: handlesToPaths.forward) { + for (const auto& pair: handlesToPaths) { // std::string normalizedPath = NormalizePath(pair.second); if (!PathStartsWith(dir, pair.second)) continue; if (dir.find_last_of(PATH_SEPARATOR) != pair.second.size()) { @@ -549,7 +476,81 @@ void FSEventsFileWatcher::process() { FileNameFromPath(dir), efsw::Actions::Modified ); + + if (pendingDestruction) return; } dirsChanged.clear(); } + +bool FSEventsFileWatcher::startNewStream() { + // Build a list of all current watched paths. We'll eventually pass this to + // `FSEventStreamCreate`. + std::vector cfStrings; + for (const auto& pair : handlesToPaths) { + CFStringRef cfStr = CFStringCreateWithCString( + kCFAllocatorDefault, + pair.second.c_str(), + kCFStringEncodingUTF8 + ); + if (cfStr) { + cfStrings.push_back(cfStr); + } + } + + CFArrayRef paths = CFArrayCreate( + NULL, + reinterpret_cast(cfStrings.data()), + cfStrings.size(), + NULL + ); + + uint32_t streamFlags = kFSEventStreamCreateFlagNone; + + streamFlags = kFSEventStreamCreateFlagFileEvents | + kFSEventStreamCreateFlagNoDefer | + kFSEventStreamCreateFlagUseExtendedData | + kFSEventStreamCreateFlagUseCFTypes; + + FSEventStreamContext ctx; + ctx.version = 0; + ctx.info = this; + ctx.retain = NULL; + ctx.release = NULL; + ctx.copyDescription = NULL; + + dispatch_queue_t queue = dispatch_queue_create(NULL, NULL); + + nextEventStream = FSEventStreamCreate( + kCFAllocatorDefault, + &FSEventsFileWatcher::FSEventCallback, + &ctx, + paths, + kFSEventStreamEventIdSinceNow, + 0., + streamFlags + ); + + FSEventStreamSetDispatchQueue(nextEventStream, queue); + bool didStart = FSEventStreamStart(nextEventStream); + + // Release all the strings we just created. + for (CFStringRef str : cfStrings) { + CFRelease(str); + } + CFRelease(paths); + + // If it started successfully, we can swap it into place as the new main + // stream. + if (didStart) { + if (currentEventStream) { + FSEventStreamStop(currentEventStream); + FSEventStreamInvalidate(currentEventStream); + FSEventStreamRelease(currentEventStream); + } + currentEventStream = nextEventStream; + nextEventStream = nullptr; + } + + return didStart; +} diff --git a/lib/platform/FSEventsFileWatcher.hpp b/lib/platform/FSEventsFileWatcher.hpp index 9751c4a..26f3c40 100644 --- a/lib/platform/FSEventsFileWatcher.hpp +++ b/lib/platform/FSEventsFileWatcher.hpp @@ -9,37 +9,6 @@ #include #include "../../vendor/efsw/include/efsw/efsw.hpp" -template -class BidirectionalMap { - -public: - void insert(const K& key, const V& value) { - forward[key] = value; - reverse[value] = key; - } - - void remove(const K& key) { - auto it = forward.find(key); - if (it != forward.end()) { - reverse.erase(it->second); - forward.erase(it); - } - } - - const V* getValue(const K& key) const { - auto it = forward.find(key); - return it != forward.end() ? &it->second : nullptr; - } - - const K* getKey(const V& value) const { - auto it = reverse.find(value); - return it != reverse.end() ? &it->second : nullptr; - } - - std::unordered_map forward; - std::unordered_map reverse; -}; - class FSEvent { public: FSEvent( @@ -103,7 +72,27 @@ class FSEventsFileWatcher { bool isValid = true; private: + // RAII guard to ensure we un-mark our “processing” flag if we're destroyed + // during processing. + class ProcessingGuard { + FSEventsFileWatcher& watcher; + public: + ProcessingGuard(FSEventsFileWatcher& w) : watcher(w) {} + ~ProcessingGuard() { + std::unique_lock lock(watcher.processingMutex); + watcher.isProcessing = false; + watcher.processingComplete.notify_all(); + } + }; + + void removeHandle(efsw::WatchID handle); + bool startNewStream(); + long nextHandleID; + std::atomic isProcessing{false}; + std::atomic pendingDestruction{false}; + std::mutex processingMutex; + std::condition_variable processingComplete; // The running event stream that subscribes to all the paths we care about. FSEventStreamRef currentEventStream = nullptr; @@ -112,8 +101,7 @@ class FSEventsFileWatcher { FSEventStreamRef nextEventStream = nullptr; std::set dirsChanged; - std::mutex dirsChangedMutex; - BidirectionalMap handlesToPaths; + std::unordered_map handlesToPaths; std::unordered_map handlesToListeners; }; From f32158f3f8275df0b7e273326967141f21a494d9 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 27 Oct 2024 20:24:44 -0700 Subject: [PATCH 147/168] Remove `NativeWatcherRegistry` (unneeded at this point) --- spec/native-watcher-registry-spec.js | 430 -------------- src/native-watcher-registry.js | 816 --------------------------- 2 files changed, 1246 deletions(-) delete mode 100644 spec/native-watcher-registry-spec.js delete mode 100644 src/native-watcher-registry.js diff --git a/spec/native-watcher-registry-spec.js b/spec/native-watcher-registry-spec.js deleted file mode 100644 index e96d499..0000000 --- a/spec/native-watcher-registry-spec.js +++ /dev/null @@ -1,430 +0,0 @@ -const path = require('path'); -const { Emitter } = require('event-kit'); -const { NativeWatcherRegistry } = require('../src/native-watcher-registry'); - -function findRootDirectory() { - let current = process.cwd(); - while (true) { - let next = path.resolve(current, '..'); - if (next === current) { - return next; - } else { - current = next; - } - } -} - -function EMPTY() {} - -const ROOT = findRootDirectory(); - -function absolute(...parts) { - const candidate = path.join(...parts); - return path.isAbsolute(candidate) ? candidate : path.join(ROOT, candidate); -} - -function parts(fullPath) { - return fullPath.split(path.sep).filter(part => part.length > 0); -} - -class MockWatcher { - constructor(normalizedPath) { - this.normalizedPath = normalizedPath; - this.native = null; - this.active = true; - } - - getNormalizedPathPromise() { - return Promise.resolve(this.normalizedPath); - } - - getNormalizedPath () { - return this.normalizedPath; - } - - attachToNative(native, nativePath) { - if (this.normalizedPath.startsWith(nativePath)) { - if (this.native) { - this.native.attached = this.native.attached.filter( - each => each !== this - ); - } - this.native = native; - this.native.onDidChange(EMPTY); - this.native.attached.push(this); - } - } - - stop () { - this.active = false; - this.native.stopIfNoListeners(); - } -} - -class MockNative { - constructor(name) { - this.name = name; - this.attached = []; - this.disposed = false; - this.stopped = true; - this.wasListenedTo = false; - - this.emitter = new Emitter(); - } - - reattachListenersTo(newNative, nativePath) { - for (const watcher of this.attached) { - watcher.attachToNative(newNative, nativePath); - } - } - - onDidChange (callback) { - this.wasListenedTo = true; - this.stopped = false; - return this.emitter.on('did-change', callback); - } - - onWillStop(callback) { - return this.emitter.on('will-stop', callback); - } - - dispose() { - this.disposed = true; - } - - get listenerCount () { - return this.emitter.listenerCountForEventName('did-change'); - } - - stopIfNoListeners () { - if (this.listenerCount > 0) return; - this.stop(); - } - - stop() { - console.log('Stopping:', this.name); - this.stopped = true; - this.emitter.emit('will-stop'); - this.dispose(); - } -} - -xdescribe('NativeWatcherRegistry', function() { - let createNative, registry; - - beforeEach(function() { - registry = new NativeWatcherRegistry(normalizedPath => - createNative(normalizedPath) - ); - }); - - it('attaches a Watcher to a newly created NativeWatcher for a new directory', async function() { - const watcher = new MockWatcher(absolute('some', 'path')); - const NATIVE = new MockNative('created'); - createNative = () => NATIVE; - - await registry.attach(watcher); - - expect(watcher.native).toBe(NATIVE); - }); - - it('reuses an existing NativeWatcher on the same directory', async function() { - const EXISTING = new MockNative('existing'); - const existingPath = absolute('existing', 'path'); - let firstTime = true; - createNative = () => { - if (firstTime) { - firstTime = false; - return EXISTING; - } - - return new MockNative('nope'); - }; - await registry.attach(new MockWatcher(existingPath)); - - const watcher = new MockWatcher(existingPath); - await registry.attach(watcher); - - expect(watcher.native).toBe(EXISTING); - }); - - it('attaches to an existing NativeWatcher on a parent directory', async function() { - const EXISTING = new MockNative('existing'); - const parentDir = absolute('existing', 'path'); - const subDir = path.join(parentDir, 'sub', 'directory'); - let firstTime = true; - createNative = () => { - if (firstTime) { - firstTime = false; - return EXISTING; - } - - return new MockNative('nope'); - }; - await registry.attach(new MockWatcher(parentDir)); - - const watcher = new MockWatcher(subDir); - await registry.attach(watcher); - - expect(watcher.native).toBe(EXISTING); - }); - - it('adopts Watchers from NativeWatchers on child directories', async function() { - const parentDir = absolute('existing', 'path'); - const childDir0 = path.join(parentDir, 'child', 'directory', 'zero'); - const childDir1 = path.join(parentDir, 'child', 'directory', 'one'); - const otherDir = absolute('another', 'path'); - - const expectedCommonDir = path.join(parentDir, 'child', 'directory'); - - const CHILD0 = new MockNative('existing0'); - const CHILD1 = new MockNative('existing1'); - const COMMON = new MockNative('commonAncestor'); - const OTHER = new MockNative('existing2'); - const PARENT = new MockNative('parent'); - - createNative = dir => { - if (dir === childDir0) { - return CHILD0; - } else if (dir === childDir1) { - return CHILD1; - } else if (dir === otherDir) { - return OTHER; - } else if (dir === parentDir) { - return PARENT; - } else if (dir === expectedCommonDir) { - return COMMON; - } else { - throw new Error(`Unexpected path: ${dir}`); - } - }; - - // With only one watcher, we expect it to be CHILD0. - const watcher0 = new MockWatcher(childDir0); - await registry.attach(watcher0); - expect(watcher0.native).toBe(CHILD0); - - // When we add another watcher at a sibling path, we expect them to unify - // under a common watcher on their closest ancestor. - const watcher1 = new MockWatcher(childDir1); - await registry.attach(watcher1); - expect(watcher1.native).toBe(COMMON); - expect(CHILD0.stopped).toBe(true); - - const watcher2 = new MockWatcher(otherDir); - await registry.attach(watcher2); - expect(watcher2.native).toBe(OTHER); - - // Consolidate all three watchers beneath the same native watcher on the - // parent directory. - const watcher = new MockWatcher(parentDir); - await registry.attach(watcher); - expect(watcher.native).toBe(PARENT); - expect(watcher0.native).toBe(PARENT); - - expect(CHILD0.stopped).toBe(true); - expect(CHILD0.disposed).toBe(true); - - expect(watcher1.native).toBe(PARENT); - // CHILD1 should never have been used. - expect(CHILD1.stopped).toBe(true); - expect(CHILD1.wasListenedTo).toBe(false); - - expect(watcher2.native).toBe(OTHER); - expect(OTHER.stopped).toBe(false); - expect(OTHER.disposed).toBe(false); - }); - - describe('removing NativeWatchers', function() { - it('happens when nothing is subscribed to them', async function() { - const STOPPED = new MockNative('stopped'); - const RUNNING = new MockNative('running'); - - const stoppedPath = absolute('watcher', 'that', 'will', 'be', 'stopped'); - const stoppedPathParts = stoppedPath - .split(path.sep) - .filter(part => part.length > 0); - const runningPath = absolute( - 'watcher', - 'that', - 'will', - 'be' - ); - const runningPathParts = runningPath - .split(path.sep) - .filter(part => part.length > 0); - - createNative = dir => { - if (dir === stoppedPath) { - return STOPPED; - } else if (dir === runningPath) { - return RUNNING; - } else { - throw new Error(`Unexpected path: ${dir}`); - } - }; - - const stoppedWatcher = new MockWatcher(stoppedPath); - await registry.attach(stoppedWatcher); - - const runningWatcher = new MockWatcher(runningPath); - await registry.attach(runningWatcher); - - stoppedWatcher.stop(); - registry.detach(stoppedWatcher); - - const runningNode = registry.tree.root.lookup(runningPathParts).when({ - parent: node => node, - missing: () => false, - children: () => false - }); - - expect(runningNode).toBeTruthy(); - expect(runningNode.getNativeWatcher()).toBe(RUNNING); - - // Either of the `parent` or `missing` outcomes would be fine here as - // long as the exact node doesn't exist. - const stoppedNode = registry.tree.root.lookup(stoppedPathParts).when({ - parent: (_, remaining) => remaining.length > 0, - missing: () => true, - children: () => false - }); - - expect(stoppedNode).toBe(true); - }); - - it('reassigns new child watchers when a parent watcher is stopped', async function() { - const CHILD0 = new MockNative('child0'); - const CHILD1 = new MockNative('child1'); - const PARENT = new MockNative('parent'); - - const parentDir = absolute('parent'); - const childDir0 = path.join(parentDir, 'child0'); - const childDir1 = path.join(parentDir, 'child1'); - - createNative = dir => { - if (dir === parentDir) { - return PARENT; - } else if (dir === childDir0) { - return CHILD0; - } else if (dir === childDir1) { - return CHILD1; - } else { - throw new Error(`Unexpected directory ${dir}`); - } - }; - - const parentWatcher = new MockWatcher(parentDir); - const childWatcher0 = new MockWatcher(childDir0); - const childWatcher1 = new MockWatcher(childDir1); - - await registry.attach(parentWatcher); - await Promise.all([ - registry.attach(childWatcher0), - registry.attach(childWatcher1) - ]); - - // All three watchers should share the parent watcher's native watcher. - expect(parentWatcher.native).toBe(PARENT); - expect(childWatcher0.native).toBe(PARENT); - expect(childWatcher1.native).toBe(PARENT); - - // Stopping the parent should detach and recreate the child watchers. - parentWatcher.stop(); - registry.detach(parentWatcher); - - expect(childWatcher0.native).toBe(CHILD0); - expect(childWatcher1.native).toBe(CHILD1); - - expect( - registry.tree.root.lookup(parts(parentDir)).when({ - parent: () => false, - missing: () => false, - children: () => true - }) - ).toBe(true); - - expect( - registry.tree.root.lookup(parts(childDir0)).when({ - parent: () => true, - missing: () => false, - children: () => false - }) - ).toBe(true); - - expect( - registry.tree.root.lookup(parts(childDir1)).when({ - parent: () => true, - missing: () => false, - children: () => false - }) - ).toBe(true); - }); - - it('consolidates children when splitting a parent watcher', async function() { - const CHILD0 = new MockNative('child0'); - const PARENT = new MockNative('parent'); - - const parentDir = absolute('parent'); - const childDir0 = path.join(parentDir, 'child0'); - const childDir1 = path.join(parentDir, 'child0', 'child1'); - - createNative = dir => { - if (dir === parentDir) { - return PARENT; - } else if (dir === childDir0) { - return CHILD0; - } else { - throw new Error(`Unexpected directory ${dir}`); - } - }; - - const parentWatcher = new MockWatcher(parentDir); - const childWatcher0 = new MockWatcher(childDir0); - const childWatcher1 = new MockWatcher(childDir1); - - await registry.attach(parentWatcher); - await Promise.all([ - registry.attach(childWatcher0), - registry.attach(childWatcher1) - ]); - - // All three watchers should share the parent watcher's native watcher. - expect(parentWatcher.native).toBe(PARENT); - expect(childWatcher0.native).toBe(PARENT); - expect(childWatcher1.native).toBe(PARENT); - - // Stopping the parent should detach and create the child watchers. Both child watchers should - // share the same native watcher. - parentWatcher.stop(); - registry.detach(parentWatcher); - - expect(childWatcher0.native).toBe(CHILD0); - expect(childWatcher1.native).toBe(CHILD0); - - expect( - registry.tree.root.lookup(parts(parentDir)).when({ - parent: () => false, - missing: () => false, - children: () => true - }) - ).toBe(true); - - expect( - registry.tree.root.lookup(parts(childDir0)).when({ - parent: () => true, - missing: () => false, - children: () => false - }) - ).toBe(true); - - expect( - registry.tree.root.lookup(parts(childDir1)).when({ - parent: () => true, - missing: () => false, - children: () => false - }) - ).toBe(true); - }); - }); -}); diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js deleted file mode 100644 index d516f4a..0000000 --- a/src/native-watcher-registry.js +++ /dev/null @@ -1,816 +0,0 @@ -// Heavily based on a similar implementation in the Atom/Pulsar codebase and -// licensed under MIT. -// -// This differs from the one in the Pulsar codebase in the following ways: -// -// * We employ more strategies for reusing watchers. For instance: we allow for -// “sibling” directories to detect each other and share a watcher pointing at -// their common parent. We also allow a `PathWatcher` whose `NativeWatcher` -// points at an ancestor to eschew it in favor of a closer `NativeWatcher` -// when it's the only consumer on that path chain. -// * We don't have any automatic behavior around `NativeWatcher` stopping. If a -// `NativeWatcher` stops, we assume it's because its `PathWatcher` stopped -// and it had no other `PathWatcher`s to assist. -// -// This registry does not manage `NativeWatcher`s and is not in charge of -// destroying them during cleanup. It _is_ in charge of knowing when and where -// to create a `NativeWatcher` — including knowing when it _doesn’t_ need to -// create a `NativeWatcher` because an existing one can already do the job. - -const path = require('path'); - -// Private: re-join the segments split from an absolute path to form another -// absolute path. -function absolute(...parts) { - const candidate = path.join(...parts); - let result = path.isAbsolute(candidate) - ? candidate - : path.join(path.sep, candidate); - return result; -} - -// Private: Map userland filesystem watcher subscriptions efficiently to -// deliver filesystem change notifications to each watcher with the most -// efficient coverage of native watchers. -// -// * If two watchers subscribe to the same directory, use a single native -// watcher for each. -// * Re-use a native watcher watching a parent directory for a watcher on a -// child directory. If the parent directory watcher is removed, it will be -// split into child watchers. -// * If any child directories already being watched, stop and replace them with -// a watcher on the parent directory. -// -// Uses a trie whose structure mirrors the directory structure. -class RegistryTree { - // Private: Construct a tree with no native watchers. - // - // * `basePathSegments` the position of this tree's root relative to the - // filesystem's root as an {Array} of directory names. - // * `createNative` {Function} used to construct new native watchers. It - // should accept an absolute path as an argument and return a new - // {NativeWatcher}. - constructor(basePathSegments, createNative, options = {}) { - this.basePathSegments = basePathSegments; - this.root = new RegistryNode(null); - this.createNative = createNative; - this.options = options; - } - - // Private: Identify the native watcher that should be used to produce events - // at a watched path, creating a new one if necessary. - // - // * `pathSegments` The path to watch represented as an {Array} of directory - // names relative to this {RegistryTree}'s root. - // * `attachToNative` {Function} invoked with the appropriate native watcher - // and the absolute path to its watch root. - add(pathSegments, attachToNative) { - const absolutePathSegments = this.basePathSegments.concat(pathSegments); - const absolutePath = absolute(...absolutePathSegments); - - // Scenario in which we're attaching to an ancestor of this path — for - // instance, if we discover that we share an ancestor directory with an - // existing watcher. - const attachToAncestor = (absolutePaths, childPaths) => { - let ancestorAbsolutePath = absolute(...absolutePaths); - let native = this.createNative(ancestorAbsolutePath, this.options); - let leaf = new RegistryWatcherNode( - native, - absolutePaths, - childPaths, - this.options - ); - this.root = this.root.insert(absolutePaths, leaf); - attachToNative(native, ancestorAbsolutePath); - return native; - }; - - // Scenario in which we're attaching directly to a specific path. - const attachToNew = (childPaths) => { - const native = this.createNative(absolutePath, this.options); - const leaf = new RegistryWatcherNode( - native, - absolutePathSegments, - childPaths, - this.options - ); - this.root = this.root.insert(pathSegments, leaf); - attachToNative(native, absolutePath); - return native; - }; - - this.root.lookup(pathSegments).when({ - parent: (parent, remaining) => { - if (!this.options.reuseAncestorWatchers) { - attachToNew([]); - return; - } - // An existing `NativeWatcher` is watching the same directory or a - // parent directory of the requested path. Attach this `PathWatcher` to - // it as a filtering watcher and record it as a dependent child path. - const native = parent.getNativeWatcher(); - parent.addChildPath(remaining); - attachToNative(native, absolute(...parent.getAbsolutePathSegments())); - }, - children: children => { - // One or more `NativeWatcher`s exist on child directories of the - // requested path. Create a new `NativeWatcher` on the parent - // directory, note the subscribed child paths, and cleanly stop the - // child native watchers. - const newNative = attachToNew(children.map(child => child.path)); - for (let i = 0; i < children.length; i++) { - const childNode = children[i].node; - const childNative = childNode.getNativeWatcher(); - childNative.reattachListenersTo(newNative, absolutePath); - childNative.dispose(); - childNative.stop(); - } - }, - missing: (lastParent) => { - if (!this.options.mergeWatchersWithCommonAncestors) { - attachToNew([]); - return; - } - - // We couldn't find an existing watcher anywhere above us in this path - // hierarchy. But we helpfully receive the last node that was already - // in the tree (i.e., created by a previous watcher), so we might be - // able to consolidate two watchers. - if (lastParent?.parent == null) { - // We're at the root node; there is no other watcher in this tree. - // Create one at the current location. - attachToNew([]); - return; - } - - let leaves = lastParent.leaves(this.basePathSegments); - - if (leaves.length === 0) { - // There's an ancestor node, but it doesn't have any native watchers - // below it. This would happen if there once was a watcher at a - // different point in the tree, but it was disposed of before we got - // here. - // - // This is functionally the same as the above case, so we'll create a - // new native watcher at the current path. - attachToNew([]); - return; - } - - // If we get this far, then one of our ancestor directories has an - // active native watcher somewhere underneath it. We can streamline - // native watchers by creating a new one to manage two or more existing - // paths, then stopping the one that was previously running. - let ancestorPathSegments = lastParent.getPathSegments(this); - - let remainingPathSegments = [...pathSegments]; - for (let i = 0; i < ancestorPathSegments.length; i++) { - remainingPathSegments.shift(); - } - - // Taken to its logical extreme, this approach would always yield - // a maximum of one watcher, since all paths have a common ancestor. - // But if we listen at the root of the volume, we'll be drinking from a - // firehose and making our wrapped watchers do a lot of work. - // - // So we should strike a balance: good to consolidate watchers when - // they’re “close enough” to one another in the tree, but bad to do it - // obsessively and create lots of churn. - // - // NOTE: We can also introduce platform-specific logic here. For - // instance, consolidating watchers seems to be important on macOS and - // less so on Windows and Linux. - // - // Let's impose some constraints: - - // Impose a max distance when moving upward. This will let us avoid - // _creating_ a new watcher that's more than a certain number of levels - // above the path we care about. - // - // This does not prevent us from _reusing_ such a watcher that is - // already present (as in the `parent` scenario above). We were already - // paying the cost of that watcher. - // - // TODO: Expose configuration options for these constraints to the - // consumer. - // - let difference = pathSegments.length - ancestorPathSegments.length; - console.debug('Tier difference:', difference); - if (difference > this.options.maxCommonAncestorLevel) { - attachToNew([]); - return; - } - - // NOTE: Future ideas for constraints: - // - // * Don't create a new watcher at the root unless explicitly told to. - // * Allow the wrapper code to specify certain paths above which we're - // not allowed to ascend unless explicitly told. (The user's home - // folder feels like a good one.) - // * Perhaps enforce a soft native-watcher quota and have it - // consolidate more aggressively when we're close to the quota than - // when we're not. - - let childPaths = leaves.map(l => l.path); - childPaths.push(remainingPathSegments); - let newNative = attachToAncestor(ancestorPathSegments, childPaths); - let absolutePath = absolute(...ancestorPathSegments); - for (let i = 0; i < leaves.length; i++) { - let leaf = leaves[i].node; - let native = leaf.getNativeWatcher(); - native.reattachListenersTo(newNative, absolutePath); - native.dispose(); - native.stop(); - // NOTE: Should not need to dispose of native watchers; it should - // happen automatically as they are left. - } - } - }); - } - - remove (pathSegments) { - this.root = this.root.remove(pathSegments, this.createNative) || - new RegistryNode(null); - } - - // Private: Access the root node of the tree. - getRoot() { - return this.root; - } - - // Private: Return a {String} representation of this tree’s structure for - // diagnostics and testing. - print() { - return this.root.print(); - } -} - -// Private: Non-leaf node in a {RegistryTree} used by the -// {NativeWatcherRegistry} to cover the allocated {Watcher} instances with the -// most efficient set of {NativeWatcher} instances possible. Each -// {RegistryNode} maps to a directory in the filesystem tree. -class RegistryNode { - // Private: Construct a new, empty node representing a node with no watchers. - constructor(parent, pathKey, options) { - this.parent = parent; - this.pathKey = pathKey; - this.children = {}; - this.options = options; - } - - getPathSegments (comparison = null) { - let result = [this.pathKey]; - let pointer = this.parent; - while (pointer && pointer.pathKey && pointer !== comparison) { - result.unshift(pointer.pathKey); - pointer = pointer.parent; - } - return result; - } - - // Private: Recursively discover any existing watchers corresponding to a - // path. - // - // * `pathSegments` filesystem path of a new {Watcher} already split into an - // Array of directory names. - // - // Returns: A {ParentResult} if the exact requested directory or a parent - // directory is being watched, a {ChildrenResult} if one or more child paths - // are being watched, or a {MissingResult} if no relevant watchers exist. - lookup(pathSegments) { - if (pathSegments.length === 0) { - return new ChildrenResult(this.leaves([])); - } - - const child = this.children[pathSegments[0]]; - if (child === undefined) { - return new MissingResult(this); - } - - return child.lookup(pathSegments.slice(1)); - } - - // Private: Insert a new {RegistryWatcherNode} into the tree, creating new - // intermediate {RegistryNode} instances as needed. Any existing children of - // the watched directory are removed. - // - // * `pathSegments` filesystem path of the new {Watcher}, already split into - // an Array of directory names. - // * `leaf` initialized {RegistryWatcherNode} to insert - // - // Returns: The root of a new tree with the {RegistryWatcherNode} inserted at - // the correct location. Callers should replace their node references with - // the returned value. - insert(pathSegments, leaf) { - // console.log('Insert:', pathSegments); - if (pathSegments.length === 0) { - return leaf; - } - - const pathKey = pathSegments[0]; - let child = this.children[pathKey]; - if (child === undefined) { - child = new RegistryNode(this, pathKey, this.options); - } - this.children[pathKey] = child.insert(pathSegments.slice(1), leaf); - return this; - } - - // Private: Remove a {RegistryWatcherNode} by its exact watched directory. - // - // * `pathSegments` absolute pre-split filesystem path of the node to remove. - // * `createSplitNative` callback to be invoked with each child path segment - // {Array} if the {RegistryWatcherNode} is split into child watchers rather - // than removed outright. See {RegistryWatcherNode.remove}. - // - // Returns: The root of a new tree with the {RegistryWatcherNode} removed. - // Callers should replace their node references with the returned value. - remove(pathSegments, createSplitNative) { - if (pathSegments.length === 0) { - // Attempt to remove a path with child watchers. Do nothing. - return this; - } - - const pathKey = pathSegments[0]; - const child = this.children[pathKey]; - if (child === undefined) { - // Attempt to remove a path that isn't watched. Do nothing. - return this; - } - - // Recurse - const newChild = child.remove(pathSegments.slice(1), createSplitNative); - if (newChild === null) { - delete this.children[pathKey]; - } else { - this.children[pathKey] = newChild; - } - - // Remove this node if all of its children have been removed - return Object.keys(this.children).length === 0 ? null : this; - } - - // Private: Discover all {RegistryWatcherNode} instances beneath this tree - // node and the child paths that they are watching. - // - // * `prefix` {Array} of intermediate path segments to prepend to the - // resulting child paths. - // - // Returns: A possibly empty {Array} of `{node, path}` objects describing - // {RegistryWatcherNode} instances beneath this node. - leaves(prefix) { - const results = []; - for (const p of Object.keys(this.children)) { - results.push(...this.children[p].leaves(prefix.concat([p]))); - } - return results; - } - - // Private: Return a {String} representation of this subtree for diagnostics - // and testing. - print(indent = 0) { - let spaces = ''; - for (let i = 0; i < indent; i++) { - spaces += ' '; - } - - let result = ''; - for (const p of Object.keys(this.children)) { - result += `${spaces}${p}\n${this.children[p].print(indent + 2)}`; - } - return result; - } -} - -// Private: Leaf node within a {NativeWatcherRegistry} tree. Represents a directory that is covered by a -// {NativeWatcher}. -class RegistryWatcherNode { - // Private: Allocate a new node to track a {NativeWatcher}. - // - // * `nativeWatcher` An existing {NativeWatcher} instance. - // * `absolutePathSegments` The absolute path to this {NativeWatcher}'s - // directory as an {Array} of path segments. - // * `childPaths` {Array} of child directories that are currently the - // responsibility of this {NativeWatcher}, if any. Directories are - // represented as arrays of the path segments between this node's directory - // and the watched child path. - constructor(nativeWatcher, absolutePathSegments, childPaths, options) { - this.nativeWatcher = nativeWatcher; - this.absolutePathSegments = absolutePathSegments; - this.options = options; - - // Store child paths as joined strings so they work as Set members. - this.childPaths = new Set(); - for (let i = 0; i < childPaths.length; i++) { - this.childPaths.add(path.join(...childPaths[i])); - } - } - - // Private: Assume responsibility for a new child path. If this node is - // removed, it will instead split into a subtree with a new - // {RegistryWatcherNode} for each child path. - // - // * `childPathSegments` the {Array} of path segments between this node's - // directory and the watched child directory. - addChildPath(childPathSegments) { - this.childPaths.add(path.join(...childPathSegments)); - } - - // Private: Stop assuming responsibility for a previously assigned child - // path. If this node is removed, the named child path will no longer be - // allocated a {RegistryWatcherNode}. - // - // * `childPathSegments` the {Array} of path segments between this node's - // directory and the no longer watched child directory. - removeChildPath(childPathSegments) { - this.childPaths.delete(path.join(...childPathSegments)); - } - - // Private: Accessor for the {NativeWatcher}. - getNativeWatcher() { - return this.nativeWatcher; - } - - // Private - insert (pathSegments, leaf) { - if (pathSegments.length === 0) return leaf; - } - - destroyNativeWatcher(shutdown) { - this.nativeWatcher.stop(shutdown); - } - - // Private: Return the absolute path watched by this {NativeWatcher} as an - // {Array} of directory names. - getAbsolutePathSegments() { - return this.absolutePathSegments; - } - - // Private: Identify how this watcher relates to a request to watch a - // directory tree. - // - // * `pathSegments` filesystem path of a new {Watcher} already split into an - // Array of directory names. - // - // Returns: A {ParentResult} referencing this node. - lookup(pathSegments) { - return new ParentResult(this, pathSegments); - } - - // Private: Remove this leaf node if the watcher's exact path matches. If - // this node is covering additional {Watcher} instances on child paths, it - // will be split into a subtree. - // - // * `pathSegments` filesystem path of the node to remove. - // * `createSplitNative` callback invoked with each {Array} of absolute child - // path segments to create a native watcher on a subtree of this node. - // - // Returns: If `pathSegments` match this watcher's path exactly, returns - // `null` if this node has no `childPaths` or a new {RegistryNode} on a newly - // allocated subtree if it did. If `pathSegments` does not match the - // watcher's path, it's an attempt to remove a subnode that doesn't exist, so - // the remove call has no effect and returns `this` unaltered. - remove(pathSegments, createSplitNative) { - // This function represents converting this `RegistryWatcherNode` into a - // plain `RegistryNode` that no longer has the direct responsibility of - // managing a native watcher. Any child paths on this node are converted to - // leaf nodes with their own native watchers. - // - // We do this if: - // - // * This path itself is being removed. - // * One of this path’s child paths is being removed and it has only one - // remaining child path. (We move the watcher down to the child in this - // instance.) - // - // TODO: Also invoke some form of this logic if more than two paths are - // being watched… but the removal of a path creates a scenario where we can - // move a watcher to a closer common descendant of the remaining paths. - let replacedWithNode = () =>{ - let newSubTree = new RegistryTree( - this.absolutePathSegments, - createSplitNative, - this.options - ); - - for (const childPath of this.childPaths) { - const childPathSegments = childPath.split(path.sep); - newSubTree.add(childPathSegments, (native, attachmentPath) => { - this.nativeWatcher.reattachListenersTo(native, attachmentPath); - }); - } - return newSubTree.getRoot(); - }; - if (pathSegments.length !== 0) { - this.removeChildPath(pathSegments); - if (this.childPaths.size === 1) { - return replacedWithNode(); - } - return this; - } else if (this.childPaths.size > 0) { - // We are here because a watcher for this path is being removed. If this - // path has descendants depending on the same watcher, this is an - // opportunity to create a new `NativeWatcher` that is more proximate to - // those descendants. - return replacedWithNode(); - } else { - return null; - } - } - - // Private: Discover this {RegistryWatcherNode} instance. - // - // * `prefix` {Array} of intermediate path segments to prepend to the - // resulting child paths. - // - // Returns: An {Array} containing a `{node, path}` object describing this - // node. - leaves(prefix) { - return [{ node: this, path: prefix }]; - } - - // Private: Return a {String} representation of this watcher for diagnostics - // and testing. Indicates the number of child paths that this node's - // {NativeWatcher} is responsible for. - print(indent = 0) { - let result = ''; - for (let i = 0; i < indent; i++) { - result += ' '; - } - result += '[watcher'; - if (this.childPaths.size > 0) { - result += ` +${this.childPaths.size}`; - } - result += ']\n'; - - return result; - } -} - -// Private: A {RegistryNode} traversal result that's returned when neither a -// directory, its children, nor its parents are present in the tree. -class MissingResult { - // Private: Instantiate a new {MissingResult}. - // - // * `lastParent` the final successfully traversed {RegistryNode}. - constructor(lastParent) { - this.lastParent = lastParent; - } - - // Private: Dispatch within a map of callback actions. - // - // * `actions` {Object} containing a `missing` key that maps to a callback to - // be invoked when no results were returned by {RegistryNode.lookup}. The - // callback will be called with the last parent node that was encountered - // during the traversal. - // - // Returns: the result of the `actions` callback. - when(actions) { - return actions.missing(this.lastParent); - } -} - -// Private: A {RegistryNode.lookup} traversal result that's returned when a -// parent or an exact match of the requested directory is being watched by an -// existing {RegistryWatcherNode}. -class ParentResult { - // Private: Instantiate a new {ParentResult}. - // - // * `parent` the {RegistryWatcherNode} that was discovered. - // * `remainingPathSegments` an {Array} of the directories that lie between - // the leaf node's watched directory and the requested directory. This will - // be empty for exact matches. - constructor(parent, remainingPathSegments) { - this.parent = parent; - this.remainingPathSegments = remainingPathSegments; - } - - getAbsolutePathSegments () { - let result = Array.from(this.remainingPathSegments); - let pointer = this.parent; - while (pointer) { - result.push(pointer.remainingPathSegments); - pointer = pointer.parent; - } - return result; - } - - // Private: Dispatch within a map of callback actions. - // - // * `actions` {Object} containing a `parent` key that maps to a callback to - // be invoked when a parent of a requested requested directory is returned - // by a {RegistryNode.lookup} call. The callback will be called with the - // {RegistryWatcherNode} instance and an {Array} of the {String} path - // segments that separate the parent node and the requested directory. - // - // Returns: the result of the `actions` callback. - when(actions) { - return actions.parent(this.parent, this.remainingPathSegments); - } -} - -// Private: A {RegistryNode.lookup} traversal result that's returned when one -// or more children of the requested directory are already being watched. -class ChildrenResult { - // Private: Instantiate a new {ChildrenResult}. - // - // * `children` {Array} of the {RegistryWatcherNode} instances that were - // discovered. - constructor(children) { - this.children = children; - } - - // Private: Dispatch within a map of callback actions. - // - // * `actions` {Object} containing a `children` key that maps to a callback - // to be invoked when a parent of a requested requested directory is - // returned by a {RegistryNode.lookup} call. The callback will be called - // with the {RegistryWatcherNode} instance. - // - // Returns: the result of the `actions` callback. - when(actions) { - return actions.children(this.children); - } -} - -// Private: Track the directories being monitored by native filesystem -// watchers. Minimize the number of native watchers allocated to receive events -// for a desired set of directories by: -// -// 1. Subscribing to the same underlying {NativeWatcher} when watching the same -// directory multiple times. -// 2. Subscribing to an existing {NativeWatcher} on a parent of a desired -// directory. -// 3. Replacing multiple {NativeWatcher} instances on child directories with a -// single new {NativeWatcher} on the parent. -class NativeWatcherRegistry { - static DEFAULT_OPTIONS = { - // When adding a watcher for `/foo/bar/baz/thud`, will reuse any of - // `/foo/bar/baz`, `/foo/bar`, or `/foo` that may already exist. - // - // When `false`, a second native watcher will be created in this scenario - // instead. - reuseAncestorWatchers: true, - - // When a single native watcher exists at `/foo/bar/baz/thud` and a watcher - // is added for `/foo/bar`, will create a new native watcher at `/foo/bar` - // and tell the existing watcher to use it instead. - // - // When `false`, a second native watcher will be created in this scenario - // instead. - relocateDescendantWatchers: true, - - // When a single native watcher at `/foo/bar` supplies watchers at both - // `/foo/bar` and `/foo/bar/baz/thud`, and the watcher at `/foo/bar` is - // removed, will relocate the native watcher to the more specific - // `/foo/bar/baz/thud` path for efficiency. - // - // When `false`, the too-broad native watcher will remain in place. - relocateAncestorWatchers: true, - - // When adding a watcher for `/foo/bar/baz/thud`, will look for an existing - // watcher at any descendant of `/foo/bar/baz`, `/foo/bar`, or `/foo` and - // create a new native watcher that supplies both the existing watcher and - // the new watcher by watching their common ancestor. - // - // When `false`, watchers will not be consolidated when one is not an - // ancestor of the other. - mergeWatchersWithCommonAncestors: true, - - // When using the strategy described above, will enforce a maximum limit on - // common ancestorship. For instance, if two directories share a - // great-great-great-great-grandfather, then it would not necessarily make - // sense for them to share a watcher; the potential firehose of file events - // they’d have to ignore would more than counterbalance the resource - // savings. - // - // When set to a positive integer X, will refuse to consolidate watchers in - // different branches of a tree unless their common ancestor is no more - // than X levels above _each_ one. - // - // When set to `0` or a negative integer, will enforce no maximum common - // ancestor level. - // - // Has no effect unless `mergeWatchersWithCommonAncestors` is `true`. - maxCommonAncestorLevel: 3 - }; - - // Private: Instantiate an empty registry. - // - // * `createNative` {Function} that will be called with a normalized - // filesystem path to create a new native filesystem watcher. - constructor(createNative, options = {}) { - this._createNative = createNative; - this.options = { - ...NativeWatcherRegistry.DEFAULT_OPTIONS, - ...options - }; - this.tree = new RegistryTree([], createNative, this.options); - } - - reset () { - this.tree = new RegistryTree([], this._createNative, this.options); - } - - // Private: Attach a watcher to a directory, assigning it a {NativeWatcher}. - // If a suitable {NativeWatcher} already exists, it will be attached to the - // new {Watcher} with an appropriate subpath configuration. Otherwise, the - // `createWatcher` callback will be invoked to create a new {NativeWatcher}, - // which will be registered in the tree and attached to the watcher. - // - // If any pre-existing child watchers are removed as a result of this - // operation, {NativeWatcher.onWillReattach} will be broadcast on each with - // the new parent watcher as an event payload to give child watchers a chance - // to attach to the new watcher. - // - // * `watcher` an unattached {Watcher}. - attach(watcher, normalizedDirectory = undefined) { - if (!normalizedDirectory) { - normalizedDirectory = watcher.getNormalizedPath(); - if (!normalizedDirectory) { - return this.attachAsync(watcher); - } - } - const pathSegments = normalizedDirectory - .split(path.sep) - .filter(segment => segment.length > 0); - - this.tree.add(pathSegments, (native, nativePath) => { - watcher.attachToNative(native, nativePath); - }); - } - - async attachAsync (watcher) { - const normalizedDirectory = await watcher.getNormalizedPathPromise(); - return this.attach(watcher, normalizedDirectory); - } - - // TODO: This registry envisions `PathWatcher` instances that can be attached - // to any number of `NativeWatcher` instances. But it also envisions an - // “ownership” model that isn't quite accurate. - // - // Ideally, we'd want something like this: - // - // 1. Someone adds a watcher for /Foo/Bar/Baz/thud.txt. - // 2. We set up a `NativeWatcher` for /Foo/Bar/Baz. - // 3. Someone adds a watcher for /Foo/Bar/Baz/A/B/C/zort.txt. - // 4. We reuse the existing `NativeWatcher`. - // 5. Someone stops the `PathWatcher` from step 1. - // - // What we want to happen: - // - // 6. We take that opportunity to streamline the `NativeWatchers`; since - // it’s the only one left, we know we can create a new `NativeWatcher` - // at /Foo/Bar/Baz/A/B/C and swap it onto the last `PathWatcher` - // instance. - // - // What actually happens: - // - // 6. The original `NativeWatcher` keeps going (since it has one remaining - // dependency) and our single `PathWatcher` stays subscribed to it. - // - // - // This is fine as a consolation prize, but it's less efficient. - // - // Frustratingly, most of what we want happens in response to the stopping of - // a `NativeWatcher`. If a `PathWatcher` relies on a `NativeWatcher` and - // finds that it has stopped, this registry will spin up a new - // `NativeWatcher` and allow it to resume. But we wouldn’t stop that - // `NativeWatcher` in the first place, since we know more than one - // `PathWatcher` is relying on it! - // - // I’ve made preliminary attempts to address this by moving some of the logic - // around, but it’s not yet had the effect I want. - - detach (watcher, normalizedDirectory = undefined) { - if (!normalizedDirectory) { - normalizedDirectory = watcher.getNormalizedPath(); - if (!normalizedDirectory) { - return this.detachAsync(watcher); - } - } - const pathSegments = normalizedDirectory - .split(path.sep) - .filter(segment => segment.length > 0); - - this.tree.remove(pathSegments, (native, nativePath) => { - watcher.attachToNative(native, nativePath); - }); - - } - - async detachAsync(watcher) { - const normalizedDirectory = await watcher.getNormalizedPathPromise(); - return this.detach(watcher, normalizedDirectory); - } - - // Private: Generate a visual representation of the currently active watchers - // managed by this registry. - // - // Returns a {String} showing the tree structure. - print() { - return this.tree.print(); - } -} - -module.exports = { NativeWatcherRegistry }; From e3f7f53caf974136108d60e949012ff587d2e2ca Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 30 Oct 2024 09:29:41 -0700 Subject: [PATCH 148/168] =?UTF-8?q?Return=20to=20previous=20pattern?= =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …for `detectResurrectionAfterDelay`. (The Pulsar specs rely on being able to stub it to circumvent the async-ness.) --- src/file.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/file.js b/src/file.js index 2d9bcb1..fcde149 100644 --- a/src/file.js +++ b/src/file.js @@ -1,6 +1,7 @@ const crypto = require('crypto'); const Path = require('path'); const { Emitter, Disposable } = require('event-kit'); +const _ = require('underscore-plus'); const FS = require('fs-plus'); const Grim = require('grim'); @@ -473,8 +474,10 @@ class File { switch (eventType) { case 'delete': this.unsubscribeFromNativeChangeEvents(); - await wait(50); - await this.detectResurrection(); + // We could just `await wait(50)` here, but this method exists so that + // we can monkeypatch it in the Pulsar specs and prevent it from going + // async. + this.detectResurrectionAfterDelay(); return; case 'rename': this.setPath(eventPath); @@ -490,6 +493,10 @@ class File { } } + detectResurrectionAfterDelay () { + return _.delay(() => this.detectResurrection(), 50); + } + async detectResurrection () { let exists = await this.exists(); if (exists) { From c14f82eac5410c621e4102e357d40c82fdf40826 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 30 Oct 2024 10:09:31 -0700 Subject: [PATCH 149/168] Fix issue where we inadvertently ignore events on a directory itself --- src/main.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.js b/src/main.js index c6128a2..4d1cda6 100644 --- a/src/main.js +++ b/src/main.js @@ -385,6 +385,7 @@ class PathWatcher { // ); let isWatchedPath = (eventPath) => { + if (eventPath === this.normalizedPath) return true; return eventPath?.startsWith(sep(this.normalizedPath)); } From 8d71fd1f55e6937f63ee5014a27bf468edb14807 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 31 Oct 2024 00:01:30 -0700 Subject: [PATCH 150/168] Fix failing specs --- lib/platform/FSEventsFileWatcher.cpp | 218 ++++++++++++++++++--------- spec/pathwatcher-spec.js | 183 +++------------------- src/main.js | 34 +++-- 3 files changed, 183 insertions(+), 252 deletions(-) diff --git a/lib/platform/FSEventsFileWatcher.cpp b/lib/platform/FSEventsFileWatcher.cpp index 845ef81..8478bd7 100644 --- a/lib/platform/FSEventsFileWatcher.cpp +++ b/lib/platform/FSEventsFileWatcher.cpp @@ -26,6 +26,10 @@ static std::string NormalizePath(std::string path) { return path + PATH_SEPARATOR; } +static bool PathsAreEqual(std::string pathA, std::string pathB) { + return NormalizePath(pathA) == NormalizePath(pathB); +} + std::string PrecomposeFileName(const std::string& name) { CFStringRef cfStringRef = CFStringCreateWithCString( kCFAllocatorDefault, @@ -76,6 +80,7 @@ bool PathExists(const std::string& path) { } bool PathStartsWith(const std::string& str, const std::string& prefix) { + if (PathsAreEqual(str, prefix)) return true; if (prefix.length() > str.length()) { return false; } @@ -283,11 +288,17 @@ void FSEventsFileWatcher::sendFileAction( ); } +struct FileEventMatch { + efsw::WatchID handle; + std::string path; +}; + void FSEventsFileWatcher::handleActions(std::vector& events) { size_t esize = events.size(); for (size_t i = 0; i < esize; i++) { FSEvent& event = events[i]; + std::vector matches; if (event.flags & ( kFSEventStreamEventFlagUserDropped | @@ -299,94 +310,147 @@ void FSEventsFileWatcher::handleActions(std::vector& events) { kFSEventStreamEventFlagRootChanged )) continue; - efsw::WatchID handle; - std::string path; - bool found = false; - + // Since we aren’t doing recursive watchers, we can seemingly get away with + // using the first match we find. In nearly all cases, it’s impossible for + // a filesystem event to pertain to more than one watcher. + // + // But one exception is directory deletion, since that directory _and_ its + // parent directory can both be watched, and that would count as a deletion + // for the first and a change for the second. + // + // For that reason, we keep an array of matches, but stop as soon as we + // find two matches, since that's the practical maximum. + // + // NOTE: In extreme situations with lots of paths, this could be a choke + // point, since it’s a nested loop. The fact that we’re just doing string + // comparisons should keep it fast, but we could optimize further by + // pre-normalizing the paths. We could also move to a better data + // structure, but better to invest that time in making this library + // obsolete in the first place. for (const auto& pair: handlesToPaths) { std::string normalizedPath = NormalizePath(pair.second); - if (!PathStartsWith(event.path, pair.second)) continue; - if (event.path.find_last_of(PATH_SEPARATOR) != pair.second.size()) { + + // Filter out everything that doesn’t equal or descend from this path. + if (!PathStartsWith(event.path, normalizedPath)) continue; + + // We let this through if (a) it matches our path exactly, or (b) it + // refers to a child file/directory of ours (rather than a deeper + // descendant.) + if ( + !PathsAreEqual(event.path, normalizedPath) && event.path.find_last_of(PATH_SEPARATOR) != normalizedPath.size() - 1 + ) { continue; } - found = true; - path = pair.second; - handle = pair.first; - break; + + FileEventMatch match; + match.path = pair.second; + match.handle = pair.first; + matches.push_back(match); + + // TODO: We can probably break after the first match in many situations. + // For instance, if we prove this can’t be a directory deletion! + if (matches.size() == 2) break; } - if (!found) continue; + if (matches.size() == 0) return; std::string dirPath(PathWithoutFileName(event.path)); std::string filePath(FileNameFromPath(event.path)); - if (event.flags & ( - kFSEventStreamEventFlagItemCreated | - kFSEventStreamEventFlagItemRemoved | - kFSEventStreamEventFlagItemRenamed - )) { - if (dirPath != path) { - dirsChanged.insert(dirPath); + size_t msize = matches.size(); + + for (size_t i = 0; i < msize; i++) { + std::string path(matches[i].path); + efsw::WatchID handle(matches[i].handle); + + if (event.flags & ( + kFSEventStreamEventFlagItemCreated | + kFSEventStreamEventFlagItemRemoved | + kFSEventStreamEventFlagItemRenamed + )) { + if (dirPath != path) { + dirsChanged.insert(dirPath); + } } - } - if (event.flags & kFSEventStreamEventFlagItemRenamed) { - if ( - (i + 1 < esize) && - (events[i + 1].flags & kFSEventStreamEventFlagItemRenamed) && - (events[i + 1].inode == event.inode) - ) { - FSEvent& nEvent = events[i + 1]; - std::string newDir(PathWithoutFileName(nEvent.path)); - std::string newFilepath(FileNameFromPath(nEvent.path)); - - if (event.path != nEvent.path) { - if (dirPath == newDir) { - if ( - !PathExists(event.path) || - 0 == strcasecmp(event.path.c_str(), nEvent.path.c_str()) - ) { - // Move from one path to the other. - sendFileAction(handle, dirPath, newFilepath, efsw::Actions::Moved, filePath); + // `efsw`‘s’ comment here suggests that you can’t reliably infer order + // from these events — so if the same file is marked as added and changed + // and deleted in consecutive events, you don't know if it was deleted/ + // added/modified, modified/deleted/added, etc. + // + // This is the equivalent logic from `WatcherFSEvents.cpp` because I + // don’t trust myself to touch it at all. + if (event.flags & kFSEventStreamEventFlagItemRenamed) { + // Does the next event also refer to this same file, and is that event + // also a rename? + if ( + (i + 1 < esize) && + (events[i + 1].flags & kFSEventStreamEventFlagItemRenamed) && + (events[i + 1].inode == event.inode) + ) { + // If so, compare this event and the next one to figure out which one + // refers to a current file on disk. + FSEvent& nEvent = events[i + 1]; + std::string newDir(PathWithoutFileName(nEvent.path)); + std::string newFilepath(FileNameFromPath(nEvent.path)); + + if (event.path != nEvent.path) { + if (dirPath == newDir) { + // This is a move within the same directory. + if ( + !PathExists(event.path) || + 0 == strcasecmp(event.path.c_str(), nEvent.path.c_str()) + ) { + // Move from one path to the other. + sendFileAction(handle, dirPath, newFilepath, efsw::Actions::Moved, filePath); + } else { + // Move in the opposite direction. + sendFileAction(handle, dirPath, filePath, efsw::Actions::Moved, newFilepath); + } } else { - // Move in the opposite direction. - sendFileAction(handle, dirPath, filePath, efsw::Actions::Moved, newFilepath); + // This is a move from one directory to another, so we'll treat + // it as one deletion and one creation. + sendFileAction(handle, dirPath, filePath, efsw::Actions::Delete); + sendFileAction(handle, newDir, newFilepath, efsw::Actions::Add); + + if (nEvent.flags & shorthandFSEventsModified) { + sendFileAction(handle, dirPath, filePath, efsw::Actions::Modified); + } } } else { - sendFileAction(handle, dirPath, filePath, efsw::Actions::Delete); - sendFileAction(handle, newDir, newFilepath, efsw::Actions::Add); - - if (nEvent.flags & shorthandFSEventsModified) { - sendFileAction(handle, dirPath, filePath, efsw::Actions::Modified); - } + // The file paths are the same, so we'll let another function + // untangle it. + handleAddModDel(handle, nEvent.flags, nEvent.path, dirPath, filePath); } - } else { - handleAddModDel(handle, nEvent.flags, nEvent.path, dirPath, filePath); - } - if (nEvent.flags & ( - kFSEventStreamEventFlagItemCreated | - kFSEventStreamEventFlagItemRemoved | - kFSEventStreamEventFlagItemRenamed - )) { - if (newDir != path) { - dirsChanged.insert(newDir); + if (nEvent.flags & ( + kFSEventStreamEventFlagItemCreated | + kFSEventStreamEventFlagItemRemoved | + kFSEventStreamEventFlagItemRenamed + )) { + if (newDir != path) { + dirsChanged.insert(newDir); + } } - } - // Skip the renamed file - i++; - } else if (PathExists(event.path)) { - sendFileAction(handle, dirPath, filePath, efsw::Actions::Add); + // Skip the renamed file. + i++; + } else if (PathExists(event.path)) { + // Treat remaining renames as creations when we know the path still + // exists… + sendFileAction(handle, dirPath, filePath, efsw::Actions::Add); - if (event.flags && shorthandFSEventsModified) { - sendFileAction(handle, dirPath, filePath, efsw::Actions::Modified); + if (event.flags & shorthandFSEventsModified) { + sendFileAction(handle, dirPath, filePath, efsw::Actions::Modified); + } + } else { + // …and as deletions when we know the path doesn’t still exist. + sendFileAction(handle, dirPath, filePath, efsw::Actions::Delete); } } else { - sendFileAction(handle, dirPath, filePath, efsw::Actions::Delete); + // Ordinary business — new files, changed, files, deleted files. + handleAddModDel(handle, event.flags, event.path, dirPath, filePath); } - } else { - handleAddModDel(handle, event.flags, event.path, dirPath, filePath); } } } @@ -399,6 +463,8 @@ void FSEventsFileWatcher::handleAddModDel( std::string& filePath ) { if (flags & kFSEventStreamEventFlagItemCreated) { + // This claims to be a file creation; make sure it exists on disk before + // triggering an event. if (PathExists(path)) { sendFileAction(handle, dirPath, filePath, efsw::Actions::Add); } @@ -409,12 +475,15 @@ void FSEventsFileWatcher::handleAddModDel( } if (flags & kFSEventStreamEventFlagItemRemoved) { + // This claims to be a file deletion; make sure it doesn't exist on disk + // before triggering an event. if (!PathExists(path)) { sendFileAction(handle, dirPath, filePath, efsw::Actions::Delete); } } } +// Private: clean up a handle from both unordered maps. void FSEventsFileWatcher::removeHandle(efsw::WatchID handle) { auto itp = handlesToPaths.find(handle); if (itp != handlesToPaths.end()) { @@ -427,10 +496,8 @@ void FSEventsFileWatcher::removeHandle(efsw::WatchID handle) { } void FSEventsFileWatcher::process() { -#ifdef DEBUG - std::cout << "FSEventsFileWatcher::process?" << std::endl; -#endif - + // We are very careful in this function to ensure that `FSEventsFileWatcher` + // doesn’t finalize while this is happening. if (!isValid || pendingDestruction) return; { std::unique_lock lock(processingMutex); @@ -449,25 +516,26 @@ void FSEventsFileWatcher::process() { // Process the copied directories for (const auto& dir : dirsCopy) { if (pendingDestruction) return; -#ifdef DEBUG - std::cout << "Changed:" << dir << std::endl; -#endif efsw::WatchID handle; std::string path; bool found = false; for (const auto& pair: handlesToPaths) { - // std::string normalizedPath = NormalizePath(pair.second); if (!PathStartsWith(dir, pair.second)) continue; - if (dir.find_last_of(PATH_SEPARATOR) != pair.second.size()) { + + if ( + !PathsAreEqual(dir, pair.second) && dir.find_last_of(PATH_SEPARATOR) != pair.second.size() - 1 + ) { continue; } + found = true; path = pair.second; handle = pair.first; break; } + if (!found) continue; sendFileAction( @@ -483,6 +551,8 @@ void FSEventsFileWatcher::process() { dirsChanged.clear(); } +// Start a new FSEvent stream and promote it to the “active” stream after it +// starts. bool FSEventsFileWatcher::startNewStream() { // Build a list of all current watched paths. We'll eventually pass this to // `FSEventStreamCreate`. diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index 9b33226..8f293bd 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -114,171 +114,6 @@ describe('PathWatcher', () => { }); } - xdescribe('when a watcher is added underneath an existing watched path', () => { - let subDirFile, subDir; - - function cleanup() { - if (subDirFile && fs.existsSync(subDirFile)) { - fs.rmSync(subDirFile); - } - if (subDir && fs.existsSync(subDir)) { - fs.rmSync(path.dirname(subDir), { recursive: true }); - } - } - - beforeEach(() => cleanup()); - afterEach(() => cleanup()); - - it('reuses the existing native watcher', async () => { - let rootCallback = jasmine.createSpy('rootCallback') - let subDirCallback = jasmine.createSpy('subDirCallback') - let handle = PathWatcher.watch(tempFile, rootCallback); - - expect(PathWatcher.getNativeWatcherCount()).toBe(1); - - subDir = path.join(tempDir, 'foo', 'bar'); - fs.mkdirSync(subDir, { recursive: true }); - - subDirFile = path.join(subDir, 'test.txt'); - - let subHandle = PathWatcher.watch(subDir, subDirCallback); - - let shouldConsolidate = subHandle.registry.options.reuseAncestorWatchers; - expect( - PathWatcher.getNativeWatcherCount() - ).toBe(shouldConsolidate ? 1 : 2); - - fs.writeFileSync(tempFile, 'change'); - await condition(() => rootCallback.calls.count() >= 1); - expect(subDirCallback.calls.count()).toBe(0); - - fs.writeFileSync(subDirFile, 'create'); - // The file might get both 'create' and 'change' here. That's fine with - // us. - await condition(() => subDirCallback.calls.count() >= 1); - - let realTempDir = fs.realpathSync(tempDir); - expect( - PathWatcher.getWatchedPaths() - ).toEqual( - shouldConsolidate ? - [realTempDir] : - [realTempDir, fs.realpathSync(subDir)] - ); - - // Closing the original watcher should not cause the native watcher to - // close, since another one is depending on it. - handle.close(); - subDirCallback.calls.reset(); - - fs.writeFileSync(subDirFile, 'change'); - await condition(() => subDirCallback.calls.count() >= 1); - - subHandle.close(); - expect(PathWatcher.getNativeWatcherCount()).toBe(0); - }); - }); - - xdescribe('when two watchers are added on sibling directories', () => { - let siblingA = path.join(tempDir, 'sibling-a'); - let siblingB = path.join(tempDir, 'sibling-b'); - - beforeEach(() => { - for (let subDir of [siblingA, siblingB]) { - if (!fs.existsSync(subDir)) { - fs.mkdirSync(subDir, { recursive: true }); - } - } - siblingA = fs.realpathSync(siblingA); - siblingB = fs.realpathSync(siblingB); - }); - - afterEach(() => { - for (let subDir of [siblingA, siblingB]) { - if (fs.existsSync(subDir)) { - fs.rmSync(subDir, { recursive: true }); - } - } - }); - - it('should consolidate them into one watcher on the parent (unless options prohibit it)', async () => { - let watchCallback = jasmine.createSpy('watch-callback'); - let watcherA = PathWatcher.watch(siblingA, watchCallback); - await wait(100); - expect(watcherA.native.path).toBe(siblingA); - let watcherB = PathWatcher.watch(siblingB, watchCallback); - await wait(100); - // The watchers will only be consolidated in this scenario if the - // registry is configured to do so. - let shouldConsolidate = watcherB.registry.options.mergeWatchersWithCommonAncestors; - expect( - watcherB.native.path - ).toBe(shouldConsolidate ? path.dirname(siblingB) : siblingB); - expect(PathWatcher.getNativeWatcherCount()).toBe(shouldConsolidate ? 1 : 2); - }); - }); - - xdescribe('when two watchers are added on cousin directories', () => { - let cousinA = path.join(tempDir, 'placeholder-a', 'cousin-a'); - let cousinB = path.join(tempDir, 'placeholder-b', 'cousin-b'); - - beforeEach(() => { - for (let subDir of [cousinA, cousinB]) { - if (!fs.existsSync(subDir)) { - fs.mkdirSync(subDir, { recursive: true }); - } - } - cousinA = fs.realpathSync(cousinA); - cousinB = fs.realpathSync(cousinB); - }); - - afterEach(() => { - for (let subDir of [cousinA, cousinB]) { - if (fs.existsSync(subDir)) { - fs.rmSync(path.dirname(subDir), { recursive: true }); - } - } - }); - - it('should consolidate them into one watcher on the grandparent (unless options prohibit it)', async () => { - let watchCallbackA = jasmine.createSpy('watch-callback-a'); - let watchCallbackB = jasmine.createSpy('watch-callback-b'); - let watcherA = PathWatcher.watch(cousinA, watchCallbackA); - await wait(100); - expect(watcherA.native.path).toBe(cousinA); - let watcherB = PathWatcher.watch(cousinB, watchCallbackB); - await wait(100); - - // The watchers will only be consolidated in this scenario if the - // registry is configured to do so. - let shouldConsolidate = watcherB.registry.options.mergeWatchersWithCommonAncestors; - shouldConsolidate &&= watcherB.registry.options.maxCommonAncestorLevel >= 2; - - expect( - watcherB.native.path - ).toBe(shouldConsolidate ? fs.realpathSync(tempDir) : cousinB); - - expect(PathWatcher.getNativeWatcherCount()).toBe(shouldConsolidate ? 1 : 2); - - fs.writeFileSync(path.join(cousinA, 'file'), 'test'); - await condition(() => watchCallbackA.calls.count() > 0); - expect(watchCallbackB.calls.count()).toBe(0); - watchCallbackA.calls.reset(); - - fs.writeFileSync(path.join(cousinB, 'file'), 'test'); - await condition(() => watchCallbackB.calls.count() > 0); - expect(watchCallbackA.calls.count()).toBe(0); - - if (!shouldConsolidate) return; - - // When we close `watcherB`, that's our opportunity to move the watcher closer to `watcherA`. - watcherB.close(); - await wait(100); - - expect(watcherA.native.path).toBe(cousinA); - }); - }); - describe('when a file under a watched directory is deleted', () => { it('fires the callback with the change event and empty path', async () => { let fileUnderDir = path.join(tempDir, 'file'); @@ -319,6 +154,24 @@ describe('PathWatcher', () => { }); }); + describe('when a directory child of a watched directory is deleted', () => { + it('fires two events IF the child had its own watcher', async () => { + let subDir = path.join(tempDir, 'subdir'); + if (!fs.existsSync(subDir)) { + fs.mkdirSync(subDir); + } + let outerSpy = jasmine.createSpy('outerSpy'); + let innerSpy = jasmine.createSpy('innerSpy'); + PathWatcher.watch(tempDir, outerSpy); + PathWatcher.watch(subDir, innerSpy); + await wait(20); + fs.rmSync(subDir, { recursive: true }); + await condition(() => outerSpy.calls.count() > 0); + await wait(200); + expect(innerSpy).toHaveBeenCalled(); + }) + }) + describe('when a file under a watched directory is moved', () => { it('fires the callback with the change event and empty path', async () => { diff --git a/src/main.js b/src/main.js index 4d1cda6..ebcb3da 100644 --- a/src/main.js +++ b/src/main.js @@ -1,19 +1,13 @@ let binding; -// console.log('ENV:', process.NODE_ENV); -// if (process.NODE_ENV === 'DEV') { - try { - binding = require('../build/Debug/pathwatcher.node'); - } catch (err) { - binding = require('../build/Release/pathwatcher.node'); - } -// } else { -// binding = require('../build/Release/pathwatcher.node'); -// } +try { + binding = require('../build/Debug/pathwatcher.node'); +} catch (err) { + binding = require('../build/Release/pathwatcher.node'); +} const fs = require('fs'); const path = require('path'); const { Emitter, Disposable, CompositeDisposable } = require('event-kit'); -// const { NativeWatcherRegistry } = require('./native-watcher-registry'); let initialized = false; @@ -71,6 +65,10 @@ class NativeWatcher { // Given a path, returns whatever existing active `NativeWatcher` is already // watching that path, or creates one if it doesn’t yet exist. + // + // It is important that we don’t ask the bindings to create a new watcher for + // a native path that already exists, so this helps us prevent that from + // happening. static findOrCreate (normalizedPath, options) { for (let instance of this.INSTANCES.values()) { if (instance.normalizedPath === normalizedPath) { @@ -80,7 +78,9 @@ class NativeWatcher { return new NativeWatcher(normalizedPath, options); } - // Returns the number of active `NativeWatcher` instances. + // Returns the number of active `NativeWatcher` instances. Depending on + // platform, a higher number may or may not be more demanding of the + // operating system. static get instanceCount() { return this.INSTANCES.size; } @@ -407,11 +407,19 @@ class PathWatcher { return; } + switch (newEvent.action) { case 'rename': + // This event needs no alteration… as long as it relates to the file + // we care about. + if (!eventPathIsEqual && !eventOldPathIsEqual) return; + break; + case 'change': case 'delete': case 'create': - // These events need no alteration. + // These events need no alteration… as long as they relate to the file + // we care about. + if (!eventPathIsEqual) return; break; case 'child-create': if (!this.isWatchingParent) { From 155cc44337b89bfa3cb2cee8c547ebbeaffe4166 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 31 Oct 2024 00:06:43 -0700 Subject: [PATCH 151/168] Too edgy and avant-garde for Windows/Linux --- spec/pathwatcher-spec.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index 8f293bd..ffeba71 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -154,7 +154,8 @@ describe('PathWatcher', () => { }); }); - describe('when a directory child of a watched directory is deleted', () => { + // New spec. Passes on macOS, fails on other platforms. Investigate! + xdescribe('when a directory child of a watched directory is deleted', () => { it('fires two events IF the child had its own watcher', async () => { let subDir = path.join(tempDir, 'subdir'); if (!fs.existsSync(subDir)) { From 84ba91fc194143d728d0e2c5295d47ef0fd42691 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 31 Oct 2024 13:00:54 -0700 Subject: [PATCH 152/168] Attempt to get new spec passing on non-macOS platforms --- lib/core.cc | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/core.h | 3 +++ 2 files changed, 63 insertions(+) diff --git a/lib/core.cc b/lib/core.cc index 23d3756..afbdaa7 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -67,6 +67,11 @@ static std::string NormalizePath(std::string path) { return path + PATH_SEPARATOR; } +static void StripTrailingSlashFromPath(std::string& path) { + if (path.empty() || (path.back() != '/')) return; + path.pop_back(); +} + static bool PathsAreEqual(std::string pathA, std::string pathB) { return NormalizePath(pathA) == NormalizePath(pathB); } @@ -153,6 +158,7 @@ void PathWatcherListener::Stop(FileWatcher* fileWatcher) { void PathWatcherListener::AddPath(PathTimestampPair pair, efsw::WatchID handle) { std::lock_guard lock(pathsMutex); paths[handle] = pair; + pathsToHandles[pair.path] = handle; } // Remove metadata for a given watch ID. @@ -164,7 +170,23 @@ void PathWatcherListener::RemovePath(efsw::WatchID handle) { #endif if (it == paths.end()) return; + auto path = it->second.path; paths.erase(it); + auto itp = pathsToHandles.find(path); + if (itp == pathsToHandles.end()) return; + pathsToHandles.erase(itp); +} + +bool PathWatcherListener::HasPath(std::string path) { + std::lock_guard lock(pathsMutex); + auto it = pathsToHandles.find(path); + return it != pathsToHandles.end(); +} + +efsw::WatchID PathWatcherListener::GetHandleForPath(std::string path) { + std::lock_guard lock(pathsMutex); + auto it = pathsToHandles.find(path); + return it->second; } bool PathWatcherListener::IsEmpty() { @@ -256,6 +278,42 @@ void PathWatcherListener::handleFileAction( } #endif + // One special case we need to handle on all platforms: + // + // * Watcher exists on directory `/foo/bar`. + // * Watcher exists on directory `/foo/bar/baz`. + // * Directory `/foo/bar/baz` is deleted. + // + // In this instance, both watchers should be notified. + // + // On macOS, we handle this in the custom watcher. On other platforms, we’ll + // handle it here. + +#ifndef _APPLE_ + if (action == efsw::Action::Delete) { +#ifdef DEBUG + std::cout << "This might be a directory deletion: [" << newPathStr << "] and we are in path responder: [" << realPath << "]" << std::endl; +#endif + } + + if (HasPath(newPathStr) && action == efsw::Action::Delete) { + +#ifdef DEBUG + std::cout << "Detected watched directory deletion inside watched parent!" << std::endl; +#endif + efsw::WatchID handle = GetHandleForPath(newPathStr); + if (watchId != handle) { + handleFileAction( + handle, + dir, + filename, + action, + "" + ); + } + } + +#endif std::vector oldPath; if (!oldFilename.empty()) { std::string oldPathStr = dir + oldFilename; @@ -336,6 +394,8 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { Napi::String path = info[0].ToString(); std::string cppPath(path); + StripTrailingSlashFromPath(cppPath); + #ifdef DEBUG std::cout << "PathWatcher::Watch path: [" << cppPath << "]" << std::endl; #endif diff --git a/lib/core.h b/lib/core.h index 0cd2fd7..36134da 100644 --- a/lib/core.h +++ b/lib/core.h @@ -111,6 +111,8 @@ class PathWatcherListener: public efsw::FileWatchListener { void AddPath(PathTimestampPair pair, efsw::WatchID handle); void RemovePath(efsw::WatchID handle); + bool HasPath(std::string path); + efsw::WatchID GetHandleForPath(std::string path); bool IsEmpty(); void Stop(); void Stop(FileWatcher* fileWatcher); @@ -121,6 +123,7 @@ class PathWatcherListener: public efsw::FileWatchListener { std::mutex pathsMutex; Napi::ThreadSafeFunction tsfn; std::unordered_map paths; + std::unordered_map pathsToHandles; }; From c6f17e76b04c130c53b9b3227c3ca85174bb83df Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 31 Oct 2024 13:21:36 -0700 Subject: [PATCH 153/168] Untangle mutexes (mutices?) --- lib/core.cc | 68 +++++++++++++++++++++------- lib/core.h | 11 +++++ lib/platform/FSEventsFileWatcher.cpp | 9 ++++ 3 files changed, 71 insertions(+), 17 deletions(-) diff --git a/lib/core.cc b/lib/core.cc index afbdaa7..fa6eabc 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -163,28 +163,35 @@ void PathWatcherListener::AddPath(PathTimestampPair pair, efsw::WatchID handle) // Remove metadata for a given watch ID. void PathWatcherListener::RemovePath(efsw::WatchID handle) { - std::lock_guard lock(pathsMutex); - auto it = paths.find(handle); + std::string path; + { + std::lock_guard lock(pathsMutex); + auto it = paths.find(handle); #ifdef DEBUG std::cout << "Unwatching handle: [" << handle << "] path: [" << it->second.path << "]" << std::endl; #endif - if (it == paths.end()) return; - auto path = it->second.path; - paths.erase(it); - auto itp = pathsToHandles.find(path); - if (itp == pathsToHandles.end()) return; - pathsToHandles.erase(itp); + if (it == paths.end()) return; + path = it->second.path; + paths.erase(it); + } + + { + std::lock_guard lock(pathsToHandlesMutex); + auto itp = pathsToHandles.find(path); + if (itp == pathsToHandles.end()) return; + pathsToHandles.erase(itp); + } } bool PathWatcherListener::HasPath(std::string path) { - std::lock_guard lock(pathsMutex); + std::lock_guard lock(pathsToHandlesMutex); auto it = pathsToHandles.find(path); return it != pathsToHandles.end(); } efsw::WatchID PathWatcherListener::GetHandleForPath(std::string path) { - std::lock_guard lock(pathsMutex); + std::lock_guard lock(pathsToHandlesMutex); auto it = pathsToHandles.find(path); return it->second; } @@ -200,15 +207,38 @@ void PathWatcherListener::handleFileAction( const std::string& filename, efsw::Action action, std::string oldFilename +) { + // When this is invoked directly by `efsw`, we always want to acquire the + // shutdown mutex. + return handleFileAction(watchId, dir, filename, action, oldFilename, true); +} + +void PathWatcherListener::handleFileAction( + efsw::WatchID watchId, + const std::string& dir, + const std::string& filename, + efsw::Action action, + std::string oldFilename, + bool shouldLock ) { #ifdef DEBUG std::cout << "PathWatcherListener::handleFileAction dir: " << dir << " filename: " << filename << " action: " << EventType(action, true) << std::endl; #endif - // Don't try to proceed if we've already started the shutdown process. - if (isShuttingDown) return; - std::lock_guard lock(shutdownMutex); + // Don't try to proceed if we've already started the shutdown process… if (isShuttingDown) return; + // …but if we haven't, make sure that shutdown doesn’t happen until we’re + // done. + // + // The `shouldLock` parameter is here because, in rare cases, we might need + // to call `handleFileAction` recursively. Inner calls of `handleFileAction` + // should not try to acquire the mutex because it's already been acquired by + // the outer call. + if (shouldLock) { + std::lock_guard lock(shutdownMutex); + if (isShuttingDown) return; + } + // Extract the expected watcher path and (on macOS) the start time of the // watcher. PathTimestampPair pair; @@ -278,6 +308,7 @@ void PathWatcherListener::handleFileAction( } #endif +#ifndef _APPLE_ // One special case we need to handle on all platforms: // // * Watcher exists on directory `/foo/bar`. @@ -288,11 +319,9 @@ void PathWatcherListener::handleFileAction( // // On macOS, we handle this in the custom watcher. On other platforms, we’ll // handle it here. - -#ifndef _APPLE_ if (action == efsw::Action::Delete) { #ifdef DEBUG - std::cout << "This might be a directory deletion: [" << newPathStr << "] and we are in path responder: [" << realPath << "]" << std::endl; + std::cout << "This might be a directory deletion: [" << newPathStr << "] and we are in path responder: [" << realPath << "] with handle: " << watchId << std::endl; #endif } @@ -302,13 +331,18 @@ void PathWatcherListener::handleFileAction( std::cout << "Detected watched directory deletion inside watched parent!" << std::endl; #endif efsw::WatchID handle = GetHandleForPath(newPathStr); +#ifdef DEBUG + std::cout << "Other handle: " << handle << std::endl; +#endif + if (watchId != handle) { handleFileAction( handle, dir, filename, action, - "" + "", + false ); } } diff --git a/lib/core.h b/lib/core.h index 36134da..c6d3760 100644 --- a/lib/core.h +++ b/lib/core.h @@ -101,6 +101,7 @@ class PathWatcherListener: public efsw::FileWatchListener { Napi::Env env, Napi::ThreadSafeFunction tsfn ); + void handleFileAction( efsw::WatchID watchId, const std::string& dir, @@ -109,6 +110,15 @@ class PathWatcherListener: public efsw::FileWatchListener { std::string oldFilename ) override; + void handleFileAction( + efsw::WatchID watchId, + const std::string& dir, + const std::string& filename, + efsw::Action action, + std::string oldFilename, + bool shouldLock + ); + void AddPath(PathTimestampPair pair, efsw::WatchID handle); void RemovePath(efsw::WatchID handle); bool HasPath(std::string path); @@ -121,6 +131,7 @@ class PathWatcherListener: public efsw::FileWatchListener { std::atomic isShuttingDown{false}; std::mutex shutdownMutex; std::mutex pathsMutex; + std::mutex pathsToHandlesMutex; Napi::ThreadSafeFunction tsfn; std::unordered_map paths; std::unordered_map pathsToHandles; diff --git a/lib/platform/FSEventsFileWatcher.cpp b/lib/platform/FSEventsFileWatcher.cpp index 8478bd7..03518cc 100644 --- a/lib/platform/FSEventsFileWatcher.cpp +++ b/lib/platform/FSEventsFileWatcher.cpp @@ -321,12 +321,21 @@ void FSEventsFileWatcher::handleActions(std::vector& events) { // For that reason, we keep an array of matches, but stop as soon as we // find two matches, since that's the practical maximum. // + // TODO: On other platforms we handle this in the listener’s + // `handleFileAction` method. We don’t handle it there on macOS because the + // behavior of this function means we don’t know which of the two watchers + // in this scenario will be matched first. We could instead consistently + // return one or the other here, but since we’d have to loop through all + // the options anyway, it wouldn’t save us much effort. + // // NOTE: In extreme situations with lots of paths, this could be a choke // point, since it’s a nested loop. The fact that we’re just doing string // comparisons should keep it fast, but we could optimize further by // pre-normalizing the paths. We could also move to a better data // structure, but better to invest that time in making this library // obsolete in the first place. + // + // TODO: We could probably do this without looping somehow, right? for (const auto& pair: handlesToPaths) { std::string normalizedPath = NormalizePath(pair.second); From 3f0de408f38d51534edd49311256d24a378aa718 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 31 Oct 2024 14:34:02 -0700 Subject: [PATCH 154/168] Fix the directory-deletion edge case on all platforms --- lib/core.cc | 99 +++----- lib/core.h | 9 - lib/platform/FSEventsFileWatcher.cpp | 346 +++++++++++++++------------ lib/platform/FSEventsFileWatcher.hpp | 4 +- spec/pathwatcher-spec.js | 58 ++++- 5 files changed, 281 insertions(+), 235 deletions(-) diff --git a/lib/core.cc b/lib/core.cc index fa6eabc..c52152a 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -207,19 +207,6 @@ void PathWatcherListener::handleFileAction( const std::string& filename, efsw::Action action, std::string oldFilename -) { - // When this is invoked directly by `efsw`, we always want to acquire the - // shutdown mutex. - return handleFileAction(watchId, dir, filename, action, oldFilename, true); -} - -void PathWatcherListener::handleFileAction( - efsw::WatchID watchId, - const std::string& dir, - const std::string& filename, - efsw::Action action, - std::string oldFilename, - bool shouldLock ) { #ifdef DEBUG std::cout << "PathWatcherListener::handleFileAction dir: " << dir << " filename: " << filename << " action: " << EventType(action, true) << std::endl; @@ -229,15 +216,8 @@ void PathWatcherListener::handleFileAction( // …but if we haven't, make sure that shutdown doesn’t happen until we’re // done. - // - // The `shouldLock` parameter is here because, in rare cases, we might need - // to call `handleFileAction` recursively. Inner calls of `handleFileAction` - // should not try to acquire the mutex because it's already been acquired by - // the outer call. - if (shouldLock) { - std::lock_guard lock(shutdownMutex); - if (isShuttingDown) return; - } + std::lock_guard lock(shutdownMutex); + if (isShuttingDown) return; // Extract the expected watcher path and (on macOS) the start time of the // watcher. @@ -308,46 +288,6 @@ void PathWatcherListener::handleFileAction( } #endif -#ifndef _APPLE_ - // One special case we need to handle on all platforms: - // - // * Watcher exists on directory `/foo/bar`. - // * Watcher exists on directory `/foo/bar/baz`. - // * Directory `/foo/bar/baz` is deleted. - // - // In this instance, both watchers should be notified. - // - // On macOS, we handle this in the custom watcher. On other platforms, we’ll - // handle it here. - if (action == efsw::Action::Delete) { -#ifdef DEBUG - std::cout << "This might be a directory deletion: [" << newPathStr << "] and we are in path responder: [" << realPath << "] with handle: " << watchId << std::endl; -#endif - } - - if (HasPath(newPathStr) && action == efsw::Action::Delete) { - -#ifdef DEBUG - std::cout << "Detected watched directory deletion inside watched parent!" << std::endl; -#endif - efsw::WatchID handle = GetHandleForPath(newPathStr); -#ifdef DEBUG - std::cout << "Other handle: " << handle << std::endl; -#endif - - if (watchId != handle) { - handleFileAction( - handle, - dir, - filename, - action, - "", - false - ); - } - } - -#endif std::vector oldPath; if (!oldFilename.empty()) { std::string oldPathStr = dir + oldFilename; @@ -362,6 +302,32 @@ void PathWatcherListener::handleFileAction( return; } + // One (rare) special case we need to handle on all platforms: + // + // * Watcher exists on directory `/foo/bar`. + // * Watcher exists on directory `/foo/bar/baz`. + // * Directory `/foo/bar/baz` is deleted. + // + // In this instance, both watchers should be notified, but `efsw` will signal + // only the `/foo/bar` watcher. (If only `/foo/bar/baz` were present, the + // `/foo/bar/baz` watcher would be signalled instead.) + // + // Our custom macOS implementation replicates this incorrect behavior so that + // we can handle this case uniformly in this one place. + bool hasSecondMatch = false; + efsw::WatchID secondHandle; + + // If we need to account for this scenario, then the full path will have its + // own watcher. Since we only watch directories, this proves that the full + // path is a directory. + if (HasPath(newPathStr) && action == efsw::Action::Delete) { + efsw::WatchID handle = GetHandleForPath(newPathStr); + if (watchId != handle) { + hasSecondMatch = true; + secondHandle = handle; + } + } + PathWatcherEvent* event = new PathWatcherEvent(action, watchId, newPath, oldPath, realPath); // TODO: Instead of calling `BlockingCall` once per event, throttle them by @@ -369,6 +335,15 @@ void PathWatcherListener::handleFileAction( // them in batches more efficiently — and for the wrapper JavaScript code to // do some elimination of redundant events. status = tsfn.BlockingCall(event, ProcessEvent); + + if (hasSecondMatch && status == napi_ok) { + // In the rare case of the scenario described above, we have a second + // callback invocation to make with a second event. Luckily, the only thing + // that changes about the event is the handle! + PathWatcherEvent* secondEvent = new PathWatcherEvent(action, secondHandle, newPath, oldPath, realPath); + tsfn.BlockingCall(secondEvent, ProcessEvent); + } + tsfn.Release(); if (status != napi_ok) { // TODO: Not sure how this could fail, or how we should present it to the diff --git a/lib/core.h b/lib/core.h index c6d3760..db6e06f 100644 --- a/lib/core.h +++ b/lib/core.h @@ -110,15 +110,6 @@ class PathWatcherListener: public efsw::FileWatchListener { std::string oldFilename ) override; - void handleFileAction( - efsw::WatchID watchId, - const std::string& dir, - const std::string& filename, - efsw::Action action, - std::string oldFilename, - bool shouldLock - ); - void AddPath(PathTimestampPair pair, efsw::WatchID handle); void RemovePath(efsw::WatchID handle); bool HasPath(std::string path); diff --git a/lib/platform/FSEventsFileWatcher.cpp b/lib/platform/FSEventsFileWatcher.cpp index 03518cc..6790770 100644 --- a/lib/platform/FSEventsFileWatcher.cpp +++ b/lib/platform/FSEventsFileWatcher.cpp @@ -74,36 +74,52 @@ std::string PrecomposeFileName(const std::string& name) { return result; } +// Returns whether `path` currently exists on disk. bool PathExists(const std::string& path) { struct stat buffer; return (stat(path.c_str(), &buffer) == 0); } +// Given two paths, determine whether the first descends from (or is equal to) +// the second. bool PathStartsWith(const std::string& str, const std::string& prefix) { if (PathsAreEqual(str, prefix)) return true; if (prefix.length() > str.length()) { return false; } - return str.compare(0, prefix.length(), prefix) == 0; + auto normalizedPrefix = NormalizePath(prefix); + // We ensure `prefix` ends with a path separator so we don't mistakenly think + // that `/foo/barbaz` descends from `/foo/bar`. + return str.compare(0, normalizedPrefix.length(), normalizedPrefix) == 0; } +// Strips a trailing slash from the path (in place). void DirRemoveSlashAtEnd (std::string& dir) { if (dir.size() >= 1 && dir[dir.size() - 1] == PATH_SEPARATOR) { dir.erase( dir.size() - 1 ); } } -std::string PathWithoutFileName( std::string filepath ) { +// Given `/foo/bar/baz.txt`, returns `/foo/bar` (with or without a trailing +// slash as desired). +std::string PathWithoutFileName(std::string filepath, bool keepTrailingSeparator) { DirRemoveSlashAtEnd(filepath); size_t pos = filepath.find_last_of(PATH_SEPARATOR); if (pos != std::string::npos) { - return filepath.substr(0, pos + 1); + return filepath.substr(0, keepTrailingSeparator ? pos + 1 : pos); } return filepath; } +std::string PathWithoutFileName(std::string filepath) { + // Default behavior of `PathWithoutFileName` is to keep the trailing + // separator. + return PathWithoutFileName(filepath, true); +} + +// Given `/foo/bar/baz.txt`, returns `baz.txt`. std::string FileNameFromPath(std::string filepath) { DirRemoveSlashAtEnd(filepath); @@ -115,6 +131,7 @@ std::string FileNameFromPath(std::string filepath) { return filepath; } +// Borrowed from `efsw`. Don’t ask me to explain it. static std::string convertCFStringToStdString( CFStringRef cfString ) { // Try to get the C string pointer directly const char* cStr = CFStringGetCStringPtr( cfString, kCFStringEncodingUTF8 ); @@ -179,8 +196,12 @@ efsw::WatchID FSEventsFileWatcher::addWatch( std::cout << "FSEventsFileWatcher::addWatch" << directory << std::endl; #endif efsw::WatchID handle = nextHandleID++; - handlesToPaths[handle] = directory; - handlesToListeners[handle] = listener; + { + std::lock_guard lock(mapMutex); + handlesToPaths[handle] = directory; + pathsToHandles[directory] = handle; + handlesToListeners[handle] = listener; + } bool didStart = startNewStream(); @@ -199,9 +220,9 @@ void FSEventsFileWatcher::removeWatch( #ifdef DEBUG std::cout << "FSEventsFileWatcher::removeWatch" << handle << std::endl; #endif - removeHandle(handle); + auto remainingCount = removeHandle(handle); - if (handlesToPaths.size() == 0) { + if (remainingCount == 0) { if (currentEventStream) { FSEventStreamStop(currentEventStream); FSEventStreamInvalidate(currentEventStream); @@ -310,156 +331,153 @@ void FSEventsFileWatcher::handleActions(std::vector& events) { kFSEventStreamEventFlagRootChanged )) continue; - // Since we aren’t doing recursive watchers, we can seemingly get away with - // using the first match we find. In nearly all cases, it’s impossible for - // a filesystem event to pertain to more than one watcher. - // - // But one exception is directory deletion, since that directory _and_ its - // parent directory can both be watched, and that would count as a deletion - // for the first and a change for the second. - // - // For that reason, we keep an array of matches, but stop as soon as we - // find two matches, since that's the practical maximum. - // - // TODO: On other platforms we handle this in the listener’s - // `handleFileAction` method. We don’t handle it there on macOS because the - // behavior of this function means we don’t know which of the two watchers - // in this scenario will be matched first. We could instead consistently - // return one or the other here, but since we’d have to loop through all - // the options anyway, it wouldn’t save us much effort. - // - // NOTE: In extreme situations with lots of paths, this could be a choke - // point, since it’s a nested loop. The fact that we’re just doing string - // comparisons should keep it fast, but we could optimize further by - // pre-normalizing the paths. We could also move to a better data - // structure, but better to invest that time in making this library - // obsolete in the first place. - // - // TODO: We could probably do this without looping somehow, right? - for (const auto& pair: handlesToPaths) { - std::string normalizedPath = NormalizePath(pair.second); - - // Filter out everything that doesn’t equal or descend from this path. - if (!PathStartsWith(event.path, normalizedPath)) continue; + efsw::WatchID handle; + std::string path; - // We let this through if (a) it matches our path exactly, or (b) it - // refers to a child file/directory of ours (rather than a deeper - // descendant.) - if ( - !PathsAreEqual(event.path, normalizedPath) && event.path.find_last_of(PATH_SEPARATOR) != normalizedPath.size() - 1 - ) { - continue; + { + // How do we match up this path change to the watcher that cares about + // it? + // + // Since we do only non-recursive watching, there are a maximum of two + // watchers that can care about something — and 99% of cases will involve + // a single such watcher. This vastly simplifies our implementation + // compared to `efsw`’s — since it has to care about the possibility of + // recursive watchers, one file change can correspond to arbitrarily many + // watchers. + // + // For that reason, we can do a simple map lookup. First we try the + // path’s parent directory; if that’s not successful, we try the full + // path. One of these is (for practical purposes) guaranteed to find a + // watcher. + // + // NOTE: What about the 1% edge case? `efsw` has an incorrect behavior + // here: in the rare case of a watcher existing on both a parent + // directory and a child directory, it will choose only the parent when + // the child is deleted. + // + // This is incorrect, but it's _conveniently_ incorrect! We can fix it + // later in the listener (with identical cross-platform code), and it + // allows us to choose a single winner here in all cases, simplifying the + // implementation further. + std::lock_guard lock(mapMutex); + auto itpth = pathsToHandles.find(PathWithoutFileName(event.path, false)); + if (itpth != pathsToHandles.end()) { + // We have an entry for this paths’s owner directory. We prefer this + // whether the entry itself is a file or a directory (to replicate + // `efsw`’s bug). + path = itpth->first; + handle = itpth->second; + } else { + // Otherwise, we check if the path has a watcher of its own. This only + // applies when the path is itself a directory and only when the + // directory is being deleted (since we don’t let you watch a directory + // before it exists). + // + // If _both_ the parent directory _and_ the child directory are being + // watched in this scenario, we won’t get this far. We'll still + // notify both watchers, but that gets handled in `core.cc`. + itpth = pathsToHandles.find(event.path); + if (itpth != pathsToHandles.end()) { + path = itpth->first; + handle = itpth->second; + } else { + // We couldn't find a handle for this path. This is odd, but it’s + // not a big deal. + continue; + } } - - FileEventMatch match; - match.path = pair.second; - match.handle = pair.first; - matches.push_back(match); - - // TODO: We can probably break after the first match in many situations. - // For instance, if we prove this can’t be a directory deletion! - if (matches.size() == 2) break; } - if (matches.size() == 0) return; - std::string dirPath(PathWithoutFileName(event.path)); std::string filePath(FileNameFromPath(event.path)); - size_t msize = matches.size(); - - for (size_t i = 0; i < msize; i++) { - std::string path(matches[i].path); - efsw::WatchID handle(matches[i].handle); - - if (event.flags & ( - kFSEventStreamEventFlagItemCreated | - kFSEventStreamEventFlagItemRemoved | - kFSEventStreamEventFlagItemRenamed - )) { - if (dirPath != path) { - dirsChanged.insert(dirPath); - } + if (event.flags & ( + kFSEventStreamEventFlagItemCreated | + kFSEventStreamEventFlagItemRemoved | + kFSEventStreamEventFlagItemRenamed + )) { + if (dirPath != path) { + dirsChanged.insert(dirPath); } + } - // `efsw`‘s’ comment here suggests that you can’t reliably infer order - // from these events — so if the same file is marked as added and changed - // and deleted in consecutive events, you don't know if it was deleted/ - // added/modified, modified/deleted/added, etc. - // - // This is the equivalent logic from `WatcherFSEvents.cpp` because I - // don’t trust myself to touch it at all. - if (event.flags & kFSEventStreamEventFlagItemRenamed) { - // Does the next event also refer to this same file, and is that event - // also a rename? - if ( - (i + 1 < esize) && - (events[i + 1].flags & kFSEventStreamEventFlagItemRenamed) && - (events[i + 1].inode == event.inode) - ) { - // If so, compare this event and the next one to figure out which one - // refers to a current file on disk. - FSEvent& nEvent = events[i + 1]; - std::string newDir(PathWithoutFileName(nEvent.path)); - std::string newFilepath(FileNameFromPath(nEvent.path)); - - if (event.path != nEvent.path) { - if (dirPath == newDir) { - // This is a move within the same directory. - if ( - !PathExists(event.path) || - 0 == strcasecmp(event.path.c_str(), nEvent.path.c_str()) - ) { - // Move from one path to the other. - sendFileAction(handle, dirPath, newFilepath, efsw::Actions::Moved, filePath); - } else { - // Move in the opposite direction. - sendFileAction(handle, dirPath, filePath, efsw::Actions::Moved, newFilepath); - } + // `efsw`‘s’ comment here suggests that you can’t reliably infer order + // from these events — so if the same file is marked as added and changed + // and deleted in consecutive events, you don't know if it was deleted/ + // added/modified, modified/deleted/added, etc. + // + // This is the equivalent logic from `WatcherFSEvents.cpp` because I + // don’t trust myself to touch it at all. + if (event.flags & kFSEventStreamEventFlagItemRenamed) { + // Does the next event also refer to this same file, and is that event + // also a rename? + if ( + (i + 1 < esize) && + (events[i + 1].flags & kFSEventStreamEventFlagItemRenamed) && + (events[i + 1].inode == event.inode) + ) { + // If so, compare this event and the next one to figure out which one + // refers to a current file on disk. + FSEvent& nEvent = events[i + 1]; + std::string newDir(PathWithoutFileName(nEvent.path)); + std::string newFilepath(FileNameFromPath(nEvent.path)); + + if (event.path != nEvent.path) { + if (dirPath == newDir) { + // This is a move within the same directory. + if ( + !PathExists(event.path) || + 0 == strcasecmp(event.path.c_str(), nEvent.path.c_str()) + ) { + // Move from one path to the other. + sendFileAction(handle, dirPath, newFilepath, efsw::Actions::Moved, filePath); } else { - // This is a move from one directory to another, so we'll treat - // it as one deletion and one creation. - sendFileAction(handle, dirPath, filePath, efsw::Actions::Delete); - sendFileAction(handle, newDir, newFilepath, efsw::Actions::Add); - - if (nEvent.flags & shorthandFSEventsModified) { - sendFileAction(handle, dirPath, filePath, efsw::Actions::Modified); - } + // Move in the opposite direction. + sendFileAction(handle, dirPath, filePath, efsw::Actions::Moved, newFilepath); } } else { - // The file paths are the same, so we'll let another function - // untangle it. - handleAddModDel(handle, nEvent.flags, nEvent.path, dirPath, filePath); - } + // This is a move from one directory to another, so we'll treat + // it as one deletion and one creation. + sendFileAction(handle, dirPath, filePath, efsw::Actions::Delete); + sendFileAction(handle, newDir, newFilepath, efsw::Actions::Add); - if (nEvent.flags & ( - kFSEventStreamEventFlagItemCreated | - kFSEventStreamEventFlagItemRemoved | - kFSEventStreamEventFlagItemRenamed - )) { - if (newDir != path) { - dirsChanged.insert(newDir); + if (nEvent.flags & shorthandFSEventsModified) { + sendFileAction(handle, dirPath, filePath, efsw::Actions::Modified); } } + } else { + // The file paths are the same, so we'll let another function + // untangle it. + handleAddModDel(handle, nEvent.flags, nEvent.path, dirPath, filePath); + } - // Skip the renamed file. - i++; - } else if (PathExists(event.path)) { - // Treat remaining renames as creations when we know the path still - // exists… - sendFileAction(handle, dirPath, filePath, efsw::Actions::Add); - - if (event.flags & shorthandFSEventsModified) { - sendFileAction(handle, dirPath, filePath, efsw::Actions::Modified); + if (nEvent.flags & ( + kFSEventStreamEventFlagItemCreated | + kFSEventStreamEventFlagItemRemoved | + kFSEventStreamEventFlagItemRenamed + )) { + if (newDir != path) { + dirsChanged.insert(newDir); } - } else { - // …and as deletions when we know the path doesn’t still exist. - sendFileAction(handle, dirPath, filePath, efsw::Actions::Delete); + } + + // Skip the renamed file. + i++; + } else if (PathExists(event.path)) { + // Treat remaining renames as creations when we know the path still + // exists… + sendFileAction(handle, dirPath, filePath, efsw::Actions::Add); + + if (event.flags & shorthandFSEventsModified) { + sendFileAction(handle, dirPath, filePath, efsw::Actions::Modified); } } else { - // Ordinary business — new files, changed, files, deleted files. - handleAddModDel(handle, event.flags, event.path, dirPath, filePath); + // …and as deletions when we know the path doesn’t still exist. + sendFileAction(handle, dirPath, filePath, efsw::Actions::Delete); } + } else { + // Ordinary business — new files, changed, files, deleted files. + handleAddModDel(handle, event.flags, event.path, dirPath, filePath); } } } @@ -493,15 +511,23 @@ void FSEventsFileWatcher::handleAddModDel( } // Private: clean up a handle from both unordered maps. -void FSEventsFileWatcher::removeHandle(efsw::WatchID handle) { +size_t FSEventsFileWatcher::removeHandle(efsw::WatchID handle) { + std::lock_guard lock(mapMutex); + std::string path; auto itp = handlesToPaths.find(handle); if (itp != handlesToPaths.end()) { + path = itp->second; handlesToPaths.erase(itp); } + auto itpth = pathsToHandles.find(path); + if (itpth != pathsToHandles.end()) { + pathsToHandles.erase(itpth); + } auto itl = handlesToListeners.find(handle); if (itl != handlesToListeners.end()) { handlesToListeners.erase(itl); } + return handlesToPaths.size(); } void FSEventsFileWatcher::process() { @@ -530,19 +556,22 @@ void FSEventsFileWatcher::process() { std::string path; bool found = false; - for (const auto& pair: handlesToPaths) { - if (!PathStartsWith(dir, pair.second)) continue; + { + std::lock_guard lock(mapMutex); + for (const auto& pair: handlesToPaths) { + if (!PathStartsWith(dir, pair.second)) continue; - if ( - !PathsAreEqual(dir, pair.second) && dir.find_last_of(PATH_SEPARATOR) != pair.second.size() - 1 - ) { - continue; - } + if ( + !PathsAreEqual(dir, pair.second) && dir.find_last_of(PATH_SEPARATOR) != pair.second.size() - 1 + ) { + continue; + } - found = true; - path = pair.second; - handle = pair.first; - break; + found = true; + path = pair.second; + handle = pair.first; + break; + } } if (!found) continue; @@ -566,14 +595,17 @@ bool FSEventsFileWatcher::startNewStream() { // Build a list of all current watched paths. We'll eventually pass this to // `FSEventStreamCreate`. std::vector cfStrings; - for (const auto& pair : handlesToPaths) { - CFStringRef cfStr = CFStringCreateWithCString( - kCFAllocatorDefault, - pair.second.c_str(), - kCFStringEncodingUTF8 - ); - if (cfStr) { - cfStrings.push_back(cfStr); + std::lock_guard lock(mapMutex); + { + for (const auto& pair : handlesToPaths) { + CFStringRef cfStr = CFStringCreateWithCString( + kCFAllocatorDefault, + pair.second.c_str(), + kCFStringEncodingUTF8 + ); + if (cfStr) { + cfStrings.push_back(cfStr); + } } } diff --git a/lib/platform/FSEventsFileWatcher.hpp b/lib/platform/FSEventsFileWatcher.hpp index 26f3c40..208d00f 100644 --- a/lib/platform/FSEventsFileWatcher.hpp +++ b/lib/platform/FSEventsFileWatcher.hpp @@ -85,13 +85,14 @@ class FSEventsFileWatcher { } }; - void removeHandle(efsw::WatchID handle); + size_t removeHandle(efsw::WatchID handle); bool startNewStream(); long nextHandleID; std::atomic isProcessing{false}; std::atomic pendingDestruction{false}; std::mutex processingMutex; + std::mutex mapMutex; std::condition_variable processingComplete; // The running event stream that subscribes to all the paths we care about. @@ -103,5 +104,6 @@ class FSEventsFileWatcher { std::set dirsChanged; std::unordered_map handlesToPaths; + std::unordered_map pathsToHandles; std::unordered_map handlesToListeners; }; diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index ffeba71..f37b669 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -154,13 +154,41 @@ describe('PathWatcher', () => { }); }); - // New spec. Passes on macOS, fails on other platforms. Investigate! - xdescribe('when a directory child of a watched directory is deleted', () => { - it('fires two events IF the child had its own watcher', async () => { - let subDir = path.join(tempDir, 'subdir'); + describe('when a directory child of a watched directory is deleted', () => { + let subDir; + beforeEach(() => { + subDir = path.join(tempDir, 'subdir'); if (!fs.existsSync(subDir)) { fs.mkdirSync(subDir); } + }); + + afterEach(() => { + if (subDir && fs.existsSync(subDir)) { + fs.rmSync(subDir, { recursive: true }); + } + }); + + it('fires one event when we watch only the inner directory', async () => { + // This test proves that, under normal circumstances, `efsw` correctly + // triggers a watcher on a directory when that very directory is deleted. + let innerSpy = jasmine.createSpy('innerSpy'); + PathWatcher.watch(subDir, innerSpy); + await wait(20); + fs.rmSync(subDir, { recursive: true }); + await condition(() => innerSpy.calls.count() > 0); + expect(innerSpy).toHaveBeenCalled(); + }); + + it('fires two events IF the child had its own watcher', async () => { + // But if the directory’s parent is _also_ being watched, `efsw` _does + // not_ trigger the directory’s own watcher; it will trigger only the + // outer watcher. + // + // This test proves that we handle this case correctly nonetheless. We do + // so in the native code by manually testing for the presence of a + // watcher at the subdirectory’s path and explicitly invoking the + // callback with that handle. let outerSpy = jasmine.createSpy('outerSpy'); let innerSpy = jasmine.createSpy('innerSpy'); PathWatcher.watch(tempDir, outerSpy); @@ -170,8 +198,26 @@ describe('PathWatcher', () => { await condition(() => outerSpy.calls.count() > 0); await wait(200); expect(innerSpy).toHaveBeenCalled(); - }) - }) + }); + + it('fires two events IF the child had its own watcher (same watchers attached in opposite order)', async () => { + // We test this scenario to prove that `efsw` consistently picks the + // outer directory’s watcher regardless of observation order. If that + // weren’t true, then this test would fail, since the native logic for + // detecting this scenario wouldn’t work if it had to manually invoke the + // outer watcher. + let outerSpy = jasmine.createSpy('outerSpy'); + let innerSpy = jasmine.createSpy('innerSpy'); + PathWatcher.watch(subDir, innerSpy); + PathWatcher.watch(tempDir, outerSpy); + await wait(20); + fs.rmSync(subDir, { recursive: true }); + await condition(() => outerSpy.calls.count() > 0); + await wait(200); + expect(innerSpy).toHaveBeenCalled(); + }); + + }); describe('when a file under a watched directory is moved', () => { it('fires the callback with the change event and empty path', async () => { From 15c112b5c903a7710ea40d92f6feff28a1897ad5 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 31 Oct 2024 14:52:30 -0700 Subject: [PATCH 155/168] Change the specs to agree with observed reality --- spec/pathwatcher-spec.js | 58 +++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index f37b669..b946972 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -154,7 +154,7 @@ describe('PathWatcher', () => { }); }); - describe('when a directory child of a watched directory is deleted', () => { + describe('watching a directory', () => { let subDir; beforeEach(() => { subDir = path.join(tempDir, 'subdir'); @@ -168,16 +168,60 @@ describe('PathWatcher', () => { fs.rmSync(subDir, { recursive: true }); } }); - - it('fires one event when we watch only the inner directory', async () => { - // This test proves that, under normal circumstances, `efsw` correctly - // triggers a watcher on a directory when that very directory is deleted. + it('fails to detect that same directory’s deletion', async () => { + // This test proves that `efsw` does not detect when a directory is + // deleted if you are watching that exact path. Our custom macOS + // implementation should behave the same way for predictability. let innerSpy = jasmine.createSpy('innerSpy'); PathWatcher.watch(subDir, innerSpy); await wait(20); fs.rmSync(subDir, { recursive: true }); - await condition(() => innerSpy.calls.count() > 0); - expect(innerSpy).toHaveBeenCalled(); + await wait(200); + expect(innerSpy).not.toHaveBeenCalled(); + }); + }); + + // NOTE: These specs are aspirational, and were based on the premise that + // + // (a) `efsw` properly detects a directory’s own deletion when you’re + // watching the directory for changes; (b) `efsw` improperly signals only the + // parent watcher when both parent and child directories are being watched + // and the child is deleted. + // + // In fact, (a) is false. I observed the behavior from (b) and considered it + // a bug, but if `efsw` is designed not to detect (a), then (b) is perfectly + // logical and consistent. + // + // I wrote an elaborate workaround for the behavior I observed on macOS (with + // my custom FSEvent implementation) without properly verifying that Linux + // and Windows behave the same way. + // + // It makes intuitive sense that `PathWatcher.watch` should invoke its + // callback when it’s watching a directory and the directory itself is + // deleted, but that doesn’t currently happen in `efsw`, and `pathwatcher` + // never seems to have expected that behavior itself. + // + // To support this properly, `efsw` would have to support the detection of a + // directory deletion when the directory itself is being watched. As it is, + // it detects this only when the directory’s parent is being watched. + // + // This means that it would be easy to support something like + // `Directory::onDidDelete` if we wanted to — we’d just set up a second + // watcher on the directory’s parent. + // + xdescribe('when a directory child of a watched directory is deleted', () => { + let subDir; + beforeEach(() => { + subDir = path.join(tempDir, 'subdir'); + if (!fs.existsSync(subDir)) { + fs.mkdirSync(subDir); + } + }); + + afterEach(() => { + if (subDir && fs.existsSync(subDir)) { + fs.rmSync(subDir, { recursive: true }); + } }); it('fires two events IF the child had its own watcher', async () => { From b7104807e4825d8494cec443547bdace8f51b079 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 31 Oct 2024 15:10:00 -0700 Subject: [PATCH 156/168] Deliver more consistent behavior around directory deletion I wasted a few hours trying to handle a case that `pathwatcher` never claimed to support. The upside is that I did manage to simplify the custom FSEvents watcher implementation and eliminate some potentially costly looping! --- lib/core.cc | 34 ------ lib/platform/FSEventsFileWatcher.cpp | 15 +++ spec/pathwatcher-spec.js | 163 +++++++++------------------ 3 files changed, 69 insertions(+), 143 deletions(-) diff --git a/lib/core.cc b/lib/core.cc index c52152a..04f213b 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -302,32 +302,6 @@ void PathWatcherListener::handleFileAction( return; } - // One (rare) special case we need to handle on all platforms: - // - // * Watcher exists on directory `/foo/bar`. - // * Watcher exists on directory `/foo/bar/baz`. - // * Directory `/foo/bar/baz` is deleted. - // - // In this instance, both watchers should be notified, but `efsw` will signal - // only the `/foo/bar` watcher. (If only `/foo/bar/baz` were present, the - // `/foo/bar/baz` watcher would be signalled instead.) - // - // Our custom macOS implementation replicates this incorrect behavior so that - // we can handle this case uniformly in this one place. - bool hasSecondMatch = false; - efsw::WatchID secondHandle; - - // If we need to account for this scenario, then the full path will have its - // own watcher. Since we only watch directories, this proves that the full - // path is a directory. - if (HasPath(newPathStr) && action == efsw::Action::Delete) { - efsw::WatchID handle = GetHandleForPath(newPathStr); - if (watchId != handle) { - hasSecondMatch = true; - secondHandle = handle; - } - } - PathWatcherEvent* event = new PathWatcherEvent(action, watchId, newPath, oldPath, realPath); // TODO: Instead of calling `BlockingCall` once per event, throttle them by @@ -336,14 +310,6 @@ void PathWatcherListener::handleFileAction( // do some elimination of redundant events. status = tsfn.BlockingCall(event, ProcessEvent); - if (hasSecondMatch && status == napi_ok) { - // In the rare case of the scenario described above, we have a second - // callback invocation to make with a second event. Luckily, the only thing - // that changes about the event is the handle! - PathWatcherEvent* secondEvent = new PathWatcherEvent(action, secondHandle, newPath, oldPath, realPath); - tsfn.BlockingCall(secondEvent, ProcessEvent); - } - tsfn.Release(); if (status != napi_ok) { // TODO: Not sure how this could fail, or how we should present it to the diff --git a/lib/platform/FSEventsFileWatcher.cpp b/lib/platform/FSEventsFileWatcher.cpp index 6790770..0df32ce 100644 --- a/lib/platform/FSEventsFileWatcher.cpp +++ b/lib/platform/FSEventsFileWatcher.cpp @@ -359,6 +359,11 @@ void FSEventsFileWatcher::handleActions(std::vector& events) { // later in the listener (with identical cross-platform code), and it // allows us to choose a single winner here in all cases, simplifying the // implementation further. + // + // NOTE: `efsw` currently does not detect a directory’s deletion when + // that directory is the one being watched. For consistency, we'll try + // to make this custom `FileWatcher` instance behave the same way. + // std::lock_guard lock(mapMutex); auto itpth = pathsToHandles.find(PathWithoutFileName(event.path, false)); if (itpth != pathsToHandles.end()) { @@ -388,6 +393,16 @@ void FSEventsFileWatcher::handleActions(std::vector& events) { } } + // Whether this event is happening to the directory itself or one of its + // children. + bool isExactMatch = PathsAreEqual(event.path, path); + + if (event.flags & kFSEventStreamEventFlagItemRemoved && isExactMatch) { + // This is a directory's own deletion. Ignore it for consistency with + // other implementations! + continue; + } + std::string dirPath(PathWithoutFileName(event.path)); std::string filePath(FileNameFromPath(event.path)); diff --git a/spec/pathwatcher-spec.js b/spec/pathwatcher-spec.js index b946972..edc61a4 100644 --- a/spec/pathwatcher-spec.js +++ b/spec/pathwatcher-spec.js @@ -56,6 +56,60 @@ describe('PathWatcher', () => { }); }); + // The purpose of this `describe` block is to ensure that our custom FSEvent + // implementation on macOS agrees with the built-in `efsw` implementations on + // Windows and Linux. + // + // Notably: in order to behave predictably, the FSEvent watcher should not + // trigger a watcher on `/foo/bar/baz` when `/foo/bar/baz` itself is deleted, + // since that’s how the other watcher implementations behave. + describe('when a watched directory is deleted', () => { + let subDir; + beforeEach(() => { + subDir = path.join(tempDir, 'subdir'); + if (!fs.existsSync(subDir)) { + fs.mkdirSync(subDir); + } + }); + + afterEach(() => { + if (subDir && fs.existsSync(subDir)) { + fs.rmSync(subDir, { recursive: true }); + } + }); + + it('does not trigger the callback', async () => { + // This test proves that `efsw` does not detect when a directory is + // deleted if you are watching that exact path. Our custom macOS + // implementation should behave the same way for predictability. + let innerSpy = jasmine.createSpy('innerSpy'); + PathWatcher.watch(subDir, innerSpy); + await wait(20); + fs.rmSync(subDir, { recursive: true }); + await wait(200); + expect(innerSpy).not.toHaveBeenCalled(); + }); + + it('triggers a callback on the directory’s parent if the parent is being watched', async () => { + // We can detect the directory’s deletion if we watch its parent + // directory. + // + // This test proves that, but it also proves that a watcher on the + // deleted directory is still not invoked in this scenario. This was a + // specific scenario I tried to handle and this test proves that said + // workaround is not present. + let outerSpy = jasmine.createSpy('outerSpy'); + let innerSpy = jasmine.createSpy('innerSpy'); + PathWatcher.watch(tempDir, outerSpy); + PathWatcher.watch(subDir, innerSpy); + await wait(20); + fs.rmSync(subDir, { recursive: true }); + await condition(() => outerSpy.calls.count() > 0); + await wait(200); + expect(innerSpy).not.toHaveBeenCalled(); + }); + }); + describe('when a watched path is changed', () => { it('fires the callback with the event type and empty path', async () => { let eventType; @@ -154,115 +208,6 @@ describe('PathWatcher', () => { }); }); - describe('watching a directory', () => { - let subDir; - beforeEach(() => { - subDir = path.join(tempDir, 'subdir'); - if (!fs.existsSync(subDir)) { - fs.mkdirSync(subDir); - } - }); - - afterEach(() => { - if (subDir && fs.existsSync(subDir)) { - fs.rmSync(subDir, { recursive: true }); - } - }); - it('fails to detect that same directory’s deletion', async () => { - // This test proves that `efsw` does not detect when a directory is - // deleted if you are watching that exact path. Our custom macOS - // implementation should behave the same way for predictability. - let innerSpy = jasmine.createSpy('innerSpy'); - PathWatcher.watch(subDir, innerSpy); - await wait(20); - fs.rmSync(subDir, { recursive: true }); - await wait(200); - expect(innerSpy).not.toHaveBeenCalled(); - }); - }); - - // NOTE: These specs are aspirational, and were based on the premise that - // - // (a) `efsw` properly detects a directory’s own deletion when you’re - // watching the directory for changes; (b) `efsw` improperly signals only the - // parent watcher when both parent and child directories are being watched - // and the child is deleted. - // - // In fact, (a) is false. I observed the behavior from (b) and considered it - // a bug, but if `efsw` is designed not to detect (a), then (b) is perfectly - // logical and consistent. - // - // I wrote an elaborate workaround for the behavior I observed on macOS (with - // my custom FSEvent implementation) without properly verifying that Linux - // and Windows behave the same way. - // - // It makes intuitive sense that `PathWatcher.watch` should invoke its - // callback when it’s watching a directory and the directory itself is - // deleted, but that doesn’t currently happen in `efsw`, and `pathwatcher` - // never seems to have expected that behavior itself. - // - // To support this properly, `efsw` would have to support the detection of a - // directory deletion when the directory itself is being watched. As it is, - // it detects this only when the directory’s parent is being watched. - // - // This means that it would be easy to support something like - // `Directory::onDidDelete` if we wanted to — we’d just set up a second - // watcher on the directory’s parent. - // - xdescribe('when a directory child of a watched directory is deleted', () => { - let subDir; - beforeEach(() => { - subDir = path.join(tempDir, 'subdir'); - if (!fs.existsSync(subDir)) { - fs.mkdirSync(subDir); - } - }); - - afterEach(() => { - if (subDir && fs.existsSync(subDir)) { - fs.rmSync(subDir, { recursive: true }); - } - }); - - it('fires two events IF the child had its own watcher', async () => { - // But if the directory’s parent is _also_ being watched, `efsw` _does - // not_ trigger the directory’s own watcher; it will trigger only the - // outer watcher. - // - // This test proves that we handle this case correctly nonetheless. We do - // so in the native code by manually testing for the presence of a - // watcher at the subdirectory’s path and explicitly invoking the - // callback with that handle. - let outerSpy = jasmine.createSpy('outerSpy'); - let innerSpy = jasmine.createSpy('innerSpy'); - PathWatcher.watch(tempDir, outerSpy); - PathWatcher.watch(subDir, innerSpy); - await wait(20); - fs.rmSync(subDir, { recursive: true }); - await condition(() => outerSpy.calls.count() > 0); - await wait(200); - expect(innerSpy).toHaveBeenCalled(); - }); - - it('fires two events IF the child had its own watcher (same watchers attached in opposite order)', async () => { - // We test this scenario to prove that `efsw` consistently picks the - // outer directory’s watcher regardless of observation order. If that - // weren’t true, then this test would fail, since the native logic for - // detecting this scenario wouldn’t work if it had to manually invoke the - // outer watcher. - let outerSpy = jasmine.createSpy('outerSpy'); - let innerSpy = jasmine.createSpy('innerSpy'); - PathWatcher.watch(subDir, innerSpy); - PathWatcher.watch(tempDir, outerSpy); - await wait(20); - fs.rmSync(subDir, { recursive: true }); - await condition(() => outerSpy.calls.count() > 0); - await wait(200); - expect(innerSpy).toHaveBeenCalled(); - }); - - }); - describe('when a file under a watched directory is moved', () => { it('fires the callback with the change event and empty path', async () => { From 724ac758b06f77f29987144c2b8e8c37190066c7 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 1 Nov 2024 00:02:37 -0700 Subject: [PATCH 157/168] Change file spec to be meaningful --- spec/file-spec.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/spec/file-spec.js b/spec/file-spec.js index 1c43027..8437630 100644 --- a/spec/file-spec.js +++ b/spec/file-spec.js @@ -161,33 +161,35 @@ describe('File', () => { }); }); + // Since all `NativeWatcher` instance watch directories, two different + // `PathWatcher` instances for two different files in the same directory will + // end up sharing a `NativeWatcher`. This test ensures that `PathWatcher`s + // know how to tell their own events apart from those of others. describe('when a native watcher is shared by several PathWatchers', () => { - let nephewPath = path.join(__dirname, 'fixtures', 'foo', 'bar.txt'); - let nephewFile = new File(nephewPath); + let siblingPath = path.join(__dirname, 'fixtures', 'file-test-a.txt'); + let siblingFile = new File(siblingPath); beforeEach(() => { - if (!fs.existsSync(path.dirname(nephewPath))) { - fs.mkdirSync(path.dirname(nephewPath)); - } - if (!fs.existsSync(nephewPath)) { - fs.writeFileSync(nephewPath, 'initial'); + if (!fs.existsSync(siblingPath)) { + fs.writeFileSync(siblingPath, 'initial'); } }); afterEach(() => { - if (fs.existsSync(path.dirname(nephewPath))) { - fs.rmSync(path.dirname(nephewPath), { recursive: true }); + if (fs.existsSync(siblingPath)) { + fs.rmSync(siblingPath); } }) + it('does not cross-fire events', async () => { - let changeHandler1 = jasmine.createSpy('rootChangeHandler'); + let changeHandler1 = jasmine.createSpy('originalChangeHandler'); file.onDidChange(changeHandler1); await wait(100); - let changeHandler2 = jasmine.createSpy('nephewChangeHandler'); - nephewFile.onDidChange(changeHandler2); + let changeHandler2 = jasmine.createSpy('siblingChangeHandler'); + siblingFile.onDidChange(changeHandler2); await wait(100); - fs.writeFileSync(nephewPath, 'changed!'); + fs.writeFileSync(siblingPath, 'changed!'); await condition(() => changeHandler2.calls.count() > 0); expect(changeHandler1).not.toHaveBeenCalled(); From 9eb8beb50a1b4f381ff65283e48fc9b23afefe71 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 1 Nov 2024 00:03:09 -0700 Subject: [PATCH 158/168] Logging cleanup, further native optimization on macOS --- lib/platform/FSEventsFileWatcher.cpp | 147 ++++++++++----------------- lib/platform/FSEventsFileWatcher.hpp | 6 +- 2 files changed, 52 insertions(+), 101 deletions(-) diff --git a/lib/platform/FSEventsFileWatcher.cpp b/lib/platform/FSEventsFileWatcher.cpp index 0df32ce..7f0f434 100644 --- a/lib/platform/FSEventsFileWatcher.cpp +++ b/lib/platform/FSEventsFileWatcher.cpp @@ -11,10 +11,10 @@ // streams created in comparison to `efsw`’s approach of using one stream per // watched path. -// NOTE: Lots of these are duplications and alternate versions of functions -// that are already present in `efsw`. We could use the `efsw` versions -// instead, but it feels like a good idea to minimize the amount of -// cross-pollination here. +// NOTE: Lots of these utility functions are duplications and alternate +// versions of functions that are already present in `efsw`. We could use the +// `efsw` versions instead, but it feels like a good idea to minimize the +// amount of cross-pollination here. int shorthandFSEventsModified = kFSEventStreamEventFlagItemFinderInfoMod | kFSEventStreamEventFlagItemModified | @@ -74,7 +74,8 @@ std::string PrecomposeFileName(const std::string& name) { return result; } -// Returns whether `path` currently exists on disk. +// Returns whether `path` currently exists on disk. Does not distiguish between +// files and directories. bool PathExists(const std::string& path) { struct stat buffer; return (stat(path.c_str(), &buffer) == 0); @@ -87,9 +88,9 @@ bool PathStartsWith(const std::string& str, const std::string& prefix) { if (prefix.length() > str.length()) { return false; } - auto normalizedPrefix = NormalizePath(prefix); // We ensure `prefix` ends with a path separator so we don't mistakenly think // that `/foo/barbaz` descends from `/foo/bar`. + auto normalizedPrefix = NormalizePath(prefix); return str.compare(0, normalizedPrefix.length(), normalizedPrefix) == 0; } @@ -100,13 +101,15 @@ void DirRemoveSlashAtEnd (std::string& dir) { } } -// Given `/foo/bar/baz.txt`, returns `/foo/bar` (with or without a trailing -// slash as desired). +// Given `/foo/bar/baz.txt`, returns `/foo/bar` (or `/foo/bar/`). +// +// Given `/foo/bar/baz`, also returns `/foo/bar` (or `/foo/bar/`). In other +// words: it works like Node’s `path.dirname` and strips the last segment of a +// path. std::string PathWithoutFileName(std::string filepath, bool keepTrailingSeparator) { DirRemoveSlashAtEnd(filepath); size_t pos = filepath.find_last_of(PATH_SEPARATOR); - if (pos != std::string::npos) { return filepath.substr(0, keepTrailingSeparator ? pos + 1 : pos); } @@ -120,6 +123,10 @@ std::string PathWithoutFileName(std::string filepath) { } // Given `/foo/bar/baz.txt`, returns `baz.txt`. +// +// Given `/foo/bar/baz`, returns `baz`. +// +// Equivalent to Node’s `path.basename`. std::string FileNameFromPath(std::string filepath) { DirRemoveSlashAtEnd(filepath); @@ -133,8 +140,8 @@ std::string FileNameFromPath(std::string filepath) { // Borrowed from `efsw`. Don’t ask me to explain it. static std::string convertCFStringToStdString( CFStringRef cfString ) { - // Try to get the C string pointer directly - const char* cStr = CFStringGetCStringPtr( cfString, kCFStringEncodingUTF8 ); + // Try to get the C string pointer directly. + const char* cStr = CFStringGetCStringPtr(cfString, kCFStringEncodingUTF8); if (cStr) { // If the pointer is valid, directly return a `std::string` from it. @@ -168,9 +175,6 @@ static std::string convertCFStringToStdString( CFStringRef cfString ) { // Empty constructor. FSEventsFileWatcher::~FSEventsFileWatcher() { -#ifdef DEBUG - std::cout << "[destroying] FSEventsFileWatcher!" << std::endl; -#endif pendingDestruction = true; // Defer cleanup until we can finish processing file events. std::unique_lock lock(processingMutex); @@ -192,9 +196,6 @@ efsw::WatchID FSEventsFileWatcher::addWatch( // The `_useRecursion` flag is ignored; it's present for API compatibility. bool _useRecursion ) { -#ifdef DEBUG - std::cout << "FSEventsFileWatcher::addWatch" << directory << std::endl; -#endif efsw::WatchID handle = nextHandleID++; { std::lock_guard lock(mapMutex); @@ -217,9 +218,6 @@ efsw::WatchID FSEventsFileWatcher::addWatch( void FSEventsFileWatcher::removeWatch( efsw::WatchID handle ) { -#ifdef DEBUG - std::cout << "FSEventsFileWatcher::removeWatch" << handle << std::endl; -#endif auto remainingCount = removeHandle(handle); if (remainingCount == 0) { @@ -248,10 +246,6 @@ void FSEventsFileWatcher::FSEventCallback( const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[] ) { -#ifdef DEBUG - std::cout << "FSEventsFileWatcher::FSEventCallback" << std::endl; -#endif - FSEventsFileWatcher* instance = static_cast(userData); if (!instance->isValid) return; @@ -338,31 +332,26 @@ void FSEventsFileWatcher::handleActions(std::vector& events) { // How do we match up this path change to the watcher that cares about // it? // - // Since we do only non-recursive watching, there are a maximum of two - // watchers that can care about something — and 99% of cases will involve - // a single such watcher. This vastly simplifies our implementation - // compared to `efsw`’s — since it has to care about the possibility of - // recursive watchers, one file change can correspond to arbitrarily many - // watchers. + // Since we do only non-recursive watching, each filesystem event can + // belong to one watcher at most. This vastly simplifies our + // implementation compared to `efsw`’s — since it has to care about the + // possibility of recursive watchers, one file change can correspond to + // arbitrarily many watchers. // // For that reason, we can do a simple map lookup. First we try the // path’s parent directory; if that’s not successful, we try the full // path. One of these is (for practical purposes) guaranteed to find a // watcher. // - // NOTE: What about the 1% edge case? `efsw` has an incorrect behavior - // here: in the rare case of a watcher existing on both a parent - // directory and a child directory, it will choose only the parent when - // the child is deleted. - // - // This is incorrect, but it's _conveniently_ incorrect! We can fix it - // later in the listener (with identical cross-platform code), and it - // allows us to choose a single winner here in all cases, simplifying the - // implementation further. - // // NOTE: `efsw` currently does not detect a directory’s deletion when - // that directory is the one being watched. For consistency, we'll try - // to make this custom `FileWatcher` instance behave the same way. + // that directory is the one being watched. For consistency, we'll try to + // make this custom `FileWatcher` instance behave the same way. + // + // This works in our favor because it means that there can be only one + // watcher responding to this filesystem event. The only way to find + // lifecycle events on directories themselves — deletions, renames, + // creations — is to listen on the directory’s parent, which neatly + // mirrors the situation with files. // std::lock_guard lock(mapMutex); auto itpth = pathsToHandles.find(PathWithoutFileName(event.path, false)); @@ -373,36 +362,12 @@ void FSEventsFileWatcher::handleActions(std::vector& events) { path = itpth->first; handle = itpth->second; } else { - // Otherwise, we check if the path has a watcher of its own. This only - // applies when the path is itself a directory and only when the - // directory is being deleted (since we don’t let you watch a directory - // before it exists). - // - // If _both_ the parent directory _and_ the child directory are being - // watched in this scenario, we won’t get this far. We'll still - // notify both watchers, but that gets handled in `core.cc`. - itpth = pathsToHandles.find(event.path); - if (itpth != pathsToHandles.end()) { - path = itpth->first; - handle = itpth->second; - } else { - // We couldn't find a handle for this path. This is odd, but it’s - // not a big deal. - continue; - } + // Couldn't match this up to a watcher. A bit unusual, but not + // catastrophic. + continue; } } - // Whether this event is happening to the directory itself or one of its - // children. - bool isExactMatch = PathsAreEqual(event.path, path); - - if (event.flags & kFSEventStreamEventFlagItemRemoved && isExactMatch) { - // This is a directory's own deletion. Ignore it for consistency with - // other implementations! - continue; - } - std::string dirPath(PathWithoutFileName(event.path)); std::string filePath(FileNameFromPath(event.path)); @@ -411,18 +376,20 @@ void FSEventsFileWatcher::handleActions(std::vector& events) { kFSEventStreamEventFlagItemRemoved | kFSEventStreamEventFlagItemRenamed )) { - if (dirPath != path) { + if (!PathsAreEqual(dirPath, path)) { dirsChanged.insert(dirPath); } } - // `efsw`‘s’ comment here suggests that you can’t reliably infer order - // from these events — so if the same file is marked as added and changed - // and deleted in consecutive events, you don't know if it was deleted/ + // `efsw`‘s comment here suggests that you can’t reliably infer order from + // these events — so if the same file is marked as added and changed and + // deleted in consecutive events, you don't know if it was deleted/ // added/modified, modified/deleted/added, etc. // - // This is the equivalent logic from `WatcherFSEvents.cpp` because I - // don’t trust myself to touch it at all. + // This is the equivalent logic from `WatcherFSEvents.cpp` because I don’t + // trust myself to touch it at all. The goal is largely to infer an + // ordering to the extent possible based on whether the path exists at the + // moment. if (event.flags & kFSEventStreamEventFlagItemRenamed) { // Does the next event also refer to this same file, and is that event // also a rename? @@ -471,7 +438,7 @@ void FSEventsFileWatcher::handleActions(std::vector& events) { kFSEventStreamEventFlagItemRemoved | kFSEventStreamEventFlagItemRenamed )) { - if (newDir != path) { + if (!PathsAreEqual(newDir, path)) { dirsChanged.insert(newDir); } } @@ -563,34 +530,24 @@ void FSEventsFileWatcher::process() { dirsChanged.clear(); } - // Process the copied directories + // Process the copied directories. for (const auto& dir : dirsCopy) { if (pendingDestruction) return; efsw::WatchID handle; std::string path; - bool found = false; { std::lock_guard lock(mapMutex); - for (const auto& pair: handlesToPaths) { - if (!PathStartsWith(dir, pair.second)) continue; - - if ( - !PathsAreEqual(dir, pair.second) && dir.find_last_of(PATH_SEPARATOR) != pair.second.size() - 1 - ) { - continue; - } - - found = true; - path = pair.second; - handle = pair.first; - break; - } + auto itpth = pathsToHandles.find(PathWithoutFileName(dir, false)); + if (itpth == pathsToHandles.end()) continue; + path = itpth->first; + handle = itpth->second; } - if (!found) continue; - + // TODO: It is questionable whether these file events are useful or + // actionable, since the listener will fail to respond to them if they come + // from an unexpected path on disk. sendFileAction( handle, PathWithoutFileName(dir), @@ -600,8 +557,6 @@ void FSEventsFileWatcher::process() { if (pendingDestruction) return; } - - dirsChanged.clear(); } // Start a new FSEvent stream and promote it to the “active” stream after it diff --git a/lib/platform/FSEventsFileWatcher.hpp b/lib/platform/FSEventsFileWatcher.hpp index 208d00f..cdbdc2c 100644 --- a/lib/platform/FSEventsFileWatcher.hpp +++ b/lib/platform/FSEventsFileWatcher.hpp @@ -16,11 +16,7 @@ class FSEvent { long flags, uint64_t id, uint64_t inode = 0 - ): path(path), flags(flags), id(id), inode(inode) { -#ifdef DEBUG - std::cout << "[creating] FSEventsFileWatcher!" << std::endl; -#endif - } + ): path(path), flags(flags), id(id), inode(inode) {} std::string path; long flags; From b88557830e48b0e9a502a53080933f26dc0e01f5 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sat, 2 Nov 2024 12:04:03 -0700 Subject: [PATCH 159/168] Remove unnecessary `devDependencies` and files --- .npmignore | 4 +--- Gruntfile.js | 53 ---------------------------------------------------- appveyor.yml | 6 ------ package.json | 10 +--------- 4 files changed, 2 insertions(+), 71 deletions(-) delete mode 100644 Gruntfile.js delete mode 100644 appveyor.yml diff --git a/.npmignore b/.npmignore index 2503ddf..24a4f71 100644 --- a/.npmignore +++ b/.npmignore @@ -1,8 +1,6 @@ build/ spec/ -script/ -*.coffee +scripts/ .npmignore -.travis.yml .node-version npm-debug.log diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index cdc2025..0000000 --- a/Gruntfile.js +++ /dev/null @@ -1,53 +0,0 @@ -const DEFAULT_COMMAND_OPTIONS = { - stdout: true, - stderr: true, - failOnError: true -}; - -function defineTasks (grunt) { - grunt.initConfig({ - pkg: grunt.file.readJSON('package.json'), - - shell: { - 'submodule-update': { - command: 'git submodule update --init', - options: DEFAULT_COMMAND_OPTIONS - }, - - rebuild: { - command: `node-gyp rebuild`, - options: DEFAULT_COMMAND_OPTIONS - }, - - test: { - command: `npm test`, - options: DEFAULT_COMMAND_OPTIONS - }, - - 'update-atomdoc': { - command: 'npm update grunt-atomdoc', - options: DEFAULT_COMMAND_OPTIONS - } - } - }); - - grunt.loadNpmTasks('grunt-shell'); - grunt.loadNpmTasks('grunt-atomdoc'); - - grunt.registerTask('default', ['shell:submodule-update', 'shell:rebuild']); - grunt.registerTask('test', ['default', 'shell:test']); - - // TODO: AtomDoc is not being generated now that we've decaffeinated the - // source files. We should use `joanna` instead, but it needs some - // modernization to understand current JS syntax. - grunt.registerTask('prepublish', ['shell:update-atomdoc', 'atomdoc']); - - grunt.registerTask('clean', () => { - let rm = require('rimraf').sync; - rm('build'); - rm('lib'); - rm('api.json'); - }); -} - -module.exports = defineTasks; diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 63206ce..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,6 +0,0 @@ -# dummy appveyor -build: off - -branches: - only: - - non-existing diff --git a/package.json b/package.json index d2369d4..af7e9c9 100644 --- a/package.json +++ b/package.json @@ -26,18 +26,10 @@ "clean": "node scripts/clean.js" }, "devDependencies": { - "chalk": "^4.1.2", - "grunt": "~0.4.1", - "grunt-atomdoc": "^1.0", - "grunt-cli": "~0.1.7", - "grunt-shell": "~0.2.2", "jasmine": "^5.3.1", "node-addon-api": "^8.1.0", - "node-cpplint": "~0.1.5", "rimraf": "~2.2.0", - "segfault-handler": "^1.3.0", - "temp": "~0.9.0", - "why-is-node-running": "^2.3.0" + "temp": "~0.9.0" }, "dependencies": { "async": "~0.2.10", From b66d621c9c9c755d5d78d5d03936cf9ffd2068cd Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sat, 2 Nov 2024 12:04:47 -0700 Subject: [PATCH 160/168] Add some comments --- spec/context-safety.js | 14 ++++++++------ spec/run.js | 8 +++++--- src/directory.js | 1 + src/file.js | 1 + 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/spec/context-safety.js b/spec/context-safety.js index b63c6d3..be5d2aa 100644 --- a/spec/context-safety.js +++ b/spec/context-safety.js @@ -6,23 +6,25 @@ // script segfaults or runs indefinitely. const spawnThread = require('./worker'); -const NUM_WORKERS = 3; -const MAX_DURATION = 15 * 1000; +const NUM_WORKERS = 1; +const MAX_DURATION_MS = 15 * 1000; -// Pick one of the workers to return earlier than the others. +// Pick one of the workers to return earlier than the others. If this causes +// the others to fail or misbehave, it suggests that cleanup logic for an +// environment is not truly context-safe. let earlyReturn = null; if (NUM_WORKERS > 1) { earlyReturn = Math.floor(Math.random() * NUM_WORKERS); } function bail () { - console.error(`Script ran for more than ${MAX_DURATION / 1000} seconds; there's an open handle somewhere!`); + console.error(`Script ran for more than ${MAX_DURATION_MS / 1000} seconds; there's an open handle somewhere!`); process.exit(2); } -// Wait to see if the script is still running MAX_DURATION milliseconds from +// Wait to see if the script is still running MAX_DURATION_MS milliseconds from // now… -let failsafe = setTimeout(bail, MAX_DURATION); +let failsafe = setTimeout(bail, MAX_DURATION_MS); // …but `unref` ourselves so that we're not the reason why the script keeps // running! failsafe.unref(); diff --git a/spec/run.js b/spec/run.js index 5504ec8..afb10ab 100644 --- a/spec/run.js +++ b/spec/run.js @@ -15,10 +15,12 @@ if (process.argv[2]) { } jasmine.loadConfig(CONFIG); -const MAX_DURATION = 10 * 1000; +// This value refers to the amount of time we allow the script to run _after_ +// the tests are done. +const MAX_DURATION_MS = 10 * 1000; function bail () { - console.error(`Script ran for more than ${MAX_DURATION / 1000} seconds after the end of the suite; there's an open handle somewhere!`); + console.error(`Script ran for more than ${MAX_DURATION_MS / 1000} seconds after the end of the suite; there's an open handle somewhere!`); process.exit(2); } @@ -30,7 +32,7 @@ function bail () { await jasmine.execute(); // Wait to see if the script is still running MAX_DURATION milliseconds from // now… - let failsafe = setTimeout(bail, MAX_DURATION); + let failsafe = setTimeout(bail, MAX_DURATION_MS); // …but `unref` ourselves so that we're not the reason why the script keeps // running! failsafe.unref(); diff --git a/src/directory.js b/src/directory.js index 030b7d3..be00e50 100644 --- a/src/directory.js +++ b/src/directory.js @@ -5,6 +5,7 @@ const async = require('async'); const { Emitter, Disposable } = require('event-kit'); const File = require('./file'); +// Lazy-load the main `PathWatcher` import to prevent circular references. let PathWatcher; // Extended: Represents a directory on disk that can be traversed or watched diff --git a/src/file.js b/src/file.js index fcde149..fdf40c2 100644 --- a/src/file.js +++ b/src/file.js @@ -12,6 +12,7 @@ async function wait (ms) { return new Promise(r => setTimeout(r, ms)); } +// Lazy-load the main `PathWatcher` import to prevent circular references. let PathWatcher; // Extended: Represents an individual file that can be watched, read from, and From 2652da2c8abbca5d242a1855497f47c902794aea Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sat, 2 Nov 2024 12:35:04 -0700 Subject: [PATCH 161/168] Update README --- README.md | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 049224b..457fe2e 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Watch files and directories for changes. > [!IMPORTANT] > This library is used in [Pulsar][] in several places for compatibility reasons. The [nsfw](https://www.npmjs.com/package/nsfw) library is more robust and more widely used; it is available in Pulsar via `atom.watchPath` and is usually a better choice. > +> If you’re here because you want a general-purpose file-watching library for Node, use `nsfw` instead. +> > The purpose of this library’s continued inclusion in Pulsar is to provide the [File][] and [Directory][] classes that have long been available as exports via `require('atom')`. ## Installing @@ -33,21 +35,38 @@ If you’re using it in an Electron renderer process, you must take extra care i const PathWatcher = require('pathwatcher'); ``` -### PathWatcher.watch(filename, listener) +### `watch(filename, listener)` -Watch for changes on `filename`, where `filename` is either a file or a directory. Returns a number that represents a specific watcher instance. +Watch for changes on `filename`, where `filename` is either a file or a directory. `filename` must be an absolute path and must exist at the time `watch` is called. The listener callback gets two arguments: `(event, path)`. `event` can be `rename`, `delete` or `change`, and `path` is the path of the file which triggered the event. -For directories, the `change` event is emitted when a file or directory under the watched directory is created, deleted, or renamed. The watcher is not recursive; changes to the contents of subdirectories will not be detected. +The watcher is not recursive; changes to the contents of subdirectories will not be detected. + +Returns an instance of `PathWatcher`. This instance is useful primarily for the `close` method that stops the watch operation. + +#### Caveats + +* Watching a specific file or directory will not notify you when that file or directory is created, since the file must already exist before you start watching the path. +* When watching a file, `event` can be any of `rename`, `delete`, or `change`, where `change` means that the file’s contents changed somehow. +* When watching a directory, `event` can only be `change`, and in this context `change` signifies that one or more of the directory’s children changed (by being renamed, deleted, added, or modified). +* A watched directory will not report when it is renamed or deleted. If you want to detect when a given directory is deleted, watch its parent directory and test for the child directory’s existence when you receive a `change` event. -### PathWatcher.close(handle) +### `PathWatcher::close()` Stop watching for changes on the given `PathWatcher`. -The `handle` argument is a number and should be the return value from the initial call to `PathWatcher.watch`. +### `closeAllWatchers()` + +Stop watching on all subscribed paths. All existing `PathWatcher` instances will stop receiving events. Call this if you’re going to end the process; it ensures that your script will exit cleanly. + +### `getWatchedPaths()` + +Returns an array of strings representing the actual paths that are being watched on disk. + +`pathwatcher` watches directories in all instances, since it’s easy to do so in a cross-platform manner. -### File and Directory +### `File` and `Directory` These are convenience wrappers around some filesystem operations. They also wrap `PathWatcher.watch` via their `onDidChange` (and similar) methods. From 2c40f68b18ee43eec5f9bb75aa10f639adf2ae7e Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sat, 2 Nov 2024 12:35:35 -0700 Subject: [PATCH 162/168] Remove `getNativeWatcherCount` (redundant and not used in the tests) --- src/main.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main.js b/src/main.js index ebcb3da..3b415c3 100644 --- a/src/main.js +++ b/src/main.js @@ -631,10 +631,6 @@ function getWatchedPaths () { return result } -function getNativeWatcherCount() { - return NativeWatcher.INSTANCES.size; -} - const File = require('./file'); const Directory = require('./directory'); @@ -642,7 +638,6 @@ module.exports = { watch, closeAllWatchers, getWatchedPaths, - getNativeWatcherCount, File, Directory }; From 68b378e3db59284efc03a7448d5455f2f78ed586 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 26 Nov 2024 22:13:39 -0800 Subject: [PATCH 163/168] Report the original path on `child-rename` events --- src/main.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main.js b/src/main.js index 3b415c3..f6b0bc9 100644 --- a/src/main.js +++ b/src/main.js @@ -466,12 +466,16 @@ class PathWatcher { // inside of the directory. if ( path.dirname(event.path) === this.normalizedPath || - path.dirname(event.oldPath) === this.normalizedPath + path.dirname(event.oldPath) === this.normalizedPath ) { // This is a direct child of the directory, so we'll fire an // event. newEvent.action = 'change'; - newEvent.path = ''; + // NOTE: Setting a specific path here (instead of the standard + // empty string as with other code paths) fixes a behavior + // regression in Pulsar. The other code paths might also be + // incorrect; needs investigation. + newEvent.path = event.path; } else { // Changes in ancestors or descendants do not concern us, so // we'll return early. From 585b76a141b9459f7ff093ad1c3b0c39b0f79b0f Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 28 Nov 2024 15:39:27 -0800 Subject: [PATCH 164/168] Change the rename-a-file fix to be more targeted --- src/main.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/main.js b/src/main.js index f6b0bc9..840fc86 100644 --- a/src/main.js +++ b/src/main.js @@ -471,11 +471,7 @@ class PathWatcher { // This is a direct child of the directory, so we'll fire an // event. newEvent.action = 'change'; - // NOTE: Setting a specific path here (instead of the standard - // empty string as with other code paths) fixes a behavior - // regression in Pulsar. The other code paths might also be - // incorrect; needs investigation. - newEvent.path = event.path; + newEvent.path = ''; } else { // Changes in ancestors or descendants do not concern us, so // we'll return early. @@ -544,11 +540,12 @@ class PathWatcher { return; } - if (eventPathIsEqual) { - // Specs require that a `delete` action carry a path of `null`; other - // actions should carry an empty path. (Weird decisions, but we can - // live with them.) - newEvent.path = newEvent.action === 'delete' ? null : ''; + if (eventPathIsEqual && newEvent.action === 'delete') { + // Specs require that a `delete` action carry a path of `null`. + // + // NOTE: We might want to revisit this and change what the specs test! + newEvent.path = null; + // newEvent.path = newEvent.action === 'delete' ? null : newEvent.path; } // console.debug( // 'FINAL EVENT ACTION:', From 6c3d8fe510d56172abfae482a1af79f24eb4d793 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Tue, 24 Dec 2024 10:43:01 -0800 Subject: [PATCH 165/168] Move lock guard inside local block --- lib/platform/FSEventsFileWatcher.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/platform/FSEventsFileWatcher.cpp b/lib/platform/FSEventsFileWatcher.cpp index 7f0f434..a4d1429 100644 --- a/lib/platform/FSEventsFileWatcher.cpp +++ b/lib/platform/FSEventsFileWatcher.cpp @@ -565,8 +565,8 @@ bool FSEventsFileWatcher::startNewStream() { // Build a list of all current watched paths. We'll eventually pass this to // `FSEventStreamCreate`. std::vector cfStrings; - std::lock_guard lock(mapMutex); { + std::lock_guard lock(mapMutex); for (const auto& pair : handlesToPaths) { CFStringRef cfStr = CFStringCreateWithCString( kCFAllocatorDefault, From 5ea87ef4e815bfa3c96cb85a7b1f28ab499265ea Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 26 Dec 2024 13:02:22 -0800 Subject: [PATCH 166/168] Fix exception encountered in `text-buffer` tests --- lib/platform/FSEventsFileWatcher.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/platform/FSEventsFileWatcher.cpp b/lib/platform/FSEventsFileWatcher.cpp index a4d1429..22e2c0d 100644 --- a/lib/platform/FSEventsFileWatcher.cpp +++ b/lib/platform/FSEventsFileWatcher.cpp @@ -494,6 +494,9 @@ void FSEventsFileWatcher::handleAddModDel( // Private: clean up a handle from both unordered maps. size_t FSEventsFileWatcher::removeHandle(efsw::WatchID handle) { + // If we're destroyed (or about to destroy ourselves), don't try to do + // anything to these maps; the mutex lock will fail. + if (!isValid || pendingDestruction) return 0; std::lock_guard lock(mapMutex); std::string path; auto itp = handlesToPaths.find(handle); From 649232b264940e27ff578f848d1ec9aa3ebb09b9 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Thu, 26 Dec 2024 13:49:02 -0800 Subject: [PATCH 167/168] =?UTF-8?q?Return=20silently=20on=20an=20attempt?= =?UTF-8?q?=20to=20unwatch=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …when we're not watching in the first place. --- lib/core.cc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/core.cc b/lib/core.cc index 04f213b..df4a5f0 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -164,6 +164,7 @@ void PathWatcherListener::AddPath(PathTimestampPair pair, efsw::WatchID handle) // Remove metadata for a given watch ID. void PathWatcherListener::RemovePath(efsw::WatchID handle) { std::string path; + if (isShuttingDown) return; { std::lock_guard lock(pathsMutex); auto it = paths.find(handle); @@ -442,6 +443,14 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { // Unwatch the given handle. Napi::Value PathWatcher::Unwatch(const Napi::CallbackInfo& info) { auto env = info.Env(); + + if (!isWatching) { + // We're not listening right now, so this is redundant at best and invalid + // at worst. Return early so we don't try to use resources that have + // already been finalized. + return env.Undefined(); + } + if (!IsV8ValueWatcherHandle(info[0])) { Napi::TypeError::New(env, "Argument must be a number").ThrowAsJavaScriptException(); return env.Null(); From d2281e60669644a4a8931f0b015d7c3792307327 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 10 Jan 2025 00:24:56 -0800 Subject: [PATCH 168/168] =?UTF-8?q?Use=20`BigInt`s=20for=20JavaScript=20ha?= =?UTF-8?q?ndles=20instead=20of=20numbers=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …because EFSW was returning handles larger than `Number.MAX_SAFE_INTEGER`. --- lib/core.cc | 34 +++++++++++++++++++++++++--------- lib/core.h | 5 ----- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/lib/core.cc b/lib/core.cc index df4a5f0..50d3235 100644 --- a/lib/core.cc +++ b/lib/core.cc @@ -39,6 +39,21 @@ static bool PredatesWatchStart(struct timespec fileSpec, timeval startTime) { } #endif +static Napi::BigInt WatcherHandleToBigInt(Napi::Env env, efsw::WatchID handle) { + int64_t handleAsInt64 = static_cast(handle); + return Napi::BigInt::New(env, handleAsInt64); +} + +static efsw::WatchID BigIntToWatcherHandle(Napi::BigInt value) { + // JavaScript `BigInt`s can be arbitrarily large, so they may not fit inside + // a `long` or `int64_t`. But if this value needs truncation, something shady + // is going on, since that value certainly didn't come from us. We conform to + // the API here, but we don't need to check whether the value was truncated. + bool lossless = false; + efsw::WatchID handle = value.Int64Value(&lossless); + return handle; +} + static std::string EventType(efsw::Action action, bool isChild) { switch (action) { case efsw::Actions::Add: @@ -123,7 +138,7 @@ static void ProcessEvent( try { callback.Call({ Napi::String::New(env, eventName), - Napi::Number::New(env, event->handle), + WatcherHandleToBigInt(env, event->handle), Napi::String::New(env, newPath), Napi::String::New(env, oldPath) }); @@ -434,10 +449,14 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo& info) { return env.Null(); } - // The `watch` function returns a JavaScript number much like `setTimeout` or + // The `watch` function returns a number much like `setTimeout` or // `setInterval` would; this is the handle that the wrapper JavaScript can // use to unwatch the path later. - return WatcherHandleToV8Value(handle, env); + // + // But EFSW defines a WatchID as a `long`, which means it's 64-bits and + // therefore possibly larger than the JS `Number` type can handle. We'll use + // `BigInt`s instead because we live in the future. + return WatcherHandleToBigInt(env, handle); } // Unwatch the given handle. @@ -451,14 +470,14 @@ Napi::Value PathWatcher::Unwatch(const Napi::CallbackInfo& info) { return env.Undefined(); } - if (!IsV8ValueWatcherHandle(info[0])) { - Napi::TypeError::New(env, "Argument must be a number").ThrowAsJavaScriptException(); + if (!info[0].IsBigInt()) { + Napi::TypeError::New(env, "Argument must be a BigInt").ThrowAsJavaScriptException(); return env.Null(); } if (!listener) return env.Undefined(); - WatcherHandle handle = V8ValueToWatcherHandle(info[0].As()); + efsw::WatchID handle = BigIntToWatcherHandle(info[0].As()); // EFSW doesn’t mind if we give it a handle that it doesn’t recognize; it’ll // just silently do nothing. @@ -472,9 +491,6 @@ Napi::Value PathWatcher::Unwatch(const Napi::CallbackInfo& info) { listener->RemovePath(handle); if (listener->IsEmpty()) { -#ifdef DEBUG - std::cout << "Cleaning up!" << std::endl; -#endif Cleanup(env); isWatching = false; } diff --git a/lib/core.h b/lib/core.h index db6e06f..e373787 100644 --- a/lib/core.h +++ b/lib/core.h @@ -128,11 +128,6 @@ class PathWatcherListener: public efsw::FileWatchListener { std::unordered_map pathsToHandles; }; - -#define WatcherHandleToV8Value(h, e) Napi::Number::New(e, h) -#define V8ValueToWatcherHandle(v) v.Int32Value() -#define IsV8ValueWatcherHandle(v) v.IsNumber() - class PathWatcher : public Napi::Addon { public: PathWatcher(Napi::Env env, Napi::Object exports);