-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
OSX FileSystemWatcher needs to root RunningInstance #51538
Conversation
Tagging subscribers to this area: @carlossanlop Issue DetailsProof of concept. Please do not review yet. I'm quickly testing the hypothesis shared here: #30056 (comment) I'm currently having trouble building on my Macbook so I need to use the CI.
|
@@ -49,6 +55,11 @@ private void StartRaisingEvents() | |||
_enabled = true; | |||
|
|||
var instance = new RunningInstance(this, _directory, _includeSubdirectories, TranslateFlags(_notifyFilters)); | |||
if (_cachedRunningInstance.IsAllocated) | |||
{ | |||
throw new Exception("Should've called StopRaisingEvents before restarting"); |
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.
Obviously need a resource string here. But before I add one, do we want to throw here? Should we simply reuse the existing running instance if the user accidentally calls StartRaisingEvents more than once in a row?
The previous behavior was to create a brand new instance every time, which is why I thought it would make sense to continue trying to do the same.
Another option is to free any allocated instances, then set the new instance with Alloc.
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.
To today if I call Start twice in a row, do I experience the same behavior as if I called Start once? Ie, it's effectively a no-op? If so then it seems to me that we don't have any good reason to break them. We essentially document that any redundant Start calls are ignored, and preserve that behavior.
If instead today the user gets into a bad state, then starting to throw might be helpful.
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.
Should we simply reuse the existing running instance if the user accidentally calls StartRaisingEvents more than once in a row?
Can you elaborate on what someone's code would look like to call StartRaisingEvents twice in a row? The method is private. What path through the code would enable that, other than erroneously using an FSW instance in parallel?
|
||
if (_cachedRunningInstance.IsAllocated) | ||
{ | ||
_cachedRunningInstance.Free(); |
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.
Do we want it freed in StopRaisingEvents, or should it be freed only on FinalizeDispose?
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 it is held by RunningInstance then it should presumably be freed in the Dispose of RunningInstance. And you call Dispose here unless you want to preserve the RunningInstance across Start/Stop calls. Which depends on the semantics of FSW. I'm guessing the semantics are that we release all native resources when you do Stop, in case it holds a handle to the directory being watched for example. So in that case StartRaisingEVents creates a RI and StopRaisingEvents disposes the RI
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.
and of course Dispose should also release it in case StopRaisingEvents never got called.
src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs
Show resolved
Hide resolved
@@ -20,11 +20,17 @@ namespace System.IO | |||
{ | |||
public partial class FileSystemWatcher | |||
{ | |||
private GCHandle _cachedRunningInstance; |
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, newline after.
@@ -20,11 +20,17 @@ namespace System.IO | |||
{ | |||
public partial class FileSystemWatcher | |||
{ | |||
private GCHandle _cachedRunningInstance; |
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.
Should this GCHandle rather be in the RunningInstance
? RunningInstance
has the callback that the GCHandle protects, it would be best if this GCHandle is close to where the protected callbacks are.
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.
You can actually simplify the RunningInstance to just use function pointer + GCHandle once you do that. The marshaled delegate that it uses right now is less efficient than function pointer + GCHandle.
There is a lot that is questionable here (in the underlying code, I'm not talking about the proposed changes per-se). The RuntimeInstance is normally de-facto rooted while the FileSystemWatcher is rooted (at least up until the FileSystemWatcher is disabled or disposed). This happens though a rather roundabout route: One of its methods ( It is worth noting that That would almost make sense if leaking RuntimeInstances could happen (i.e. having the CancellationTokenSource not reference a still executing RuntimeInstance), and if the RuntimeInstance also rooted itself, because it would let it eventually clean itself after the watcher has gone away when/if the callback gets triggered, but if this were supposed to be such a weird edge case failsafe, I would have expected a comment saying as much. Instead this just looks a lot like confusion of responsibilities. It is also very much unclear why @stephentoub's analysis would basically be that the RuntimeInstance was somehow not rooted via the CTS, so the FSW's finalizer does not trigger It sure looks like that would only happen if: |
I have posted #52019 with a different take on this issue. |
Fixes #30056
This is an implementation of the hypothesis shared here: #30056 (comment)
FileSystemWatcher is currently causing CI failures, and one potential root cause may be that we are using an event that was generated by an object that is not rooted. The issue might go away if we root the
RunningInstance
instance and get it freed when the FileSystemWatcher is disposed.