-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Fix invoking of FileSystemWatcher
#53151
Conversation
Tagging subscribers to this area: @carlossanlop Issue DetailsCall Fix #52644
|
src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem.Watcher/tests/Utility/TestFileSystemWatcher.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.cs
Outdated
Show resolved
Hide resolved
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.
I have made the change you suggested and reverted, except I have removed the handler != null check, since the check is also in InvokeOn().
Please put back the null checks. We don't want to do work like allocating the event args or performing a name match if there's no handler registered. The extra null check won't matter.
src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.cs
Outdated
Show resolved
Hide resolved
@@ -102,24 +102,24 @@ public void SynchronizingObject_CalledOnEvent(WatcherChangeTypes expectedChangeT | |||
if (expectedChangeType == WatcherChangeTypes.Created) | |||
{ | |||
watcher.Created += dele; | |||
watcher.CallOnCreated(new FileSystemEventArgs(WatcherChangeTypes.Created, "test", "name")); | |||
watcher.CallNotifyFileSystemEventArgs(WatcherChangeTypes.Created, "name"); |
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.
These tests shouldn't be changing. They were validating the behavior for when the OnXx methods were explicitly called, which, for example, a type derived from FileSystemWatcher could do (that's why these are all using a TestFileSystemWatcher, which is a derived type that just exposes those On methods for use).
So all of these tests changes should be reverted. Instead, tests should be added that validate that actually making file changes which in turn trigger the FileSystemWatcher to invoke event handlers does so with those invocations in the right place.
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.
Thanks for working on this. The product changes now look good. The tests need some further changes, as noted in my comments. Let us know if you need any assistance.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Are the runtime/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.cs Lines 121 to 137 in 6474c72
If so, should public IAsyncResult BeginInvoke(Delegate method, object[] args)
{
Assert.Equal(ExpectedDelegate, method);
BeginInvoke_Called = true;
method.DynamicInvoke(args[0], args[1]);
return null;
} |
This comment has been minimized.
This comment has been minimized.
// block the handling thread | ||
watcher.Changed += (o, e) => unblockHandler.WaitOne(); |
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.
Will this work with a recreated watcher?
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.
I don't understand the question. Can you elaborate?
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.
If watcher
is created in RecreateWatcher()
, will the correct thread be blocked?
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.
Ah, no. The new watcher you're creating won't have the event handler that was originally stored into the first watcher.
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.
I assume I should leave this as is, since this is a pre-existing flaw with FileSystemWatcher_InternalBufferSize()
?
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.
Yeah, we can clean up stuff later if it's an issue.
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.
Except, is this actually pre-existing? Where is the existing FileSystemWatcher_InternalBufferSize recreating the watcher and re-setting this handler?
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.
In the main
branch, FileSystemWatcher_InternalBufferSize()
calls one of the test methods:
runtime/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.InternalBufferSize.cs
Lines 76 to 79 in 016851a
if (setToHigherCapacity) | |
ExpectNoError(watcher, action, cleanup); | |
else | |
ExpectError(watcher, action, cleanup); |
Both of them call TryErrorEvent()
:
runtime/src/libraries/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs
Lines 351 to 355 in 016851a
public static void ExpectError(FileSystemWatcher watcher, Action action, Action cleanup, int attempts = DefaultAttemptsForExpectedEvent) | |
{ | |
string message = string.Format("Did not observe an error event within {0}ms and {1} attempts.", WaitForExpectedEventTimeout, attempts); | |
Assert.True(TryErrorEvent(watcher, action, cleanup, attempts, expected: true), message); | |
} |
runtime/src/libraries/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs
Lines 364 to 368 in 016851a
public static void ExpectNoError(FileSystemWatcher watcher, Action action, Action cleanup, int attempts = DefaultAttemptsForUnExpectedEvent) | |
{ | |
string message = string.Format("Should not observe an error event within {0}ms. Attempted {1} times and received the event each time.", WaitForExpectedEventTimeout, attempts); | |
Assert.False(TryErrorEvent(watcher, action, cleanup, attempts, expected: true), message); | |
} |
TryErrorEvent()
recreates the watcher, but the handler is never re-set:
runtime/src/libraries/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs
Lines 370 to 431 in 016851a
/// /// <summary> | |
/// Helper method for the ExpectError/ExpectNoError functions. | |
/// </summary> | |
/// <param name="watcher">The FileSystemWatcher to test</param> | |
/// <param name="action">The Action to execute.</param> | |
/// <param name="cleanup">Undoes the action and cleans up the watcher so the test may be run again if necessary.</param> | |
/// <param name="attempts">Number of times the test should be executed if it's failing.</param> | |
/// <param name="expected">Whether it is expected that an error event will be arisen.</param> | |
/// <returns>True if an Error event was raised by the watcher when the given action was executed; else, false.</returns> | |
public static bool TryErrorEvent(FileSystemWatcher watcher, Action action, Action cleanup, int attempts, bool expected) | |
{ | |
int attemptsCompleted = 0; | |
bool result = !expected; | |
while (result != expected && attemptsCompleted++ < attempts) | |
{ | |
if (attemptsCompleted > 1) | |
{ | |
// Re-create the watcher to get a clean iteration. | |
watcher = new FileSystemWatcher() | |
{ | |
IncludeSubdirectories = watcher.IncludeSubdirectories, | |
NotifyFilter = watcher.NotifyFilter, | |
Filter = watcher.Filter, | |
Path = watcher.Path, | |
InternalBufferSize = watcher.InternalBufferSize | |
}; | |
// Most intermittent failures in FSW are caused by either a shortage of resources (e.g. inotify instances) | |
// or by insufficient time to execute (e.g. CI gets bogged down). Immediately re-running a failed test | |
// won't resolve the first issue, so we wait a little while hoping that things clear up for the next run. | |
Thread.Sleep(500); | |
} | |
AutoResetEvent errorOccurred = new AutoResetEvent(false); | |
watcher.Error += (o, e) => | |
{ | |
errorOccurred.Set(); | |
}; | |
// Enable raising events but be careful with the possibility of the max user inotify instances being reached already. | |
if (attemptsCompleted <= attempts) | |
{ | |
try | |
{ | |
watcher.EnableRaisingEvents = true; | |
} | |
catch (IOException) // Max User INotify instances. Isn't the type of error we're checking for. | |
{ | |
continue; | |
} | |
} | |
else | |
{ | |
watcher.EnableRaisingEvents = true; | |
} | |
action(); | |
result = errorOccurred.WaitOne(WaitForExpectedEventTimeout); | |
watcher.EnableRaisingEvents = false; | |
cleanup(); | |
} | |
return result; | |
} |
src/libraries/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs
Show resolved
Hide resolved
@thomsj, all fine questions, but most of them don't seem related to this PR. Are there specific questions related to this fix you still have? Thanks. |
[Fact] | ||
public void FileSystemWatcher_Directory_Move_SynchronizingObject() | ||
{ | ||
TestISynchronizeInvoke invoker = new TestISynchronizeInvoke(); |
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.
nit: for all of these tests, you can move this into the using block, just above where you store it into SynchronizingObject.
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.
I've moved this for all the tests. I think I probably put it where I did and didn't use var
because that's how it's done in tests/FileSystemWatcher.cs
src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.InternalBufferSize.cs
Outdated
Show resolved
Hide resolved
public IAsyncResult BeginInvoke(Delegate method, object[] args) | ||
{ | ||
if (ExpectedDelegate != null) | ||
Assert.Equal(ExpectedDelegate, method); | ||
|
||
BeginInvoke_Called = true; | ||
method.DynamicInvoke(args[0], args[1]); | ||
return null; | ||
} |
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.
Are the
TestISynchronizeInvoke
delegates, such as the following, supposed to be invoked?runtime/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.cs
Lines 121 to 137 in 6474c72
/// <summary> /// Ensure that the SynchronizeObject is invoked when an Renamed event occurs /// </summary> [Fact] public void SynchronizingObject_CalledOnRenamed() { RenamedEventHandler dele = (sender, e) => { Assert.Equal(WatcherChangeTypes.Renamed, e.ChangeType); }; TestISynchronizeInvoke invoker = new TestISynchronizeInvoke() { ExpectedDelegate = dele }; using (var testDirectory = new TempDirectory(GetTestFilePath())) using (var watcher = new TestFileSystemWatcher(testDirectory.Path, "*")) { watcher.SynchronizingObject = invoker; watcher.Renamed += dele; watcher.CallOnRenamed(new RenamedEventArgs(WatcherChangeTypes.Changed, "test", "name", "oldname")); Assert.True(invoker.BeginInvoke_Called); } } If so, should
TestISynchronizeInvoke.BeginInvoke()
be changed to something like the below?public IAsyncResult BeginInvoke(Delegate method, object[] args) { Assert.Equal(ExpectedDelegate, method); BeginInvoke_Called = true; method.DynamicInvoke(args[0], args[1]); return null; }
I changed this to something like the above, so the following handler would get invoked:
As you've noted, I've noticed a few things which aren't directly related to this PR, so my only question is, do you want me to remove anything from this PR, such as the changes to |
Yes, please, thanks. I'd like to keep this focused on the fix. |
I've reverted the |
{ | ||
_output?.WriteLine(ex.ToString()); | ||
throw; | ||
} |
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.
Why is all of this stuff changing?
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.
I've reverted the vast majority of the changes to this file.
@stephentoub is next action here on us or @thomsj |
It's on me. |
Call `protected` `On...` event raising methods from `Notify...` methods, to invoke `SynchronizingObject` when required. Fix dotnet#52644
d07203f
to
d8706a1
Compare
LGTM. Thanks, @thomsj. I rebased your branch on top of main. |
Thanks a lot for your guidance, @stephentoub. |
Thanks for the fix :-) |
Call
protected
On...
event raising methods fromNotify...
methods,to invoke
SynchronizingObject
when required.Fix #52644