2014-08-13 10:39:27 +01:00
|
|
|
//
|
|
|
|
// System.IO.KeventWatcher.cs: interface with osx kevent
|
|
|
|
//
|
|
|
|
// Authors:
|
|
|
|
// Geoff Norton (gnorton@customerdna.com)
|
2015-01-13 10:44:36 +00:00
|
|
|
// Cody Russell (cody@xamarin.com)
|
|
|
|
// Alexis Christoforides (lexas@xamarin.com)
|
2014-08-13 10:39:27 +01:00
|
|
|
//
|
|
|
|
// (c) 2004 Geoff Norton
|
|
|
|
// Copyright 2014 Xamarin Inc
|
|
|
|
//
|
|
|
|
// Permission is hereby granted, free of charge, to any person obtaining
|
|
|
|
// a copy of this software and associated documentation files (the
|
|
|
|
// "Software"), to deal in the Software without restriction, including
|
|
|
|
// without limitation the rights to use, copy, modify, merge, publish,
|
|
|
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
|
|
|
// permit persons to whom the Software is furnished to do so, subject to
|
|
|
|
// the following conditions:
|
|
|
|
//
|
|
|
|
// The above copyright notice and this permission notice shall be
|
|
|
|
// included in all copies or substantial portions of the Software.
|
|
|
|
//
|
|
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
|
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
|
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
|
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
|
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
|
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
|
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
//
|
|
|
|
|
|
|
|
using System;
|
|
|
|
using System.Collections;
|
2015-01-13 10:44:36 +00:00
|
|
|
using System.Collections.Generic;
|
2014-08-13 10:39:27 +01:00
|
|
|
using System.ComponentModel;
|
|
|
|
using System.Runtime.CompilerServices;
|
|
|
|
using System.Runtime.InteropServices;
|
|
|
|
using System.Text;
|
|
|
|
using System.Threading;
|
2015-01-13 10:44:36 +00:00
|
|
|
using System.Reflection;
|
2014-08-13 10:39:27 +01:00
|
|
|
|
|
|
|
namespace System.IO {
|
|
|
|
|
|
|
|
[Flags]
|
|
|
|
enum EventFlags : ushort {
|
|
|
|
Add = 0x0001,
|
|
|
|
Delete = 0x0002,
|
|
|
|
Enable = 0x0004,
|
|
|
|
Disable = 0x0008,
|
|
|
|
OneShot = 0x0010,
|
|
|
|
Clear = 0x0020,
|
|
|
|
Receipt = 0x0040,
|
|
|
|
Dispatch = 0x0080,
|
|
|
|
|
|
|
|
Flag0 = 0x1000,
|
|
|
|
Flag1 = 0x2000,
|
|
|
|
SystemFlags = unchecked (0xf000),
|
|
|
|
|
|
|
|
// Return values.
|
|
|
|
EOF = 0x8000,
|
|
|
|
Error = 0x4000,
|
|
|
|
}
|
|
|
|
|
|
|
|
enum EventFilter : short {
|
|
|
|
Read = -1,
|
|
|
|
Write = -2,
|
|
|
|
Aio = -3,
|
|
|
|
Vnode = -4,
|
|
|
|
Proc = -5,
|
|
|
|
Signal = -6,
|
|
|
|
Timer = -7,
|
|
|
|
MachPort = -8,
|
|
|
|
FS = -9,
|
|
|
|
User = -10,
|
|
|
|
VM = -11
|
|
|
|
}
|
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
[Flags]
|
2014-08-13 10:39:27 +01:00
|
|
|
enum FilterFlags : uint {
|
|
|
|
ReadPoll = EventFlags.Flag0,
|
|
|
|
ReadOutOfBand = EventFlags.Flag1,
|
|
|
|
ReadLowWaterMark = 0x00000001,
|
|
|
|
|
|
|
|
WriteLowWaterMark = ReadLowWaterMark,
|
|
|
|
|
|
|
|
NoteTrigger = 0x01000000,
|
|
|
|
NoteFFNop = 0x00000000,
|
|
|
|
NoteFFAnd = 0x40000000,
|
|
|
|
NoteFFOr = 0x80000000,
|
|
|
|
NoteFFCopy = 0xc0000000,
|
|
|
|
NoteFFCtrlMask = 0xc0000000,
|
|
|
|
NoteFFlagsMask = 0x00ffffff,
|
|
|
|
|
|
|
|
VNodeDelete = 0x00000001,
|
|
|
|
VNodeWrite = 0x00000002,
|
|
|
|
VNodeExtend = 0x00000004,
|
|
|
|
VNodeAttrib = 0x00000008,
|
|
|
|
VNodeLink = 0x00000010,
|
|
|
|
VNodeRename = 0x00000020,
|
|
|
|
VNodeRevoke = 0x00000040,
|
|
|
|
VNodeNone = 0x00000080,
|
|
|
|
|
|
|
|
ProcExit = 0x80000000,
|
|
|
|
ProcFork = 0x40000000,
|
|
|
|
ProcExec = 0x20000000,
|
|
|
|
ProcReap = 0x10000000,
|
|
|
|
ProcSignal = 0x08000000,
|
|
|
|
ProcExitStatus = 0x04000000,
|
|
|
|
ProcResourceEnd = 0x02000000,
|
|
|
|
|
|
|
|
// iOS only
|
|
|
|
ProcAppactive = 0x00800000,
|
|
|
|
ProcAppBackground = 0x00400000,
|
|
|
|
ProcAppNonUI = 0x00200000,
|
|
|
|
ProcAppInactive = 0x00100000,
|
|
|
|
ProcAppAllStates = 0x00f00000,
|
|
|
|
|
|
|
|
// Masks
|
|
|
|
ProcPDataMask = 0x000fffff,
|
|
|
|
ProcControlMask = 0xfff00000,
|
|
|
|
|
|
|
|
VMPressure = 0x80000000,
|
|
|
|
VMPressureTerminate = 0x40000000,
|
|
|
|
VMPressureSuddenTerminate = 0x20000000,
|
|
|
|
VMError = 0x10000000,
|
|
|
|
TimerSeconds = 0x00000001,
|
|
|
|
TimerMicroSeconds = 0x00000002,
|
|
|
|
TimerNanoSeconds = 0x00000004,
|
|
|
|
TimerAbsolute = 0x00000008,
|
|
|
|
}
|
2015-01-13 10:44:36 +00:00
|
|
|
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
2014-08-13 10:39:27 +01:00
|
|
|
struct kevent : IDisposable {
|
2015-01-13 10:44:36 +00:00
|
|
|
public UIntPtr ident;
|
2014-08-13 10:39:27 +01:00
|
|
|
public EventFilter filter;
|
|
|
|
public EventFlags flags;
|
|
|
|
public FilterFlags fflags;
|
2015-01-13 10:44:36 +00:00
|
|
|
public IntPtr data;
|
2014-08-13 10:39:27 +01:00
|
|
|
public IntPtr udata;
|
|
|
|
|
|
|
|
public void Dispose ()
|
|
|
|
{
|
|
|
|
if (udata != IntPtr.Zero)
|
|
|
|
Marshal.FreeHGlobal (udata);
|
|
|
|
}
|
2015-01-13 10:44:36 +00:00
|
|
|
|
|
|
|
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
2014-08-13 10:39:27 +01:00
|
|
|
struct timespec {
|
2015-01-13 10:44:36 +00:00
|
|
|
public IntPtr tv_sec;
|
2016-02-22 11:00:01 -05:00
|
|
|
public IntPtr tv_nsec;
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
class PathData
|
|
|
|
{
|
|
|
|
public string Path;
|
|
|
|
public bool IsDirectory;
|
|
|
|
public int Fd;
|
|
|
|
}
|
2014-08-13 10:39:27 +01:00
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
class KqueueMonitor : IDisposable
|
|
|
|
{
|
2016-01-18 21:29:19 -05:00
|
|
|
static bool initialized;
|
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
public int Connection
|
|
|
|
{
|
|
|
|
get { return conn; }
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
public KqueueMonitor (FileSystemWatcher fsw)
|
|
|
|
{
|
|
|
|
this.fsw = fsw;
|
|
|
|
this.conn = -1;
|
2016-01-18 21:29:19 -05:00
|
|
|
if (!initialized){
|
|
|
|
int t;
|
|
|
|
initialized = true;
|
|
|
|
var maxenv = Environment.GetEnvironmentVariable ("MONO_DARWIN_WATCHER_MAXFDS");
|
|
|
|
if (maxenv != null && Int32.TryParse (maxenv, out t))
|
|
|
|
maxFds = t;
|
|
|
|
}
|
2015-01-13 10:44:36 +00:00
|
|
|
}
|
2014-08-13 10:39:27 +01:00
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
public void Dispose ()
|
2014-08-13 10:39:27 +01:00
|
|
|
{
|
2015-01-13 10:44:36 +00:00
|
|
|
CleanUp ();
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
2015-01-13 10:44:36 +00:00
|
|
|
|
|
|
|
public void Start ()
|
2014-08-13 10:39:27 +01:00
|
|
|
{
|
2015-01-13 10:44:36 +00:00
|
|
|
lock (stateLock) {
|
|
|
|
if (started)
|
|
|
|
return;
|
2014-08-13 10:39:27 +01:00
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
conn = kqueue ();
|
2014-08-13 10:39:27 +01:00
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
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 ();
|
2014-08-13 10:39:27 +01:00
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
startedEvent.WaitOne ();
|
|
|
|
|
2015-04-07 09:35:12 +01:00
|
|
|
if (exc != null) {
|
2015-01-13 10:44:36 +00:00
|
|
|
thread.Join ();
|
|
|
|
CleanUp ();
|
2015-04-07 09:35:12 +01:00
|
|
|
throw exc;
|
2015-01-13 10:44:36 +00:00
|
|
|
}
|
2015-04-07 09:35:12 +01:00
|
|
|
|
|
|
|
started = true;
|
2015-01-13 10:44:36 +00:00
|
|
|
}
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
2015-01-13 10:44:36 +00:00
|
|
|
|
|
|
|
public void Stop ()
|
2014-08-13 10:39:27 +01:00
|
|
|
{
|
2015-01-13 10:44:36 +00:00
|
|
|
lock (stateLock) {
|
|
|
|
if (!started)
|
|
|
|
return;
|
|
|
|
|
|
|
|
requestStop = true;
|
2014-08-13 10:39:27 +01:00
|
|
|
|
2015-04-07 09:35:12 +01:00
|
|
|
if (inDispatch)
|
|
|
|
return;
|
|
|
|
// This will break the wait in Monitor ()
|
|
|
|
lock (connLock) {
|
|
|
|
if (conn != -1)
|
|
|
|
close (conn);
|
|
|
|
conn = -1;
|
|
|
|
}
|
|
|
|
|
2018-08-07 15:19:03 +00:00
|
|
|
while (!thread.Join (2000))
|
|
|
|
thread.Interrupt ();
|
2015-04-07 09:35:12 +01:00
|
|
|
|
|
|
|
requestStop = false;
|
2015-01-13 10:44:36 +00:00
|
|
|
started = false;
|
2015-04-07 09:35:12 +01:00
|
|
|
|
|
|
|
if (exc != null)
|
|
|
|
throw exc;
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
2015-01-13 10:44:36 +00:00
|
|
|
}
|
2014-08-13 10:39:27 +01:00
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
void CleanUp ()
|
|
|
|
{
|
2015-04-07 09:35:12 +01:00
|
|
|
lock (connLock) {
|
|
|
|
if (conn != -1)
|
|
|
|
close (conn);
|
|
|
|
conn = -1;
|
|
|
|
}
|
2015-01-13 10:44:36 +00:00
|
|
|
|
|
|
|
foreach (int fd in fdsDict.Keys)
|
|
|
|
close (fd);
|
|
|
|
|
|
|
|
fdsDict.Clear ();
|
|
|
|
pathsDict.Clear ();
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
void DoMonitor ()
|
2015-04-07 09:35:12 +01:00
|
|
|
{
|
2015-01-13 10:44:36 +00:00
|
|
|
try {
|
|
|
|
Setup ();
|
|
|
|
} catch (Exception e) {
|
|
|
|
exc = e;
|
|
|
|
} finally {
|
|
|
|
startedEvent.Set ();
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
|
|
|
|
2015-04-07 09:35:12 +01:00
|
|
|
if (exc != null) {
|
|
|
|
fsw.DispatchErrorEvents (new ErrorEventArgs (exc));
|
2014-08-13 10:39:27 +01:00
|
|
|
return;
|
2015-01-13 10:44:36 +00:00
|
|
|
}
|
2014-08-13 10:39:27 +01:00
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
try {
|
|
|
|
Monitor ();
|
|
|
|
} catch (Exception e) {
|
|
|
|
exc = e;
|
|
|
|
} finally {
|
2015-04-07 09:35:12 +01:00
|
|
|
CleanUp ();
|
2015-01-13 10:44:36 +00:00
|
|
|
if (!requestStop) { // failure
|
|
|
|
started = false;
|
2015-04-07 09:35:12 +01:00
|
|
|
inDispatch = false;
|
|
|
|
fsw.EnableRaisingEvents = false;
|
2015-01-13 10:44:36 +00:00
|
|
|
}
|
|
|
|
if (exc != null)
|
2015-04-07 09:35:12 +01:00
|
|
|
fsw.DispatchErrorEvents (new ErrorEventArgs (exc));
|
|
|
|
requestStop = false;
|
2015-01-13 10:44:36 +00:00
|
|
|
}
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
void Setup ()
|
|
|
|
{
|
|
|
|
var initialFds = new List<int> ();
|
2014-08-13 10:39:27 +01:00
|
|
|
|
2015-04-07 09:35:12 +01:00
|
|
|
// fsw.FullPath may end in '/', see https://bugzilla.xamarin.com/show_bug.cgi?id=5747
|
|
|
|
if (fsw.FullPath != "/" && fsw.FullPath.EndsWith ("/", StringComparison.Ordinal))
|
|
|
|
fullPathNoLastSlash = fsw.FullPath.Substring (0, fsw.FullPath.Length - 1);
|
|
|
|
else
|
|
|
|
fullPathNoLastSlash = fsw.FullPath;
|
|
|
|
|
2016-08-03 10:59:49 +00:00
|
|
|
// realpath() returns the *realpath* which can be different than fsw.FullPath because symlinks.
|
2015-01-13 10:44:36 +00:00
|
|
|
// If so, introduce a fixup step.
|
2016-08-03 10:59:49 +00:00
|
|
|
var sb = new StringBuilder (__DARWIN_MAXPATHLEN);
|
|
|
|
if (realpath(fsw.FullPath, sb) == IntPtr.Zero) {
|
|
|
|
var errMsg = String.Format ("realpath({0}) failed, error code = '{1}'", fsw.FullPath, Marshal.GetLastWin32Error ());
|
|
|
|
throw new IOException (errMsg);
|
|
|
|
}
|
|
|
|
var resolvedFullPath = sb.ToString();
|
2014-08-13 10:39:27 +01:00
|
|
|
|
2015-04-07 09:35:12 +01:00
|
|
|
if (resolvedFullPath != fullPathNoLastSlash)
|
2015-01-13 10:44:36 +00:00
|
|
|
fixupPath = resolvedFullPath;
|
|
|
|
else
|
|
|
|
fixupPath = null;
|
|
|
|
|
2015-04-07 09:35:12 +01:00
|
|
|
Scan (fullPathNoLastSlash, false, ref initialFds);
|
2014-08-13 10:39:27 +01:00
|
|
|
|
2016-02-22 11:00:01 -05:00
|
|
|
var immediate_timeout = new timespec { tv_sec = (IntPtr)0, tv_nsec = (IntPtr)0 };
|
2015-01-13 10:44:36 +00:00
|
|
|
var eventBuffer = new kevent[0]; // we don't want to take any events from the queue at this point
|
|
|
|
var changes = CreateChangeList (ref initialFds);
|
|
|
|
|
2016-08-03 10:59:49 +00:00
|
|
|
int numEvents;
|
|
|
|
int errno = 0;
|
|
|
|
do {
|
|
|
|
numEvents = kevent (conn, changes, changes.Length, eventBuffer, eventBuffer.Length, ref immediate_timeout);
|
|
|
|
if (numEvents == -1) {
|
|
|
|
errno = Marshal.GetLastWin32Error ();
|
|
|
|
}
|
|
|
|
} while (numEvents == -1 && errno == EINTR);
|
2015-01-13 10:44:36 +00:00
|
|
|
|
|
|
|
if (numEvents == -1) {
|
2016-08-03 10:59:49 +00:00
|
|
|
var errMsg = String.Format ("kevent() error at initial event registration, error code = '{0}'", errno);
|
2015-01-13 10:44:36 +00:00
|
|
|
throw new IOException (errMsg);
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
kevent[] CreateChangeList (ref List<int> FdList)
|
2014-08-13 10:39:27 +01:00
|
|
|
{
|
2015-01-13 10:44:36 +00:00
|
|
|
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 ();
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void Monitor ()
|
|
|
|
{
|
2015-01-13 10:44:36 +00:00
|
|
|
var eventBuffer = new kevent[32];
|
|
|
|
var newFds = new List<int> ();
|
|
|
|
List<PathData> removeQueue = new List<PathData> ();
|
|
|
|
List<string> rescanQueue = new List<string> ();
|
|
|
|
|
2015-04-07 09:35:12 +01:00
|
|
|
int retries = 0;
|
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
while (!requestStop) {
|
|
|
|
var changes = CreateChangeList (ref newFds);
|
|
|
|
|
2016-02-22 11:00:01 -05:00
|
|
|
// We are calling an icall, so have to marshal manually
|
|
|
|
// Marshal in
|
|
|
|
int ksize = Marshal.SizeOf<kevent> ();
|
|
|
|
var changesNative = Marshal.AllocHGlobal (ksize * changes.Length);
|
|
|
|
for (int i = 0; i < changes.Length; ++i)
|
|
|
|
Marshal.StructureToPtr (changes [i], changesNative + (i * ksize), false);
|
|
|
|
var eventBufferNative = Marshal.AllocHGlobal (ksize * eventBuffer.Length);
|
|
|
|
|
|
|
|
int numEvents = kevent_notimeout (ref conn, changesNative, changes.Length, eventBufferNative, eventBuffer.Length);
|
|
|
|
|
|
|
|
// Marshal out
|
|
|
|
Marshal.FreeHGlobal (changesNative);
|
|
|
|
for (int i = 0; i < numEvents; ++i)
|
|
|
|
eventBuffer [i] = Marshal.PtrToStructure<kevent> (eventBufferNative + (i * ksize));
|
|
|
|
Marshal.FreeHGlobal (eventBufferNative);
|
2015-01-13 10:44:36 +00:00
|
|
|
|
|
|
|
if (numEvents == -1) {
|
2015-04-07 09:35:12 +01:00
|
|
|
// Stop () signals us to stop by closing the connection
|
|
|
|
if (requestStop)
|
|
|
|
break;
|
2016-08-03 10:59:49 +00:00
|
|
|
int errno = Marshal.GetLastWin32Error ();
|
|
|
|
if (errno != EINTR && ++retries == 3)
|
2015-04-07 09:35:12 +01:00
|
|
|
throw new IOException (String.Format (
|
2016-08-03 10:59:49 +00:00
|
|
|
"persistent kevent() error, error code = '{0}'", errno));
|
2014-08-13 10:39:27 +01:00
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
continue;
|
2015-04-07 09:35:12 +01:00
|
|
|
}
|
|
|
|
retries = 0;
|
2015-01-13 10:44:36 +00:00
|
|
|
|
|
|
|
for (var i = 0; i < numEvents; i++) {
|
|
|
|
var kevt = eventBuffer [i];
|
2015-08-26 07:17:56 -04:00
|
|
|
|
|
|
|
if (!fdsDict.ContainsKey ((int)kevt.ident))
|
|
|
|
// The event is for a file that was removed
|
|
|
|
continue;
|
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
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);
|
2015-04-07 09:35:12 +01:00
|
|
|
fsw.DispatchErrorEvents (new ErrorEventArgs (new IOException (errMsg)));
|
2015-01-13 10:44:36 +00:00
|
|
|
continue;
|
|
|
|
}
|
2015-04-07 09:35:12 +01:00
|
|
|
|
|
|
|
if ((kevt.fflags & FilterFlags.VNodeDelete) == FilterFlags.VNodeDelete || (kevt.fflags & FilterFlags.VNodeRevoke) == FilterFlags.VNodeRevoke) {
|
2015-07-24 04:03:39 -04:00
|
|
|
if (pathData.Path == fullPathNoLastSlash)
|
2015-08-26 07:17:56 -04:00
|
|
|
// The root path is deleted; exit silently
|
2015-07-24 04:03:39 -04:00
|
|
|
return;
|
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
removeQueue.Add (pathData);
|
2015-04-07 09:35:12 +01:00
|
|
|
continue;
|
|
|
|
}
|
2015-01-13 10:44:36 +00:00
|
|
|
|
2015-04-07 09:35:12 +01:00
|
|
|
if ((kevt.fflags & FilterFlags.VNodeRename) == FilterFlags.VNodeRename) {
|
|
|
|
UpdatePath (pathData);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((kevt.fflags & FilterFlags.VNodeWrite) == FilterFlags.VNodeWrite) {
|
|
|
|
if (pathData.IsDirectory) //TODO: Check if dirs trigger Changed events on .NET
|
2015-01-13 10:44:36 +00:00
|
|
|
rescanQueue.Add (pathData.Path);
|
|
|
|
else
|
|
|
|
PostEvent (FileAction.Modified, pathData.Path);
|
2015-04-07 09:35:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if ((kevt.fflags & FilterFlags.VNodeAttrib) == FilterFlags.VNodeAttrib || (kevt.fflags & FilterFlags.VNodeExtend) == FilterFlags.VNodeExtend)
|
2015-01-13 10:44:36 +00:00
|
|
|
PostEvent (FileAction.Modified, pathData.Path);
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
2015-01-13 10:44:36 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
|
2015-04-07 09:35:12 +01:00
|
|
|
if (fdsDict.Count >= maxFds)
|
2015-08-26 07:17:56 -04:00
|
|
|
throw new IOException ("kqueue() FileSystemWatcher has reached the maximum number of files to watch.");
|
2015-04-07 09:35:12 +01:00
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
var fd = open (path, O_EVTONLY, 0);
|
|
|
|
|
|
|
|
if (fd == -1) {
|
2015-04-07 09:35:12 +01:00
|
|
|
fsw.DispatchErrorEvents (new ErrorEventArgs (new IOException (String.Format (
|
2015-01-13 10:44:36 +00:00
|
|
|
"open() error while attempting to process path '{0}', error code = '{1}'", path, Marshal.GetLastWin32Error ()))));
|
|
|
|
return null;
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
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);
|
2015-04-07 09:35:12 +01:00
|
|
|
fsw.DispatchErrorEvents (new ErrorEventArgs (e));
|
2015-01-13 10:44:36 +00:00
|
|
|
return null;
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
2015-01-13 10:44:36 +00:00
|
|
|
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
void Remove (PathData pathData)
|
2014-08-13 10:39:27 +01:00
|
|
|
{
|
2015-01-13 10:44:36 +00:00
|
|
|
fdsDict.Remove (pathData.Fd);
|
|
|
|
pathsDict.Remove (pathData.Path);
|
|
|
|
close (pathData.Fd);
|
|
|
|
PostEvent (FileAction.Removed, pathData.Path);
|
|
|
|
}
|
2014-08-13 10:39:27 +01:00
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
void RemoveTree (PathData pathData)
|
|
|
|
{
|
|
|
|
var toRemove = new List<PathData> ();
|
2014-08-13 10:39:27 +01:00
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
toRemove.Add (pathData);
|
2014-08-13 10:39:27 +01:00
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
if (pathData.IsDirectory) {
|
|
|
|
var prefix = pathData.Path + Path.DirectorySeparatorChar;
|
|
|
|
foreach (var path in pathsDict.Keys)
|
|
|
|
if (path.StartsWith (prefix)) {
|
|
|
|
toRemove.Add (pathsDict [path]);
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
2015-01-13 10:44:36 +00:00
|
|
|
}
|
|
|
|
toRemove.ForEach (Remove);
|
|
|
|
}
|
2014-08-13 10:39:27 +01:00
|
|
|
|
2015-04-07 09:35:12 +01:00
|
|
|
void UpdatePath (PathData pathData)
|
2015-01-13 10:44:36 +00:00
|
|
|
{
|
2015-04-07 09:35:12 +01:00
|
|
|
var newRoot = GetFilenameFromFd (pathData.Fd);
|
|
|
|
if (!newRoot.StartsWith (fullPathNoLastSlash)) { // moved outside of our watched path (so stop observing it)
|
|
|
|
RemoveTree (pathData);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
var toRename = new List<PathData> ();
|
|
|
|
var oldRoot = pathData.Path;
|
|
|
|
|
|
|
|
toRename.Add (pathData);
|
|
|
|
|
2015-04-07 09:35:12 +01:00
|
|
|
if (pathData.IsDirectory) { // anything under the directory must have their paths updated
|
2015-01-13 10:44:36 +00:00
|
|
|
var prefix = oldRoot + Path.DirectorySeparatorChar;
|
|
|
|
foreach (var path in pathsDict.Keys)
|
|
|
|
if (path.StartsWith (prefix))
|
|
|
|
toRename.Add (pathsDict [path]);
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
2015-04-07 09:35:12 +01:00
|
|
|
|
|
|
|
foreach (var renaming in toRename) {
|
|
|
|
var oldPath = renaming.Path;
|
2015-01-13 10:44:36 +00:00
|
|
|
var newPath = newRoot + oldPath.Substring (oldRoot.Length);
|
2015-04-07 09:35:12 +01:00
|
|
|
|
|
|
|
renaming.Path = newPath;
|
2015-01-13 10:44:36 +00:00
|
|
|
pathsDict.Remove (oldPath);
|
|
|
|
|
2015-04-07 09:35:12 +01:00
|
|
|
// destination may exist in our records from a Created event, take care of it
|
|
|
|
if (pathsDict.ContainsKey (newPath)) {
|
|
|
|
var conflict = pathsDict [newPath];
|
|
|
|
if (GetFilenameFromFd (renaming.Fd) == GetFilenameFromFd (conflict.Fd))
|
|
|
|
Remove (conflict);
|
|
|
|
else
|
|
|
|
UpdatePath (conflict);
|
|
|
|
}
|
|
|
|
|
|
|
|
pathsDict.Add (newPath, renaming);
|
|
|
|
}
|
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
PostEvent (FileAction.RenamedNewName, oldRoot, newRoot);
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
void Scan (string path, bool postEvents, ref List<int> fds)
|
|
|
|
{
|
|
|
|
if (requestStop)
|
2014-08-13 10:39:27 +01:00
|
|
|
return;
|
2015-01-13 10:44:36 +00:00
|
|
|
|
|
|
|
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);
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
|
|
|
}
|
2015-01-13 10:44:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void PostEvent (FileAction action, string path, string newPath = null)
|
|
|
|
{
|
|
|
|
RenamedEventArgs renamed = null;
|
|
|
|
|
2015-04-07 09:35:12 +01:00
|
|
|
if (requestStop || action == 0)
|
2014-08-13 10:39:27 +01:00
|
|
|
return;
|
|
|
|
|
2015-04-07 09:35:12 +01:00
|
|
|
// e.Name
|
2016-08-03 10:59:49 +00:00
|
|
|
string name = (path.Length > fullPathNoLastSlash.Length) ? path.Substring (fullPathNoLastSlash.Length + 1) : String.Empty;
|
2015-04-07 09:35:12 +01:00
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
// only post events that match filter pattern. check both old and new paths for renames
|
2015-04-07 09:35:12 +01:00
|
|
|
if (!fsw.Pattern.IsMatch (path) && (newPath == null || !fsw.Pattern.IsMatch (newPath)))
|
2015-01-13 10:44:36 +00:00
|
|
|
return;
|
|
|
|
|
2015-04-07 09:35:12 +01:00
|
|
|
if (action == FileAction.RenamedNewName) {
|
2016-08-03 10:59:49 +00:00
|
|
|
string newName = (newPath.Length > fullPathNoLastSlash.Length) ? newPath.Substring (fullPathNoLastSlash.Length + 1) : String.Empty;
|
2015-04-07 09:35:12 +01:00
|
|
|
renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, fsw.Path, newName, name);
|
|
|
|
}
|
|
|
|
|
|
|
|
fsw.DispatchEvents (action, name, ref renamed);
|
2015-01-13 10:44:36 +00:00
|
|
|
|
2015-04-07 09:35:12 +01:00
|
|
|
if (fsw.Waiting) {
|
|
|
|
lock (fsw) {
|
2014-08-13 10:39:27 +01:00
|
|
|
fsw.Waiting = false;
|
|
|
|
System.Threading.Monitor.PulseAll (fsw);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
private string GetFilenameFromFd (int fd)
|
|
|
|
{
|
|
|
|
var sb = new StringBuilder (__DARWIN_MAXPATHLEN);
|
|
|
|
|
|
|
|
if (fcntl (fd, F_GETPATH, sb) != -1) {
|
2015-04-07 09:35:12 +01:00
|
|
|
if (fixupPath != null)
|
|
|
|
sb.Replace (fixupPath, fullPathNoLastSlash, 0, fixupPath.Length); // see Setup()
|
|
|
|
|
2015-01-13 10:44:36 +00:00
|
|
|
return sb.ToString ();
|
|
|
|
} else {
|
2015-04-07 09:35:12 +01:00
|
|
|
fsw.DispatchErrorEvents (new ErrorEventArgs (new IOException (String.Format (
|
2015-01-13 10:44:36 +00:00
|
|
|
"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;
|
2016-08-03 10:59:49 +00:00
|
|
|
const int EINTR = 4;
|
2015-01-13 10:44:36 +00:00
|
|
|
static readonly kevent[] emptyEventList = new System.IO.kevent[0];
|
2016-01-18 21:29:19 -05:00
|
|
|
int maxFds = Int32.MaxValue;
|
2015-01-13 10:44:36 +00:00
|
|
|
|
|
|
|
FileSystemWatcher fsw;
|
|
|
|
int conn;
|
|
|
|
Thread thread;
|
|
|
|
volatile bool requestStop = false;
|
|
|
|
AutoResetEvent startedEvent = new AutoResetEvent (false);
|
|
|
|
bool started = false;
|
2015-04-07 09:35:12 +01:00
|
|
|
bool inDispatch = false;
|
|
|
|
Exception exc = null;
|
2015-01-13 10:44:36 +00:00
|
|
|
object stateLock = new object ();
|
2015-04-07 09:35:12 +01:00
|
|
|
object connLock = new object ();
|
2015-01-13 10:44:36 +00:00
|
|
|
|
|
|
|
readonly Dictionary<string, PathData> pathsDict = new Dictionary<string, PathData> ();
|
|
|
|
readonly Dictionary<int, PathData> fdsDict = new Dictionary<int, PathData> ();
|
|
|
|
string fixupPath = null;
|
2015-04-07 09:35:12 +01:00
|
|
|
string fullPathNoLastSlash = null;
|
2015-01-13 10:44:36 +00:00
|
|
|
|
2016-08-03 10:59:49 +00:00
|
|
|
[DllImport ("libc", CharSet=CharSet.Auto, SetLastError=true)]
|
2015-01-13 10:44:36 +00:00
|
|
|
static extern int fcntl (int file_names_by_descriptor, int cmd, StringBuilder sb);
|
|
|
|
|
2016-08-03 10:59:49 +00:00
|
|
|
[DllImport ("libc", CharSet=CharSet.Auto, SetLastError=true)]
|
|
|
|
static extern IntPtr realpath (string pathname, StringBuilder sb);
|
|
|
|
|
|
|
|
[DllImport ("libc", SetLastError=true)]
|
2015-01-13 10:44:36 +00:00
|
|
|
extern static int open (string path, int flags, int mode_t);
|
|
|
|
|
2014-08-13 10:39:27 +01:00
|
|
|
[DllImport ("libc")]
|
2015-01-13 10:44:36 +00:00
|
|
|
extern static int close (int fd);
|
|
|
|
|
2016-08-03 10:59:49 +00:00
|
|
|
[DllImport ("libc", SetLastError=true)]
|
2015-01-13 10:44:36 +00:00
|
|
|
extern static int kqueue ();
|
2014-08-13 10:39:27 +01:00
|
|
|
|
2016-08-03 10:59:49 +00:00
|
|
|
[DllImport ("libc", SetLastError=true)]
|
2015-01-13 10:44:36 +00:00
|
|
|
extern static int kevent (int kq, [In]kevent[] ev, int nchanges, [Out]kevent[] evtlist, int nevents, [In] ref timespec time);
|
2015-04-07 09:35:12 +01:00
|
|
|
|
2016-02-22 11:00:01 -05:00
|
|
|
[MethodImplAttribute(MethodImplOptions.InternalCall)]
|
|
|
|
extern static int kevent_notimeout (ref int kq, IntPtr ev, int nchanges, IntPtr evtlist, int nevents);
|
2015-01-13 10:44:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-05-10 08:37:03 +00:00
|
|
|
public void StartDispatching (object handle)
|
2015-01-13 10:44:36 +00:00
|
|
|
{
|
2018-05-10 08:37:03 +00:00
|
|
|
var fsw = handle as FileSystemWatcher;
|
2015-01-13 10:44:36 +00:00
|
|
|
KqueueMonitor monitor;
|
|
|
|
|
|
|
|
if (watches.ContainsKey (fsw)) {
|
|
|
|
monitor = (KqueueMonitor)watches [fsw];
|
|
|
|
} else {
|
|
|
|
monitor = new KqueueMonitor (fsw);
|
|
|
|
watches.Add (fsw, monitor);
|
|
|
|
}
|
|
|
|
|
|
|
|
monitor.Start ();
|
|
|
|
}
|
|
|
|
|
2018-05-10 08:37:03 +00:00
|
|
|
public void StopDispatching (object handle)
|
2015-01-13 10:44:36 +00:00
|
|
|
{
|
2018-05-10 08:37:03 +00:00
|
|
|
var fsw = handle as FileSystemWatcher;
|
2015-01-13 10:44:36 +00:00
|
|
|
KqueueMonitor monitor = (KqueueMonitor)watches [fsw];
|
|
|
|
if (monitor == null)
|
|
|
|
return;
|
|
|
|
|
|
|
|
monitor.Stop ();
|
|
|
|
}
|
2018-05-10 08:37:03 +00:00
|
|
|
|
|
|
|
public void Dispose (object handle)
|
|
|
|
{
|
|
|
|
// does nothing
|
|
|
|
}
|
2015-01-13 10:44:36 +00:00
|
|
|
|
|
|
|
[DllImport ("libc")]
|
|
|
|
extern static int close (int fd);
|
2014-08-13 10:39:27 +01:00
|
|
|
|
|
|
|
[DllImport ("libc")]
|
2015-01-13 10:44:36 +00:00
|
|
|
extern static int kqueue ();
|
2014-08-13 10:39:27 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|