1 /**
2 * watchdog_fsevents.c: Python-C bridge to the OS X FSEvents API.
3 *
4 * Copyright 2018-2024 Mickaël Schoentgen & contributors
5 * Copyright 2012-2018 Google, Inc.
6 * Copyright 2011-2012 Yesudeep Mangalapilly <[email protected]>
7 * Copyright 2010-2011 Malthe Borch <[email protected]>
8 */
9
10
11 #include <Python.h>
12 #include <Availability.h>
13 #include <CoreFoundation/CoreFoundation.h>
14 #include <CoreServices/CoreServices.h>
15 #include <stdlib.h>
16 #include <signal.h>
17
18
19 /* Compatibility; since fsevents won't set these on earlier macOS versions the properties will always be False */
20 #if MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_13
21 #error Watchdog module requires at least macOS 10.13
22 #endif
23
24 /* Convenience macros to make code more readable. */
25 #define G_NOT(o) !o
26 #define G_IS_NULL(o) o == NULL
27 #define G_IS_NOT_NULL(o) o != NULL
28 #define G_RETURN_NULL_IF_NULL(o) do { if (NULL == o) { return NULL; } } while (0)
29 #define G_RETURN_NULL_IF(condition) do { if (condition) { return NULL; } } while (0)
30 #define G_RETURN_NULL_IF_NOT(condition) do { if (!condition) { return NULL; } } while (0)
31 #define G_RETURN_IF(condition) do { if (condition) { return; } } while (0)
32 #define G_RETURN_IF_NOT(condition) do { if (!condition) { return; } } while (0)
33 #define UNUSED(x) (void)x
34
35 /* Error message definitions. */
36 #define ERROR_CANNOT_CALL_CALLBACK "Unable to call Python callback."
37
38 /* Other information. */
39 #define MODULE_NAME "_watchdog_fsevents"
40
41 /**
42 * Event stream callback contextual information passed to
43 * our ``watchdog_FSEventStreamCallback`` function by the
44 * FSEvents API whenever an event occurs.
45 */
46 typedef struct {
47 /**
48 * A pointer to the Python callback which will
49 * will in turn be called by our event handler
50 * with event information. The Python callback
51 * function must accept 2 arguments, both of which
52 * are Python lists::
53 *
54 * def python_callback(event_paths, event_inodes, event_flags, event_ids):
55 * pass
56 */
57 PyObject *python_callback;
58 /**
59 * A pointer to the associated ``FSEventStream``
60 * instance.
61 */
62 FSEventStreamRef stream_ref;
63 /**
64 * A pointer to the associated ``CFRunLoop``
65 * instance.
66 */
67 CFRunLoopRef run_loop_ref;
68 /**
69 * A pointer to the state of the Python thread.
70 */
71 PyThreadState *thread_state;
72 } StreamCallbackInfo;
73
74
75 /**
76 * NativeEvent type so that we don't need to expose the FSEvents constants to Python land
77 */
78 typedef struct {
79 PyObject_HEAD
80 const char *path;
81 PyObject *inode;
82 FSEventStreamEventFlags flags;
83 FSEventStreamEventId id;
84 } NativeEventObject;
85
NativeEventRepr(PyObject * instance)86 PyObject* NativeEventRepr(PyObject* instance) {
87 NativeEventObject *self = (NativeEventObject*)instance;
88
89 return PyUnicode_FromFormat(
90 "NativeEvent(path=\"%s\", inode=%S, flags=%x, id=%llu)",
91 self->path,
92 self->inode,
93 self->flags,
94 self->id
95 );
96 }
97
NativeEventTypeFlags(PyObject * instance,void * closure)98 PyObject* NativeEventTypeFlags(PyObject* instance, void* closure)
99 {
100 UNUSED(closure);
101 NativeEventObject *self = (NativeEventObject*)instance;
102 return PyLong_FromLong(self->flags);
103 }
104
NativeEventTypePath(PyObject * instance,void * closure)105 PyObject* NativeEventTypePath(PyObject* instance, void* closure)
106 {
107 UNUSED(closure);
108 NativeEventObject *self = (NativeEventObject*)instance;
109 return PyUnicode_FromString(self->path);
110 }
111
NativeEventTypeInode(PyObject * instance,void * closure)112 PyObject* NativeEventTypeInode(PyObject* instance, void* closure)
113 {
114 UNUSED(closure);
115 NativeEventObject *self = (NativeEventObject*)instance;
116 Py_INCREF(self->inode);
117 return self->inode;
118 }
119
NativeEventTypeID(PyObject * instance,void * closure)120 PyObject* NativeEventTypeID(PyObject* instance, void* closure)
121 {
122 UNUSED(closure);
123 NativeEventObject *self = (NativeEventObject*)instance;
124 return PyLong_FromLong(self->id);
125 }
126
NativeEventTypeIsCoalesced(PyObject * instance,void * closure)127 PyObject* NativeEventTypeIsCoalesced(PyObject* instance, void* closure)
128 {
129 UNUSED(closure);
130 NativeEventObject *self = (NativeEventObject*)instance;
131
132 // if any of these bitmasks match then we have a coalesced event and need to do sys calls to figure out what happened
133 FSEventStreamEventFlags coalesced_masks[] = {
134 kFSEventStreamEventFlagItemCreated | kFSEventStreamEventFlagItemRemoved,
135 kFSEventStreamEventFlagItemCreated | kFSEventStreamEventFlagItemRenamed,
136 kFSEventStreamEventFlagItemRemoved | kFSEventStreamEventFlagItemRenamed,
137 };
138 for (size_t i = 0; i < sizeof(coalesced_masks) / sizeof(FSEventStreamEventFlags); ++i) {
139 if ((self->flags & coalesced_masks[i]) == coalesced_masks[i]) {
140 Py_RETURN_TRUE;
141 }
142 }
143
144 Py_RETURN_FALSE;
145 }
146
147 #define FLAG_PROPERTY(suffix, flag) \
148 PyObject* NativeEventType##suffix(PyObject* instance, void* closure) \
149 { \
150 UNUSED(closure); \
151 NativeEventObject *self = (NativeEventObject*)instance; \
152 if (self->flags & flag) { \
153 Py_RETURN_TRUE; \
154 } \
155 Py_RETURN_FALSE; \
156 }
157
FLAG_PROPERTY(IsMustScanSubDirs,kFSEventStreamEventFlagMustScanSubDirs)158 FLAG_PROPERTY(IsMustScanSubDirs, kFSEventStreamEventFlagMustScanSubDirs)
159 FLAG_PROPERTY(IsUserDropped, kFSEventStreamEventFlagUserDropped)
160 FLAG_PROPERTY(IsKernelDropped, kFSEventStreamEventFlagKernelDropped)
161 FLAG_PROPERTY(IsEventIdsWrapped, kFSEventStreamEventFlagEventIdsWrapped)
162 FLAG_PROPERTY(IsHistoryDone, kFSEventStreamEventFlagHistoryDone)
163 FLAG_PROPERTY(IsRootChanged, kFSEventStreamEventFlagRootChanged)
164 FLAG_PROPERTY(IsMount, kFSEventStreamEventFlagMount)
165 FLAG_PROPERTY(IsUnmount, kFSEventStreamEventFlagUnmount)
166 FLAG_PROPERTY(IsCreated, kFSEventStreamEventFlagItemCreated)
167 FLAG_PROPERTY(IsRemoved, kFSEventStreamEventFlagItemRemoved)
168 FLAG_PROPERTY(IsInodeMetaMod, kFSEventStreamEventFlagItemInodeMetaMod)
169 FLAG_PROPERTY(IsRenamed, kFSEventStreamEventFlagItemRenamed)
170 FLAG_PROPERTY(IsModified, kFSEventStreamEventFlagItemModified)
171 FLAG_PROPERTY(IsItemFinderInfoMod, kFSEventStreamEventFlagItemFinderInfoMod)
172 FLAG_PROPERTY(IsChangeOwner, kFSEventStreamEventFlagItemChangeOwner)
173 FLAG_PROPERTY(IsXattrMod, kFSEventStreamEventFlagItemXattrMod)
174 FLAG_PROPERTY(IsFile, kFSEventStreamEventFlagItemIsFile)
175 FLAG_PROPERTY(IsDirectory, kFSEventStreamEventFlagItemIsDir)
176 FLAG_PROPERTY(IsSymlink, kFSEventStreamEventFlagItemIsSymlink)
177 FLAG_PROPERTY(IsOwnEvent, kFSEventStreamEventFlagOwnEvent)
178 FLAG_PROPERTY(IsHardlink, kFSEventStreamEventFlagItemIsHardlink)
179 FLAG_PROPERTY(IsLastHardlink, kFSEventStreamEventFlagItemIsLastHardlink)
180 FLAG_PROPERTY(IsCloned, kFSEventStreamEventFlagItemCloned)
181
182 static int NativeEventInit(NativeEventObject *self, PyObject *args, PyObject *kwds)
183 {
184 static char *kwlist[] = {"path", "inode", "flags", "id", NULL};
185
186 self->inode = NULL;
187
188 if (!PyArg_ParseTupleAndKeywords(args, kwds, "|sOIL", kwlist, &self->path, &self->inode, &self->flags, &self->id)) {
189 return -1;
190 }
191
192 Py_INCREF(self->inode);
193
194 return 0;
195 }
196
NativeEventDealloc(NativeEventObject * self)197 static void NativeEventDealloc(NativeEventObject *self) {
198 Py_XDECREF(self->inode);
199 }
200
201 static PyGetSetDef NativeEventProperties[] = {
202 {"flags", NativeEventTypeFlags, NULL, "The raw mask of flags as returned by FSEvents", NULL},
203 {"path", NativeEventTypePath, NULL, "The path for which this event was generated", NULL},
204 {"inode", NativeEventTypeInode, NULL, "The inode for which this event was generated", NULL},
205 {"event_id", NativeEventTypeID, NULL, "The id of the generated event", NULL},
206 {"is_coalesced", NativeEventTypeIsCoalesced, NULL, "True if multiple ambiguous changes to the monitored path happened", NULL},
207 {"must_scan_subdirs", NativeEventTypeIsMustScanSubDirs, NULL, "True if application must rescan all subdirectories", NULL},
208 {"is_user_dropped", NativeEventTypeIsUserDropped, NULL, "True if a failure during event buffering occurred", NULL},
209 {"is_kernel_dropped", NativeEventTypeIsKernelDropped, NULL, "True if a failure during event buffering occurred", NULL},
210 {"is_event_ids_wrapped", NativeEventTypeIsEventIdsWrapped, NULL, "True if event_id wrapped around", NULL},
211 {"is_history_done", NativeEventTypeIsHistoryDone, NULL, "True if all historical events are done", NULL},
212 {"is_root_changed", NativeEventTypeIsRootChanged, NULL, "True if a change to one of the directories along the path to one of the directories you watch occurred", NULL},
213 {"is_mount", NativeEventTypeIsMount, NULL, "True if a volume is mounted underneath one of the paths being monitored", NULL},
214 {"is_unmount", NativeEventTypeIsUnmount, NULL, "True if a volume is unmounted underneath one of the paths being monitored", NULL},
215 {"is_created", NativeEventTypeIsCreated, NULL, "True if self.path was created on the filesystem", NULL},
216 {"is_removed", NativeEventTypeIsRemoved, NULL, "True if self.path was removed from the filesystem", NULL},
217 {"is_inode_meta_mod", NativeEventTypeIsInodeMetaMod, NULL, "True if meta data for self.path was modified ", NULL},
218 {"is_renamed", NativeEventTypeIsRenamed, NULL, "True if self.path was renamed on the filesystem", NULL},
219 {"is_modified", NativeEventTypeIsModified, NULL, "True if self.path was modified", NULL},
220 {"is_item_finder_info_modified", NativeEventTypeIsItemFinderInfoMod, NULL, "True if FinderInfo for self.path was modified", NULL},
221 {"is_owner_change", NativeEventTypeIsChangeOwner, NULL, "True if self.path had its ownership changed", NULL},
222 {"is_xattr_mod", NativeEventTypeIsXattrMod, NULL, "True if extended attributes for self.path were modified ", NULL},
223 {"is_file", NativeEventTypeIsFile, NULL, "True if self.path is a file", NULL},
224 {"is_directory", NativeEventTypeIsDirectory, NULL, "True if self.path is a directory", NULL},
225 {"is_symlink", NativeEventTypeIsSymlink, NULL, "True if self.path is a symbolic link", NULL},
226 {"is_own_event", NativeEventTypeIsOwnEvent, NULL, "True if the event originated from our own process", NULL},
227 {"is_hardlink", NativeEventTypeIsHardlink, NULL, "True if self.path is a hard link", NULL},
228 {"is_last_hardlink", NativeEventTypeIsLastHardlink, NULL, "True if self.path was the last hard link", NULL},
229 {"is_cloned", NativeEventTypeIsCloned, NULL, "True if self.path is a clone or was cloned", NULL},
230 {NULL, NULL, NULL, NULL, NULL},
231 };
232
233
234 static PyTypeObject NativeEventType = {
235 PyVarObject_HEAD_INIT(NULL, 0)
236 .tp_name = "_watchdog_fsevents.NativeEvent",
237 .tp_doc = "A wrapper around native FSEvents events",
238 .tp_basicsize = sizeof(NativeEventObject),
239 .tp_itemsize = 0,
240 .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
241 .tp_new = PyType_GenericNew,
242 .tp_getset = NativeEventProperties,
243 .tp_init = (initproc) NativeEventInit,
244 .tp_repr = (reprfunc) NativeEventRepr,
245 .tp_dealloc = (destructor) NativeEventDealloc,
246 };
247
248
249 /**
250 * Dictionary to keep track of which run loop
251 * belongs to which emitter thread.
252 */
253 PyObject *thread_to_run_loop = NULL;
254
255 /**
256 * Dictionary to keep track of which stream
257 * belongs to which watch.
258 */
259 PyObject *watch_to_stream = NULL;
260
261
262 /**
263 * PyCapsule destructor.
264 */
watchdog_pycapsule_destructor(PyObject * ptr)265 static void watchdog_pycapsule_destructor(PyObject *ptr)
266 {
267 void *p = PyCapsule_GetPointer(ptr, NULL);
268 if (p) {
269 PyMem_Free(p);
270 }
271 }
272
273
274
275 /**
276 * Converts a ``CFStringRef`` to a Python string object.
277 *
278 * :param cf_string:
279 * A ``CFStringRef``.
280 * :returns:
281 * A Python unicode or utf-8 encoded bytestring object.
282 */
CFString_AsPyUnicode(CFStringRef cf_string_ref)283 PyObject * CFString_AsPyUnicode(CFStringRef cf_string_ref)
284 {
285
286 if (G_IS_NULL(cf_string_ref)) {
287 return PyUnicode_FromString("");
288 }
289
290 PyObject *py_string;
291
292 const char *c_string_ptr = CFStringGetCStringPtr(cf_string_ref, kCFStringEncodingUTF8);
293
294 if (G_IS_NULL(c_string_ptr)) {
295 CFIndex length = CFStringGetLength(cf_string_ref);
296 CFIndex max_size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
297 char *buffer = (char *)malloc(max_size);
298 if (CFStringGetCString(cf_string_ref, buffer, max_size, kCFStringEncodingUTF8)) {
299 py_string = PyUnicode_FromString(buffer);
300 }
301 else {
302 py_string = PyUnicode_FromString("");
303 }
304 free(buffer);
305 } else {
306 py_string = PyUnicode_FromString(c_string_ptr);
307 }
308
309 return py_string;
310
311 }
312
313 /**
314 * Converts a ``CFNumberRef`` to a Python string object.
315 *
316 * :param cf_number:
317 * A ``CFNumberRef``.
318 * :returns:
319 * A Python unicode or utf-8 encoded bytestring object.
320 */
CFNumberRef_AsPyLong(CFNumberRef cf_number)321 PyObject * CFNumberRef_AsPyLong(CFNumberRef cf_number)
322 {
323 long c_int;
324 PyObject *py_long;
325
326 CFNumberGetValue(cf_number, kCFNumberSInt64Type, &c_int);
327
328 py_long = PyLong_FromLong(c_int);
329
330 return py_long;
331 }
332
333
334 /**
335 * This is the callback passed to the FSEvents API, which calls
336 * the Python callback function, in turn, by passing in event data
337 * as Python objects.
338 *
339 * :param stream_ref:
340 * A pointer to an ``FSEventStream`` instance.
341 * :param stream_callback_info_ref:
342 * Callback context information passed by the FSEvents API.
343 * This contains a reference to the Python callback that this
344 * function calls in turn with information about the events.
345 * :param num_events:
346 * An unsigned integer representing the number of events
347 * captured by the FSEvents API.
348 * :param event_paths:
349 * An array of NUL-terminated C strings representing event paths.
350 * :param event_flags:
351 * An array of ``FSEventStreamEventFlags`` unsigned integral
352 * mask values.
353 * :param event_ids:
354 * An array of 64-bit unsigned integers representing event
355 * identifiers.
356 */
357 static void
watchdog_FSEventStreamCallback(ConstFSEventStreamRef stream_ref,StreamCallbackInfo * stream_callback_info_ref,size_t num_events,CFArrayRef event_path_info_array_ref,const FSEventStreamEventFlags event_flags[],const FSEventStreamEventId event_ids[])358 watchdog_FSEventStreamCallback(ConstFSEventStreamRef stream_ref,
359 StreamCallbackInfo *stream_callback_info_ref,
360 size_t num_events,
361 CFArrayRef event_path_info_array_ref,
362 const FSEventStreamEventFlags event_flags[],
363 const FSEventStreamEventId event_ids[])
364 {
365 UNUSED(stream_ref);
366 size_t i = 0;
367 CFDictionaryRef path_info_dict;
368 CFStringRef cf_path;
369 CFNumberRef cf_inode;
370 PyObject *callback_result = NULL;
371 PyObject *path = NULL;
372 PyObject *inode = NULL;
373 PyObject *id = NULL;
374 PyObject *flags = NULL;
375 PyObject *py_event_flags = NULL;
376 PyObject *py_event_ids = NULL;
377 PyObject *py_event_paths = NULL;
378 PyObject *py_event_inodes = NULL;
379 PyThreadState *saved_thread_state = NULL;
380
381 /* Acquire interpreter lock and save original thread state. */
382 PyGILState_STATE gil_state = PyGILState_Ensure();
383 saved_thread_state = PyThreadState_Swap(stream_callback_info_ref->thread_state);
384
385 /* Convert event flags and paths to Python ints and strings. */
386 py_event_paths = PyList_New(num_events);
387 py_event_inodes = PyList_New(num_events);
388 py_event_flags = PyList_New(num_events);
389 py_event_ids = PyList_New(num_events);
390 if (G_NOT(py_event_paths && py_event_inodes && py_event_flags && py_event_ids))
391 {
392 Py_XDECREF(py_event_paths);
393 Py_XDECREF(py_event_inodes);
394 Py_XDECREF(py_event_ids);
395 Py_XDECREF(py_event_flags);
396 return /*NULL*/;
397 }
398 for (i = 0; i < num_events; ++i)
399 {
400 id = PyLong_FromLongLong(event_ids[i]);
401 flags = PyLong_FromLong(event_flags[i]);
402
403 path_info_dict = CFArrayGetValueAtIndex(event_path_info_array_ref, i);
404 cf_path = CFDictionaryGetValue(path_info_dict, kFSEventStreamEventExtendedDataPathKey);
405 cf_inode = CFDictionaryGetValue(path_info_dict, kFSEventStreamEventExtendedFileIDKey);
406
407 path = CFString_AsPyUnicode(cf_path);
408
409 if (G_IS_NOT_NULL(cf_inode)) {
410 inode = CFNumberRef_AsPyLong(cf_inode);
411 } else {
412 Py_INCREF(Py_None);
413 inode = Py_None;
414 }
415
416 if (G_NOT(path && inode && flags && id))
417 {
418 Py_DECREF(py_event_paths);
419 Py_DECREF(py_event_inodes);
420 Py_DECREF(py_event_ids);
421 Py_DECREF(py_event_flags);
422 return /*NULL*/;
423 }
424 PyList_SET_ITEM(py_event_paths, i, path);
425 PyList_SET_ITEM(py_event_inodes, i, inode);
426 PyList_SET_ITEM(py_event_flags, i, flags);
427 PyList_SET_ITEM(py_event_ids, i, id);
428 }
429
430 /* Call the Python callback function supplied by the stream information
431 * struct. The Python callback function should accept two arguments,
432 * both being Python lists:
433 *
434 * def python_callback(event_paths, event_flags, event_ids):
435 * pass
436 */
437 callback_result = \
438 PyObject_CallFunction(stream_callback_info_ref->python_callback,
439 "OOOO", py_event_paths, py_event_inodes, py_event_flags, py_event_ids);
440 if (G_IS_NULL(callback_result))
441 {
442 if (G_NOT(PyErr_Occurred()))
443 {
444 PyErr_SetString(PyExc_ValueError, ERROR_CANNOT_CALL_CALLBACK);
445 }
446 CFRunLoopStop(stream_callback_info_ref->run_loop_ref);
447 }
448
449 /* Release the lock and restore thread state. */
450 PyThreadState_Swap(saved_thread_state);
451 PyGILState_Release(gil_state);
452 }
453
454
455 /**
456 * Converts a Python string object to an UTF-8 encoded ``CFStringRef``.
457 *
458 * :param py_string:
459 * A Python unicode or utf-8 encoded bytestring object.
460 * :returns:
461 * A new ``CFStringRef`` with the contents of ``py_string``, or ``NULL`` if an error occurred.
462 */
PyString_AsUTF8EncodedCFStringRef(PyObject * py_string)463 CFStringRef PyString_AsUTF8EncodedCFStringRef(PyObject *py_string)
464 {
465 CFStringRef cf_string = NULL;
466
467 if (PyUnicode_Check(py_string)) {
468 PyObject* helper = PyUnicode_AsUTF8String(py_string);
469 if (!helper) {
470 return NULL;
471 }
472 cf_string = CFStringCreateWithCString(kCFAllocatorDefault, PyBytes_AS_STRING(helper), kCFStringEncodingUTF8);
473 Py_DECREF(helper);
474 } else if (PyBytes_Check(py_string)) {
475 PyObject *utf8 = PyUnicode_FromEncodedObject(py_string, NULL, "strict");
476 if (!utf8) {
477 return NULL;
478 }
479 Py_DECREF(utf8);
480 cf_string = CFStringCreateWithCString(kCFAllocatorDefault, PyBytes_AS_STRING(py_string), kCFStringEncodingUTF8);
481 } else {
482 PyErr_SetString(PyExc_TypeError, "Path to watch must be a string or a UTF-8 encoded bytes object.");
483 return NULL;
484 }
485
486 return cf_string;
487 }
488
489 /**
490 * Converts a list of Python strings to a ``CFMutableArray`` of
491 * UTF-8 encoded ``CFString`` instances and returns a pointer to
492 * the array.
493 *
494 * :param py_string_list:
495 * List of Python strings.
496 * :returns:
497 * A pointer to ``CFMutableArray`` (that is, a
498 * ``CFMutableArrayRef``) of UTF-8 encoded ``CFString``
499 * instances.
500 */
501 static CFMutableArrayRef
watchdog_CFMutableArrayRef_from_PyStringList(PyObject * py_string_list)502 watchdog_CFMutableArrayRef_from_PyStringList(PyObject *py_string_list)
503 {
504 Py_ssize_t i = 0;
505 Py_ssize_t string_list_size = 0;
506 CFMutableArrayRef array_of_cf_string = NULL;
507 CFStringRef cf_string = NULL;
508 PyObject *py_string = NULL;
509
510 G_RETURN_NULL_IF_NULL(py_string_list);
511
512 string_list_size = PyList_Size(py_string_list);
513
514 /* Allocate a CFMutableArray. */
515 array_of_cf_string = CFArrayCreateMutable(kCFAllocatorDefault, 1,
516 &kCFTypeArrayCallBacks);
517 G_RETURN_NULL_IF_NULL(array_of_cf_string);
518
519 /* Loop through the Python string list and copy strings to the
520 * CFString array list. */
521 for (i = 0; i < string_list_size; ++i)
522 {
523 py_string = PyList_GetItem(py_string_list, i);
524 G_RETURN_NULL_IF_NULL(py_string);
525 cf_string = PyString_AsUTF8EncodedCFStringRef(py_string);
526 G_RETURN_NULL_IF_NULL(cf_string);
527 CFArraySetValueAtIndex(array_of_cf_string, i, cf_string);
528 CFRelease(cf_string);
529 }
530
531 return array_of_cf_string;
532 }
533
534
535 /**
536 * Creates an instance of ``FSEventStream`` and returns a pointer
537 * to the instance.
538 *
539 * :param stream_callback_info_ref:
540 * Pointer to the callback context information that will be
541 * passed by the FSEvents API to the callback handler specified
542 * by the ``callback`` argument to this function. This
543 * information contains a reference to the Python callback that
544 * it must call in turn passing on the event information
545 * as Python objects to the the Python callback.
546 * :param py_paths:
547 * A Python list of Python strings representing path names
548 * to monitor.
549 * :param callback:
550 * A function pointer of type ``FSEventStreamCallback``.
551 * :returns:
552 * A pointer to an ``FSEventStream`` instance (that is, it returns
553 * an ``FSEventStreamRef``).
554 */
555 static FSEventStreamRef
watchdog_FSEventStreamCreate(StreamCallbackInfo * stream_callback_info_ref,PyObject * py_paths,FSEventStreamCallback callback)556 watchdog_FSEventStreamCreate(StreamCallbackInfo *stream_callback_info_ref,
557 PyObject *py_paths,
558 FSEventStreamCallback callback)
559 {
560 CFAbsoluteTime stream_latency = 0.01;
561 CFMutableArrayRef paths = NULL;
562 FSEventStreamRef stream_ref = NULL;
563
564 /* Check arguments. */
565 G_RETURN_NULL_IF_NULL(py_paths);
566 G_RETURN_NULL_IF_NULL(callback);
567
568 /* Convert the Python paths list to a CFMutableArray. */
569 paths = watchdog_CFMutableArrayRef_from_PyStringList(py_paths);
570 G_RETURN_NULL_IF_NULL(paths);
571
572 /* Create the event stream. */
573 FSEventStreamContext stream_context = {
574 0, stream_callback_info_ref, NULL, NULL, NULL
575 };
576 stream_ref = FSEventStreamCreate(kCFAllocatorDefault,
577 callback,
578 &stream_context,
579 paths,
580 kFSEventStreamEventIdSinceNow,
581 stream_latency,
582 kFSEventStreamCreateFlagNoDefer
583 | kFSEventStreamCreateFlagFileEvents
584 | kFSEventStreamCreateFlagWatchRoot
585 | kFSEventStreamCreateFlagUseExtendedData
586 | kFSEventStreamCreateFlagUseCFTypes);
587 CFRelease(paths);
588 return stream_ref;
589 }
590
591
592 PyDoc_STRVAR(watchdog_add_watch__doc__,
593 MODULE_NAME ".add_watch(emitter_thread, watch, callback, paths) -> None\
594 \nAdds a watch into the event loop for the given emitter thread.\n\n\
595 :param emitter_thread:\n\
596 The emitter thread.\n\
597 :param watch:\n\
598 The watch to add.\n\
599 :param callback:\n\
600 The callback function to call when an event occurs.\n\n\
601 Example::\n\n\
602 def callback(paths, flags, ids):\n\
603 for path, flag, event_id in zip(paths, flags, ids):\n\
604 print(\"%d: %s=%ul\" % (event_id, path, flag))\n\
605 :param paths:\n\
606 A list of paths to monitor.\n");
607 static PyObject *
watchdog_add_watch(PyObject * self,PyObject * args)608 watchdog_add_watch(PyObject *self, PyObject *args)
609 {
610 UNUSED(self);
611 FSEventStreamRef stream_ref = NULL;
612 StreamCallbackInfo *stream_callback_info_ref = NULL;
613 CFRunLoopRef run_loop_ref = NULL;
614 PyObject *emitter_thread = NULL;
615 PyObject *watch = NULL;
616 PyObject *paths_to_watch = NULL;
617 PyObject *python_callback = NULL;
618 PyObject *value = NULL;
619
620 /* Ensure all arguments are received. */
621 G_RETURN_NULL_IF_NOT(PyArg_ParseTuple(args, "OOOO:schedule",
622 &emitter_thread, &watch,
623 &python_callback, &paths_to_watch));
624
625 /* Watch must not already be scheduled. */
626 if(PyDict_Contains(watch_to_stream, watch) == 1) {
627 PyErr_Format(PyExc_RuntimeError, "Cannot add watch %S - it is already scheduled", watch);
628 return NULL;
629 }
630
631 /* Create an instance of the callback information structure. */
632 stream_callback_info_ref = PyMem_New(StreamCallbackInfo, 1);
633 if(stream_callback_info_ref == NULL) {
634 PyErr_SetString(PyExc_SystemError, "Failed allocating stream callback info");
635 return NULL;
636 }
637
638 /* Create an FSEvent stream and
639 * Save the stream reference to the global watch-to-stream dictionary. */
640 stream_ref = watchdog_FSEventStreamCreate(stream_callback_info_ref,
641 paths_to_watch,
642 (FSEventStreamCallback) &watchdog_FSEventStreamCallback);
643 if (!stream_ref) {
644 PyMem_Del(stream_callback_info_ref);
645 PyErr_SetString(PyExc_RuntimeError, "Failed creating fsevent stream");
646 return NULL;
647 }
648 value = PyCapsule_New(stream_ref, NULL, watchdog_pycapsule_destructor);
649 if (!value || !PyCapsule_IsValid(value, NULL)) {
650 PyMem_Del(stream_callback_info_ref);
651 FSEventStreamInvalidate(stream_ref);
652 FSEventStreamRelease(stream_ref);
653 return NULL;
654 }
655 PyDict_SetItem(watch_to_stream, watch, value);
656
657 /* Get a reference to the runloop for the emitter thread
658 * or to the current runloop. */
659 value = PyDict_GetItem(thread_to_run_loop, emitter_thread);
660 if (G_IS_NULL(value))
661 {
662 run_loop_ref = CFRunLoopGetCurrent();
663 }
664 else
665 {
666 run_loop_ref = PyCapsule_GetPointer(value, NULL);
667 }
668
669 /* Schedule the stream with the obtained runloop. */
670 FSEventStreamScheduleWithRunLoop(stream_ref, run_loop_ref, kCFRunLoopDefaultMode);
671
672 /* Set the stream information for the callback.
673 * This data will be passed to our watchdog_FSEventStreamCallback function
674 * by the FSEvents API whenever an event occurs.
675 */
676 stream_callback_info_ref->python_callback = python_callback;
677 stream_callback_info_ref->stream_ref = stream_ref;
678 stream_callback_info_ref->run_loop_ref = run_loop_ref;
679 stream_callback_info_ref->thread_state = PyThreadState_Get();
680 Py_INCREF(python_callback);
681
682 /* Start the event stream. */
683 if (G_NOT(FSEventStreamStart(stream_ref)))
684 {
685 FSEventStreamInvalidate(stream_ref);
686 FSEventStreamRelease(stream_ref);
687 // There's no documentation on _why_ this might fail - "it ought to always succeed". But if it fails the
688 // documentation says to "fall back to performing recursive scans of the directories [...] as appropriate".
689 PyErr_SetString(PyExc_SystemError, "Cannot start fsevents stream. Use a kqueue or polling observer instead.");
690 return NULL;
691 }
692
693 Py_INCREF(Py_None);
694 return Py_None;
695 }
696
697
698 PyDoc_STRVAR(watchdog_read_events__doc__,
699 MODULE_NAME ".read_events(emitter_thread) -> None\n\
700 Blocking function that runs an event loop associated with an emitter thread.\n\n\
701 :param emitter_thread:\n\
702 The emitter thread for which the event loop will be run.\n");
703 static PyObject *
watchdog_read_events(PyObject * self,PyObject * args)704 watchdog_read_events(PyObject *self, PyObject *args)
705 {
706 UNUSED(self);
707 CFRunLoopRef run_loop_ref = NULL;
708 PyObject *emitter_thread = NULL;
709 PyObject *value = NULL;
710
711 G_RETURN_NULL_IF_NOT(PyArg_ParseTuple(args, "O:loop", &emitter_thread));
712
713 // PyEval_InitThreads() does nothing as of Python 3.7 and is deprecated in 3.9.
714 // https://docs.python.org/3/c-api/init.html#c.PyEval_InitThreads
715 #if PY_VERSION_HEX < 0x030700f0
716 PyEval_InitThreads();
717 #endif
718
719 /* Allocate information and store thread state. */
720 value = PyDict_GetItem(thread_to_run_loop, emitter_thread);
721 if (G_IS_NULL(value))
722 {
723 run_loop_ref = CFRunLoopGetCurrent();
724 value = PyCapsule_New(run_loop_ref, NULL, watchdog_pycapsule_destructor);
725 PyDict_SetItem(thread_to_run_loop, emitter_thread, value);
726 Py_INCREF(emitter_thread);
727 Py_INCREF(value);
728 }
729
730 /* No timeout, block until events. */
731 Py_BEGIN_ALLOW_THREADS;
732 CFRunLoopRun();
733 Py_END_ALLOW_THREADS;
734
735 /* Clean up state information. */
736 if (PyDict_DelItem(thread_to_run_loop, emitter_thread) == 0)
737 {
738 Py_DECREF(emitter_thread);
739 Py_INCREF(value);
740 }
741
742 G_RETURN_NULL_IF(PyErr_Occurred());
743
744 Py_INCREF(Py_None);
745 return Py_None;
746 }
747
748 PyDoc_STRVAR(watchdog_flush_events__doc__,
749 MODULE_NAME ".flush_events(watch) -> None\n\
750 Flushes events for the watch.\n\n\
751 :param watch:\n\
752 The watch to flush.\n");
753 static PyObject *
watchdog_flush_events(PyObject * self,PyObject * watch)754 watchdog_flush_events(PyObject *self, PyObject *watch)
755 {
756 UNUSED(self);
757 PyObject *value = PyDict_GetItem(watch_to_stream, watch);
758
759 FSEventStreamRef stream_ref = PyCapsule_GetPointer(value, NULL);
760
761 FSEventStreamFlushSync(stream_ref);
762
763 Py_INCREF(Py_None);
764 return Py_None;
765 }
766
767 PyDoc_STRVAR(watchdog_remove_watch__doc__,
768 MODULE_NAME ".remove_watch(watch) -> None\n\
769 Removes a watch from the event loop.\n\n\
770 :param watch:\n\
771 The watch to remove.\n");
772 static PyObject *
watchdog_remove_watch(PyObject * self,PyObject * watch)773 watchdog_remove_watch(PyObject *self, PyObject *watch)
774 {
775 UNUSED(self);
776 PyObject *streamref_capsule = PyDict_GetItem(watch_to_stream, watch);
777 if (!streamref_capsule) {
778 // A watch might have been removed explicitly before, in which case we can simply early out.
779 Py_RETURN_NONE;
780 }
781 PyDict_DelItem(watch_to_stream, watch);
782
783 FSEventStreamRef stream_ref = PyCapsule_GetPointer(streamref_capsule, NULL);
784
785 FSEventStreamStop(stream_ref);
786 FSEventStreamInvalidate(stream_ref);
787 FSEventStreamRelease(stream_ref);
788
789 Py_RETURN_NONE;
790 }
791
792 PyDoc_STRVAR(watchdog_stop__doc__,
793 MODULE_NAME ".stop(emitter_thread) -> None\n\
794 Stops running the event loop from the specified thread.\n\n\
795 :param emitter_thread:\n\
796 The thread for which the event loop will be stopped.\n");
797 static PyObject *
watchdog_stop(PyObject * self,PyObject * emitter_thread)798 watchdog_stop(PyObject *self, PyObject *emitter_thread)
799 {
800 UNUSED(self);
801 PyObject *value = PyDict_GetItem(thread_to_run_loop, emitter_thread);
802 if (G_IS_NULL(value)) {
803 goto success;
804 }
805
806 CFRunLoopRef run_loop_ref = PyCapsule_GetPointer(value, NULL);
807 G_RETURN_NULL_IF(PyErr_Occurred());
808
809 /* Stop the run loop. */
810 if (G_IS_NOT_NULL(run_loop_ref))
811 {
812 CFRunLoopStop(run_loop_ref);
813 }
814
815 success:
816 Py_INCREF(Py_None);
817 return Py_None;
818 }
819
820
821 /******************************************************************************
822 * Module initialization.
823 *****************************************************************************/
824
825 PyDoc_STRVAR(watchdog_fsevents_module__doc__,
826 "Low-level FSEvents Python/C API bridge.");
827
828 static PyMethodDef watchdog_fsevents_methods[] =
829 {
830 {"add_watch", watchdog_add_watch, METH_VARARGS, watchdog_add_watch__doc__},
831 {"read_events", watchdog_read_events, METH_VARARGS, watchdog_read_events__doc__},
832 {"flush_events", watchdog_flush_events, METH_O, watchdog_flush_events__doc__},
833 {"remove_watch", watchdog_remove_watch, METH_O, watchdog_remove_watch__doc__},
834
835 /* Aliases for compatibility with macfsevents. */
836 {"schedule", watchdog_add_watch, METH_VARARGS, "Alias for add_watch."},
837 {"loop", watchdog_read_events, METH_VARARGS, "Alias for read_events."},
838 {"unschedule", watchdog_remove_watch, METH_O, "Alias for remove_watch."},
839
840 {"stop", watchdog_stop, METH_O, watchdog_stop__doc__},
841
842 {NULL, NULL, 0, NULL},
843 };
844
845
846 /**
847 * Initialize the module globals.
848 */
849 static void
watchdog_module_init(void)850 watchdog_module_init(void)
851 {
852 thread_to_run_loop = PyDict_New();
853 watch_to_stream = PyDict_New();
854 }
855
856
857 /**
858 * Adds various attributes to the Python module.
859 *
860 * :param module:
861 * A pointer to the Python module object to inject
862 * the attributes into.
863 */
864 static void
watchdog_module_add_attributes(PyObject * module)865 watchdog_module_add_attributes(PyObject *module)
866 {
867 PyObject *version_tuple = Py_BuildValue("(iii)",
868 WATCHDOG_VERSION_MAJOR,
869 WATCHDOG_VERSION_MINOR,
870 WATCHDOG_VERSION_BUILD);
871 PyModule_AddIntConstant(module,
872 "POLLIN",
873 kCFFileDescriptorReadCallBack);
874 PyModule_AddIntConstant(module,
875 "POLLOUT",
876 kCFFileDescriptorWriteCallBack);
877
878 /* Adds version information. */
879 PyModule_AddObject(module,
880 "__version__",
881 version_tuple);
882 PyModule_AddObject(module,
883 "version_string",
884 Py_BuildValue("s", WATCHDOG_VERSION_STRING));
885 }
886
887 static struct PyModuleDef watchdog_fsevents_module = {
888 PyModuleDef_HEAD_INIT,
889 MODULE_NAME,
890 watchdog_fsevents_module__doc__,
891 -1,
892 watchdog_fsevents_methods,
893 NULL, /* m_slots */
894 NULL, /* m_traverse */
895 0, /* m_clear */
896 NULL /* m_free */
897 };
898
899 /**
900 * Initialize the Python 3.x module.
901 */
902 PyMODINIT_FUNC
PyInit__watchdog_fsevents(void)903 PyInit__watchdog_fsevents(void){
904 G_RETURN_NULL_IF(PyType_Ready(&NativeEventType) < 0);
905 PyObject *module = PyModule_Create(&watchdog_fsevents_module);
906 G_RETURN_NULL_IF_NULL(module);
907 Py_INCREF(&NativeEventType);
908 if (PyModule_AddObject(module, "NativeEvent", (PyObject*)&NativeEventType) < 0) {
909 Py_DECREF(&NativeEventType);
910 Py_DECREF(module);
911 return NULL;
912 }
913 watchdog_module_add_attributes(module);
914 watchdog_module_init();
915 return module;
916 }
917