The Pedigree Project 0.1
inotify-syscalls.cc
1/*
2 * Copyright (c) 2026, Pedigree Developers
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 */
8
9#include "inotify-syscalls.h"
10#include "pedigree/kernel/LockGuard.h"
11#include "pedigree/kernel/errors.h"
12#include "pedigree/kernel/process/ConditionVariable.h"
13#include "pedigree/kernel/process/Mutex.h"
14#include "pedigree/kernel/process/Process.h"
15#include "pedigree/kernel/processor/Processor.h"
16#include "pedigree/kernel/processor/ProcessorInformation.h"
17#include "pedigree/kernel/syscallError.h"
18#include "pedigree/kernel/utilities/List.h"
19#include "pedigree/kernel/utilities/SharedPointer.h"
20#include "pedigree/kernel/utilities/String.h"
21#include "pedigree/kernel/utilities/Vector.h"
22#include "pedigree/kernel/utilities/utility.h"
23
24#include <fcntl.h>
25#include <limits.h>
26
27#include "modules/subsys/posix/FileDescriptor.h"
28#include "modules/subsys/posix/PosixSubsystem.h"
29#include "modules/subsys/posix/ResolvedPath.h"
30#include "modules/subsys/posix/file-syscalls.h"
31#include "modules/system/vfs/Directory.h"
32#include "modules/system/vfs/File.h"
33#include "modules/system/vfs/FileEvent.h"
34#include "modules/system/vfs/VFS.h"
35
36namespace {
37constexpr size_t MaxQueuedEvents = 16384;
38constexpr FileEventMask AllFileEvents =
39 FileEvents::Access | FileEvents::Modify | FileEvents::Attributes | FileEvents::CloseWrite |
40 FileEvents::CloseNoWrite | FileEvents::Open | FileEvents::Created | FileEvents::Removed |
41 FileEvents::DeletedSelf;
42constexpr uint32_t AcceptedMask = LinuxInotify::AllBits;
43
44// Capture before path and descriptor retirement can replace the selected error.
45struct InotifyResult {
46 InotifyResult(int result)
47 : value(result),
48 error(result < 0 ? Processor::information().getCurrentThread()->getErrno() : 0) {}
49 int value;
50 int error;
51};
52
53size_t paddedNameLength(const String& name) {
54 if (!name.length()) {
55 return 0;
56 }
57 return (name.length() + 1 + sizeof(LinuxInotifyEvent) - 1) & ~(sizeof(LinuxInotifyEvent) - 1);
58}
59
60uint32_t linuxMaskFor(FileEventMask mask) {
61 uint32_t result = 0;
62 if (mask & FileEvents::Access)
63 result |= LinuxInotify::Access;
64 if (mask & FileEvents::Modify)
65 result |= LinuxInotify::Modify;
66 if (mask & FileEvents::Attributes)
67 result |= LinuxInotify::Attributes;
68 if (mask & FileEvents::CloseWrite)
69 result |= LinuxInotify::CloseWrite;
70 if (mask & FileEvents::CloseNoWrite)
71 result |= LinuxInotify::CloseNoWrite;
72 if (mask & FileEvents::Open)
73 result |= LinuxInotify::Open;
74 if (mask & FileEvents::Created)
75 result |= LinuxInotify::Create;
76 if (mask & FileEvents::Removed)
77 result |= LinuxInotify::Delete;
78 if (mask & FileEvents::DeletedSelf)
79 result |= LinuxInotify::DeleteSelf;
80 return result;
81}
82
83struct QueuedEvent {
84 QueuedEvent(int descriptor, uint32_t eventMask, uint32_t eventCookie, const StringView& eventName)
85 : wd(descriptor), mask(eventMask), cookie(eventCookie), name(eventName.toString()) {}
86
87 int wd;
88 uint32_t mask;
89 uint32_t cookie;
90 String name;
91};
92
93class InotifyQueue {
94 public:
95 explicit InotifyQueue(InotifyInstance* readinessOwner)
96 : owner(readinessOwner),
97 lock(),
98 changed(),
99 events(),
100 generations(),
101 open(true),
102 overflowQueued(false) {}
103
104 ~InotifyQueue() {
105 while (events.count()) {
106 delete events.popFront();
107 }
108 }
109
110 bool enqueue(int wd, uint32_t mask, uint32_t cookie, const StringView& name) {
111 bool becameReadable = false;
112 lock.acquire();
113 if (!open) {
114 lock.release();
115 return false;
116 }
117
118 if (events.count() >= MaxQueuedEvents) {
119 if (!overflowQueued) {
120 becameReadable = !events.count();
121 events.pushBack(new QueuedEvent(-1, LinuxInotify::QueueOverflow, 0, StringView()));
122 overflowQueued = true;
123 if (becameReadable) {
124 ++generations.read;
125 }
126 }
127 lock.release();
128 changed.broadcast();
129 owner->eventsQueued();
130 return becameReadable;
131 }
132
133 if (events.count()) {
134 QueuedEvent* last = *events.rbegin();
135 if (last && !(last->mask & LinuxInotify::Ignored) && last->wd == wd && last->mask == mask &&
136 last->name.view() == name) {
137 lock.release();
138 return false;
139 }
140 }
141
142 becameReadable = !events.count();
143 events.pushBack(new QueuedEvent(wd, mask, cookie, name));
144 if (becameReadable) {
145 ++generations.read;
146 }
147 lock.release();
148 changed.broadcast();
149 owner->eventsQueued();
150 return becameReadable;
151 }
152
153 int readOne(uint8_t* buffer, size_t length, bool canBlock) {
154 lock.acquire();
155 while (!events.count()) {
156 if (!open) {
157 lock.release();
158 SYSCALL_ERROR(BadFileDescriptor);
159 return -1;
160 }
161 if (!canBlock) {
162 lock.release();
163 SYSCALL_ERROR(NoMoreProcesses);
164 return -1;
165 }
166 ConditionVariable::Error error = ConditionVariable::NoError;
167 if (!changed.wait(lock, error)) {
169 lock.release();
170 }
171 if (error == ConditionVariable::Interrupted ||
172 error == ConditionVariable::TerminationDeferred) {
173 SYSCALL_ERROR(Interrupted);
174 } else {
175 SYSCALL_ERROR(BadFileDescriptor);
176 }
177 return -1;
178 }
179 }
180
181 // Linux reports EAGAIN for an empty nonblocking queue regardless of the
182 // buffer size. EINVAL applies only once a queued first record cannot fit.
183 const size_t firstSize = sizeof(LinuxInotifyEvent) + paddedNameLength((*events.begin())->name);
184 if (firstSize > length) {
185 lock.release();
186 SYSCALL_ERROR(InvalidArgument);
187 return -1;
188 }
189
190 QueuedEvent* event = *events.begin();
191 const size_t nameLength = paddedNameLength(event->name);
192 const size_t recordSize = sizeof(LinuxInotifyEvent) + nameLength;
193 LinuxInotifyEvent header = {event->wd, event->mask, event->cookie,
194 static_cast<uint32_t>(nameLength)};
195 MemoryCopy(buffer, &header, sizeof(header));
196 if (nameLength) {
197 ByteSet(buffer + sizeof(header), 0, nameLength);
198 MemoryCopy(buffer + sizeof(header), event->name.cstr(), event->name.length());
199 }
200 event = events.popFront();
201 if (event->mask & LinuxInotify::QueueOverflow) {
202 overflowQueued = false;
203 }
204 delete event;
205 lock.release();
206 return static_cast<int>(recordSize);
207 }
208
209 ReadyMask queryReady() {
210 LockGuard<Mutex> guard(lock);
211 return events.count() ? ReadyRead : ReadyNone;
212 }
213
214 ReadinessGenerations readinessGenerations() {
215 LockGuard<Mutex> guard(lock);
216 return generations;
217 }
218
219 void close() {
220 lock.acquire();
221 if (!open) {
222 lock.release();
223 return;
224 }
225 open = false;
226 while (events.count()) {
227 delete events.popFront();
228 }
229 overflowQueued = false;
230 lock.release();
231 changed.broadcast();
232 }
233
234 private:
235 InotifyInstance* owner;
236 Mutex lock;
237 ConditionVariable changed;
238 List<QueuedEvent*> events;
239 ReadinessGenerations generations;
240 bool open;
241 bool overflowQueued;
242};
243
244class InotifyWatchObserver final : public FileEventObserver {
245 public:
246 InotifyWatchObserver(const SharedPointer<InotifyQueue>& eventQueue, int descriptor,
247 uint32_t eventMask, bool directory)
248 : queue(eventQueue),
249 lock(),
250 wd(descriptor),
251 mask(eventMask),
252 isDirectory(directory),
253 active(true) {}
254
255 void updateMask(uint32_t eventMask, bool add) {
256 LockGuard<Mutex> guard(lock);
257 if (add) {
258 mask |= eventMask & ~LinuxInotify::MaskAdd;
259 } else {
260 mask = eventMask;
261 }
262 }
263
264 bool deactivate() {
265 LockGuard<Mutex> guard(lock);
266 const bool wasActive = active;
267 active = false;
268 return wasActive;
269 }
270
271 bool isActive() {
272 LockGuard<Mutex> guard(lock);
273 return active;
274 }
275
276 void fileEvent(const FileEvent& event) override {
277 LockGuard<Mutex> guard(lock);
278 if (!active) {
279 return;
280 }
281 uint32_t selected = linuxMaskFor(event.mask) & mask;
282 const bool deleted = event.mask & FileEvents::DeletedSelf;
283 if (!selected && !deleted) {
284 return;
285 }
286 if (!(selected & (LinuxInotify::DeleteSelf | LinuxInotify::MoveSelf)) &&
287 ((event.name.length() && event.targetIsDirectory) ||
288 (!event.name.length() && isDirectory))) {
289 selected |= LinuxInotify::IsDirectory;
290 }
291 const bool retire = deleted || ((mask & LinuxInotify::OneShot) && selected);
292 if (retire) {
293 active = false;
294 }
295 if (selected) {
296 queue->enqueue(wd, selected, 0, event.name);
297 }
298 if (retire) {
299 queue->enqueue(wd, LinuxInotify::Ignored, 0, StringView());
300 }
301 }
302
303 private:
305 Mutex lock;
306 int wd;
307 uint32_t mask;
308 bool isDirectory;
309 bool active;
310};
311
312struct InotifyWatch {
313 InotifyWatch(File* watchedTarget, bool retainedTarget,
314 const SharedPointer<FileEventObserver>& watchObserver,
315 InotifyWatchObserver* concreteObserver, int descriptor)
316 : target(watchedTarget),
317 retained(retainedTarget),
318 observer(watchObserver),
319 observerImpl(concreteObserver),
320 wd(descriptor),
321 subscription() {}
322
323 File* target;
324 bool retained;
326 InotifyWatchObserver* observerImpl;
327 int wd;
328 FileEventSubscription subscription;
329};
330
331void retireWatch(InotifyWatch* watch) {
332 if (!watch) {
333 return;
334 }
335 watch->subscription.reset();
336 if (watch->retained) {
337 watch->target->releaseVfsReference();
338 }
339 delete watch;
340}
341
342} // namespace
343
345 public:
346 explicit InotifyState(InotifyInstance* owner)
347 : lock(), watches(), queue(new InotifyQueue(owner)), nextWd(1), descriptorOpen(true) {}
348
349 Mutex lock;
350 List<InotifyWatch*> watches;
352 int nextWd;
353 bool descriptorOpen;
354};
355
356InotifyInstance::InotifyInstance() : ReadinessSource(), m_State(new InotifyState(this)) {}
357
358InotifyInstance::~InotifyInstance() {
359 lastDescriptorClosed();
360 delete m_State;
362}
363
364void InotifyInstance::reapInactiveWatches() {
365 List<InotifyWatch*> retiring;
366 m_State->lock.acquire();
367 for (auto it = m_State->watches.begin(); it != m_State->watches.end();) {
368 InotifyWatch* watch = *it;
369 if (!watch->observerImpl->isActive()) {
370 it = m_State->watches.erase(it);
371 retiring.pushBack(watch);
372 } else {
373 ++it;
374 }
375 }
376 m_State->lock.release();
377
378 while (retiring.count()) {
379 retireWatch(retiring.popFront());
380 }
381}
382
383int InotifyInstance::addWatch(File* target, uint32_t mask) {
384 if (!target || (mask & ~AcceptedMask) || !(mask & AcceptedMask) ||
385 ((mask & LinuxInotify::MaskAdd) && (mask & LinuxInotify::MaskCreate))) {
386 SYSCALL_ERROR(InvalidArgument);
387 return -1;
388 }
389 if ((mask & LinuxInotify::OnlyDirectory) && !target->isDirectory()) {
390 SYSCALL_ERROR(NotADirectory);
391 return -1;
392 }
393
394 reapInactiveWatches();
395
396 m_State->lock.acquire();
397 if (!m_State->descriptorOpen) {
398 m_State->lock.release();
399 SYSCALL_ERROR(BadFileDescriptor);
400 return -1;
401 }
402 for (auto watch : m_State->watches) {
403 if (watch->target != target || !watch->observerImpl->isActive()) {
404 continue;
405 }
406 if (mask & LinuxInotify::MaskCreate) {
407 m_State->lock.release();
408 SYSCALL_ERROR(FileExists);
409 return -1;
410 }
411 watch->observerImpl->updateMask(mask, mask & LinuxInotify::MaskAdd);
412 const int wd = watch->wd;
413 m_State->lock.release();
414 return wd;
415 }
416
417 int wd = m_State->nextWd++;
418 if (wd <= 0) {
419 wd = 1;
420 m_State->nextWd = 2;
421 }
422 const bool retained = target->retainVfsReference();
423 if (!retained && !target->isStableVfsRoot()) {
424 m_State->lock.release();
425 SYSCALL_ERROR(DoesNotExist);
426 return -1;
427 }
428
429 InotifyWatchObserver* observerImpl =
430 new InotifyWatchObserver(m_State->queue, wd, mask, target->isDirectory());
431 SharedPointer<FileEventObserver> observer(observerImpl);
432 InotifyWatch* watch = new InotifyWatch(target, retained, observer, observerImpl, wd);
433 if (!target->subscribeFileEvents(AllFileEvents, observer, watch->subscription)) {
434 if (retained) {
435 target->releaseVfsReference();
436 }
437 delete watch;
438 m_State->lock.release();
439 SYSCALL_ERROR(DoesNotExist);
440 return -1;
441 }
442 m_State->watches.pushBack(watch);
443 m_State->lock.release();
444 return wd;
445}
446
447int InotifyInstance::removeWatch(int wd) {
448 reapInactiveWatches();
449
450 InotifyWatch* retiring = nullptr;
451 bool wasActive = false;
452 m_State->lock.acquire();
453 for (auto it = m_State->watches.begin(); it != m_State->watches.end(); ++it) {
454 if ((*it)->wd == wd) {
455 retiring = *it;
456 retiring->subscription.reset();
457 wasActive = retiring->observerImpl->deactivate();
458 m_State->watches.erase(it);
459 if (wasActive) {
460 m_State->queue->enqueue(wd, LinuxInotify::Ignored, 0, StringView());
461 }
462 break;
463 }
464 }
465 m_State->lock.release();
466 if (!retiring) {
467 SYSCALL_ERROR(InvalidArgument);
468 return -1;
469 }
470
471 if (retiring->retained) {
472 retiring->target->releaseVfsReference();
473 }
474 delete retiring;
475 if (!wasActive) {
476 SYSCALL_ERROR(InvalidArgument);
477 return -1;
478 }
479 return 0;
480}
481
482int InotifyInstance::readEvents(uint8_t* buffer, size_t length, bool canBlock) {
483 int outcome = -1;
484 size_t copied = 0;
485 bool attempted = false;
486 while (!attempted || copied < length) {
487 attempted = true;
488 uint8_t* destination = copied ? buffer + copied : buffer;
489 const int result = m_State->queue->readOne(destination, length - copied, canBlock && !copied);
490 if (result < 0) {
491 if (copied) {
492 Processor::information().getCurrentThread()->setErrno(0);
493 outcome = static_cast<int>(copied);
494 } else {
495 outcome = result;
496 }
497 break;
498 }
499 copied += static_cast<size_t>(result);
500 outcome = static_cast<int>(copied);
501 }
502 reapInactiveWatches();
503 return outcome;
504}
505
506int InotifyInstance::readEventsToUser(uint8_t* buffer, size_t length, bool canBlock) {
507 constexpr size_t MaximumInotifyRecord =
508 sizeof(LinuxInotifyEvent) +
509 ((PATH_MAX + 1 + sizeof(LinuxInotifyEvent) - 1) & ~(sizeof(LinuxInotifyEvent) - 1));
510 const size_t bounceCapacity = length < MaximumInotifyRecord ? length : MaximumInotifyRecord;
512
513 int outcome = -1;
514 size_t copied = 0;
515 bool attempted = false;
516 while (!attempted || copied < length) {
517 attempted = true;
518 const size_t remaining = length - copied;
519 const size_t eventCapacity = remaining < bounceCapacity ? remaining : bounceCapacity;
520 const int result = m_State->queue->readOne(bounce.get(), eventCapacity, canBlock && !copied);
521 if (result < 0) {
522 if (copied) {
523 Processor::information().getCurrentThread()->setErrno(0);
524 outcome = static_cast<int>(copied);
525 } else {
526 outcome = result;
527 }
528 break;
529 }
530
531 const uintptr_t base = reinterpret_cast<uintptr_t>(buffer);
532 if (copied > ~static_cast<uintptr_t>(0) - base ||
533 !PosixSubsystem::copyToUser(reinterpret_cast<void*>(base + copied), bounce.get(),
534 static_cast<size_t>(result))) {
535 SYSCALL_ERROR(BadAddress);
536 outcome = -1;
537 break;
538 }
539 copied += static_cast<size_t>(result);
540 outcome = static_cast<int>(copied);
541 }
542 reapInactiveWatches();
543 return outcome;
544}
545
546ReadyMask InotifyInstance::queryReady() {
547 return m_State->queue->queryReady();
548}
549
551 return m_State->queue->readinessGenerations();
552}
553
555 notifyReadiness(ReadyRead);
556}
557
558void InotifyInstance::lastDescriptorClosed() {
559 List<InotifyWatch*> retiring;
560 m_State->lock.acquire();
561 if (!m_State->descriptorOpen) {
562 m_State->lock.release();
563 return;
564 }
565 m_State->descriptorOpen = false;
566 while (m_State->watches.count()) {
567 retiring.pushBack(m_State->watches.popFront());
568 }
569 m_State->lock.release();
570
571 while (retiring.count()) {
572 retireWatch(retiring.popFront());
573 }
574 m_State->queue->close();
575 closeReadiness(ReadyInvalid | ReadyHangup);
576}
577
578int posix_inotify_init() {
579 return posix_inotify_init1(0);
580}
581
582int posix_inotify_init1(int flags) {
583 constexpr int AllowedFlags = LinuxInotify::NonBlock | LinuxInotify::CloseOnExec;
584 if (flags & ~AllowedFlags) {
585 SYSCALL_ERROR(InvalidArgument);
586 return -1;
587 }
588
589 const size_t fd = getAvailableDescriptor();
590 const int descriptorFlags = flags & LinuxInotify::CloseOnExec ? FD_CLOEXEC : 0;
591 const int statusFlags = O_RDONLY | (flags & LinuxInotify::NonBlock ? O_NONBLOCK : 0);
592 FileDescriptor* descriptor = new FileDescriptor(nullptr, 0, fd, descriptorFlags, statusFlags);
594 addDescriptor(static_cast<int>(fd), descriptor);
595 return static_cast<int>(fd);
596}
597
598static InotifyResult addWatch(int fd, const char* pathname, uint32_t mask) {
599 ResolvedPath targetLease;
600 // Linux validates the UAPI bitset before looking up either the descriptor
601 // or pathname.
602 if ((mask & ~AcceptedMask) || !(mask & AcceptedMask)) {
603 SYSCALL_ERROR(InvalidArgument);
604 return -1;
605 }
606
607 DescriptorLease descriptor;
608 Process* process = Processor::information().getCurrentThread()->getParent();
609 PosixSubsystem* subsystem = static_cast<PosixSubsystem*>(process->getSubsystem());
610 if (!subsystem || !subsystem->acquireFileDescriptor(fd, descriptor)) {
611 SYSCALL_ERROR(BadFileDescriptor);
612 return -1;
613 }
614 if ((mask & LinuxInotify::MaskAdd) && (mask & LinuxInotify::MaskCreate)) {
615 SYSCALL_ERROR(InvalidArgument);
616 return -1;
617 }
618 SharedPointer<InotifyInstance> instance = descriptor->getInotifyImpl();
619 if (!instance) {
620 SYSCALL_ERROR(InvalidArgument);
621 return -1;
622 }
623
624 String path;
625 const PosixSubsystem::UserStringResult copied =
626 PosixSubsystem::copyUserString(pathname, path, PATH_MAX);
627 if (copied == PosixSubsystem::UserStringBadAddress) {
628 SYSCALL_ERROR(BadAddress);
629 return -1;
630 }
631 if (copied == PosixSubsystem::UserStringTooLong) {
632 SYSCALL_ERROR(NameTooLong);
633 return -1;
634 }
635 if (!path.length()) {
636 SYSCALL_ERROR(DoesNotExist);
637 return -1;
638 }
639 String normalised;
640 normalisePath(normalised, path.cstr());
641 const bool requireDirectory = path[path.length() - 1] == '/';
642 Processor::information().getCurrentThread()->setErrno(0);
643 File* target = findFilePath(normalised, targetLease, FilesystemPathRef(),
644 !(mask & LinuxInotify::DontFollow) || requireDirectory);
645 if (!target) {
646 if (!Processor::information().getCurrentThread()->getErrno())
647 SYSCALL_ERROR(DoesNotExist);
648 return -1;
649 }
650 if (((mask & LinuxInotify::OnlyDirectory) || requireDirectory) && !target->isDirectory()) {
651 SYSCALL_ERROR(NotADirectory);
652 return -1;
653 }
654 if (!VFS::checkAccess(target, true, false, false)) {
655 return -1;
656 }
657 return instance->addWatch(target, mask);
658}
659
660int posix_inotify_add_watch(int fd, const char* pathname, uint32_t mask) {
661 const InotifyResult result = addWatch(fd, pathname, mask);
662 syscallError(result.error);
663 return result.value;
664}
665
666int posix_inotify_rm_watch(int fd, int wd) {
667 DescriptorLease descriptor;
668 Process* process = Processor::information().getCurrentThread()->getParent();
669 PosixSubsystem* subsystem = static_cast<PosixSubsystem*>(process->getSubsystem());
670 if (!subsystem || !subsystem->acquireFileDescriptor(fd, descriptor)) {
671 SYSCALL_ERROR(BadFileDescriptor);
672 return -1;
673 }
674 SharedPointer<InotifyInstance> instance = descriptor->getInotifyImpl();
675 if (!instance) {
676 SYSCALL_ERROR(InvalidArgument);
677 return -1;
678 }
679 return instance->removeWatch(wd);
680}
static bool mutexAcquired(Error error)
SharedPointer< InotifyInstance > getInotifyImpl() const
void setInotifyImpl(const SharedPointer< InotifyInstance > &implementation)
virtual void fileEvent(const FileEvent &event)=0
Definition File.h:74
virtual bool retainVfsReference()
Definition File.cc:798
virtual void releaseVfsReference()
Definition File.cc:802
virtual bool isStableVfsRoot() const
Definition File.cc:806
virtual bool isDirectory()
Definition File.cc:692
ReadinessGenerations readinessGenerations() override
Definition List.h:61
Iterator begin()
Definition List.h:122
Iterator end()
Definition List.h:132
Definition Mutex.h:56
bool acquireFileDescriptor(size_t fd, DescriptorLease &descriptor)
static UserStringResult copyUserString(const char *userString, String &copy, size_t maxLength)
static bool copyToUser(void *destination, const void *source, size_t count, size_t elementSize=1)
Process * getParent()
Definition Process.h:568
static ProcessorInformation & information()
void notifyReadiness(ReadyMask mask)
Definition Readiness.cc:201
void closeReadiness(ReadyMask mask=ReadyInvalid|ReadyHangup)
Definition Readiness.cc:208
void release(size_t n=1)
Definition Semaphore.cc:546
bool acquire(size_t n=1, size_t timeoutSecs=0, size_t timeoutUsecs=0)
Definition Semaphore.cc:352
static bool checkAccess(File *pFile, bool bRead, bool bWrite, bool bExecute)
Definition VFS.cc:1353
Iterator erase(Iterator &Iter)
Definition List.h:352
T popFront()
Definition List.h:330
size_t count() const
Definition List.h:212
void pushBack(const T &value)
Definition List.h:216
StringView name
Definition FileEvent.h:45