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
|
#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);
|
||||||
|
|
Загрузка…
Ссылка в новой задаче