Lunatik

fsnotifyModule

Filesystem notification.

Places marks on filesystem objects and hands the events they report to a Lua callback. Event masks are the linux.fsnotify bits, named without the FS_ prefix: ACCESS, MODIFY, ATTRIB, CLOSE_WRITE, CLOSE_NOWRITE, OPEN, MOVED_FROM, MOVED_TO, CREATE, DELETE, DELETE_SELF, MOVE_SELF, OPEN_EXEC, UNMOUNT, Q_OVERFLOW, ERROR and RENAME; the flags EVENT_ON_CHILD and ISDIR; and the permission events OPEN_PERM, ACCESS_PERM and OPEN_EXEC_PERM.

A mark goes on one inode, on a mount, or on a whole filesystem. The last two reach every file they cover, so a mark on the mount or the superblock of / sends every access on the machine through the callback; mark a scratch subtree.

Delivery is synchronous: the callback runs inside the syscall of the process performing the access, holding the runtime lock, so every watched access on the machine is serialized behind it. That task may also hold the lock of the directory the event is about, so watch:mark, watch:find and mark:mask called from a callback resolve their path from the directory cache alone.

A permission event parks the syscall in the callback and takes its return value as the answer: fsnotify.action.ALLOW lets the access happen, fsnotify.action.DENY fails it with EPERM, and any other negative errno fails it with that errno, but for the kernel’s own, from ERESTARTSYS on, which no user program is given: those allow and log invalid errno. Anything else allows, a callback that returns nothing or raises included, so a rule that fails, or forgets to answer, takes nothing away. An exec asks twice, OPEN_EXEC_PERM and then OPEN_PERM, each through the marks that carry it, and a denial on the first ends it there. The kernel asks once the LSMs' open and read hooks have allowed the access, and a check an LSM makes later, AppArmor’s on an exec among them, still applies, so a verdict can refuse what they let through and never let through what they refuse.

Class fsnotify_watch

fsnotify_watch:find (path[, kind]) Finds the mark this watch placed on an object.
fsnotify_watch:mark (path, mask[, kind]) Places a mark on a filesystem object.
fsnotify_watch:stop () Stops the watch.

Class fsnotify_mark

fsnotify_mark:ignore ([mask]) Reads the mark’s ignore mask, or sets it.
fsnotify_mark:mask ([mask]) Reads the mark’s event mask, or sets it.
fsnotify_mark:stop () Stops the mark.

Class fsnotify_event

fsnotify_event:dir () Returns the inode number of the directory the entry the event names lives in.
fsnotify_event:ino () Returns the inode number of the object the event is about.
fsnotify_event:isdir () Tells whether the event is about a directory.
fsnotify_event:name () Returns the directory entry name the event is about.
fsnotify_event:path () Returns the full path of the object the event is about.
fsnotify_event:pid () Returns the pid of the task performing the access.

fsnotify

action What a callback answers a permission event with.
watch (callback) Creates a watch.

Class fsnotify_watch in detail

Watches filesystem objects. A userdata returned by fsnotify.watch(), owning an fsnotify group and every mark placed through it.
fsnotify_watch:find (path[, kind])
Finds the mark this watch placed on an object. Resolves path and asks the kernel, which keys the marks by the object they are on, so a script does not have to keep a table of its own.

Parameters

  • path string path of the marked object
  • kind string "inode" (default), "mount" or "sb"; "mount" needs kernel 6.10 or later (optional)

Returns

    fsnotify_mark the mark this watch placed there, or nil when it has none

Raises

if path does not resolve, EAGAIN when it is resolved from a callback and the directory cache alone cannot answer it, if kind is not one this kernel offers, or if the watch has been stopped

Usage

local mark = watch:find("/tmp/scratch", "mount")
fsnotify_watch:mark (path, mask[, kind])
Places a mark on a filesystem object. Resolves path and marks what kind names for the events in mask: the inode it resolves to, the mount it is on, or its whole filesystem. An inode mark on a directory reports events on the files directly inside it only when mask carries linux.fsnotify.EVENT_ON_CHILD, and never on anything deeper; a mount or superblock mark reports every file it covers without it.

EVENT_ON_CHILD reaches a file through its parent in the directory cache. A file reached through a disconnected dentry, opened by handle with open_by_handle_at after the cache dropped its entry, has no parent there: its events reach its own inode mark and the mount and superblock marks only, never the directory’s.

Parameters

  • path string path of the object to mark
  • mask integer event mask, a combination of linux.fsnotify bits
  • kind string "inode" (default), "mount" or "sb"; "mount" needs kernel 6.10 or later (optional)

Returns

    fsnotify_mark the mark, which the watch keeps until it is removed

Raises

if path does not resolve, EAGAIN when it is resolved from a callback and the directory cache alone cannot answer it, if kind is not one this kernel offers, if this watch already marks the object, if the watch has been stopped, or if mask carries a permission event on a kernel built without CONFIG_FANOTIFY_ACCESS_PERMISSIONS; “out of bounds” if mask is negative or past 32 bits

Usage

local mark = watch:mark("/tmp/scratch", fs.OPEN | fs.MODIFY, "mount")
fsnotify_watch:stop ()
Stops the watch. Removes every mark it placed and releases its group, so no further event reaches the callback. Calling it again does nothing. A to-be-closed variable holding the watch stops it the same way.

Returns

    nil

Usage

watch:stop()

Class fsnotify_mark in detail

A mark on a filesystem object. A userdata watch:mark returns and watch:find hands back. The watch owns the mark: dropping the handle leaves the mark in place, and watch:stop removes every mark the watch placed. A handle whose mark is gone raises, but for stop.
fsnotify_mark:ignore ([mask])
Reads the mark’s ignore mask, or sets it. An event in the ignore mask is not reported through this mark, whatever its event mask carries, and the ignore mask a script sets holds until it sets another: a write to the object does not clear it.

Parameters

  • mask integer the events to ignore, a combination of linux.fsnotify bits (optional)

Returns

    integer the mark’s ignore mask

Raises

if the mark has been removed, or “out of bounds” if mask is negative or past 32 bits

Usage

mark:ignore(fs.OPEN)
fsnotify_mark:mask ([mask])
Reads the mark’s event mask, or sets it. Setting it removes the mark and adds it again, because the mask an object is watched for is a sum over its marks that only the kernel’s own add recalculates: path is resolved once more, and an event in between is not reported. The handle stays the same.

Parameters

  • mask integer the new event mask, a combination of linux.fsnotify bits (optional)

Returns

    integer the mark’s event mask

Raises

if the mark has been removed, “out of bounds” if mask is negative or past 32 bits, if mask carries a permission event on a kernel built without CONFIG_FANOTIFY_ACCESS_PERMISSIONS, or if the mark cannot be added again, because the path no longer resolves, EAGAIN when a callback resolving it finds the directory cache alone cannot answer it, or the kernel refuses it, which leaves the mark removed

Usage

mark:mask(mark:mask() | fs.MODIFY)
fsnotify_mark:stop ()
Stops the mark. No further event reaches the callback through it, and the watch drops it. Calling it again, or after watch:stop, does nothing. A to-be-closed variable holding the mark stops it the same way.

Returns

    nil

Usage

mark:stop()

Class fsnotify_event in detail

The event a watch hands to its callback. A userdata reused for every event of the watch that made it, holding what the kernel passed the dispatcher. It is cleared when the callback returns, so a script that keeps it and reads it afterwards gets an error rather than a pointer into a stack frame that is gone.

What an accessor can answer depends on what the kernel attached to the event: an event on an open file carries a struct path and answers everything, while a directory entry event carries only the entry’s dentry or inode and has no path. Each accessor returns nil for a field the event does not carry.

fsnotify_event:dir ()
Returns the inode number of the directory the entry the event names lives in. Carried by the directory entry events, the old directory for RENAME, and by an event on an object whose parent directory watches its children, whether or not name is.

Returns

    integer inode number, or nil when the event names no directory

Raises

if the event is used after its callback returned
fsnotify_event:ino ()
Returns the inode number of the object the event is about. Absent only when the kernel attached neither an inode, a dentry nor a path to the event, and for a directory entry whose inode the filesystem instantiates after reporting the entry.

Returns

    integer inode number, or nil when the event carries no inode

Raises

if the event is used after its callback returned
fsnotify_event:isdir ()
Tells whether the event is about a directory. Reads linux.fsnotify.ISDIR off the mask, so it answers for every event.

Returns

    boolean

Raises

if the event is used after its callback returned
fsnotify_event:name ()
Returns the directory entry name the event is about. Carried by the directory entry events (CREATE, DELETE, MOVED_FROM, MOVED_TO, and RENAME, whose name is the old one) and by an event a parent directory marked with linux.fsnotify.EVENT_ON_CHILD is interested in; an event only the object’s own mark reports carries none.

Returns

    string entry name, or nil when the event carries none

Raises

if the event is used after its callback returned
fsnotify_event:path ()
Returns the full path of the object the event is about. Only an event the kernel raised from an open file carries a struct path; a directory entry event does not, and answers nil. This is the expensive accessor: it resolves the path into a PATH_MAX buffer on every call, so a callback that matches on name never pays for it.

The path is the one the accessing task sees: d_path renders it against that task’s own root, so a chrooted or containerised accessor gets its own view of it, and a file already unlinked carries a trailing " (deleted)".

Returns

    string path, or nil when the event carries none

Raises

if the path cannot be resolved, or if the event is used after its callback returned
fsnotify_event:pid ()
Returns the pid of the task performing the access. Delivery is synchronous, in the syscall of the process being watched, so this is that process and not a bookkeeping artefact. It is a thread id, the same number task:pid() reports.

Returns

    integer pid

Raises

if the event is used after its callback returned

fsnotify in detail

action
What a callback answers a permission event with.

Fields

  • ALLOW let the access happen
  • DENY refuse it; the parked syscall fails with EPERM

Usage

if event:path() == "/srv/secret" then return fsnotify.action.DENY end
watch (callback)
Creates a watch. Allocates an fsnotify group whose events are delivered to callback. Nothing arrives until watch:mark places a mark. An event reaches the callback once per watch, however many of its marks match it: when a file and its parent directory, marked with linux.fsnotify.EVENT_ON_CHILD, are both marked by the same watch, an event on the file arrives once, tagged EVENT_ON_CHILD and carrying the entry’s name and dir.

An event whose delivery would need a lock its own task already holds is dropped rather than run: this callback touching a path it marks, and equally a thread body, a device file operation or another module’s callback in the same runtime.

Parameters

  • callback function invoked as callback(mask, event), where mask is the event mask that fired, testable against linux.fsnotify bits, and event is an fsnotify_event valid only for the length of the call. For a permission event its return value is the verdict, an fsnotify.action; for every other event it is ignored.

Returns

    fsnotify_watch the watch, which fsnotify.watch keeps for its runtime: dropping it stops nothing, and its marks stay until stop or the end of the runtime

Raises

if the group cannot be allocated, if called from an interrupt-context runtime, or if called from a percpu runtime; not allowed while the runtime closes from a finalizer that runs at its close

Usage

local watch = fsnotify.watch(function (mask, event) print(mask, event:name()) end)
watch:mark("/tmp/scratch", fs.OPEN)