Watch a Directory

Treat inotify events as change hints and design callback, reconfiguration, and teardown boundaries explicitly.

AdvancedIOLinux/inotify

Scope and prerequisites

This tutorial is for the selected Linux/inotify backend. Non-Linux enablement throws PlatformNotSupportedException. The watcher observes one existing directory; recursive subdirectories are not implemented even though the property is stored.

set(SHARP_RUNTIME_COMPONENTS IO)
target_link_libraries(app PRIVATE SharpRuntime::IO)

Subscribe before enabling

#include <System/IO/FileSystemWatcher.hpp>

#include <iostream>

System::IO::FileSystemWatcher watcher("/tmp/incoming", "*.json");

watcher.Created.push_back(
    [](void*, const System::IO::FileSystemEventArgs& event) {
        std::cout << "created: "
                  << event.getFullPathProperty() << '\n';
    });

watcher.Error.push_back(
    [](void*, const System::IO::ErrorEventArgs& event) {
        try {
            event.GetException();
        } catch (const std::exception& error) {
            std::cerr << "watch error: " << error.what() << '\n';
        }
    });

watcher.setEnableRaisingEventsProperty(true);

Handlers run on the watcher thread. The watcher object, handler vectors, captured objects, and output facilities must remain valid and appropriately synchronized.

Choose the notification class

FileName/DirectoryName admit Created, Deleted, and Renamed events. Content-class values admit Changed. Changing NotifyFilter on a live watcher stops, stores, and rearms the kernel watch, discarding queued undelivered events.

Linux cannot distinguish every .NET vocabulary item. Size and LastWrite share inotify information; several attribute-like values collapse; FileName and DirectoryName are not yet separated. Treat the filter as a current coarse class policy.

Self-stop is supported

watcher.Created.push_back(
    [&watcher](void*, const System::IO::FileSystemEventArgs&) {
        watcher.setEnableRaisingEventsProperty(false);
    });

At the selected pin this no longer joins the watcher thread from itself. It signals stop, returns from the setter, and defers joining until later external reconfiguration or destruction. This is the #2347 remediation.

Do not rearm from the callback

Changing Path or NotifyFilter from a live watcher handler throws InvalidOperationException and leaves state unchanged. Schedule the reconfiguration on an external owner thread. External live changes stop the old watch before storing/arming the new value, so old-directory events are not mislabeled with the new path.

Reconcile hints with filesystem truth

Filesystem notifications are hints. Events can coalesce, race file completion, arrive in batches, lose rename pairing across batches, or overflow a kernel queue. On a meaningful event, rescan or stat the authoritative path and make processing idempotent. A Created event does not guarantee the writer has closed the file.

Current residual limits

  • Linux only, one watched directory, no recursive watches.
  • InternalBufferSize is clamped/stored but the loop still uses a fixed read buffer.
  • No explicit public translation for IN_Q_OVERFLOW.
  • Rename pairing is limited to one read batch; unmatched move-from can appear deleted.
  • Public filter/handler vectors are not synchronized for concurrent mutation.
  • Whether an already-running handler can still be executing when an external disable returns remains a separately deferred measurement.

Teardown checklist

  1. Stop accepting new work in the owner.
  2. Disable from an external thread when possible.
  3. Do not destroy objects captured by handlers until dispatch is quiescent under your protocol.
  4. Reconcile the directory once more if missing events would matter.
  5. Let watcher destruction reap a self-stopped thread.