New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fsmonitor: Implement fsmonitor for Linux #1352
base: master
Are you sure you want to change the base?
Conversation
|
There are issues in commit 39f88e5: |
8622a77
to
20f3e77
Compare
|
There are issues in commit c74ffa4: |
73d2435
to
77ed35b
Compare
|
/submit |
|
Submitted as pull.1352.git.git.1665326258.gitgitgadget@gmail.com To fetch this version into To fetch this version to local tag |
|
This branch is now known as |
|
This patch series was integrated into seen via 529e7ca. |
|
On the Git mailing list, Junio C Hamano wrote (reply to this): "Eric DeCosta via GitGitGadget" <gitgitgadget@gmail.com> writes:
> Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for
> Windows and Mac OS.
>
> This patch set builds upon previous work for done for Windows and Mac OS
> (first 6 patches) ...
For those who are watching from sidelines...
The Windows part is already in Git 2.38; the changes needed for
macOS are already in 'next' and the first 6 patches in this 12-patch
series are identical to them. The patches 7-12 are new.
> to implement a fsmonitor back-end for Linux based on the
> Linux inotify API. inotify differs significantly from the equivalent Windows
> and Mac OS APIs in that a watch must be registered for every directory of
> interest (rather than a singular watch at the root of the directory tree)
> and special care must be taken to handle directory renames correctly.
|
|
On the Git mailing list, Eric Sunshine wrote (reply to this): On Sun, Oct 9, 2022 at 7:55 PM Junio C Hamano <gitster@pobox.com> wrote:
> "Eric DeCosta via GitGitGadget" <gitgitgadget@gmail.com> writes:
> > Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for
> > Windows and Mac OS.
> >
> > This patch set builds upon previous work for done for Windows and Mac OS
> > (first 6 patches) ...
>
> For those who are watching from sidelines...
>
> The Windows part is already in Git 2.38; the changes needed for
> macOS are already in 'next' and the first 6 patches in this 12-patch
> series are identical to them. The patches 7-12 are new.
Thanks for clarifying. I found it confusing that there were a number
of patches in this series which I had already seen despite the cover
letter's claim that this series builds upon "previous work". Thus, it
wasn't clear whether this series was a reroll (refining some
already-seen patches) with additional patches for Linux, or if it was
purely new work with the original patches included by accident[1].
[1]: In the few times I've used GitGitGadget, I've had to jump through
hoops to make it send just the "new" patches when I've built a series
atop some other series only in 'next' or 'seen', so I can understand
the inclusion of the first six patches being accidental. (Regarding
the hoop-jumping, it may be that I just don't understand how to "work"
GitGitGadget or GitHub pull-requests.) |
|
User |
| @@ -0,0 +1,163 @@ | |||
| #include "fsmonitor.h" | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the Git mailing list, Ævar Arnfjörð Bjarmason wrote (reply to this):
On Sun, Oct 09 2022, Eric DeCosta via GitGitGadget wrote:
> From: Eric DeCosta <edecosta@mathworks.com>
>
> Compare the given path to the mounted filesystems. Find the mount that is
> the longest prefix of the path (if any) and determine if that mount is on a
> local or remote filesystem.
>
> Signed-off-by: Eric DeCosta <edecosta@mathworks.com>
> ---
> compat/fsmonitor/fsm-path-utils-linux.c | 163 ++++++++++++++++++++++++
> 1 file changed, 163 insertions(+)
> create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c
>
> diff --git a/compat/fsmonitor/fsm-path-utils-linux.c b/compat/fsmonitor/fsm-path-utils-linux.c
> new file mode 100644
> index 00000000000..369692a788f
> --- /dev/null
> +++ b/compat/fsmonitor/fsm-path-utils-linux.c
> @@ -0,0 +1,163 @@
> +#include "fsmonitor.h"
> +#include "fsmonitor-path-utils.h"
> +#include <errno.h>
> +#include <mntent.h>
> +#include <sys/mount.h>
> +#include <sys/statvfs.h>
> +
> +/*
> + * https://github.com/coreutils/gnulib/blob/master/lib/mountlist.c
> + */
> +#ifndef ME_REMOTE
> +/* A file system is "remote" if its Fs_name contains a ':'
> + or if (it is of type (smbfs or cifs) and its Fs_name starts with '//')
> + or if it is of any other of the listed types
> + or Fs_name is equal to "-hosts" (used by autofs to mount remote fs).
> + "VM" file systems like prl_fs or vboxsf are not considered remote here. */
> +# define ME_REMOTE(Fs_name, Fs_type) \
> + (strchr (Fs_name, ':') != NULL \
> + || ((Fs_name)[0] == '/' \
> + && (Fs_name)[1] == '/' \
> + && (strcmp (Fs_type, "smbfs") == 0 \
> + || strcmp (Fs_type, "smb3") == 0 \
> + || strcmp (Fs_type, "cifs") == 0)) \
> + || strcmp (Fs_type, "acfs") == 0 \
> + || strcmp (Fs_type, "afs") == 0 \
> + || strcmp (Fs_type, "coda") == 0 \
> + || strcmp (Fs_type, "auristorfs") == 0 \
> + || strcmp (Fs_type, "fhgfs") == 0 \
> + || strcmp (Fs_type, "gpfs") == 0 \
> + || strcmp (Fs_type, "ibrix") == 0 \
> + || strcmp (Fs_type, "ocfs2") == 0 \
> + || strcmp (Fs_type, "vxfs") == 0 \
> + || strcmp ("-hosts", Fs_name) == 0)
> +#endif
So, this is just copy/pasted GPLv3 code into our GPLv2-only codebase?:
https://github.com/coreutils/gnulib/blob/cd1fdabe8b66c102124b6a5b0c97dded20246b46/lib/mountlist.c#L230-L247
> +static struct mntent *find_mount(const char *path, const struct statvfs *fs)
> +{
> + const char *const mounts = "/proc/mounts";
> + const char *rp = real_pathdup(path, 1);
> + struct mntent *ment = NULL;
> + struct mntent *found = NULL;
> + struct statvfs mntfs;
> + FILE *fp;
> + int dlen, plen, flen = 0;
> +
> + fp = setmntent(mounts, "r");
> + if (!fp) {
> + error_errno(_("setmntent('%s') failed"), mounts);
> + return NULL;
This code would probably be nicer if you returned int, and passed a
pointer to a struct to populate as a paremeter. Then you could just
"return error..." for this and the below cases.
> + }
> +
> + plen = strlen(rp);
> +
> + /* read all the mount information and compare to path */
> + while ((ment = getmntent(fp)) != NULL) {
> + if (statvfs(ment->mnt_dir, &mntfs)) {
> + switch (errno) {
> + case EPERM:
> + case ESRCH:
> + case EACCES:
> + continue;
> + default:
> + error_errno(_("statvfs('%s') failed"), ment->mnt_dir);
> + endmntent(fp);is
> + return NULL;
> + }
> + }
> +
> + /* is mount on the same filesystem and is a prefix of the path */
> + if ((fs->f_fsid == mntfs.f_fsid) &&
> + !strncmp(ment->mnt_dir, rp, strlen(ment->mnt_dir))) {
> + dlen = strlen(ment->mnt_dir);
> + if (dlen > plen)
> + continue;
> + /*
> + * root is always a potential match; otherwise look for
> + * directory prefix
> + */
> + if ((dlen == 1 && ment->mnt_dir[0] == '/') ||
> + (dlen > flen && (!rp[dlen] || rp[dlen] == '/'))) {
> + flen = dlen;
> + /*
> + * https://man7.org/linux/man-pages/man3/getmntent.3.html
> + *
> + * The pointer points to a static area of memory which is
> + * overwritten by subsequent calls to getmntent().
> + */
> + if (!found)
> + CALLOC_ARRAY(found, 1);
It seems we never populate >1 of these, so don't you just want
xmalloc(). Or actually...
> + free(found->mnt_dir);
> + free(found->mnt_type);
> + free(found->mnt_fsname);
> + found->mnt_dir = xmemdupz(ment->mnt_dir, strlen(ment->mnt_dir));
> + found->mnt_type = xmemdupz(ment->mnt_type, strlen(ment->mnt_type));
> + found->mnt_fsname = xmemdupz(ment->mnt_fsname, strlen(ment->mnt_fsname));
Don't mix mem*() and str*(). In this case we need the string to be '\0'
delimited, so use xstrndup().
And once we do that, we might wonder why we're explicitly finding the
'\0', just to pass it to the xstrn*() function, when we can just do:
found->mnt_dir = xstrdup(ment->mnt_dir);
...
Which would AFAICT be equivalent to what you're doing here.
> + }
> + }
> + }
> + endmntent(fp);
> +
> + return found;
> +}
> +
> +int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)
> +{
> + struct mntent *ment;
...make this (or above) a "struct mntent ment", then pass &ment down, so
we can fill that struct? Dunno...
> + struct statvfs fs;
> +
> + if (statvfs(path, &fs))
> + return error_errno(_("statvfs('%s') failed"), path);
> +
> + ment = find_mount(path, &fs);
> + if (!ment)
> + return -1;
> +
> + trace_printf_key(&trace_fsmonitor,
> + "statvfs('%s') [flags 0x%08lx] '%s' '%s'",
> + path, fs.f_flag, ment->mnt_type, ment->mnt_fsname);
> +
> + if (ME_REMOTE(ment->mnt_fsname, ment->mnt_type))
> + fs_info->is_remote = 1;
> + else
> + fs_info->is_remote = 0;
Shorter:
fs_info->is_remote = ME_REMOTE(ment->mnt_fsname, ment->mnt_type);There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the Git mailing list, Eric DeCosta wrote (reply to this):
> -----Original Message-----
> From: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> Sent: Monday, October 10, 2022 6:04 AM
> To: Eric DeCosta via GitGitGadget <gitgitgadget@gmail.com>
> Cc: git@vger.kernel.org; Eric DeCosta <edecosta@mathworks.com>
> Subject: Re: [PATCH 08/12] fsmonitor: determine if filesystem is local or
> remote
>
>
> On Sun, Oct 09 2022, Eric DeCosta via GitGitGadget wrote:
>
> > From: Eric DeCosta <edecosta@mathworks.com>
> >
> > Compare the given path to the mounted filesystems. Find the mount that
> > is the longest prefix of the path (if any) and determine if that mount
> > is on a local or remote filesystem.
> >
> > Signed-off-by: Eric DeCosta <edecosta@mathworks.com>
> > ---
> > compat/fsmonitor/fsm-path-utils-linux.c | 163
> ++++++++++++++++++++++++
> > 1 file changed, 163 insertions(+)
> > create mode 100644 compat/fsmonitor/fsm-path-utils-linux.c
> >
> > diff --git a/compat/fsmonitor/fsm-path-utils-linux.c
> > b/compat/fsmonitor/fsm-path-utils-linux.c
> > new file mode 100644
> > index 00000000000..369692a788f
> > --- /dev/null
> > +++ b/compat/fsmonitor/fsm-path-utils-linux.c
> > @@ -0,0 +1,163 @@
> > +#include "fsmonitor.h"
> > +#include "fsmonitor-path-utils.h"
> > +#include <errno.h>
> > +#include <mntent.h>
> > +#include <sys/mount.h>
> > +#include <sys/statvfs.h>
> > +
> > +/*
> > + * https://github.com/coreutils/gnulib/blob/master/lib/mountlist.c
> > +<https://protect-
> us.mimecast.com/s/wKJsC9rj7lc3WKmYUOq9E9?domain=gith
> > +ub.com>
> > + */
> > +#ifndef ME_REMOTE
> > +/* A file system is "remote" if its Fs_name contains a ':'
> > + or if (it is of type (smbfs or cifs) and its Fs_name starts with
> > +'//') or if it is of any other of the listed types or Fs_name is
> > +equal to "-hosts" (used by autofs to mount remote fs).
> > + "VM" file systems like prl_fs or vboxsf are not considered remote
> > +here. */ # define ME_REMOTE(Fs_name, Fs_type) \ (strchr (Fs_name,
> > +':') != NULL \
> > + || ((Fs_name)[0] == '/' \
> > + && (Fs_name)[1] == '/' \
> > + && (strcmp (Fs_type, "smbfs") == 0 \
> > + || strcmp (Fs_type, "smb3") == 0 \
> > + || strcmp (Fs_type, "cifs") == 0)) \ strcmp (Fs_type, "acfs") == 0 \
> > + || strcmp (Fs_type, "afs") == 0 \ strcmp (Fs_type, "coda") == 0 \
> > + || strcmp (Fs_type, "auristorfs") == 0 \ strcmp (Fs_type, "fhgfs")
> > + || == 0 \ strcmp (Fs_type, "gpfs") == 0 \ strcmp (Fs_type, "ibrix")
> > + || == 0 \ strcmp (Fs_type, "ocfs2") == 0 \ strcmp (Fs_type, "vxfs")
> > + || == 0 \ strcmp ("-hosts", Fs_name) == 0)
> > +#endif
>
> So, this is just copy/pasted GPLv3 code into our GPLv2-only codebase?:
> https://github.com/coreutils/gnulib/blob/cd1fdabe8b66c102124b6a5b0c97d
> ded20246b46/lib/mountlist.c#L230-L247 <https://protect-
> us.mimecast.com/s/zxPbC0RMy1hoXW2rCOSXJD?domain=github.com>
>
Yes. I was hoping for some guidance as to what to do with ME_REMOTE.
I also found it, verbatim, here in midnight commander:
https://github.com/MidnightCommander/mc/blob/e48cd98ac13a9b4366bd88287f632413766b967f/src/filemanager/mountlist.c#L258-L281
But that's just another GPLv3 code base.
> > +static struct mntent *find_mount(const char *path, const struct
> > +statvfs *fs) { const char *const mounts = "/proc/mounts"; const
> > +char *rp = real_pathdup(path, 1); struct mntent *ment = NULL;
> > +struct mntent *found = NULL; struct statvfs mntfs; FILE *fp; int
> > +dlen, plen, flen = 0;
> > +
> > + fp = setmntent(mounts, "r");
> > + if (!fp) {
> > + error_errno(_("setmntent('%s') failed"), mounts); return NULL;
>
> This code would probably be nicer if you returned int, and passed a pointer
> to a struct to populate as a paremeter. Then you could just "return error..."
> for this and the below cases.
>
> > + }
> > +
> > + plen = strlen(rp);
> > +
> > + /* read all the mount information and compare to path */ while
> > + ((ment = getmntent(fp)) != NULL) { if (statvfs(ment->mnt_dir,
> > + &mntfs)) { switch (errno) { case EPERM:
> > + case ESRCH:
> > + case EACCES:
> > + continue;
> > + default:
> > + error_errno(_("statvfs('%s') failed"), ment->mnt_dir);
> > + endmntent(fp);is return NULL; } }
> > +
> > + /* is mount on the same filesystem and is a prefix of the path */ if
> > + ((fs->f_fsid == mntfs.f_fsid) && !strncmp(ment->mnt_dir, rp,
> > + strlen(ment->mnt_dir))) { dlen = strlen(ment->mnt_dir); if (dlen >
> > + plen) continue;
> > + /*
> > + * root is always a potential match; otherwise look for
> > + * directory prefix
> > + */
> > + if ((dlen == 1 && ment->mnt_dir[0] == '/') || (dlen > flen &&
> > + (!rp[dlen] || rp[dlen] == '/'))) { flen = dlen;
> > + /*
> > + * https://man7.org/linux/man-pages/man3/getmntent.3.html
> > + <https://protect-
> us.mimecast.com/s/aOmSCgJyVrT01WlYc76tSR?domain=man
> > + 7.org>
> > + *
> > + * The pointer points to a static area of memory which is
> > + * overwritten by subsequent calls to getmntent().
> > + */
> > + if (!found)
> > + CALLOC_ARRAY(found, 1);
>
> It seems we never populate >1 of these, so don't you just want xmalloc(). Or
> actually...
>
> > + free(found->mnt_dir);
> > + free(found->mnt_type);
> > + free(found->mnt_fsname);
> > + found->mnt_dir = xmemdupz(ment->mnt_dir, strlen(ment->mnt_dir));
> > + found->mnt_type = xmemdupz(ment->mnt_type, strlen(ment-
> >mnt_type));
> > + found->mnt_fsname = xmemdupz(ment->mnt_fsname,
> > + found->strlen(ment->mnt_fsname));
>
> Don't mix mem*() and str*(). In this case we need the string to be '\0'
> delimited, so use xstrndup().
>
> And once we do that, we might wonder why we're explicitly finding the '\0',
> just to pass it to the xstrn*() function, when we can just do:
>
> found->mnt_dir = xstrdup(ment->mnt_dir);
> ...
>
> Which would AFAICT be equivalent to what you're doing here.
>
> > + }
> > + }
> > + }
> > + endmntent(fp);
> > +
> > + return found;
> > +}
> > +
> > +int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)
> > +{ struct mntent *ment;
>
> ...make this (or above) a "struct mntent ment", then pass &ment down, so
> we can fill that struct? Dunno...
>
> > + struct statvfs fs;
> > +
> > + if (statvfs(path, &fs))
> > + return error_errno(_("statvfs('%s') failed"), path);
> > +
> > + ment = find_mount(path, &fs);
> > + if (!ment)
> > + return -1;
> > +
> > + trace_printf_key(&trace_fsmonitor,
> > + "statvfs('%s') [flags 0x%08lx] '%s' '%s'", path, fs.f_flag,
> > + ment->mnt_type, ment->mnt_fsname);
> > +
> > + if (ME_REMOTE(ment->mnt_fsname, ment->mnt_type)) fs_info-
> >is_remote
> > + = 1; else fs_info->is_remote = 0;
>
> Shorter:
>
> fs_info->is_remote = ME_REMOTE(ment->mnt_fsname, ment->mnt_type);
|
User |
|
On the Git mailing list, Junio C Hamano wrote (reply to this): "Eric DeCosta via GitGitGadget" <gitgitgadget@gmail.com> writes:
> Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for
> Windows and Mac OS.
>
> This patch set builds upon previous work for done for Windows and Mac OS
> (first 6 patches) to implement a fsmonitor back-end for Linux based on the
> Linux inotify API. inotify differs significantly from the equivalent Windows
> and Mac OS APIs in that a watch must be registered for every directory of
> interest (rather than a singular watch at the root of the directory tree)
> and special care must be taken to handle directory renames correctly.
I am seeing occasional breakages of t7527.16 that does not seem to
reproduce reliably. |
|
There was a status update in the "New Topics" section about the branch Bundled fsmonitor for Linux using inotify API. Needs review. source: <pull.1326.v15.git.1664904751.gitgitgadget@gmail.com> source: <pull.1352.git.git.1665326258.gitgitgadget@gmail.com> |
Provide a common interface for getting basic filesystem information including filesystem type and whether the filesystem is remote. Refactor existing code for getting basic filesystem info and detecting remote file systems to the new interface. Refactor filesystem checks to leverage new interface. For macOS, error-out if the Unix Domain socket (UDS) file is on a remote filesystem. Signed-off-by: Eric DeCosta <edecosta@mathworks.com>
If the .git directory is on a remote filesystem, create the socket file in 'fsmonitor.socketDir' if it is defined, else create it in $HOME. Signed-off-by: Eric DeCosta <edecosta@mathworks.com>
If monitoring is done via fsmonitor hook rather than IPC there is no need to check if the location of the Unix Domain socket (UDS) file is on a remote filesystem. Signed-off-by: Eric DeCosta <edecosta@mathworks.com>
Starting with macOS 10.15 (Catalina), Apple introduced a new feature called 'firmlinks' in order to separate the boot volume into two volumes, one read-only and one writable but still present them to the user as a single volume. Along with this change, Apple removed the ability to create symlinks in the root directory and replaced them with 'synthetic firmlinks'. See 'man synthetic.conf' When FSEevents reports the path of changed files, if the path involves a synthetic firmlink, the path is reported from the point of the synthetic firmlink and not the real path. For example: Real path: /System/Volumes/Data/network/working/directory/foo.txt Synthetic firmlink: /network -> /System/Volumes/Data/network FSEvents path: /network/working/directory/foo.txt This causes the FSEvents path to not match against the worktree directory. There are several ways in which synthetic firmlinks can be created: they can be defined in /etc/synthetic.conf, the automounter can create them, and there may be other means. Simply reading /etc/synthetic.conf is insufficient. No matter what process creates synthetic firmlinks, they all get created in the root directory. Therefore, in order to deal with synthetic firmlinks, the root directory is scanned and the first possible synthetic firmink that, when resolved, is a prefix of the worktree is used to map FSEvents paths to worktree paths. Signed-off-by: Eric DeCosta <edecosta@mathworks.com>
If fsmonitor is not in a compatible state, warn with an appropriate message. Signed-off-by: Eric DeCosta <edecosta@mathworks.com>
Add documentation for 'fsmonitor.allowRemote' and 'fsmonitor.socketDir'. Call-out experimental nature of 'fsmonitor.allowRemote' and limited filesystem support for 'fsmonitor.socketDir'. Signed-off-by: Eric DeCosta <edecosta@mathworks.com>
|
There was a status update in the "Cooking" section about the branch Bundled fsmonitor for Linux using inotify API. Needs review. source: <pull.1352.git.git.1665326258.gitgitgadget@gmail.com> |
704e978
to
bc207fe
Compare
Linux and Mac OS can share some of the code originally developed for Mac OS. Minor update to compat/fsmonitor/fsm-ipc-unix.c to make it cross-platform. Mac OS and Linux can share fsm-ipc-unix.c Both platforms can also share compat/fsmonitor/fsm-settings-unix.c but we will leave room for future, platform-specific checks by having the platform- specific implementations call into fsm-settings-unix. Signed-off-by: Eric DeCosta <edecosta@mathworks.com>
Compare the given path to the mounted filesystems. Find the mount that is the longest prefix of the path (if any) and determine if that mount is on a local or remote filesystem. Signed-off-by: Eric DeCosta <edecosta@mathworks.com>
Implement a filesystem change listener for Linux based on the inotify API: https://man7.org/linux/man-pages/man7/inotify.7.html inotify requires registering a watch on every directory in the worktree and special handling of moves/renames. Signed-off-by: Eric DeCosta <edecosta@mathworks.com>
Update build to enable fsmonitor for Linux. Signed-off-by: Eric DeCosta <edecosta@mathworks.com>
t7527-builtin-fsmonitor was leaking fsmonitor--daemon processes in some cases. Accomodate slight difference in the number of events generated on Linux. On lower-powered systems, spin a little to give the daemon time to respond to and log filesystem events. Signed-off-by: Eric DeCosta <edecosta@mathworks.com>
Update the documentation for Linux. Signed-off-by: Eric DeCosta <edecosta@mathworks.com>
|
/submit |
|
Submitted as pull.1352.v2.git.git.1665783944.gitgitgadget@gmail.com To fetch this version into To fetch this version to local tag |
|
On the Git mailing list, Junio C Hamano wrote (reply to this): "Eric DeCosta via GitGitGadget" <gitgitgadget@gmail.com> writes:
> Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for
> Windows and Mac OS.
>
> This patch set builds upon previous work for done for Windows and Mac OS
> (first 6 patches) to implement a fsmonitor back-end for Linux based on the
> Linux inotify API.
Again, the first six patches are a part of what is queued as
ed/fsmonitor-on-networked-macos that is now in 'next' but lacks a
fix-up commit from Jeff King.
I understand that it might not be easy/possible (e.g. perhaps it is
a limitation of GGG?), but I really prefer not to see them re-posted
as part of this series, as I have to apply them and make sure there
are no changes from the last one before discarding them.
Anyway, thanks for an update. Will requeue. |
|
This patch series was integrated into seen via c7e67d6. |


Goal is to deliver fsmonitor for Linux that is on par with fsmonitor for Windows and Mac OS.
This patch set builds upon previous work for done for Windows and Mac OS (first 6 patches) to implement a fsmonitor back-end for Linux based on the Linux inotify API. inotify differs significantly from the equivalent Windows and Mac OS APIs in that a watch must be registered for every directory of interest (rather than a singular watch at the root of the directory tree) and special care must be taken to handle directory renames correctly.
More information about inotify: https://man7.org/linux/man-pages/man7/inotify.7.html
v1 differs from v0:
cc: Eric Sunshine sunshine@sunshineco.com
cc: Ævar Arnfjörð Bjarmason avarab@gmail.com