diff --git a/src/CoreServices/FSEvents.cs b/src/CoreServices/FSEvents.cs index deca713aad..7b7f1e18c8 100644 --- a/src/CoreServices/FSEvents.cs +++ b/src/CoreServices/FSEvents.cs @@ -11,6 +11,7 @@ #if MONOMAC using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using ObjCRuntime; @@ -163,6 +164,82 @@ namespace CoreServices } } + /// + /// Creation options for . + /// + public sealed class FSEventStreamCreateOptions + { + /// + /// The allocator to use to allocate memory for the stream. If null, the default + /// allocator will be used. + /// + public CFAllocator? Allocator { get; set; } + + /// + /// A dev_t corresponding to the device which you want to receive notifications from. + /// The dev_t is the same as the st_dev field from a stat structure of a + /// file on that device or the f_fsid[0] field of a statfs structure. + /// + public ulong? DeviceToWatch { get; set; } + + /// + /// A list of directory paths, signifying the root of a filesystem hierarchy to be watched + /// for modifications. If 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). + /// + public IReadOnlyList? PathsToWatch { get; set; } + + // NB. to be set only by the FSEventStream .ctors + internal NSArray? NSPathsToWatch { get; set; } + + /// + /// The service will supply events that have happened after the given event ID. To ask for + /// events "since now," set to null or . Often, clients + /// will supply the highest-numbered event ID they have received in a callback, which they can + /// obtain via . Do not set to zero, unless you want to + /// receive events for every directory modified since "the beginning of time" -- an unlikely scenario. + /// + public ulong? SinceWhenId { get; set; } + + /// + /// 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. + /// + public TimeSpan Latency { get; set; } + + /// + /// Flags that modify the behavior of the stream being created. + /// See . + /// + 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 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); diff --git a/src/Foundation/NSArray.cs b/src/Foundation/NSArray.cs index 14cfc0a50f..d9b071172b 100644 --- a/src/Foundation/NSArray.cs +++ b/src/Foundation/NSArray.cs @@ -197,14 +197,16 @@ namespace Foundation { return arr; } - static public NSArray FromStrings (params string [] items) + static public NSArray FromStrings (params string [] items) => FromStrings ((IReadOnlyList)items); + + static public NSArray FromStrings (IReadOnlyList 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.FromObjects (buf, items.Length)); + NSArray arr = Runtime.GetNSObject (NSArray.FromObjects (buf, items.Count)); return arr; } finally { Marshal.FreeHGlobal (buf); diff --git a/tests/monotouch-test/CoreServices/FSEventStreamTest.cs b/tests/monotouch-test/CoreServices/FSEventStreamTest.cs index 07d7ef4138..f410fb2351 100644 --- a/tests/monotouch-test/CoreServices/FSEventStreamTest.cs +++ b/tests/monotouch-test/CoreServices/FSEventStreamTest.cs @@ -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);