1 /// Reactor friendly interface for Linux's inotify
2 module mecca.reactor.io.inotify;
3 
4 // Licensed under the Boost license. Full copyright information in the AUTHORS file
5 
6 version (linux):
7 
8 import core.sys.linux.sys.inotify;
9 
10 import mecca.lib.exception;
11 import mecca.lib..string;
12 import mecca.lib.typedid;
13 import mecca.log;
14 import mecca.reactor.io.fd;
15 import mecca.reactor;
16 
17 private extern(C) nothrow @nogc {
18     int inotify_init1(int flags) @safe;
19     //int inotify_add_watch(int fd, const(char)* name, uint mask);
20     //int inotify_rm_watch(int fd, uint wd) @safe;
21 
22     enum NAME_MAX = 255;
23 }
24 
25 /// Watch point handle type
26 alias WatchDescriptor = TypedIdentifier!("WatchDescriptor", int, -1, -1);
27 
28 /**
29  * iNotify type.
30  *
31  * Create as many instances of this as you need. Each one has its own inotify file descriptor, and manages its own events
32  */
33 struct Inotifier {
34     /**
35      * The type of reported events
36      *
37      * Full documentation is available in the <a href="https://linux.die.net/man/7/inotify">inotify man page</a>, except for the name
38      * field, which returns a D string.
39      */
40     struct Event {
41         @disable this(this);
42 
43         inotify_event event;
44         @property WatchDescriptor wd() const pure nothrow @safe @nogc {
45             return WatchDescriptor( event.wd );
46         }
47 
48         @property string name() const nothrow @trusted @nogc {
49             if( event.len==0 )
50                 return null;
51 
52             import std..string : indexOf;
53 
54             const char* basePtr = event.name.ptr;
55             string ret = cast(immutable(char)[])basePtr[0 .. event.len];
56             auto nullIdx = indexOf( ret, '\0' );
57             ASSERT!"Name returned from inotify not null terminated"( nullIdx>=0 );
58             return ret[0..nullIdx];
59         }
60 
61         alias event this;
62     }
63 
64     enum IN_ACCESS = .IN_ACCESS; /// See definition in the <a href="https://linux.die.net/man/7/inotify">inotify man page</a>.
65     enum IN_MODIFY = .IN_MODIFY; /// ditto
66     enum IN_ATTRIB = .IN_ATTRIB; /// ditto
67     enum IN_CLOSE_WRITE = .IN_CLOSE_WRITE; /// ditto
68     enum IN_CLOSE_NOWRITE = .IN_CLOSE_NOWRITE; /// ditto
69     enum IN_OPEN = .IN_OPEN; /// ditto
70     enum IN_MOVED_FROM = .IN_MOVED_FROM; /// ditto
71     enum IN_MOVED_TO = .IN_MOVED_TO; /// ditto
72     enum IN_CREATE = .IN_CREATE; /// ditto
73     enum IN_DELETE = .IN_DELETE; /// ditto
74     enum IN_DELETE_SELF = .IN_DELETE_SELF; /// ditto
75     enum IN_MOVE_SELF = .IN_MOVE_SELF; /// ditto
76     //enum IN_UNMOUNT = .IN_UNMOUNT; /// ditto
77     enum IN_Q_OVERFLOW = .IN_Q_OVERFLOW; /// ditto
78     enum IN_IGNORED = .IN_IGNORED; /// ditto
79     enum IN_CLOSE = .IN_CLOSE; /// ditto
80     enum IN_MOVE = .IN_MOVE; /// ditto
81     enum IN_ONLYDIR = .IN_ONLYDIR; /// ditto
82     enum IN_DONT_FOLLOW = .IN_DONT_FOLLOW; /// ditto
83     enum IN_EXCL_UNLINK = .IN_EXCL_UNLINK; /// ditto
84     enum IN_MASK_ADD = .IN_MASK_ADD; /// ditto
85     enum IN_ISDIR = .IN_ISDIR; /// ditto
86     enum IN_ONESHOT = .IN_ONESHOT; /// ditto
87     enum IN_ALL_EVENTS = .IN_ALL_EVENTS; /// ditto
88 private:
89     ReactorFD fd;
90 
91     enum EVENTS_BUFFER_SIZE = 512;
92 
93     // Cache for already extracted events
94     static assert( EVENTS_BUFFER_SIZE >= inotify_event.sizeof + NAME_MAX + 1, "events buffer not big enough for one event" );
95     void[EVENTS_BUFFER_SIZE] eventsBuffer;
96     uint bufferSize;
97     uint bufferConsumed;
98 
99 public:
100     /// call before using the inotifyer
101     void open() @safe @nogc {
102         ASSERT!"Inotifier.open called twice"( !fd.isValid );
103         int inotifyFd = inotify_init1(IN_NONBLOCK|IN_CLOEXEC);
104         errnoEnforceNGC( inotifyFd>=0, "inotify_init failed" );
105         fd = ReactorFD( inotifyFd, true );
106     }
107 
108     ~this() nothrow @safe @nogc {
109         close();
110     }
111 
112     /// call when you're done with the inotifyer
113     void close() nothrow @safe @nogc {
114         fd.close();
115     }
116 
117     /// Report whether Inotifier is open
118     bool isOpen() const nothrow @safe @nogc {
119         return fd.isValid;
120     }
121 
122     /**
123      * add or change a watch point
124      *
125      * Params:
126      * path = path to be watched
127      * mask = the mask of events to be watched
128      *
129      * Returns:
130      * the WatchDescriptor of the added or modified watch point.
131      */
132     WatchDescriptor watch( const(char)[] path, uint mask ) @trusted @nogc {
133         auto wd = WatchDescriptor( fd.osCallErrno!(.inotify_add_watch)(ToStringz!(NAME_MAX+1)(path), mask) );
134 
135         return wd;
136     }
137 
138     /**
139      * remove a previously registered watch point
140      *
141      * Params:
142      * wd = the WatchDescriptor returned by watch
143      */
144     void removeWatch( WatchDescriptor wd ) @trusted @nogc {
145         fd.osCallErrno!(.inotify_rm_watch)( wd.value );
146     }
147 
148     /**
149      * get one pending inotify event
150      *
151      * This function may sleep.
152      *
153      * Returns:
154      * A pointer to a const Event. This pointer remains valid until the next call to `getEvent` or `consumeAllEvents`.
155      *
156      * Bugs:
157      * Only one fiber at a time may use this function
158      */
159     const(Event)* getEvent() @trusted @nogc {
160         if( bufferSize==bufferConsumed ) {
161             bufferConsumed = bufferSize = 0;
162 
163             bufferSize = cast(uint) fd.read( eventsBuffer );
164         }
165 
166         assertGT( bufferSize, bufferConsumed, "getEvent with no events" );
167         assertGE( bufferConsumed - bufferSize, inotify_event.sizeof, "events buffer with partial event" );
168 
169         const(Event)* ret = cast(Event*)(&eventsBuffer[bufferConsumed]);
170         bufferConsumed += inotify_event.sizeof + ret.len;
171 
172         return ret;
173     }
174 
175     /**
176      * Discard all pending events.
177      *
178      * This function discards all pending events, both from the memory cache and from the OS. All events in the inotify fd are discarded,
179      * regardless of which watch descriptor they belong to.
180      *
181      * This function does not sleep.
182      */
183     void consumeAllEvents() @trusted @nogc {
184         with( theReactor.criticalSection ) {
185             // Clear the memory cache
186             bufferConsumed = bufferSize = 0;
187 
188             bool moreEvents = true;
189             do {
190                 import core.sys.posix.unistd : read;
191                 import core.stdc.errno;
192 
193                 auto size = fd.osCall!(read)(eventsBuffer.ptr, eventsBuffer.length);
194                 if( size<0 ) {
195                     errnoEnforceNGC( errno==EAGAIN || errno==EWOULDBLOCK, "inotify read failed" );
196                     moreEvents = false;
197                 }
198             } while( moreEvents );
199         }
200     }
201 }
202 
203 unittest {
204     void watcher(string path) {
205         Inotifier inote;
206         inote.open();
207 
208         auto wd = inote.watch(path, IN_MODIFY|IN_ATTRIB|IN_CREATE|IN_DELETE|IN_MOVED_FROM|IN_MOVED_TO|IN_ONLYDIR);
209         DEBUG!"watch registration returned %s"(wd);
210 
211         while( true ) {
212             auto event = inote.getEvent();
213             INFO!"handle %s mask %x cookie %s len %s name %s"( event.wd, event.mask, event.cookie, event.len, event.name );
214         }
215     }
216 
217     void testBody() {
218         import core.sys.posix.stdlib;
219         import std..string;
220         import std.file;
221         import std.process;
222         import std.exception;
223 
224         import mecca.lib.time;
225 
226         string iPath = format("%s/meccaUT-XXXXXX\0", tempDir());
227         char[] path;
228         path.length = iPath.length;
229         path[] = iPath[];
230         errnoEnforce( mkdtemp(path.ptr) !is null );
231         scope(exit) execute( ["rm", "-rf", path] );
232 
233         iPath.length = 0;
234         iPath ~= path[0..$-1];
235         DEBUG!"Using directory %s for inotify tests"( iPath );
236 
237         theReactor.spawnFiber( &watcher, iPath );
238 
239         theReactor.yield();
240 
241         execute( ["touch", iPath ~ "/file1"] );
242         rename( iPath ~ "/file1", iPath ~ "/file2" );
243 
244         theReactor.sleep(1.msecs);
245     }
246 
247     testWithReactor(&testBody);
248 }