1 | #include "Watcher.hh"
|
---|
2 | #include <unordered_set>
|
---|
3 |
|
---|
4 | using namespace Napi;
|
---|
5 |
|
---|
6 | struct WatcherHash {
|
---|
7 | std::size_t operator() (WatcherRef const &k) const {
|
---|
8 | return std::hash<std::string>()(k->mDir);
|
---|
9 | }
|
---|
10 | };
|
---|
11 |
|
---|
12 | struct WatcherCompare {
|
---|
13 | size_t operator() (WatcherRef const &a, WatcherRef const &b) const {
|
---|
14 | return *a == *b;
|
---|
15 | }
|
---|
16 | };
|
---|
17 |
|
---|
18 | static std::unordered_set<WatcherRef , WatcherHash, WatcherCompare> sharedWatchers;
|
---|
19 |
|
---|
20 | WatcherRef Watcher::getShared(std::string dir, std::unordered_set<std::string> ignorePaths, std::unordered_set<Glob> ignoreGlobs) {
|
---|
21 | WatcherRef watcher = std::make_shared<Watcher>(dir, ignorePaths, ignoreGlobs);
|
---|
22 | auto found = sharedWatchers.find(watcher);
|
---|
23 | if (found != sharedWatchers.end()) {
|
---|
24 | return *found;
|
---|
25 | }
|
---|
26 |
|
---|
27 | sharedWatchers.insert(watcher);
|
---|
28 | return watcher;
|
---|
29 | }
|
---|
30 |
|
---|
31 | void removeShared(Watcher *watcher) {
|
---|
32 | for (auto it = sharedWatchers.begin(); it != sharedWatchers.end(); it++) {
|
---|
33 | if (it->get() == watcher) {
|
---|
34 | sharedWatchers.erase(it);
|
---|
35 | break;
|
---|
36 | }
|
---|
37 | }
|
---|
38 |
|
---|
39 | // Free up memory.
|
---|
40 | if (sharedWatchers.size() == 0) {
|
---|
41 | sharedWatchers.rehash(0);
|
---|
42 | }
|
---|
43 | }
|
---|
44 |
|
---|
45 | Watcher::Watcher(std::string dir, std::unordered_set<std::string> ignorePaths, std::unordered_set<Glob> ignoreGlobs)
|
---|
46 | : mDir(dir),
|
---|
47 | mIgnorePaths(ignorePaths),
|
---|
48 | mIgnoreGlobs(ignoreGlobs) {
|
---|
49 | mDebounce = Debounce::getShared();
|
---|
50 | mDebounce->add(this, [this] () {
|
---|
51 | triggerCallbacks();
|
---|
52 | });
|
---|
53 | }
|
---|
54 |
|
---|
55 | Watcher::~Watcher() {
|
---|
56 | mDebounce->remove(this);
|
---|
57 | }
|
---|
58 |
|
---|
59 | void Watcher::wait() {
|
---|
60 | std::unique_lock<std::mutex> lk(mMutex);
|
---|
61 | mCond.wait(lk);
|
---|
62 | }
|
---|
63 |
|
---|
64 | void Watcher::notify() {
|
---|
65 | std::unique_lock<std::mutex> lk(mMutex);
|
---|
66 | mCond.notify_all();
|
---|
67 |
|
---|
68 | if (mCallbacks.size() > 0 && mEvents.size() > 0) {
|
---|
69 | // We must release our lock before calling into the debouncer
|
---|
70 | // to avoid a deadlock: the debouncer thread itself will require
|
---|
71 | // our lock from its thread when calling into `triggerCallbacks`
|
---|
72 | // while holding its own debouncer lock.
|
---|
73 | lk.unlock();
|
---|
74 | mDebounce->trigger();
|
---|
75 | }
|
---|
76 | }
|
---|
77 |
|
---|
78 | struct CallbackData {
|
---|
79 | std::string error;
|
---|
80 | std::vector<Event> events;
|
---|
81 | CallbackData(std::string error, std::vector<Event> events) : error(error), events(events) {}
|
---|
82 | };
|
---|
83 |
|
---|
84 | Value callbackEventsToJS(const Env &env, std::vector<Event> &events) {
|
---|
85 | EscapableHandleScope scope(env);
|
---|
86 | Array arr = Array::New(env, events.size());
|
---|
87 | size_t currentEventIndex = 0;
|
---|
88 | for (auto eventIterator = events.begin(); eventIterator != events.end(); eventIterator++) {
|
---|
89 | arr.Set(currentEventIndex++, eventIterator->toJS(env));
|
---|
90 | }
|
---|
91 | return scope.Escape(arr);
|
---|
92 | }
|
---|
93 |
|
---|
94 | void callJSFunction(Napi::Env env, Function jsCallback, CallbackData *data) {
|
---|
95 | HandleScope scope(env);
|
---|
96 | auto err = data->error.size() > 0 ? Error::New(env, data->error).Value() : env.Null();
|
---|
97 | auto events = callbackEventsToJS(env, data->events);
|
---|
98 | jsCallback.Call({err, events});
|
---|
99 | delete data;
|
---|
100 |
|
---|
101 | // Throw errors from the callback as fatal exceptions
|
---|
102 | // If we don't handle these node segfaults...
|
---|
103 | if (env.IsExceptionPending()) {
|
---|
104 | Napi::Error err = env.GetAndClearPendingException();
|
---|
105 | napi_fatal_exception(env, err.Value());
|
---|
106 | }
|
---|
107 | }
|
---|
108 |
|
---|
109 | void Watcher::notifyError(std::exception &err) {
|
---|
110 | std::unique_lock<std::mutex> lk(mMutex);
|
---|
111 | for (auto it = mCallbacks.begin(); it != mCallbacks.end(); it++) {
|
---|
112 | CallbackData *data = new CallbackData(err.what(), {});
|
---|
113 | it->tsfn.BlockingCall(data, callJSFunction);
|
---|
114 | }
|
---|
115 |
|
---|
116 | clearCallbacks();
|
---|
117 | }
|
---|
118 |
|
---|
119 | // This function is called from the debounce thread.
|
---|
120 | void Watcher::triggerCallbacks() {
|
---|
121 | std::unique_lock<std::mutex> lk(mMutex);
|
---|
122 | if (mCallbacks.size() > 0 && mEvents.size() > 0) {
|
---|
123 | auto events = mEvents.getEvents();
|
---|
124 | mEvents.clear();
|
---|
125 |
|
---|
126 | for (auto it = mCallbacks.begin(); it != mCallbacks.end(); it++) {
|
---|
127 | it->tsfn.BlockingCall(new CallbackData("", events), callJSFunction);
|
---|
128 | }
|
---|
129 | }
|
---|
130 | }
|
---|
131 |
|
---|
132 | // This should be called from the JavaScript thread.
|
---|
133 | bool Watcher::watch(Function callback) {
|
---|
134 | std::unique_lock<std::mutex> lk(mMutex);
|
---|
135 |
|
---|
136 | auto it = findCallback(callback);
|
---|
137 | if (it != mCallbacks.end()) {
|
---|
138 | return false;
|
---|
139 | }
|
---|
140 |
|
---|
141 | auto tsfn = ThreadSafeFunction::New(
|
---|
142 | callback.Env(),
|
---|
143 | callback,
|
---|
144 | "Watcher callback",
|
---|
145 | 0, // Unlimited queue
|
---|
146 | 1 // Initial thread count
|
---|
147 | );
|
---|
148 |
|
---|
149 | mCallbacks.push_back(Callback {
|
---|
150 | tsfn,
|
---|
151 | Napi::Persistent(callback),
|
---|
152 | std::this_thread::get_id()
|
---|
153 | });
|
---|
154 |
|
---|
155 | return true;
|
---|
156 | }
|
---|
157 |
|
---|
158 | // This should be called from the JavaScript thread.
|
---|
159 | std::vector<Callback>::iterator Watcher::findCallback(Function callback) {
|
---|
160 | for (auto it = mCallbacks.begin(); it != mCallbacks.end(); it++) {
|
---|
161 | // Only consider callbacks created by the same thread, or V8 will panic.
|
---|
162 | if (it->threadId == std::this_thread::get_id() && it->ref.Value() == callback) {
|
---|
163 | return it;
|
---|
164 | }
|
---|
165 | }
|
---|
166 |
|
---|
167 | return mCallbacks.end();
|
---|
168 | }
|
---|
169 |
|
---|
170 | // This should be called from the JavaScript thread.
|
---|
171 | bool Watcher::unwatch(Function callback) {
|
---|
172 | std::unique_lock<std::mutex> lk(mMutex);
|
---|
173 |
|
---|
174 | bool removed = false;
|
---|
175 | auto it = findCallback(callback);
|
---|
176 | if (it != mCallbacks.end()) {
|
---|
177 | it->tsfn.Release();
|
---|
178 | it->ref.Unref();
|
---|
179 | mCallbacks.erase(it);
|
---|
180 | removed = true;
|
---|
181 | }
|
---|
182 |
|
---|
183 | if (removed && mCallbacks.size() == 0) {
|
---|
184 | unref();
|
---|
185 | return true;
|
---|
186 | }
|
---|
187 |
|
---|
188 | return false;
|
---|
189 | }
|
---|
190 |
|
---|
191 | void Watcher::unref() {
|
---|
192 | if (mCallbacks.size() == 0) {
|
---|
193 | removeShared(this);
|
---|
194 | }
|
---|
195 | }
|
---|
196 |
|
---|
197 | void Watcher::destroy() {
|
---|
198 | std::unique_lock<std::mutex> lk(mMutex);
|
---|
199 | clearCallbacks();
|
---|
200 | }
|
---|
201 |
|
---|
202 | // Private because it doesn't lock.
|
---|
203 | void Watcher::clearCallbacks() {
|
---|
204 | for (auto it = mCallbacks.begin(); it != mCallbacks.end(); it++) {
|
---|
205 | it->tsfn.Release();
|
---|
206 | it->ref.Unref();
|
---|
207 | }
|
---|
208 |
|
---|
209 | mCallbacks.clear();
|
---|
210 | unref();
|
---|
211 | }
|
---|
212 |
|
---|
213 | bool Watcher::isIgnored(std::string path) {
|
---|
214 | for (auto it = mIgnorePaths.begin(); it != mIgnorePaths.end(); it++) {
|
---|
215 | auto dir = *it + DIR_SEP;
|
---|
216 | if (*it == path || path.compare(0, dir.size(), dir) == 0) {
|
---|
217 | return true;
|
---|
218 | }
|
---|
219 | }
|
---|
220 |
|
---|
221 | auto basePath = mDir + DIR_SEP;
|
---|
222 |
|
---|
223 | if (path.rfind(basePath, 0) != 0) {
|
---|
224 | return false;
|
---|
225 | }
|
---|
226 |
|
---|
227 | auto relativePath = path.substr(basePath.size());
|
---|
228 |
|
---|
229 | for (auto it = mIgnoreGlobs.begin(); it != mIgnoreGlobs.end(); it++) {
|
---|
230 | if (it->isIgnored(relativePath)) {
|
---|
231 | return true;
|
---|
232 | }
|
---|
233 | }
|
---|
234 |
|
---|
235 | return false;
|
---|
236 | }
|
---|