Skip to content
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

windows: expose support for recursive directory watches #339

Closed
wants to merge 2 commits into from

Conversation

nicks
Copy link
Contributor

@nicks nicks commented May 7, 2020

What does this pull request do?

On Windows, ReadDirectoryChanges allows recursive watches, so I added a function call to enable it

I saw some discussion on this in these issues:
#18
#21

but given that there's not much activity in this repo, it didn't seem worth blocking on a user-space solutoin to expose this

How should this be manually tested?

I've run the unittest on Windows and confirms it watches things successfully. There's also a new test in integration_test, adapted from the existing subdirectory test.

fen.go Outdated Show resolved Hide resolved
windows.go Outdated Show resolved Hide resolved
kqueue.go Outdated Show resolved Hide resolved
inotify.go Outdated Show resolved Hide resolved
@nicks nicks force-pushed the nicks/recursive-patch branch 2 times, most recently from d4b92eb to 020ef21 Compare May 7, 2020 23:56
@cobolbaby
Copy link

LGTM.

@houndci-bot , could merge this PR?

@nicks Another question is, after configuring the recursive option of syscall.ReadDirectoryChanges, will it hold a lot of memory like the recursive directory addWatch?

The problem I currently encounter is that due to the limitations of business scenarios, there will be a lot of monitoring subdirectories, and the recursive addWatch operation of a directory will occupy more than 1G of memory space, which obviously takes up a lot of resources.

@hu13
Copy link
Contributor

hu13 commented Mar 3, 2021

@nicksantos if this gets merged, you may need to include this #361 when your recursive watch hits a filepath that exceeds windows' syscall.MAX_PATH.

fen.go Outdated Show resolved Hide resolved
inotify.go Outdated Show resolved Hide resolved
integration_test.go Outdated Show resolved Hide resolved
integration_test.go Outdated Show resolved Hide resolved
integration_test.go Outdated Show resolved Hide resolved
integration_test.go Outdated Show resolved Hide resolved
integration_test.go Outdated Show resolved Hide resolved
kqueue.go Outdated Show resolved Hide resolved
@nathany
Copy link
Contributor

nathany commented Jan 19, 2022

@mattn Thanks for doing the review. Since this PR is nearly 2 years old, if @nicks doesn't respond, it may be necessary to gh pr checkout this branch and open a new pull request with the suggested fixes.

@nathany
Copy link
Contributor

nathany commented Jan 19, 2022

#397

@nicks
Copy link
Contributor Author

nicks commented Jan 19, 2022

thanks! addressed all comments. most of the comments were on to changed things that were also issues in other tests, so i took the liberty of addressing those too. also rebased on main.

Comment on lines +53 to +56
// SetRecursive makes future Add() calls watch directories recursively
// if this file system supports that.
// Must be called before Add/Remove.
func (w *Watcher) SetRecursive() error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this API -- what do others think?

Adding a separate API is good in that there aren't any breaking changes for existing users.

There is no way to disable the recursive flag. Is that important?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ya, i could go either way on this.

my thinking was that you want the API to be as restrictive as possible - no switching back and forth between recursive and non-recursive, no switching modes once the watcher has started listening on events.

But certainly SetRecursive(bool) would be a lot more idiomatic.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a lot of different options, and it's important to get the API right, because the same API would need to support a user-space recursive watcher (#18) and FSEvents on macOS.

@pbnjay @rogpeppe Having look at the Mac side, do you have any opinions on how this API should work?

If I recall, macOS is always recursive when making a watch?

This option seems pretty good and is backwards compatible -- a method on the watcher.

Other options would be some other form of NewWatcher with configuration. If we want the watcher to be 100% recursive or 100% not, this may be better.

Or having a separate AddRecursive function, which would allow intermixing of recursive and not-recursive. But we may wind up with too many variants -- like with and without Symlinks, Recursive, Ops filtering, etc.

Copy link
Contributor

@hu13 hu13 Mar 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nathany

I can confirm that fsevents on macOS is recursive by default.

I am in support of Other options would be some other form of NewWatcher with configuration. If we want the watcher to be 100% recursive or 100% not, this may be better.

I don't see a concrete use case in which we want to keep switching back and forth. Most of the time, people want a recursive watch. If there are paths or sub-trees that they are not interested in, they can reasonably add additional filtering-out step at their application level to ignore such events.

Please let us know what else to do for this to be acceptable. thanks

Copy link
Contributor

@hu13 hu13 Mar 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a side note, we forked this (https://github.com/PreVeil/fsnotify/commits/preveil) and enabled a recursive watch like this in our production software and we don't observe issues #259 and #243.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One possibility would be to use Add("dir/**") or Add("dir/...").

Especially since recursive operations may be expensive on some platforms, there may be use cases where you might want things like "recursively watch ~/.cache/go-build, and non-recursively watch ~/.cache/go-mod", or something like that.

The advantage of using a string like that is that #352 can be added without too much friction too as e.g. AddMask("path", fsnotify.CREATE | fsnotify.DELETE) or some such.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One challenge with this approach is what happens on BSD (kqueue) or Linux (inotify), since only Windows and FSEvents on macOS support recursive watching.

Is it an error on some platforms? At least until such time as a user-space recursive watcher (#18) covers those platforms?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One challenge with this approach is what happens on BSD (kqueue) or Linux (inotify), since only Windows and FSEvents on macOS support recursive watching.

Yeah, .Add() can return an error in those cases, similar to how SetRecursive() does now. Something like:

func recursive(path string) (string, bool) {
	if strings.HasSuffix(path, "/...") || strings.HasSuffix(path, `\...`) {
		return path[:len(path)-4], true
	}
	return path, false
}

path, recurse := recursive(path)
if recurse {
	return errors.New("recursion not supported on this system")
}

Or for Windows and other systems where it works:

recurse := false
path, recurse = recursive(path)

Would it make sense to have a syntax like Glob?

I'd be in favour of that; there's an old issue for that (#10).

Using the /... syntax is probably better, since e.g. something like **/*.zip won't work and I don't think we need to support that (it'll be tricky to get that to work well for all cases).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that makes sense, and there is precedence for that format (e.g. go test ./...). Sounds good.

@nathany nathany added the API label Jan 20, 2022
arp242 added a commit that referenced this pull request Jul 25, 2022
This adds a recursive watcher for inotify; per my suggestion in [1] it
uses the "/..." syntax in the path to indicate recursive watches,
similar to how Go tools work:

	w, _ := fsnotify.NewWatcher()
	w.Add("some/dir/...")

This will watch "some/dir" as well as any subdirectories of "some/dir".

The upshot of using a syntax like this is that we can:

1. With AddRecursive(path string) adding new Add* methods would become
   hard; for example AddMask("path", fsnotify.Create) to only get CREATE
   events; we'd then also have to add AddMaskRecursive(). Plus we'd
   have to add a RemoveRecursive() as well.

2. With Watcher.SetRecursive() like in #339 it's not possible to add
   some paths recursively and others non-recursively, which may be
   useful in some cases. Also, it makes it a bit easier to accept user
   input; in the CLI or config you can set "dir/..." and just pass that
   as-is to fsnotify, without applications having to write special code.

For other watchers it will return ErrRecursionUnsupported for now;
Windows support is already mostly finished in #339, and kqueue can be
added in much the same way as inotify in a future PR.

I also moved all test helpers to helper_test.go, and added a bunch of
"shell-like" functions so you're not forever typing error checks and
filepath.Join(). The new "eventCollector" is also useful in tests to
conveniently collect a slice of events.

TODO:

- Also support recursion in Remove(), and deal with paths added with
  "...". I just updated the documentation but didn't actually implement
  anything.

- A few test cases don't seem quite right; want to get #470 merged first
  as it really confuses things.

- Maybe think of a few more test cases.

[1]: #339 (comment)
@arp242 arp242 removed the API label Jul 29, 2022
arp242 added a commit that referenced this pull request Aug 4, 2022
This adds a recursive watcher for inotify; per my suggestion in [1] it
uses the "/..." syntax in the path to indicate recursive watches,
similar to how Go tools work:

	w, _ := fsnotify.NewWatcher()
	w.Add("some/dir/...")

This will watch "some/dir" as well as any subdirectories of "some/dir".

The upshot of using a syntax like this is that we can:

1. With AddRecursive(path string) adding new Add* methods would become
   hard; for example AddMask("path", fsnotify.Create) to only get CREATE
   events; we'd then also have to add AddMaskRecursive(). Plus we'd
   have to add a RemoveRecursive() as well.

2. With Watcher.SetRecursive() like in #339 it's not possible to add
   some paths recursively and others non-recursively, which may be
   useful in some cases. Also, it makes it a bit easier to accept user
   input; in the CLI or config you can set "dir/..." and just pass that
   as-is to fsnotify, without applications having to write special code.

For other watchers it will return ErrRecursionUnsupported for now;
Windows support is already mostly finished in #339, and kqueue can be
added in much the same way as inotify in a future PR.

I also moved all test helpers to helper_test.go, and added a bunch of
"shell-like" functions so you're not forever typing error checks and
filepath.Join(). The new "eventCollector" is also useful in tests to
conveniently collect a slice of events.

TODO:

- Also support recursion in Remove(), and deal with paths added with
  "...". I just updated the documentation but didn't actually implement
  anything.

- A few test cases don't seem quite right; want to get #470 merged first
  as it really confuses things.

- Maybe think of a few more test cases.

[1]: #339 (comment)
arp242 added a commit that referenced this pull request Aug 6, 2022
This adds a recursive watcher for inotify; per my suggestion in [1] it
uses the "/..." syntax in the path to indicate recursive watches,
similar to how Go tools work:

	w, _ := fsnotify.NewWatcher()
	w.Add("some/dir/...")

This will watch "some/dir" as well as any subdirectories of "some/dir".

The upshot of using a syntax like this is that we can:

1. With AddRecursive(path string) adding new Add* methods would become
   hard; for example AddMask("path", fsnotify.Create) to only get CREATE
   events; we'd then also have to add AddMaskRecursive(). Plus we'd
   have to add a RemoveRecursive() as well.

2. With Watcher.SetRecursive() like in #339 it's not possible to add
   some paths recursively and others non-recursively, which may be
   useful in some cases. Also, it makes it a bit easier to accept user
   input; in the CLI or config you can set "dir/..." and just pass that
   as-is to fsnotify, without applications having to write special code.

For other watchers it will return ErrRecursionUnsupported for now;
Windows support is already mostly finished in #339, and kqueue can be
added in much the same way as inotify in a future PR.

I also moved all test helpers to helper_test.go, and added a bunch of
"shell-like" functions so you're not forever typing error checks and
filepath.Join(). The new "eventCollector" is also useful in tests to
conveniently collect a slice of events.

TODO:

- Also support recursion in Remove(), and deal with paths added with
  "...". I just updated the documentation but didn't actually implement
  anything.

- A few test cases don't seem quite right; want to get #470 merged first
  as it really confuses things.

- Maybe think of a few more test cases.

[1]: #339 (comment)
arp242 added a commit that referenced this pull request Aug 6, 2022
This adds a recursive watcher for inotify; per my suggestion in [1] it
uses the "/..." syntax in the path to indicate recursive watches,
similar to how Go tools work:

	w, _ := fsnotify.NewWatcher()
	w.Add("some/dir/...")

This will watch "some/dir" as well as any subdirectories of "some/dir".

The upshot of using a syntax like this is that we can:

1. With AddRecursive(path string) adding new Add* methods would become
   hard; for example AddMask("path", fsnotify.Create) to only get CREATE
   events; we'd then also have to add AddMaskRecursive(). Plus we'd
   have to add a RemoveRecursive() as well.

2. With Watcher.SetRecursive() like in #339 it's not possible to add
   some paths recursively and others non-recursively, which may be
   useful in some cases. Also, it makes it a bit easier to accept user
   input; in the CLI or config you can set "dir/..." and just pass that
   as-is to fsnotify, without applications having to write special code.

For other watchers it will return ErrRecursionUnsupported for now;
Windows support is already mostly finished in #339, and kqueue can be
added in much the same way as inotify in a future PR.

I also moved all test helpers to helper_test.go, and added a bunch of
"shell-like" functions so you're not forever typing error checks and
filepath.Join(). The new "eventCollector" is also useful in tests to
conveniently collect a slice of events.

TODO:

- Also support recursion in Remove(), and deal with paths added with
  "...". I just updated the documentation but didn't actually implement
  anything.

- A few test cases don't seem quite right; want to get #470 merged first
  as it really confuses things.

- Maybe think of a few more test cases.

[1]: #339 (comment)
@arp242
Copy link
Member

arp242 commented Oct 14, 2022

There's still quite a bit of work that needs to be done in terms of API; not 100% sure what it will look like yet, but certainly different from this patch. For example #521 adds a very different way of passing options, and I probably want to use path/... anyway rather than options.

The core is pretty trivial (just pass a boolean around), just need to figure out an API that works well. Either way, much of this will have to be re-done, and I don't think there's much value in keeping this PR open or basing it this PR, as most things will be different.

Can subscribe to #21 for notification when it's added.

@arp242 arp242 closed this Oct 14, 2022
arp242 added a commit that referenced this pull request Nov 17, 2022
Recursive watches can be added by using a "/..." parameter, similar to
the Go command:

	w.Add("dir")         // Behaves as before.
	w.Add("dir/...")     // Watch dir and all paths underneath it.

	w.Remove("dir")      // Remove the watch for dir and, if
	                     // recursive, all paths underneath it too

	w.Remove("dir/...")  // Behaves like just "dir" if the path was
	                     // recursive, error otherwise (probably
	                     // want to add recursive remove too at some
	                     // point).

The advantage of using "/..." vs. an option is that it can be easily
specified in configuration files and the like; for example from a TOML
file:

	[watches]
	dirs = ["/tmp/one", "/tmp/two/..."]

Options for this were previously discussed at:
#339 (comment)

This should be expanded to other backends too; I started with Windows
because the implementation is the both the easiest and has the least
amount of control (just setting a boolean parameter), and we can focus
mostly on writing tests and documentation and the for it, and we can
then match the inotify and kqueue behaviour to the Windows one.

Updates #18
arp242 added a commit that referenced this pull request Nov 17, 2022
Recursive watches can be added by using a "/..." parameter, similar to
the Go command:

	w.Add("dir")         // Behaves as before.
	w.Add("dir/...")     // Watch dir and all paths underneath it.

	w.Remove("dir")      // Remove the watch for dir and, if
	                     // recursive, all paths underneath it too

	w.Remove("dir/...")  // Behaves like just "dir" if the path was
	                     // recursive, error otherwise (probably
	                     // want to add recursive remove too at some
	                     // point).

The advantage of using "/..." vs. an option is that it can be easily
specified in configuration files and the like; for example from a TOML
file:

	[watches]
	dirs = ["/tmp/one", "/tmp/two/..."]

Options for this were previously discussed at:
#339 (comment)

This should be expanded to other backends too; I started with Windows
because the implementation is the both the easiest and has the least
amount of control (just setting a boolean parameter), and we can focus
mostly on writing tests and documentation and the for it, and we can
then match the inotify and kqueue behaviour to the Windows one.

Fixes #21
arp242 added a commit that referenced this pull request Nov 17, 2022
Recursive watches can be added by using a "/..." parameter, similar to
the Go command:

	w.Add("dir")         // Behaves as before.
	w.Add("dir/...")     // Watch dir and all paths underneath it.

	w.Remove("dir")      // Remove the watch for dir and, if
	                     // recursive, all paths underneath it too

	w.Remove("dir/...")  // Behaves like just "dir" if the path was
	                     // recursive, error otherwise (probably
	                     // want to add recursive remove too at some
	                     // point).

The advantage of using "/..." vs. an option is that it can be easily
specified in configuration files and the like; for example from a TOML
file:

	[watches]
	dirs = ["/tmp/one", "/tmp/two/..."]

Options for this were previously discussed at:
#339 (comment)

This should be expanded to other backends too; I started with Windows
because the implementation is the both the easiest and has the least
amount of control (just setting a boolean parameter), and we can focus
mostly on writing tests and documentation and the for it, and we can
then match the inotify and kqueue behaviour to the Windows one.

Fixes #21
arp242 added a commit that referenced this pull request Nov 18, 2022
Recursive watches can be added by using a "/..." parameter, similar to
the Go command:

	w.Add("dir")         // Behaves as before.
	w.Add("dir/...")     // Watch dir and all paths underneath it.

	w.Remove("dir")      // Remove the watch for dir and, if
	                     // recursive, all paths underneath it too

	w.Remove("dir/...")  // Behaves like just "dir" if the path was
	                     // recursive, error otherwise (probably
	                     // want to add recursive remove too at some
	                     // point).

The advantage of using "/..." vs. an option is that it can be easily
specified in configuration files and the like; for example from a TOML
file:

	[watches]
	dirs = ["/tmp/one", "/tmp/two/..."]

Options for this were previously discussed at:
#339 (comment)

This should be expanded to other backends too; I started with Windows
because the implementation is the both the easiest and has the least
amount of control (just setting a boolean parameter), and we can focus
mostly on writing tests and documentation and the for it, and we can
then match the inotify and kqueue behaviour to the Windows one.

Fixes #21
arp242 added a commit that referenced this pull request Nov 28, 2022
Recursive watches can be added by using a "/..." parameter, similar to
the Go command:

	w.Add("dir")         // Behaves as before.
	w.Add("dir/...")     // Watch dir and all paths underneath it.

	w.Remove("dir")      // Remove the watch for dir and, if
	                     // recursive, all paths underneath it too

	w.Remove("dir/...")  // Behaves like just "dir" if the path was
	                     // recursive, error otherwise (probably
	                     // want to add recursive remove too at some
	                     // point).

The advantage of using "/..." vs. an option is that it can be easily
specified in configuration files and the like; for example from a TOML
file:

	[watches]
	dirs = ["/tmp/one", "/tmp/two/..."]

Options for this were previously discussed at:
#339 (comment)

This should be expanded to other backends too; I started with Windows
because the implementation is the both the easiest and has the least
amount of control (just setting a boolean parameter), and we can focus
mostly on writing tests and documentation and the for it, and we can
then match the inotify and kqueue behaviour to the Windows one.

Fixes #21
arp242 added a commit that referenced this pull request Dec 20, 2022
Recursive watches can be added by using a "/..." parameter, similar to
the Go command:

	w.Add("dir")         // Behaves as before.
	w.Add("dir/...")     // Watch dir and all paths underneath it.

	w.Remove("dir")      // Remove the watch for dir and, if
	                     // recursive, all paths underneath it too

	w.Remove("dir/...")  // Behaves like just "dir" if the path was
	                     // recursive, error otherwise (probably
	                     // want to add recursive remove too at some
	                     // point).

The advantage of using "/..." vs. an option is that it can be easily
specified in configuration files and the like; for example from a TOML
file:

	[watches]
	dirs = ["/tmp/one", "/tmp/two/..."]

Options for this were previously discussed at:
#339 (comment)

This should be expanded to other backends too; I started with Windows
because the implementation is the both the easiest and has the least
amount of control (just setting a boolean parameter), and we can focus
mostly on writing tests and documentation and the for it, and we can
then match the inotify and kqueue behaviour to the Windows one.

Fixes #21

Co-authored-by: Milas Bowman <milasb@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants