Watch files and directories.

Wait for portable change hints without blocking an event-loop pthread. Then reconcile application state after coalescing, invalidation, or lost kernel detail.

PURPOSE

Treat each notification as a reason to inspect.

A file event is a change hint, not an operation log. The kernel can combine many changes into one event. It can also report one operation through more than one category.

After every event, inspect the current object and update application state from that observation. Do not count events to infer writes, renames, or directory-entry changes.

File watch
Use it when content, metadata, or identity changes to one existing object are relevant. Prepare to recreate the registration after a move or deletion.
Directory watch
Use it when files can appear, disappear, or be replaced by an atomic rename. Rescan the relevant pathname after a directory-content hint.
Recursive tree
Use the recursive child package to discover real directories and reconcile their registrations after each tree hint.
WATCH 01

Set a bound for active registrations.

An unconstrained declaration uses Default_Capacity, which is 64. Set the discriminant when the application needs another bound.

Flyology.IO.File_Watches.Recursive uses the same default for Recursive_Watcher. Set its discriminant to change the maximum directory count.

default and explicit watcher capacities
with Flyology.IO.File_Watches;
with Flyology.IO.File_Watches.Recursive;

package Watches renames Flyology.IO.File_Watches;
package Trees renames Flyology.IO.File_Watches.Recursive;

Configuration_Watcher : Watches.Watcher;
Default_Tree_Watcher  : Trees.Recursive_Watcher;
Large_Tree_Watcher    : Trees.Recursive_Watcher (Capacity => 256);

For Watcher, the capacity limits logical registrations. A recursive watcher needs one capacity slot for the root and each real subdirectory.

The capacity does not set the kernel queue length or the number of events in one drain.

The bound is fixed for the object's lifetime. Declare a replacement watcher when the application needs a different bound.

Each successful Add returns a distinct Watch_Id. Duplicate pathname registrations consume separate capacity and receive separate logical events.

Add follows the final symbolic-link component and requires an existing file or directory. It raises Device_Error for an empty path, an embedded NUL, an exhausted capacity, or an operating-system failure.

WATCH 02

Open, register, wait, and close explicitly.

Call Open before the first registration. Repeated calls are harmless while the watcher remains open.

Call Remove when a logical registration is no longer needed. The call discards its queued hints. Remove retires the identifier even if host cleanup reports an error. Do not retry the same identifier. An unknown identifier and No_Watch are errors.

Call Close to release all registrations and the platform queue. Repeated calls are harmless. The watcher becomes closed even when cleanup reports an error.

Finalization also releases resources and does not propagate errors. Use explicit Close when the application must observe a cleanup failure. Use Is_Open only to inspect lifecycle state, not to synchronize tasks.

basic watcher lifecycle
declare
   Monitor : Watches.Watcher;
   Config_Directory : Watches.Watch_Id;
begin
   Monitor.Open;
   Config_Directory := Monitor.Add ("/etc/my-app");

   --  Call Monitor.Next from the owner task.

   Monitor.Remove (Config_Directory);
   Monitor.Close;
end;
WATCH 03

Carry one deadline through each wait.

Next first returns one pending event. If none is pending, it waits for the platform queue. One monotonic deadline covers readiness retries and stale kernel records.

A negative timeout, including Infinite, waits without a deadline. A zero timeout performs an immediate drain. A positive value sets a relative deadline in seconds.

The call reports a Wait_Outcome. Ready supplies an event. Timed_Out reports deadline expiry. Interrupted reports a readable interrupt descriptor.

The result is a File_Event. Its Change_Set contains one or more hints when the outcome is Ready. Otherwise, the result contains No_Changes and a No_Watch identifier.

Pass an Interrupt_Set when shutdown or another lifecycle event must wake the owner task. The watcher observes borrowed descriptors but does not read or close them.

After an interrupted wait, consume or reset the application's wake source before waiting again. Otherwise, the readable descriptor can cause another immediate interruption.

bounded and interruptible wait
declare
   Event   : Watches.File_Event;
   Outcome : Flyology.IO.Wait_Outcome;
begin
   Monitor.Next
     (Result     => Event,
      Outcome    => Outcome,
      Timeout    => 30.0,
      Interrupts => Shutdown_Descriptors);

   case Outcome is
      when Flyology.IO.Ready =>
         Reconcile (Event);
      when Flyology.IO.Timed_Out =>
         Run_Periodic_Check;
      when Flyology.IO.Interrupted =>
         Consume_Shutdown_Wake;
   end case;
end;
WATCH 04

Interpret every change category.

A File_Event identifies one logical registration and supplies a Change_Set. More than one category can be true.

Contents_Changed
File data or directory entries may differ. Read the file again or rescan the relevant directory state.
Metadata_Changed
Attributes or link metadata may differ. Recheck permissions, ownership, size, timestamps, and other metadata that affects the application.
Identity_Changed
The pathname may now identify another object. Stop relying on cached identity or an earlier open-by-name decision.
Watch_Invalidated
The pathname association is no longer portable. Remove the logical registration and add it again only after the path exists.
Events_Lost
The kernel lost detail or Flyology could not preserve a complete batch. Rebuild all state covered by the watcher.

The categories describe possible effects. For example, a rename can report both Identity_Changed and Watch_Invalidated. Handle the strongest recovery requirement in the set.

WATCH 05

Reconcile state instead of replaying events.

A robust monitor stores the desired pathname state separately from watcher registrations. The event loop uses each hint to compare that desired state with the filesystem.

  1. If Events_Lost is set, inspect every pathname covered by the watcher.
  2. If Watch_Invalidated is set, forget cached identity and remove the registration.
  3. Inspect the current file or directory before publishing new application state.
  4. If an invalidated pathname exists again, add a new registration and store its new identifier.
  5. Repeat Next. Do not assume that the reconciliation itself generated no new changes.

The following loop watches a configuration directory. It reloads the target from its current pathname after any relevant directory change.

directory-based configuration reconciliation
with Flyology.IO;
with Flyology.IO.File_Watches;

procedure Monitor_Configuration is
   package Watches renames Flyology.IO.File_Watches;

   use type Watches.Watch_Id;

   Monitor   : Watches.Watcher;
   Directory : Watches.Watch_Id;
   Event     : Watches.File_Event;
   Outcome   : Flyology.IO.Wait_Outcome;
begin
   Monitor.Open;
   Directory := Monitor.Add ("/etc/my-app");

   loop
      Monitor.Next
        (Event,
         Outcome,
         Timeout    => 60.0,
         Interrupts => Shutdown_Descriptors);
      case Outcome is
         when Flyology.IO.Ready =>
            if Event.Watch = Directory
              and then Event.Changes (Watches.Watch_Invalidated)
            then
               Monitor.Remove (Directory);
               exit; --  Recreate the watch after the directory exists again.
            elsif Event.Watch = Directory
              and then
                (Event.Changes (Watches.Contents_Changed)
                 or else Event.Changes (Watches.Metadata_Changed)
                 or else Event.Changes (Watches.Events_Lost))
            then
               Reload_If_Changed ("/etc/my-app/config.toml");
            end if;

         when Flyology.IO.Timed_Out =>
            Verify_Current_Configuration;

         when Flyology.IO.Interrupted =>
            Consume_Shutdown_Wake;
            exit;
      end case;
   end loop;

   Monitor.Close;
end Monitor_Configuration;
WATCH 06

Reconcile a bounded directory tree.

Flyology.IO.File_Watches.Recursive owns the registrations for one directory tree. A Recursive_Watcher discovers directories and updates its bounded registration set.

Open the complete initial tree.

Call the recursive Open with an existing directory. The call discovers and registers the root and each real subdirectory. Initial discovery is transactional. If the tree exceeds the capacity, the call raises Device_Error and leaves the watcher closed.

Open follows a symbolic link in the final component of the root path. Discovery does not traverse symbolic links below the root. This rule prevents a nested link from expanding the tree or creating a traversal cycle.

A repeated call with the same root is harmless. A call with another root raises Device_Error. Close the watcher before you select a different root.

Use tree-wide events.

The recursive Next waits for one hint and then scans the current tree. It removes obsolete registrations and adds discovered directories before it returns.

The additive scoped overload composes an ordinary watcher Next as a hidden child. The recursive parent becomes terminal only after reconciliation. Parent and child occupy two completion-set slots; add one slot for each gate that observes the parent. Cancellation drains the child before releasing the watcher borrow.

The result is a Recursive_Event. Its Changes apply to the complete tree. The event does not expose internal watch identifiers.

Registrations_Changed reports whether reconciliation changed the registration set. Directory_Count reports the number of retained logical registrations. The count can include an obsolete path while coverage is incomplete.

Use the recursive Is_Open query to detect terminal root invalidation. Do not use the query to synchronize tasks.

Recover bounded coverage.

Tree growth can exceed the object capacity after Open. A scan can also stop because directory metadata is unavailable. In both cases, reconciliation preserves the existing registration set. The event sets Events_Lost and clears Coverage_Complete.

Incomplete coverage means that some directories have no registration. Changes below an unregistered directory do not necessarily produce another wake. Use the explicit Refresh operation from an application retry or periodic check. A later complete scan restores coverage when the tree fits again.

The Coverage_Is_Complete query reports the current state outside an event. Treat a false result as a requirement to reconcile all application state covered by the root.

recursive watcher with explicit capacity recovery
with Flyology.IO;
with Flyology.IO.File_Watches.Recursive;

procedure Monitor_Tree is
   package Trees renames Flyology.IO.File_Watches.Recursive;

   Tree    : Trees.Recursive_Watcher; --  Capacity defaults to 64.
   Event   : Trees.Recursive_Event;
   Outcome : Flyology.IO.Wait_Outcome;
begin
   Tree.Open ("/srv/my-app/config");

   loop
      Tree.Next
        (Event,
         Outcome,
         Timeout    => 60.0,
         Interrupts => Shutdown_Descriptors);
      case Outcome is
         when Flyology.IO.Ready =>
            Reconcile_Complete_Tree;
            exit when not Tree.Is_Open;
            if not Event.Coverage_Complete then
               Schedule_Refresh;
            end if;

         when Flyology.IO.Timed_Out =>
            Tree.Refresh (Event);
            Reconcile_Complete_Tree;
            exit when not Tree.Is_Open;

         when Flyology.IO.Interrupted =>
            exit;
      end case;
   end loop;

   Tree.Close;
end Monitor_Tree;

Reconcile_Complete_Tree and Schedule_Refresh are application policy. The scheduled refresh must call Refresh without waiting for another filesystem hint.

Keep discovery time outside the wait deadline.

The timeout on recursive Next applies only to the readiness wait. Tree discovery and registration reconciliation start after readiness. They do not share that deadline.

Discovery performs directory and metadata operations on the calling lane. A large tree or slow remote filesystem can occupy an event-loop pthread. Use a native-task boundary when that delay is not acceptable.

Handle removal of the root.

If reconciliation cannot find the root, the event sets Identity_Changed and Watch_Invalidated. The recursive watcher releases its registrations and becomes closed. Recreate the root before you call Open again.

Explicit close releases every registration and owned pathname. It reports cleanup failures. Finalization performs the same cleanup without propagating an exception.

WATCH 07

Separate waiting from metadata work.

Lightweight task
Next suspends only the calling task. The execution group's event-loop pthread can run other lightweight tasks.
Native task
Next can block only the calling pthread. The event contract and deadline behavior remain the same.
Linux
One nonblocking close-on-exec inotify descriptor stores persistent registrations and becomes the task-aware readiness source.
macOS
A private close-on-exec kqueue stores persistent EVFILT_VNODE registrations. Flyology waits for that queue through ordinary descriptor readiness.

Open, Add, Remove, and Close execute direct operating-system calls in both lanes. Flyology does not move them to a worker task.

Add resolves the pathname on the calling lane. A slow remote filesystem can therefore occupy an event-loop pthread during registration.

Move unpredictable pathname discovery or remote-filesystem metadata work to a native-task boundary. Keep Next in the lightweight lane when its nonblocking wait fits the application structure.

The watcher owns one persistent platform queue. A lightweight owner can migrate between execution groups because each wait observes that queue through the destination group's normal poller.

WATCH 08

Give one task exclusive control.

Watcher operations are not task safe. One task must serialize opening, registration changes, waiting, and closing.

Do not call Close or Remove concurrently with Next. Instead, wake the owner through an interrupt descriptor. The owner can then consume the wake and change watcher state.

Send reconciled application updates through an application-owned channel or protected object. Do not share the watcher as the notification channel.

A Watch_Id is stable only during its watcher owner's lifetime. Do not persist it, send it to another process, or reuse it after removal.

WATCH 09

Keep platform limits visible.

  • File watching is available on supported macOS and Linux hosts. Flyology does not provide a Windows backend.
  • Notifications are advisory, coalesced hints. They are not a durable journal or a security boundary.
  • A base directory watch is not recursive. The recursive child package maintains a bounded registration set for a tree.
  • The portable event omits Linux child names, rename cookies, and other platform-only detail.
  • The final symbolic-link component is followed. The watch identifies the resulting object, not the link as an independent object.
  • A file can change again between notification and inspection. Use an application protocol when a consistent snapshot is required.
  • Events_Lost requires complete reconciliation. Selecting a larger watcher capacity does not increase the kernel queue.
  • Explicit close reports cleanup failures. Finalization remains nonraising and cannot report them.
  • Use the exact exception and parameter contracts in the generated API reference.