FSEventStream: fix crash when asking for extended data; support dispatch queues (#12007) (#14318)

Implements support for `FSEventStreamCreateFlags.UseExtendedData`, fixing #12007.

When `.UseExtendedData` is specified, the event data type changes from `CFString` to `CFDictionary`; the dictionary contains the path (`path` key) and inode (`fileID` key) information for the file and may be extended in the future with other fields. Previously this was crashing because we assumed `CFString` always.

Further add a convenience constructor for monitoring a single path, add the missing `UnscheduleFromRunLoop` APIs, and add `SetDispatchQueue` to allow using dispatch queues directly instead of run loops.

Finally, this PR adds a fairly exhaustive file system test which covers the existing (non-extended) and fixed (extended) creation modes, along with using a dispatch queue instead of run loop.

Fixes https://github.com/xamarin/xamarin-macios/issues/12007
This commit is contained in:
Aaron Bockover 2022-03-09 13:05:10 -05:00 коммит произвёл GitHub
Родитель a130485c7e
Коммит 9fe2a5c28c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
2 изменённых файлов: 243 добавлений и 9 удалений

Просмотреть файл

@ -88,10 +88,11 @@ namespace CoreServices
public ulong Id { get; internal set; }
public string? Path { get; internal set; }
public FSEventStreamEventFlags Flags { get; internal set; }
public ulong FileId { get; internal set; }
public override string ToString ()
{
return String.Format ("[FSEvent: Id={0}, Path={1}, Flags={2}]", Id, Path, Flags);
return String.Format ("[FSEvent: Id={0}, Path={1}, Flags={2}, FileId={3}]", Id, Path, Flags, FileId);
}
public const ulong SinceNowId = UInt64.MaxValue;
@ -248,6 +249,14 @@ namespace CoreServices
GCHandle.FromIntPtr (gchandle).Free ();
}
static readonly nint CFStringTypeID = CFString.GetTypeID ();
static readonly nint CFDictionaryTypeID = CFDictionary.GetTypeID ();
// These constants are defined in FSEvents.h but do not end up exported in any binaries,
// so we cannot use Dlfcn.GetStringConstant against CoreServices. -abock, 2022-03-04
static readonly NSString kFSEventStreamEventExtendedDataPathKey = new ("path");
static readonly NSString kFSEventStreamEventExtendedFileIDKey = new ("fileID");
#if NET
[UnmanagedCallersOnly]
#endif
@ -259,12 +268,35 @@ namespace CoreServices
}
var events = new FSEvent[numEvents];
var pathArray = new CFArray (eventPaths, false);
for (int i = 0; i < events.Length; i++) {
events[i].Flags = (FSEventStreamEventFlags)(uint)Marshal.ReadInt32 (eventFlags, i * 4);
events[i].Id = (uint)Marshal.ReadInt64 (eventIds, i * 8);
events[i].Path = CFString.FromHandle (pathArray.GetValue (i));
string? path = null;
long fileId = 0;
var eventDataHandle = CFArray.CFArrayGetValueAtIndex (eventPaths, i);
var eventDataType = CFType.GetTypeID (eventDataHandle);
if (eventDataType == CFStringTypeID) {
path = CFString.FromHandle (eventDataHandle);
} else if (eventDataType == CFDictionaryTypeID) {
path = CFString.FromHandle (CFDictionary.GetValue (
eventDataHandle,
kFSEventStreamEventExtendedDataPathKey.Handle));
var fileIdHandle = CFDictionary.GetValue (
eventDataHandle,
kFSEventStreamEventExtendedFileIDKey.Handle);
if (fileIdHandle != IntPtr.Zero)
CFDictionary.CFNumberGetValue (fileIdHandle, 4 /*kCFNumberSInt64Type*/, out fileId);
}
events[i] = new FSEvent
{
Id = (ulong)Marshal.ReadInt64 (eventIds, i * 8),
Path = path,
Flags = (FSEventStreamEventFlags)(uint)Marshal.ReadInt32 (eventFlags, i * 4),
FileId = (ulong)fileId,
};
}
var instance = GCHandle.FromIntPtr (userData).Target as FSEventStream;
@ -348,6 +380,36 @@ namespace CoreServices
ScheduleWithRunLoop (runLoop.GetCFRunLoop (), CFRunLoop.ModeDefault);
}
[DllImport (Constants.CoreServicesLibrary)]
static extern void FSEventStreamUnscheduleFromRunLoop (IntPtr handle,
IntPtr runLoop, IntPtr runLoopMode);
public void UnscheduleFromRunLoop (CFRunLoop runLoop, NSString runLoopMode)
{
FSEventStreamScheduleWithRunLoop (GetCheckedHandle (), runLoop.Handle, runLoopMode.Handle);
}
public void UnscheduleFromRunLoop (CFRunLoop runLoop)
{
UnscheduleFromRunLoop (runLoop, CFRunLoop.ModeDefault);
}
public void UnscheduleFromRunLoop (NSRunLoop runLoop, NSString runLoopMode)
{
UnscheduleFromRunLoop (runLoop.GetCFRunLoop (), runLoopMode);
}
public void UnscheduleFromRunLoop (NSRunLoop runLoop)
{
UnscheduleFromRunLoop (runLoop.GetCFRunLoop (), CFRunLoop.ModeDefault);
}
[DllImport (Constants.CoreServicesLibrary)]
static extern void FSEventStreamSetDispatchQueue (IntPtr handle, IntPtr dispatchQueue);
public void SetDispatchQueue (DispatchQueue? dispatchQueue)
=> FSEventStreamSetDispatchQueue (GetCheckedHandle (), dispatchQueue.GetHandle ());
[DllImport (Constants.CoreServicesLibrary)]
static extern IntPtr FSEventStreamCopyPathsBeingWatched (IntPtr handle);

Просмотреть файл

@ -5,24 +5,196 @@
#if __MACOS__
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using CoreFoundation;
using CoreServices;
using Foundation;
using ObjCRuntime;
using NUnit.Framework;
namespace MonoTouchFixtures.CoreServices {
using static FSEventStreamCreateFlags;
using static FSEventStreamEventFlags;
[TestFixture]
[Preserve (AllMembers = true)]
public class FSEventStreamTest {
public sealed class FSEventStreamTest {
[Test]
public void TestFileEvents ()
=> RunTest (FileEvents);
[Test]
public void Create ()
public void TestExtendedFileEvents ()
=> RunTest (FileEvents | UseExtendedData);
static void RunTest (FSEventStreamCreateFlags createFlags)
=> new TestFSMonitor (
Xamarin.Cache.CreateTemporaryDirectory (),
createFlags,
maxFilesToCreate: 256).Run ();
/// <summary>
/// Creates a slew of files on a background thread in some directory
/// while simultaneously running an FSEventStream against a private
/// dispatch queue for that directory, and blocks/pumps the main thread
/// while the following work is settling on the two other threads:
///
/// (1) create a bunch of files and directories;
///
/// (2) as the FSEventStream raises events on the dispatch queue,
/// reflect the events (e.g. file created vs file deleted) in our state;
/// if a file was created, delete it, which will trigger another event
/// for the deletion to be recorded.
///
/// (3) when everything has settled (created + deleted), ensure that
/// all created files were seen as created through the FSEventStream and
/// then subsequently seen as deleted.
/// </summary>
sealed class TestFSMonitor : FSEventStream {
static readonly TimeSpan s_testTimeout = TimeSpan.FromSeconds (10);
readonly int _directoriesToCreate;
readonly int _filesPerDirectoryToCreate;
readonly List<string> _createdDirectories = new ();
readonly List<string> _createdThenRemovedFiles = new ();
readonly List<string> _createdFiles = new ();
readonly List<string> _removedFiles = new ();
readonly AutoResetEvent _monitor = new (false);
readonly DispatchQueue _dispatchQueue = new (nameof (FSEventStreamTest));
readonly List<Exception> _exceptions = new ();
readonly string _rootPath;
readonly FSEventStreamCreateFlags _createFlags;
public TestFSMonitor (
string rootPath,
FSEventStreamCreateFlags createFlags,
long maxFilesToCreate)
: base (new [] { rootPath }, TimeSpan.Zero, createFlags)
{
using var eventStream = new FSEventStream (new [] { Path.Combine (Environment.GetEnvironmentVariable ("HOME"), "Desktop") }, TimeSpan.FromSeconds (5), FSEventStreamCreateFlags.FileEvents);
_rootPath = rootPath;
_createFlags = createFlags;
_directoriesToCreate = (int)Math.Sqrt(maxFilesToCreate);
_filesPerDirectoryToCreate = _directoriesToCreate;
}
public void Run ()
{
SetDispatchQueue (_dispatchQueue);
Assert.IsTrue (Start ());
var isWorking = true;
Task.Run (CreateFilesAndWaitForFSEventsThread)
.ContinueWith (task => {
isWorking = false;
if (task.Exception is not null)
_exceptions.Add (task.Exception);
});
while (isWorking)
NSRunLoop.Current.RunUntil (NSDate.Now.AddSeconds (0.1));
if (_exceptions.Count > 0) {
if (_exceptions.Count > 1)
throw new AggregateException (_exceptions);
else
throw _exceptions[0];
}
Assert.IsEmpty (_createdDirectories);
Assert.IsEmpty (_createdFiles);
Assert.IsNotEmpty (_removedFiles);
_removedFiles.Sort ();
_createdThenRemovedFiles.Sort ();
CollectionAssert.AreEqual (_createdThenRemovedFiles, _removedFiles);
Console.WriteLine(
"Observed {0} files created and then removed (flags: {1})",
_createdThenRemovedFiles.Count,
_createFlags);
}
void CreateFilesAndWaitForFSEventsThread ()
{
for (var i = 0; i < _directoriesToCreate; i++) {
var level1Path = Path.Combine (_rootPath, Guid.NewGuid ().ToString ());
lock (_monitor) {
_createdDirectories.Add (level1Path);
Directory.CreateDirectory (level1Path);
}
for (var j = 0; j < _filesPerDirectoryToCreate; j++) {
var level2Path = Path.Combine (level1Path, Guid.NewGuid ().ToString ());
lock (_monitor) {
_createdFiles.Add (level2Path);
_createdThenRemovedFiles.Add (level2Path);
File.Create (level2Path).Dispose ();
}
}
FlushSync ();
}
while (true) {
if (!_monitor.WaitOne (s_testTimeout))
throw new TimeoutException (
$"test has timed out at {s_testTimeout.TotalSeconds}s; " +
"increase the timeout or reduce the number of files created");
if (_createdDirectories.Count == 0 &&
_createdFiles.Count == 0 &&
_removedFiles.Count == _createdThenRemovedFiles.Count)
break;
}
}
protected override void OnEvents (FSEvent[] events) {
try {
lock (_monitor) {
foreach (var evnt in events)
HandleEvent (evnt);
}
} catch (Exception e) {
_exceptions.Add (e);
} finally {
_monitor.Set ();
}
void HandleEvent (FSEvent evnt)
{
Assert.IsNotNull (evnt.Path);
// Roslyn analyzer doesn't consider the assert above wrt nullability
if (evnt.Path is null)
return;
if (_createFlags.HasFlag (UseExtendedData))
Assert.Greater (evnt.FileId, 0);
if (evnt.Flags.HasFlag (ItemCreated)) {
if (evnt.Flags.HasFlag (ItemIsFile)) {
_createdFiles.Remove (evnt.Path);
File.Delete (evnt.Path);
}
if (evnt.Flags.HasFlag (ItemIsDir))
_createdDirectories.Remove (evnt.Path);
}
if (evnt.Flags.HasFlag (ItemRemoved) && !_removedFiles.Contains (evnt.Path))
_removedFiles.Add (evnt.Path);
}
}
}
}
}