//
// Authors:
// Geoff Norton (gnorton@customerdna.com)
+// Cody Russell (cody@xamarin.com)
+// Alexis Christoforides (lexas@xamarin.com)
//
// (c) 2004 Geoff Norton
// Copyright 2014 Xamarin Inc
using System;
using System.Collections;
+using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
+using System.Reflection;
namespace System.IO {
VM = -11
}
+ [Flags]
enum FilterFlags : uint {
ReadPoll = EventFlags.Flag0,
ReadOutOfBand = EventFlags.Flag1,
TimerNanoSeconds = 0x00000004,
TimerAbsolute = 0x00000008,
}
-
+
+ [StructLayout(LayoutKind.Sequential)]
struct kevent : IDisposable {
- public int ident;
+ public UIntPtr ident;
public EventFilter filter;
public EventFlags flags;
public FilterFlags fflags;
- public int data;
+ public IntPtr data;
public IntPtr udata;
public void Dispose ()
if (udata != IntPtr.Zero)
Marshal.FreeHGlobal (udata);
}
+
+
}
+ [StructLayout(LayoutKind.Sequential)]
struct timespec {
- public int tv_sec;
- public int tv_usec;
+ public IntPtr tv_sec;
+ public IntPtr tv_usec;
}
- class KeventFileData {
- public FileSystemInfo fsi;
- public DateTime LastAccessTime;
- public DateTime LastWriteTime;
+ class PathData
+ {
+ public string Path;
+ public bool IsDirectory;
+ public int Fd;
+ }
- public KeventFileData(FileSystemInfo fsi, DateTime LastAccessTime, DateTime LastWriteTime) {
- this.fsi = fsi;
- this.LastAccessTime = LastAccessTime;
- this.LastWriteTime = LastWriteTime;
+ class KqueueMonitor : IDisposable
+ {
+ public int Connection
+ {
+ get { return conn; }
}
- }
- class KeventData {
- public FileSystemWatcher FSW;
- public string Directory;
- public string FileMask;
- public bool IncludeSubdirs;
- public bool Enabled;
- public Hashtable DirEntries;
- public kevent ev;
- }
+ public KqueueMonitor (FileSystemWatcher fsw)
+ {
+ this.fsw = fsw;
+ this.conn = -1;
+ }
- class KeventWatcher : IFileWatcher
- {
- static bool failed;
- static KeventWatcher instance;
- static Hashtable watches;
- static Hashtable requests;
- static Thread thread;
- static int conn;
- static bool stop;
-
- private KeventWatcher ()
+ public void Dispose ()
{
+ CleanUp ();
}
-
- // Locked by caller
- public static bool GetInstance (out IFileWatcher watcher)
+
+ public void Start ()
{
- if (failed == true) {
- watcher = null;
- return false;
- }
+ lock (stateLock) {
+ if (started)
+ return;
- if (instance != null) {
- watcher = instance;
- return true;
- }
+ conn = kqueue ();
- watches = Hashtable.Synchronized (new Hashtable ());
- requests = Hashtable.Synchronized (new Hashtable ());
- conn = kqueue();
- if (conn == -1) {
- failed = true;
- watcher = null;
- return false;
- }
+ if (conn == -1)
+ throw new IOException (String.Format (
+ "kqueue() error at init, error code = '{0}'", Marshal.GetLastWin32Error ()));
+
+ thread = new Thread (() => DoMonitor ());
+ thread.IsBackground = true;
+ thread.Start ();
- instance = new KeventWatcher ();
- watcher = instance;
- return true;
+ startedEvent.WaitOne ();
+
+ if (failedInit) {
+ thread.Join ();
+ CleanUp ();
+ throw new IOException ("Monitor thread failed while initializing.");
+ }
+ else
+ started = true;
+ }
}
-
- public void StartDispatching (FileSystemWatcher fsw)
+
+ public void Stop ()
{
- KeventData data;
- lock (this) {
- if (thread == null) {
- thread = new Thread (new ThreadStart (Monitor));
- thread.IsBackground = true;
- thread.Start ();
- }
+ lock (stateLock) {
+ if (!started)
+ return;
+
+ requestStop = true;
+ thread.Join ();
+ requestStop = false;
- data = (KeventData) watches [fsw];
+ CleanUp ();
+ started = false;
}
+ }
- if (data == null) {
- data = new KeventData ();
- data.FSW = fsw;
- data.Directory = fsw.FullPath;
- data.FileMask = fsw.MangledFilter;
- data.IncludeSubdirs = fsw.IncludeSubdirectories;
-
- data.Enabled = true;
- lock (this) {
- StartMonitoringDirectory (data);
- watches [fsw] = data;
- stop = false;
- }
- }
+ void CleanUp ()
+ {
+ if (conn != -1)
+ close (conn);
+
+ conn = -1;
+
+ foreach (int fd in fdsDict.Keys)
+ close (fd);
+
+ fdsDict.Clear ();
+ pathsDict.Clear ();
}
- static void StartMonitoringDirectory (KeventData data)
+ void DoMonitor ()
{
- DirectoryInfo dir = new DirectoryInfo (data.Directory);
- if(data.DirEntries == null) {
- data.DirEntries = new Hashtable();
- foreach (FileSystemInfo fsi in dir.GetFileSystemInfos() )
- data.DirEntries.Add(fsi.FullName, new KeventFileData(fsi, fsi.LastAccessTime, fsi.LastWriteTime));
+ Exception exc = null;
+ failedInit = false;
+
+ try {
+ Setup ();
+ } catch (Exception e) {
+ failedInit = true;
+ exc = e;
+ } finally {
+ startedEvent.Set ();
}
- int fd = open(data.Directory, 0, 0);
- kevent ev = new kevent();
- ev.udata = IntPtr.Zero;
- timespec nullts = new timespec();
- nullts.tv_sec = 0;
- nullts.tv_usec = 0;
- if (fd > 0) {
- ev.ident = fd;
- ev.filter = EventFilter.Vnode;
- ev.flags = EventFlags.Add | EventFlags.Enable | EventFlags.OneShot;
- ev.fflags = // 20 | 2 | 1 | 8;
- FilterFlags.VNodeDelete |
- FilterFlags.VNodeWrite |
- FilterFlags.VNodeAttrib |
- // The following two values are the equivalent of the original value "20", but we suspect the original author meant
- // 0x20, we will review later with some test cases
- FilterFlags.VNodeLink |
- FilterFlags.VNodeExtend;
- ev.data = 0;
- ev.udata = Marshal.StringToHGlobalAuto (data.Directory);
- kevent outev = new kevent();
- outev.udata = IntPtr.Zero;
- kevent (conn, ref ev, 1, ref outev, 0, ref nullts);
- data.ev = ev;
- requests [fd] = data;
- }
-
- if (!data.IncludeSubdirs)
+ if (failedInit) {
+ fsw.OnError (new ErrorEventArgs (exc));
return;
+ }
+ try {
+ Monitor ();
+ } catch (Exception e) {
+ exc = e;
+ } finally {
+ if (!requestStop) { // failure
+ CleanUp ();
+ started = false;
+ }
+ if (exc != null)
+ fsw.OnError (new ErrorEventArgs (exc));
+ }
}
- public void StopDispatching (FileSystemWatcher fsw)
- {
- KeventData data;
- lock (this) {
- data = (KeventData) watches [fsw];
- if (data == null)
- return;
+ void Setup ()
+ {
+ var initialFds = new List<int> ();
- StopMonitoringDirectory (data);
- watches.Remove (fsw);
- if (watches.Count == 0)
- stop = true;
+ // GetFilenameFromFd() returns the *realpath* which can be different than fsw.FullPath because symlinks.
+ // If so, introduce a fixup step.
+ int fd = open (fsw.FullPath, O_EVTONLY, 0);
+ var resolvedFullPath = GetFilenameFromFd (fd);
+ close (fd);
- if (!data.IncludeSubdirs)
- return;
+ if (resolvedFullPath != fsw.FullPath)
+ fixupPath = resolvedFullPath;
+ else
+ fixupPath = null;
+
+ Scan (fsw.FullPath, false, ref initialFds);
+ var immediate_timeout = new timespec { tv_sec = (IntPtr)0, tv_usec = (IntPtr)0 };
+ var eventBuffer = new kevent[0]; // we don't want to take any events from the queue at this point
+ var changes = CreateChangeList (ref initialFds);
+
+ int numEvents = kevent (conn, changes, changes.Length, eventBuffer, eventBuffer.Length, ref immediate_timeout);
+
+ if (numEvents == -1) {
+ var errMsg = String.Format ("kevent() error at initial event registration, error code = '{0}'", Marshal.GetLastWin32Error ());
+ throw new IOException (errMsg);
}
}
- static void StopMonitoringDirectory (KeventData data)
+ kevent[] CreateChangeList (ref List<int> FdList)
{
- close(data.ev.ident);
+ if (FdList.Count == 0)
+ return emptyEventList;
+
+ var changes = new List<kevent> ();
+ foreach (int fd in FdList) {
+ var change = new kevent {
+
+ ident = (UIntPtr)fd,
+ filter = EventFilter.Vnode,
+ flags = EventFlags.Add | EventFlags.Enable | EventFlags.Clear,
+ fflags = FilterFlags.VNodeDelete | FilterFlags.VNodeExtend |
+ FilterFlags.VNodeRename | FilterFlags.VNodeAttrib |
+ FilterFlags.VNodeLink | FilterFlags.VNodeRevoke |
+ FilterFlags.VNodeWrite,
+ data = IntPtr.Zero,
+ udata = IntPtr.Zero
+ };
+
+ changes.Add (change);
+ }
+ FdList.Clear ();
+
+ return changes.ToArray ();
}
void Monitor ()
{
-
- while (!stop) {
- kevent ev = new kevent();
- ev.udata = IntPtr.Zero;
- kevent nullev = new kevent();
- nullev.udata = IntPtr.Zero;
- timespec ts = new timespec();
- ts.tv_sec = 0;
- ts.tv_usec = 0;
- int haveEvents;
- lock (this) {
- haveEvents = kevent (conn, ref nullev, 0, ref ev, 1, ref ts);
+ var timeout = new timespec { tv_sec = (IntPtr)0, tv_usec = (IntPtr)500000000 };
+ var eventBuffer = new kevent[32];
+ var newFds = new List<int> ();
+ List<PathData> removeQueue = new List<PathData> ();
+ List<string> rescanQueue = new List<string> ();
+
+ while (!requestStop) {
+ var changes = CreateChangeList (ref newFds);
+
+ int numEvents = kevent (conn, changes, changes.Length, eventBuffer, eventBuffer.Length, ref timeout);
+
+ if (numEvents == -1) {
+ var errMsg = String.Format ("kevent() error, error code = '{0}'", Marshal.GetLastWin32Error ());
+ fsw.OnError (new ErrorEventArgs (new IOException (errMsg)));
}
- if (haveEvents > 0) {
- // Restart monitoring
- KeventData data = (KeventData) requests [ev.ident];
- StopMonitoringDirectory (data);
- StartMonitoringDirectory (data);
- ProcessEvent (ev);
- } else {
- System.Threading.Thread.Sleep (500);
+ if (numEvents == 0)
+ continue;
+
+ for (var i = 0; i < numEvents; i++) {
+ var kevt = eventBuffer [i];
+ var pathData = fdsDict [(int)kevt.ident];
+
+ if ((kevt.flags & EventFlags.Error) == EventFlags.Error) {
+ var errMsg = String.Format ("kevent() error watching path '{0}', error code = '{1}'", pathData.Path, kevt.data);
+ fsw.OnError (new ErrorEventArgs (new IOException (errMsg)));
+ continue;
+ }
+
+ if ((kevt.fflags & FilterFlags.VNodeDelete) == FilterFlags.VNodeDelete || (kevt.fflags & FilterFlags.VNodeRevoke) == FilterFlags.VNodeRevoke)
+ removeQueue.Add (pathData);
+
+ else if ((kevt.fflags & FilterFlags.VNodeWrite) == FilterFlags.VNodeWrite) {
+ if (pathData.IsDirectory)
+ rescanQueue.Add (pathData.Path);
+ else
+ PostEvent (FileAction.Modified, pathData.Path);
+ }
+
+ else if ((kevt.fflags & FilterFlags.VNodeRename) == FilterFlags.VNodeRename) {
+ var newFilename = GetFilenameFromFd (pathData.Fd);
+
+ if (newFilename.StartsWith (fsw.FullPath))
+ Rename (pathData, newFilename);
+ else //moved outside of our watched dir so stop watching
+ RemoveTree (pathData);
+ }
+
+ else if ((kevt.fflags & FilterFlags.VNodeAttrib) == FilterFlags.VNodeAttrib || (kevt.fflags & FilterFlags.VNodeExtend) == FilterFlags.VNodeExtend)
+ PostEvent (FileAction.Modified, pathData.Path);
}
+
+ removeQueue.ForEach (Remove);
+ removeQueue.Clear ();
+
+ rescanQueue.ForEach (path => {
+ Scan (path, true, ref newFds);
+ });
+ rescanQueue.Clear ();
+ }
+ }
+
+ PathData Add (string path, bool postEvents, ref List<int> fds)
+ {
+ PathData pathData;
+ pathsDict.TryGetValue (path, out pathData);
+
+ if (pathData != null)
+ return pathData;
+
+ var fd = open (path, O_EVTONLY, 0);
+
+ if (fd == -1) {
+ fsw.OnError (new ErrorEventArgs (new IOException (String.Format (
+ "open() error while attempting to process path '{0}', error code = '{1}'", path, Marshal.GetLastWin32Error ()))));
+ return null;
}
- lock (this) {
- thread = null;
- stop = false;
+ try {
+ fds.Add (fd);
+
+ var attrs = File.GetAttributes (path);
+
+ pathData = new PathData {
+ Path = path,
+ Fd = fd,
+ IsDirectory = (attrs & FileAttributes.Directory) == FileAttributes.Directory
+ };
+
+ pathsDict.Add (path, pathData);
+ fdsDict.Add (fd, pathData);
+
+ if (postEvents)
+ PostEvent (FileAction.Added, path);
+
+ return pathData;
+ } catch (Exception e) {
+ close (fd);
+ fsw.OnError (new ErrorEventArgs (e));
+ return null;
}
+
}
- void ProcessEvent (kevent ev)
+ void Remove (PathData pathData)
{
- lock (this) {
- KeventData data = (KeventData) requests [ev.ident];
- if (!data.Enabled)
- return;
+ fdsDict.Remove (pathData.Fd);
+ pathsDict.Remove (pathData.Path);
+ close (pathData.Fd);
+ PostEvent (FileAction.Removed, pathData.Path);
+ }
- FileSystemWatcher fsw;
- string filename = "";
+ void RemoveTree (PathData pathData)
+ {
+ var toRemove = new List<PathData> ();
- fsw = data.FSW;
- FileAction fa = 0;
- DirectoryInfo dir = new DirectoryInfo (data.Directory);
- FileSystemInfo changedFsi = null;
+ toRemove.Add (pathData);
- try {
- foreach (FileSystemInfo fsi in dir.GetFileSystemInfos() )
- if (data.DirEntries.ContainsKey (fsi.FullName) && (fsi is FileInfo)) {
- KeventFileData entry = (KeventFileData) data.DirEntries [fsi.FullName];
- if (entry.LastWriteTime != fsi.LastWriteTime) {
- filename = fsi.Name;
- fa = FileAction.Modified;
- data.DirEntries [fsi.FullName] = new KeventFileData(fsi, fsi.LastAccessTime, fsi.LastWriteTime);
- if (fsw.IncludeSubdirectories && fsi is DirectoryInfo) {
- data.Directory = filename;
- requests [ev.ident] = data;
- ProcessEvent(ev);
- }
- changedFsi = fsi;
- PostEvent(filename, fsw, fa, changedFsi);
- }
- }
- } catch (Exception) {
- // The file system infos were changed while we processed them
- }
- // Deleted
- try {
- bool deleteMatched = true;
- while(deleteMatched) {
- foreach (KeventFileData entry in data.DirEntries.Values) {
- if (!File.Exists (entry.fsi.FullName) && !Directory.Exists (entry.fsi.FullName)) {
- filename = entry.fsi.Name;
- fa = FileAction.Removed;
- data.DirEntries.Remove (entry.fsi.FullName);
- changedFsi = entry.fsi;
- PostEvent(filename, fsw, fa, changedFsi);
- break;
- }
- }
- deleteMatched = false;
+ if (pathData.IsDirectory) {
+ var prefix = pathData.Path + Path.DirectorySeparatorChar;
+ foreach (var path in pathsDict.Keys)
+ if (path.StartsWith (prefix)) {
+ toRemove.Add (pathsDict [path]);
}
- } catch (Exception) {
- // The file system infos were changed while we processed them
- }
- // Added
- try {
- foreach (FileSystemInfo fsi in dir.GetFileSystemInfos())
- if (!data.DirEntries.ContainsKey (fsi.FullName)) {
- changedFsi = fsi;
- filename = fsi.Name;
- fa = FileAction.Added;
- data.DirEntries [fsi.FullName] = new KeventFileData(fsi, fsi.LastAccessTime, fsi.LastWriteTime);
- PostEvent(filename, fsw, fa, changedFsi);
- }
- } catch (Exception) {
- // The file system infos were changed while we processed them
- }
-
+ }
+ toRemove.ForEach (Remove);
+ }
+ void Rename (PathData pathData, string newRoot)
+ {
+ var toRename = new List<PathData> ();
+ var oldRoot = pathData.Path;
+
+ toRename.Add (pathData);
+
+ if (pathData.IsDirectory) {
+ var prefix = oldRoot + Path.DirectorySeparatorChar;
+ foreach (var path in pathsDict.Keys)
+ if (path.StartsWith (prefix))
+ toRename.Add (pathsDict [path]);
}
+
+ toRename.ForEach ((pd) => {
+ var oldPath = pd.Path;
+ var newPath = newRoot + oldPath.Substring (oldRoot.Length);
+ pd.Path = newPath;
+ pathsDict.Remove (oldPath);
+ pathsDict.Add (newPath, pd);
+ });
+
+ PostEvent (FileAction.RenamedNewName, oldRoot, newRoot);
}
- private void PostEvent (string filename, FileSystemWatcher fsw, FileAction fa, FileSystemInfo changedFsi) {
- RenamedEventArgs renamed = null;
- if (fa == 0)
+ void Scan (string path, bool postEvents, ref List<int> fds)
+ {
+ if (requestStop)
return;
-
- if (fsw.IncludeSubdirectories && fa == FileAction.Added) {
- if (changedFsi is DirectoryInfo) {
- KeventData newdirdata = new KeventData ();
- newdirdata.FSW = fsw;
- newdirdata.Directory = changedFsi.FullName;
- newdirdata.FileMask = fsw.MangledFilter;
- newdirdata.IncludeSubdirs = fsw.IncludeSubdirectories;
-
- newdirdata.Enabled = true;
- lock (this) {
- StartMonitoringDirectory (newdirdata);
- }
+
+ var pathData = Add (path, postEvents, ref fds);
+
+ if (pathData == null)
+ return;
+
+ if (!pathData.IsDirectory)
+ return;
+
+ var dirsToProcess = new List<string> ();
+ dirsToProcess.Add (path);
+
+ while (dirsToProcess.Count > 0) {
+ var tmp = dirsToProcess [0];
+ dirsToProcess.RemoveAt (0);
+
+ var info = new DirectoryInfo (tmp);
+ FileSystemInfo[] fsInfos = null;
+ try {
+ fsInfos = info.GetFileSystemInfos ();
+
+ } catch (IOException) {
+ // this can happen if the directory has been deleted already.
+ // that's okay, just keep processing the other dirs.
+ fsInfos = new FileSystemInfo[0];
+ }
+
+ foreach (var fsi in fsInfos) {
+ if ((fsi.Attributes & FileAttributes.Directory) == FileAttributes.Directory && !fsw.IncludeSubdirectories)
+ continue;
+
+ if ((fsi.Attributes & FileAttributes.Directory) != FileAttributes.Directory && !fsw.Pattern.IsMatch (fsi.FullName))
+ continue;
+
+ var currentPathData = Add (fsi.FullName, postEvents, ref fds);
+
+ if (currentPathData != null && currentPathData.IsDirectory)
+ dirsToProcess.Add (fsi.FullName);
}
}
-
- if (!fsw.Pattern.IsMatch(filename, true))
+ }
+
+ void PostEvent (FileAction action, string path, string newPath = null)
+ {
+ RenamedEventArgs renamed = null;
+
+ if (action == 0)
return;
+ // only post events that match filter pattern. check both old and new paths for renames
+ if (!fsw.Pattern.IsMatch (path) && (newPath == null || !fsw.Pattern.IsMatch (newPath)))
+ return;
+
+ if (action == FileAction.RenamedNewName)
+ renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, "", newPath, path);
+
lock (fsw) {
- if (changedFsi.FullName.StartsWith (fsw.FullPath, StringComparison.Ordinal)) {
- if (fsw.FullPath.EndsWith ("/", StringComparison.Ordinal)) {
- filename = changedFsi.FullName.Substring (fsw.FullPath.Length);
- } else {
- filename = changedFsi.FullName.Substring (fsw.FullPath.Length + 1);
- }
- }
- fsw.DispatchEvents (fa, filename, ref renamed);
+ fsw.DispatchEvents (action, path, ref renamed);
+
if (fsw.Waiting) {
fsw.Waiting = false;
System.Threading.Monitor.PulseAll (fsw);
}
}
+ private string GetFilenameFromFd (int fd)
+ {
+ var sb = new StringBuilder (__DARWIN_MAXPATHLEN);
+
+ if (fcntl (fd, F_GETPATH, sb) != -1) {
+ if (fixupPath != null)
+ sb.Replace (fixupPath, fsw.FullPath, 0, fixupPath.Length); // see Setup()
+ return sb.ToString ();
+ } else {
+ fsw.OnError (new ErrorEventArgs (new IOException (String.Format (
+ "fcntl() error while attempting to get path for fd '{0}', error code = '{1}'", fd, Marshal.GetLastWin32Error ()))));
+ return String.Empty;
+ }
+ }
+
+ const int O_EVTONLY = 0x8000;
+ const int F_GETPATH = 50;
+ const int __DARWIN_MAXPATHLEN = 1024;
+ static readonly kevent[] emptyEventList = new System.IO.kevent[0];
+
+ FileSystemWatcher fsw;
+ int conn;
+ Thread thread;
+ volatile bool requestStop = false;
+ AutoResetEvent startedEvent = new AutoResetEvent (false);
+ bool started = false;
+ bool failedInit = false;
+ object stateLock = new object ();
+
+ readonly Dictionary<string, PathData> pathsDict = new Dictionary<string, PathData> ();
+ readonly Dictionary<int, PathData> fdsDict = new Dictionary<int, PathData> ();
+ string fixupPath = null;
+
+ [DllImport ("libc", EntryPoint="fcntl", CharSet=CharSet.Auto, SetLastError=true)]
+ static extern int fcntl (int file_names_by_descriptor, int cmd, StringBuilder sb);
+
+ [DllImport ("libc")]
+ extern static int open (string path, int flags, int mode_t);
+
[DllImport ("libc")]
- extern static int open(string path, int flags, int mode_t);
-
+ extern static int close (int fd);
+
[DllImport ("libc")]
- extern static int close(int fd);
+ extern static int kqueue ();
[DllImport ("libc")]
- extern static int kqueue();
+ extern static int kevent (int kq, [In]kevent[] ev, int nchanges, [Out]kevent[] evtlist, int nevents, [In] ref timespec time);
+ }
+
+ class KeventWatcher : IFileWatcher
+ {
+ static bool failed;
+ static KeventWatcher instance;
+ static Hashtable watches; // <FileSystemWatcher, KqueueMonitor>
+
+ private KeventWatcher ()
+ {
+ }
+
+ // Locked by caller
+ public static bool GetInstance (out IFileWatcher watcher)
+ {
+ if (failed == true) {
+ watcher = null;
+ return false;
+ }
+
+ if (instance != null) {
+ watcher = instance;
+ return true;
+ }
+
+ watches = Hashtable.Synchronized (new Hashtable ());
+ var conn = kqueue();
+ if (conn == -1) {
+ failed = true;
+ watcher = null;
+ return false;
+ }
+ close (conn);
+
+ instance = new KeventWatcher ();
+ watcher = instance;
+ return true;
+ }
+
+ public void StartDispatching (FileSystemWatcher fsw)
+ {
+ KqueueMonitor monitor;
+
+ if (watches.ContainsKey (fsw)) {
+ monitor = (KqueueMonitor)watches [fsw];
+ } else {
+ monitor = new KqueueMonitor (fsw);
+ watches.Add (fsw, monitor);
+ }
+
+ monitor.Start ();
+ }
+
+ public void StopDispatching (FileSystemWatcher fsw)
+ {
+ KqueueMonitor monitor = (KqueueMonitor)watches [fsw];
+ if (monitor == null)
+ return;
+
+ monitor.Stop ();
+ }
+
+ [DllImport ("libc")]
+ extern static int close (int fd);
[DllImport ("libc")]
- extern static int kevent(int kqueue, ref kevent ev, int nchanges, ref kevent evtlist, int nevents, ref timespec ts);
+ extern static int kqueue ();
}
}