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

Fix invoking of FileSystemWatcher #53151

Merged
merged 1 commit into from
Aug 13, 2021

Conversation

thomsj
Copy link
Contributor

@thomsj thomsj commented May 23, 2021

Call protected On... event raising methods from Notify... methods,
to invoke SynchronizingObject when required.

Fix #52644

@ghost
Copy link

ghost commented May 23, 2021

Tagging subscribers to this area: @carlossanlop
See info in area-owners.md if you want to be subscribed.

Issue Details

Call protected On... event raising methods from Notify... methods,
to invoke SynchronizingObject when required.

Fix #52644

Author: thomsj
Assignees: -
Labels:

area-System.IO

Milestone: -

@dnfadmin
Copy link

dnfadmin commented May 23, 2021

CLA assistant check
All CLA requirements met.

Copy link
Contributor Author

@thomsj thomsj left a 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.

@@ -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");
Copy link
Member

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.

Copy link
Member

@stephentoub stephentoub left a 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.

@thomsj

This comment has been minimized.

@thomsj

This comment has been minimized.

@thomsj

This comment has been minimized.

@thomsj
Copy link
Contributor Author

thomsj commented Jun 14, 2021

Are the TestISynchronizeInvoke delegates, such as the following, supposed to be invoked?

/// <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;
}

@jozkee jozkee added this to the 6.0.0 milestone Jun 16, 2021
@thomsj

This comment has been minimized.

Comment on lines +96 to +97
// block the handling thread
watcher.Changed += (o, e) => unblockHandler.WaitOne();
Copy link
Contributor Author

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?

Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

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.

Copy link
Contributor Author

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()?

Copy link
Member

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.

Copy link
Member

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?

Copy link
Contributor Author

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:

if (setToHigherCapacity)
ExpectNoError(watcher, action, cleanup);
else
ExpectError(watcher, action, cleanup);

Both of them call TryErrorEvent():

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);
}

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:

/// /// <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;
}

@thomsj thomsj requested a review from stephentoub June 29, 2021 00:15
@stephentoub
Copy link
Member

@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();
Copy link
Member

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.

Copy link
Contributor Author

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

Comment on lines +611 to +562
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;
}
Copy link
Contributor Author

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?

/// <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:

https://github.com/dotnet/runtime/blob/9fa24a29c84ddb49492ca9ce7cfcc9bda82ff264/src/libraries/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs#L399-L403

@thomsj
Copy link
Contributor Author

thomsj commented Jul 10, 2021

@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.

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 FileSystemWatcher.OSX.cs?

@thomsj thomsj requested a review from stephentoub July 10, 2021 23:38
@stephentoub
Copy link
Member

do you want me to remove anything from this PR, such as the changes to FileSystemWatcher.OSX.cs?

Yes, please, thanks. I'd like to keep this focused on the fix.

@terrajobst terrajobst added the community-contribution Indicates that the PR has been added by a community member label Jul 19, 2021
@thomsj
Copy link
Contributor Author

thomsj commented Jul 19, 2021

do you want me to remove anything from this PR, such as the changes to FileSystemWatcher.OSX.cs?

Yes, please, thanks. I'd like to keep this focused on the fix.

I've reverted the FileSystemWatcher.OSX.cs changes.

{
_output?.WriteLine(ex.ToString());
throw;
}
Copy link
Member

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?

Copy link
Contributor Author

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.

@danmoseley
Copy link
Member

@stephentoub is next action here on us or @thomsj

@stephentoub
Copy link
Member

@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
@stephentoub stephentoub force-pushed the fix-FileSystemWatcher-invoke branch from d07203f to d8706a1 Compare August 13, 2021 14:24
@stephentoub
Copy link
Member

LGTM. Thanks, @thomsj. I rebased your branch on top of main.

@carlossanlop carlossanlop merged commit 5817dc8 into dotnet:main Aug 13, 2021
@thomsj
Copy link
Contributor Author

thomsj commented Aug 18, 2021

Thanks a lot for your guidance, @stephentoub.

@stephentoub
Copy link
Member

Thanks for the fix :-)

@ghost ghost locked as resolved and limited conversation to collaborators Sep 18, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.IO community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

FileSystemWatcher with winforms Form as SynchronizingObject: event handlers not called on forms thread
7 participants