FSEventStream: bind FSEventStreamCreateRelativeToDevice, FSEventStreamGetDeviceBeingWatched (#14357)

Introduce `FSEventStreamCreateOptions` to avoid a slew of .ctor overrides, and make it easier to specify `DeviceToWatch` and `SinceWhenId`. `SinceWhenId` was previously only exposed on the "low level" .ctor, and it's a rather important parameter for supporting events that may have happened while the application was not running.

Make the existing constructors wrap `FSEventStreamCreateOptions` to avoid API break.

This is a followup to #14318. When using device-relative watches, files can be tracked via a tuple of their device ID and inode instead of paths. #14318 exposes inode data on `FSEvent`.
This commit is contained in:
Aaron Bockover 2022-03-10 15:30:57 -05:00 коммит произвёл GitHub
Родитель d8d90572ce
Коммит 23c8620af0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
3 изменённых файлов: 197 добавлений и 15 удалений

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

@ -11,6 +11,7 @@
#if MONOMAC #if MONOMAC
using System; using System;
using System.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using ObjCRuntime; using ObjCRuntime;
@ -163,6 +164,82 @@ namespace CoreServices
} }
} }
/// <summary>
/// Creation options for <see cref="FSEventStream"/>.
/// </summary>
public sealed class FSEventStreamCreateOptions
{
/// <summary>
/// The allocator to use to allocate memory for the stream. If <c>null</c>, the default
/// allocator will be used.
/// </summary>
public CFAllocator? Allocator { get; set; }
/// <summary>
/// A <c>dev_t</c> corresponding to the device which you want to receive notifications from.
/// The <c>dev_t</c> is the same as the <c>st_dev</c> field from a <c>stat</c> structure of a
/// file on that device or the <c>f_fsid[0]</c> field of a <c>statfs</c> structure.
/// </summary>
public ulong? DeviceToWatch { get; set; }
/// <summary>
/// A list of directory paths, signifying the root of a filesystem hierarchy to be watched
/// for modifications. If <see cref="DeviceToWatch"/> is set, the list of paths should be
/// relative to the root of the device. For example, if a volume "MyData" is mounted at
/// "/Volumes/MyData" and you want to watch "/Volumes/MyData/Pictures/July", specify a path
/// string of "Pictures/July". To watch the root of a volume pass a path of "" (the empty string).
/// </summary>
public IReadOnlyList<string>? PathsToWatch { get; set; }
// NB. to be set only by the FSEventStream .ctors
internal NSArray? NSPathsToWatch { get; set; }
/// <summary>
/// The service will supply events that have happened after the given event ID. To ask for
/// events "since now," set to <c>null</c> or <see cref="FSEvent.SinceNowId"/>. Often, clients
/// will supply the highest-numbered event ID they have received in a callback, which they can
/// obtain via <see cref="FSEventStream.LatestEventId">. Do not set to zero, unless you want to
/// receive events for every directory modified since "the beginning of time" -- an unlikely scenario.
/// </summary>
public ulong? SinceWhenId { get; set; }
/// <summary>
/// The amount of time the service should wait after hearing about an event from the kernel
/// before passing it along to the client via its callback. Specifying a larger value may result
/// in more effective temporal coalescing, resulting in fewer callbacks.
/// </summary>
public TimeSpan Latency { get; set; }
/// <summary>
/// Flags that modify the behavior of the stream being created.
/// See <see cref="FSEventStreamCreateFlags"/>.
/// </summary>
public FSEventStreamCreateFlags Flags { get; set; }
public FSEventStreamCreateOptions ()
{
}
public FSEventStreamCreateOptions (FSEventStreamCreateFlags flags, TimeSpan latency,
params string [] pathsToWatch)
{
Flags = flags;
Latency = latency;
PathsToWatch = pathsToWatch;
}
public FSEventStreamCreateOptions (FSEventStreamCreateFlags flags, TimeSpan latency,
ulong deviceToWatch, params string [] pathsToWatchRelativeToDevice)
{
Flags = flags;
Latency = latency;
DeviceToWatch = deviceToWatch;
PathsToWatch = pathsToWatchRelativeToDevice;
}
public FSEventStream CreateStream () => new (this);
}
public class FSEventStream : NativeObject public class FSEventStream : NativeObject
{ {
[DllImport (Constants.CoreServicesLibrary)] [DllImport (Constants.CoreServicesLibrary)]
@ -194,11 +271,33 @@ namespace CoreServices
ref FSEventStreamContext context, IntPtr pathsToWatch, ref FSEventStreamContext context, IntPtr pathsToWatch,
ulong sinceWhen, double latency, FSEventStreamCreateFlags flags); ulong sinceWhen, double latency, FSEventStreamCreateFlags flags);
public FSEventStream (CFAllocator? allocator, NSArray pathsToWatch, [DllImport (Constants.CoreServicesLibrary)]
ulong sinceWhenId, TimeSpan latency, FSEventStreamCreateFlags flags) unsafe static extern IntPtr FSEventStreamCreateRelativeToDevice (IntPtr allocator,
#if NET
delegate* unmanaged<IntPtr, IntPtr, nint, IntPtr, IntPtr, IntPtr, void> callback,
#else
FSEventStreamCallback callback,
#endif
ref FSEventStreamContext context, ulong deviceToWatch, IntPtr pathsToWatchRelativeToDevice,
ulong sinceWhen, double latency, FSEventStreamCreateFlags flags);
public FSEventStream (FSEventStreamCreateOptions options)
{ {
if (pathsToWatch is null) if (options is null)
throw new ArgumentNullException (nameof (pathsToWatch)); throw new ArgumentNullException (nameof (options));
NSArray pathsToWatch;
if (options.NSPathsToWatch is not null) {
pathsToWatch = options.NSPathsToWatch;
} else if (options.PathsToWatch?.Count == 0) {
throw new ArgumentException (
$"must specify at least one path to watch on " +
$"{nameof (FSEventStreamCreateOptions)}.{nameof (FSEventStreamCreateOptions.PathsToWatch)}",
nameof (options));
} else {
pathsToWatch = NSArray.FromStrings (options.PathsToWatch);
}
var gch = GCHandle.Alloc (this); var gch = GCHandle.Alloc (this);
@ -212,24 +311,58 @@ namespace CoreServices
context.Release = releaseContextCallback; context.Release = releaseContextCallback;
#endif #endif
var allocator = options.Allocator.GetHandle ();
var sinceWhenId = options.SinceWhenId ?? FSEvent.SinceNowId;
var latency = options.Latency.TotalSeconds;
var flags = options.Flags |= (FSEventStreamCreateFlags)0x1 /* UseCFTypes */;
IntPtr handle; IntPtr handle;
unsafe { unsafe {
handle = FSEventStreamCreate ( if (options.DeviceToWatch.HasValue) {
allocator.GetHandle (), handle = FSEventStreamCreateRelativeToDevice (
allocator,
#if NET #if NET
&EventsCallback, &EventsCallback,
#else #else
eventsCallback, eventsCallback,
#endif #endif
ref context, pathsToWatch.Handle, ref context,
sinceWhenId, latency.TotalSeconds, flags | (FSEventStreamCreateFlags)0x1 /* UseCFTypes */); options.DeviceToWatch.Value,
pathsToWatch.Handle, sinceWhenId, latency, flags);
} else {
handle = FSEventStreamCreate (
allocator,
#if NET
&EventsCallback,
#else
eventsCallback,
#endif
ref context,
pathsToWatch.Handle, sinceWhenId, latency, flags);
}
} }
InitializeHandle (handle); InitializeHandle (handle);
} }
public FSEventStream (CFAllocator? allocator, NSArray pathsToWatch,
ulong sinceWhenId, TimeSpan latency, FSEventStreamCreateFlags flags)
: this (new () {
Allocator = allocator,
NSPathsToWatch = pathsToWatch ?? throw new ArgumentNullException (nameof (pathsToWatch)),
SinceWhenId = sinceWhenId,
Latency = latency,
Flags = flags
})
{
}
public FSEventStream (string [] pathsToWatch, TimeSpan latency, FSEventStreamCreateFlags flags) public FSEventStream (string [] pathsToWatch, TimeSpan latency, FSEventStreamCreateFlags flags)
: this (null, NSArray.FromStrings (pathsToWatch), FSEvent.SinceNowId, latency, flags) : this (new () {
PathsToWatch = pathsToWatch ?? throw new ArgumentNullException (nameof (pathsToWatch)),
Latency = latency,
Flags = flags
})
{ {
} }
@ -409,6 +542,11 @@ namespace CoreServices
public void SetDispatchQueue (DispatchQueue? dispatchQueue) public void SetDispatchQueue (DispatchQueue? dispatchQueue)
=> FSEventStreamSetDispatchQueue (GetCheckedHandle (), dispatchQueue.GetHandle ()); => FSEventStreamSetDispatchQueue (GetCheckedHandle (), dispatchQueue.GetHandle ());
[DllImport (Constants.CoreServicesLibrary)]
static extern ulong FSEventStreamGetDeviceBeingWatched (IntPtr handle);
public ulong DeviceBeingWatched => FSEventStreamGetDeviceBeingWatched (GetCheckedHandle ());
[DllImport (Constants.CoreServicesLibrary)] [DllImport (Constants.CoreServicesLibrary)]
static extern IntPtr FSEventStreamCopyPathsBeingWatched (IntPtr handle); static extern IntPtr FSEventStreamCopyPathsBeingWatched (IntPtr handle);

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

@ -197,14 +197,16 @@ namespace Foundation {
return arr; return arr;
} }
static public NSArray FromStrings (params string [] items) static public NSArray FromStrings (params string [] items) => FromStrings ((IReadOnlyList<string>)items);
static public NSArray FromStrings (IReadOnlyList<string> items)
{ {
if (items == null) if (items == null)
throw new ArgumentNullException (nameof (items)); throw new ArgumentNullException (nameof (items));
IntPtr buf = Marshal.AllocHGlobal (items.Length * IntPtr.Size); IntPtr buf = Marshal.AllocHGlobal (items.Count * IntPtr.Size);
try { try {
for (int i = 0; i < items.Length; i++){ for (int i = 0; i < items.Count; i++){
IntPtr val; IntPtr val;
if (items [i] == null) if (items [i] == null)
@ -215,7 +217,7 @@ namespace Foundation {
Marshal.WriteIntPtr (buf, i * IntPtr.Size, val); Marshal.WriteIntPtr (buf, i * IntPtr.Size, val);
} }
NSArray arr = Runtime.GetNSObject<NSArray> (NSArray.FromObjects (buf, items.Length)); NSArray arr = Runtime.GetNSObject<NSArray> (NSArray.FromObjects (buf, items.Count));
return arr; return arr;
} finally { } finally {
Marshal.FreeHGlobal (buf); Marshal.FreeHGlobal (buf);

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

@ -25,6 +25,46 @@ namespace MonoTouchFixtures.CoreServices {
[TestFixture] [TestFixture]
[Preserve (AllMembers = true)] [Preserve (AllMembers = true)]
public sealed class FSEventStreamTest { public sealed class FSEventStreamTest {
[Test]
public void TestPathsBeingWatched ()
{
FSEventStreamCreateOptions createOptions = new () {
Flags = FileEvents | UseExtendedData,
PathsToWatch = new [] {
Xamarin.Cache.CreateTemporaryDirectory (),
Xamarin.Cache.CreateTemporaryDirectory (),
Xamarin.Cache.CreateTemporaryDirectory (),
Xamarin.Cache.CreateTemporaryDirectory ()
}
};
var stream = createOptions.CreateStream ();
CollectionAssert.AreEqual (
createOptions.PathsToWatch,
stream.PathsBeingWatched);
Assert.AreEqual (0, stream.DeviceBeingWatched);
}
[Test]
public void TestPathsBeingWatchedRelativeToDevice ()
{
FSEventStreamCreateOptions createOptions = new () {
Flags = FileEvents | UseExtendedData,
DeviceToWatch = 123456789,
PathsToWatch = new [] { string.Empty }
};
var stream = createOptions.CreateStream ();
CollectionAssert.AreEqual (
createOptions.PathsToWatch,
stream.PathsBeingWatched);
Assert.AreEqual (123456789, stream.DeviceBeingWatched);
}
[Test] [Test]
public void TestFileEvents () public void TestFileEvents ()
=> RunTest (FileEvents); => RunTest (FileEvents);
@ -101,6 +141,8 @@ namespace MonoTouchFixtures.CoreServices {
while (isWorking) while (isWorking)
NSRunLoop.Current.RunUntil (NSDate.Now.AddSeconds (0.1)); NSRunLoop.Current.RunUntil (NSDate.Now.AddSeconds (0.1));
Invalidate ();
if (_exceptions.Count > 0) { if (_exceptions.Count > 0) {
if (_exceptions.Count > 1) if (_exceptions.Count > 1)
throw new AggregateException (_exceptions); throw new AggregateException (_exceptions);