Skip to content

Commit 6614e3c

Browse files
Introduce an alternative Linux implementation using inotify
…that fits our feature set better than EFSW’s does and doesn’t have the complexity of recursive watching.
1 parent 72486cc commit 6614e3c

5 files changed

Lines changed: 357 additions & 2 deletions

File tree

binding.gyp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@
119119
"vendor/efsw",
120120
],
121121
"conditions": [
122+
['OS=="linux"', {
123+
"sources+": [
124+
"lib/platform/InotifyFileWatcher.cpp"
125+
],
126+
}],
122127
['OS=="mac"', {
123128
"sources+": [
124129
"lib/platform/FSEventsFileWatcher.cpp",

lib/core.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ Napi::Value PathWatcher::Watch(const Napi::CallbackInfo &info) {
431431

432432
listener = new PathWatcherListener(env, tsfn);
433433

434-
#ifdef __APPLE__
434+
#if defined(__APPLE__) || defined(__linux__)
435435
fileWatcher = new FileWatcher();
436436
#else
437437
fileWatcher = new efsw::FileWatcher();

lib/core.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ typedef FSEventsFileWatcher FileWatcher;
1717
#endif // USE_KQUEUE
1818
#endif // __APPLE__
1919

20+
#ifdef __linux__
21+
#include "./platform/InotifyFileWatcher.hpp"
22+
typedef InotifyFileWatcher FileWatcher;
23+
#endif // __linux__
24+
2025
#ifndef _WIN32
2126
#include <sys/time.h>
2227
#endif
@@ -27,7 +32,7 @@ typedef FSEventsFileWatcher FileWatcher;
2732
#define PATH_SEPARATOR '/'
2833
#endif
2934

30-
#ifndef __APPLE__
35+
#if !defined(__APPLE__) && !defined(__linux__)
3136
typedef efsw::FileWatcher FileWatcher;
3237
#endif
3338

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
2+
#include "InotifyFileWatcher.hpp"
3+
4+
#include <errno.h>
5+
#include <limits.h>
6+
#include <poll.h>
7+
#include <string.h>
8+
#include <sys/inotify.h>
9+
#include <unistd.h>
10+
11+
#ifdef DEBUG
12+
#include <iostream>
13+
#endif
14+
15+
namespace {
16+
17+
// IN_CREATE/IN_DELETE/IN_MOVED_*: children appearing, disappearing, or being
18+
// renamed within the watched directory.
19+
// IN_MODIFY/IN_CLOSE_WRITE: content changes to children.
20+
// IN_DELETE_SELF/IN_MOVE_SELF: the watched directory itself goes away.
21+
constexpr uint32_t kWatchMask = IN_CREATE | IN_DELETE | IN_DELETE_SELF |
22+
IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO |
23+
IN_MODIFY | IN_CLOSE_WRITE;
24+
25+
constexpr size_t kEventBufLen =
26+
64 * (sizeof(struct inotify_event) + NAME_MAX + 1);
27+
28+
} // namespace
29+
30+
InotifyFileWatcher::InotifyFileWatcher() {
31+
inotifyFd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK);
32+
if (inotifyFd == -1) {
33+
isValid = false;
34+
return;
35+
}
36+
37+
// A pipe lets the destructor unblock poll() cleanly without signals.
38+
if (pipe(wakeupPipe) == -1) {
39+
close(inotifyFd);
40+
inotifyFd = -1;
41+
isValid = false;
42+
return;
43+
}
44+
45+
eventThread = std::thread(&InotifyFileWatcher::eventLoop, this);
46+
}
47+
48+
InotifyFileWatcher::~InotifyFileWatcher() {
49+
isValid = false;
50+
stopping = true;
51+
52+
// Unblock the event loop thread.
53+
char byte = 0;
54+
write(wakeupPipe[1], &byte, 1);
55+
56+
if (eventThread.joinable())
57+
eventThread.join();
58+
59+
close(wakeupPipe[0]);
60+
close(wakeupPipe[1]);
61+
// Closing the inotify fd automatically removes all of its watches.
62+
if (inotifyFd >= 0)
63+
close(inotifyFd);
64+
}
65+
66+
efsw::WatchID InotifyFileWatcher::addWatch(const std::string &path,
67+
efsw::FileWatchListener *listener,
68+
bool /* _useRecursion */) {
69+
#ifdef DEBUG
70+
std::cout << "InotifyFileWatcher::addWatch: " << path << std::endl;
71+
#endif
72+
if (!isValid)
73+
return efsw::Errors::WatcherFailed;
74+
75+
std::string dir = path;
76+
if (dir.empty() || dir.back() != '/')
77+
dir += '/';
78+
79+
int wd = inotify_add_watch(inotifyFd, dir.c_str(), kWatchMask);
80+
if (wd < 0) {
81+
switch (errno) {
82+
case ENOENT:
83+
return efsw::Errors::FileNotFound;
84+
case EACCES:
85+
return efsw::Errors::FileNotReadable;
86+
default:
87+
return efsw::Errors::WatcherFailed;
88+
}
89+
}
90+
91+
std::lock_guard<std::mutex> lock(mapMutex);
92+
efsw::WatchID handle = nextHandleID++;
93+
handlesToWatches[handle] = {dir, listener, wd};
94+
wdToHandles.insert({wd, handle});
95+
return handle;
96+
}
97+
98+
void InotifyFileWatcher::removeWatch(efsw::WatchID handle) {
99+
int wd = -1;
100+
bool wasLastHandleForWd = false;
101+
{
102+
std::lock_guard<std::mutex> lock(mapMutex);
103+
auto it = handlesToWatches.find(handle);
104+
if (it == handlesToWatches.end())
105+
return;
106+
wd = it->second.wd;
107+
handlesToWatches.erase(it);
108+
109+
auto range = wdToHandles.equal_range(wd);
110+
for (auto wit = range.first; wit != range.second; ++wit) {
111+
if (wit->second == handle) {
112+
wdToHandles.erase(wit);
113+
break;
114+
}
115+
}
116+
wasLastHandleForWd = (wdToHandles.find(wd) == wdToHandles.end());
117+
}
118+
119+
if (wasLastHandleForWd) {
120+
// EINVAL here just means the kernel already removed this watch (e.g. the
121+
// directory was deleted); safe to ignore.
122+
inotify_rm_watch(inotifyFd, wd);
123+
}
124+
}
125+
126+
void InotifyFileWatcher::forgetWatch(int wd) {
127+
std::lock_guard<std::mutex> lock(mapMutex);
128+
auto range = wdToHandles.equal_range(wd);
129+
for (auto it = range.first; it != range.second;) {
130+
handlesToWatches.erase(it->second);
131+
it = wdToHandles.erase(it);
132+
}
133+
}
134+
135+
void InotifyFileWatcher::sendFileAction(int wd, const std::string &filename,
136+
efsw::Action action,
137+
const std::string &oldFilename) {
138+
// Copy out (handle, dir, listener) under the lock, then call into listener
139+
// code without holding it.
140+
std::vector<std::tuple<efsw::WatchID, std::string, efsw::FileWatchListener *>>
141+
targets;
142+
{
143+
std::lock_guard<std::mutex> lock(mapMutex);
144+
auto range = wdToHandles.equal_range(wd);
145+
for (auto it = range.first; it != range.second; ++it) {
146+
auto hit = handlesToWatches.find(it->second);
147+
if (hit != handlesToWatches.end())
148+
targets.emplace_back(it->second, hit->second.dir, hit->second.listener);
149+
}
150+
}
151+
152+
for (auto &[handle, dir, listener] : targets)
153+
listener->handleFileAction(handle, dir, filename, action, oldFilename);
154+
}
155+
156+
void InotifyFileWatcher::eventLoop() {
157+
char buf[kEventBufLen];
158+
159+
// State for pairing IN_MOVED_FROM with a following IN_MOVED_TO that shares
160+
// its cookie and watch — i.e. a rename within the same directory.
161+
bool hasPendingMove = false;
162+
int pendingWd = -1;
163+
uint32_t pendingCookie = 0;
164+
std::string pendingFilename;
165+
166+
auto flushPendingMove = [&]() {
167+
if (!hasPendingMove)
168+
return;
169+
// No matching IN_MOVED_TO arrived: the file was moved somewhere we're not
170+
// watching (or out of the filesystem entirely).
171+
sendFileAction(pendingWd, pendingFilename, efsw::Actions::Delete);
172+
hasPendingMove = false;
173+
};
174+
175+
while (!stopping) {
176+
struct pollfd fds[2];
177+
fds[0] = {inotifyFd, POLLIN, 0};
178+
fds[1] = {wakeupPipe[0], POLLIN, 0};
179+
180+
// If we're sitting on an unpaired IN_MOVED_FROM, don't block forever —
181+
// give its IN_MOVED_TO a brief window to show up, then resolve it.
182+
int timeoutMs = hasPendingMove ? 10 : -1;
183+
184+
int r = poll(fds, 2, timeoutMs);
185+
if (r < 0) {
186+
if (errno == EINTR)
187+
continue;
188+
break;
189+
}
190+
191+
if (stopping || (fds[1].revents & POLLIN))
192+
break;
193+
194+
if (r == 0) {
195+
flushPendingMove();
196+
continue;
197+
}
198+
199+
if (!(fds[0].revents & POLLIN))
200+
continue;
201+
202+
ssize_t len = read(inotifyFd, buf, sizeof(buf));
203+
if (len <= 0)
204+
continue;
205+
206+
ssize_t i = 0;
207+
while (i < len) {
208+
auto *event = reinterpret_cast<struct inotify_event *>(&buf[i]);
209+
i += sizeof(struct inotify_event) + event->len;
210+
211+
if (event->mask & IN_Q_OVERFLOW)
212+
continue;
213+
214+
std::string filename = event->len > 0 ? std::string(event->name) : "";
215+
216+
if (hasPendingMove) {
217+
if ((event->mask & IN_MOVED_TO) && event->wd == pendingWd &&
218+
event->cookie == pendingCookie) {
219+
// Same-directory rename — almost always an atomic save.
220+
sendFileAction(event->wd, filename, efsw::Actions::Moved,
221+
pendingFilename);
222+
hasPendingMove = false;
223+
continue;
224+
}
225+
flushPendingMove();
226+
// fall through and process this event normally
227+
}
228+
229+
if (event->mask & IN_IGNORED) {
230+
// The kernel already removed this watch (explicit removeWatch,
231+
// directory deleted, or filesystem unmounted).
232+
forgetWatch(event->wd);
233+
continue;
234+
}
235+
236+
if (event->mask & IN_MOVED_FROM) {
237+
hasPendingMove = true;
238+
pendingWd = event->wd;
239+
pendingCookie = event->cookie;
240+
pendingFilename = filename;
241+
continue;
242+
}
243+
244+
if (event->mask & (IN_DELETE_SELF | IN_MOVE_SELF)) {
245+
// The watched directory itself is gone or has moved. Report it as a
246+
// deletion of the directory; the caller can re-addWatch if it cares
247+
// to keep following it (we have no way to learn its new path).
248+
sendFileAction(event->wd, "", efsw::Actions::Delete);
249+
continue;
250+
}
251+
252+
if (event->mask & IN_CREATE) {
253+
sendFileAction(event->wd, filename, efsw::Actions::Add);
254+
} else if (event->mask & IN_DELETE) {
255+
sendFileAction(event->wd, filename, efsw::Actions::Delete);
256+
} else if (event->mask & IN_MOVED_TO) {
257+
// Arrived from outside this directory (or its IN_MOVED_FROM partner
258+
// already timed out): treat it as a brand-new file.
259+
sendFileAction(event->wd, filename, efsw::Actions::Add);
260+
sendFileAction(event->wd, filename, efsw::Actions::Modified);
261+
} else if (event->mask & (IN_MODIFY | IN_CLOSE_WRITE)) {
262+
sendFileAction(event->wd, filename, efsw::Actions::Modified);
263+
}
264+
}
265+
}
266+
267+
flushPendingMove();
268+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#pragma once
2+
3+
#include "../../vendor/efsw/include/efsw/efsw.hpp"
4+
#include <atomic>
5+
#include <mutex>
6+
#include <string>
7+
#include <thread>
8+
#include <unordered_map>
9+
10+
// An API-compatible replacement for `efsw::FileWatcher` that talks to inotify
11+
// directly. Plays the same role on Linux that `KqueueFileWatcher` and
12+
// `FSEventsFileWatcher` play on macOS.
13+
//
14+
// Key differences from `FileWatcherInotify`:
15+
//
16+
// * A single `inotify` instance backs every watch; `addWatch()` just calls
17+
// `inotify_add_watch()` against it and gets back a watch descriptor (wd). No
18+
// per-path file descriptors, and no fd-limit juggling.
19+
// * No recursion: the `_useRecursion` flag is ignored, and only the directory
20+
// passed to addWatch() is watched. The existing JS layer only ever calls
21+
// `addWatch()` with a directory path — either the directory being watched
22+
// directly, or the parent of a watched file — so this matches how the old
23+
// inotify backend was used in practice anyway.
24+
// * Renames are reconstructed from `IN_MOVED_FROM`/`IN_MOVED_TO` pairs that
25+
// share a cookie and land on the same watch. This is how editors' atomic
26+
// saves (write tmp file, rename over target) show up, and it's reported as
27+
// `Moved`, which the existing JS rename-handling collapses into a `change`
28+
// event for the watched file. Cross-directory moves are reported as a Delete
29+
// (on the source watch) plus an Add+Modified (on the destination watch),
30+
// matching the old inotify-based behavior.
31+
// * If the watched directory itself is removed or renamed (`IN_DELETE_SELF` /
32+
// `IN_MOVE_SELF`), we report a single Delete for the directory. `inotify`
33+
// gives us no way to recover the new path of a moved directory, so the
34+
// caller must re-`addWatch` if it wants to keep following it.
35+
//
36+
class InotifyFileWatcher {
37+
public:
38+
InotifyFileWatcher();
39+
~InotifyFileWatcher();
40+
41+
efsw::WatchID addWatch(const std::string &path,
42+
efsw::FileWatchListener *listener,
43+
bool _useRecursion = false);
44+
45+
void removeWatch(efsw::WatchID handle);
46+
47+
bool isValid = true;
48+
49+
private:
50+
struct Watch {
51+
std::string dir; // always ends with '/'
52+
efsw::FileWatchListener *listener;
53+
int wd;
54+
};
55+
56+
void eventLoop();
57+
58+
// Looks up every handle registered for `wd` and dispatches `action` to each
59+
// of their listeners.
60+
void sendFileAction(int wd, const std::string &filename, efsw::Action action,
61+
const std::string &oldFilename = "");
62+
63+
// Drops all bookkeeping for `wd` without calling `inotify_rm_watch()`; used
64+
// when the kernel tells us (via IN_IGNORED) that it already dropped the
65+
// watch on its own (e.g. the directory was deleted or unmounted).
66+
void forgetWatch(int wd);
67+
68+
long nextHandleID = 1;
69+
int inotifyFd = -1;
70+
int wakeupPipe[2] = {-1, -1};
71+
std::atomic<bool> stopping{false};
72+
std::mutex mapMutex;
73+
std::thread eventThread;
74+
75+
std::unordered_map<efsw::WatchID, Watch> handlesToWatches;
76+
std::unordered_multimap<int, efsw::WatchID> wdToHandles;
77+
};

0 commit comments

Comments
 (0)