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:
Родитель
d8d90572ce
Коммит
23c8620af0
|
@ -11,6 +11,7 @@
|
|||
#if MONOMAC
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
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
|
||||
{
|
||||
[DllImport (Constants.CoreServicesLibrary)]
|
||||
|
@ -194,11 +271,33 @@ namespace CoreServices
|
|||
ref FSEventStreamContext context, IntPtr pathsToWatch,
|
||||
ulong sinceWhen, double latency, FSEventStreamCreateFlags flags);
|
||||
|
||||
public FSEventStream (CFAllocator? allocator, NSArray pathsToWatch,
|
||||
ulong sinceWhenId, TimeSpan latency, FSEventStreamCreateFlags flags)
|
||||
[DllImport (Constants.CoreServicesLibrary)]
|
||||
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)
|
||||
throw new ArgumentNullException (nameof (pathsToWatch));
|
||||
if (options is null)
|
||||
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);
|
||||
|
||||
|
@ -212,24 +311,58 @@ namespace CoreServices
|
|||
context.Release = releaseContextCallback;
|
||||
#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;
|
||||
unsafe {
|
||||
handle = FSEventStreamCreate (
|
||||
allocator.GetHandle (),
|
||||
if (options.DeviceToWatch.HasValue) {
|
||||
handle = FSEventStreamCreateRelativeToDevice (
|
||||
allocator,
|
||||
#if NET
|
||||
&EventsCallback,
|
||||
&EventsCallback,
|
||||
#else
|
||||
eventsCallback,
|
||||
eventsCallback,
|
||||
#endif
|
||||
ref context, pathsToWatch.Handle,
|
||||
sinceWhenId, latency.TotalSeconds, flags | (FSEventStreamCreateFlags)0x1 /* UseCFTypes */);
|
||||
ref context,
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
: 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)
|
||||
=> FSEventStreamSetDispatchQueue (GetCheckedHandle (), dispatchQueue.GetHandle ());
|
||||
|
||||
[DllImport (Constants.CoreServicesLibrary)]
|
||||
static extern ulong FSEventStreamGetDeviceBeingWatched (IntPtr handle);
|
||||
|
||||
public ulong DeviceBeingWatched => FSEventStreamGetDeviceBeingWatched (GetCheckedHandle ());
|
||||
|
||||
[DllImport (Constants.CoreServicesLibrary)]
|
||||
static extern IntPtr FSEventStreamCopyPathsBeingWatched (IntPtr handle);
|
||||
|
||||
|
|
|
@ -197,14 +197,16 @@ namespace Foundation {
|
|||
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)
|
||||
throw new ArgumentNullException (nameof (items));
|
||||
|
||||
IntPtr buf = Marshal.AllocHGlobal (items.Length * IntPtr.Size);
|
||||
IntPtr buf = Marshal.AllocHGlobal (items.Count * IntPtr.Size);
|
||||
try {
|
||||
for (int i = 0; i < items.Length; i++){
|
||||
for (int i = 0; i < items.Count; i++){
|
||||
IntPtr val;
|
||||
|
||||
if (items [i] == null)
|
||||
|
@ -215,7 +217,7 @@ namespace Foundation {
|
|||
|
||||
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;
|
||||
} finally {
|
||||
Marshal.FreeHGlobal (buf);
|
||||
|
|
|
@ -25,6 +25,46 @@ namespace MonoTouchFixtures.CoreServices {
|
|||
[TestFixture]
|
||||
[Preserve (AllMembers = true)]
|
||||
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]
|
||||
public void TestFileEvents ()
|
||||
=> RunTest (FileEvents);
|
||||
|
@ -101,6 +141,8 @@ namespace MonoTouchFixtures.CoreServices {
|
|||
while (isWorking)
|
||||
NSRunLoop.Current.RunUntil (NSDate.Now.AddSeconds (0.1));
|
||||
|
||||
Invalidate ();
|
||||
|
||||
if (_exceptions.Count > 0) {
|
||||
if (_exceptions.Count > 1)
|
||||
throw new AggregateException (_exceptions);
|
||||
|
|
Загрузка…
Ссылка в новой задаче