a575963da9
Former-commit-id: da6be194a6b1221998fc28233f2503bd61dd9d14
628 lines
17 KiB
C#
628 lines
17 KiB
C#
//
|
|
// System.IO.Inotify.cs: interface with inotify
|
|
//
|
|
// Authors:
|
|
// Gonzalo Paniagua (gonzalo@novell.com)
|
|
// Anders Rune Jensen (anders@iola.dk)
|
|
//
|
|
// (c) 2006 Novell, Inc. (http://www.novell.com)
|
|
|
|
//
|
|
// 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;
|
|
using System.ComponentModel;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Threading;
|
|
|
|
namespace System.IO {
|
|
|
|
[Flags]
|
|
enum InotifyMask : uint {
|
|
Access = 1 << 0,
|
|
Modify = 1 << 1,
|
|
Attrib = 1 << 2,
|
|
CloseWrite = 1 << 3,
|
|
CloseNoWrite = 1 << 4,
|
|
Open = 1 << 5,
|
|
MovedFrom = 1 << 6,
|
|
MovedTo = 1 << 7,
|
|
Create = 1 << 8,
|
|
Delete = 1 << 9,
|
|
DeleteSelf = 1 << 10,
|
|
MoveSelf = 1 << 11,
|
|
BaseEvents = 0x00000fff,
|
|
// Can be sent at any time
|
|
Umount = 0x0002000,
|
|
Overflow = 0x0004000,
|
|
Ignored = 0x0008000,
|
|
|
|
// Special flags.
|
|
OnlyDir = 0x01000000,
|
|
DontFollow = 0x02000000,
|
|
AddMask = 0x20000000,
|
|
Directory = 0x40000000,
|
|
OneShot = 0x80000000
|
|
}
|
|
|
|
struct InotifyEvent { // Our internal representation for the data returned by the kernel
|
|
public static readonly InotifyEvent Default = new InotifyEvent ();
|
|
public int WatchDescriptor;
|
|
public InotifyMask Mask;
|
|
public string Name;
|
|
|
|
public override string ToString ()
|
|
{
|
|
return String.Format ("[Descriptor: {0} Mask: {1} Name: {2}]", WatchDescriptor, Mask, Name);
|
|
}
|
|
}
|
|
|
|
class ParentInotifyData
|
|
{
|
|
public bool IncludeSubdirs;
|
|
public bool Enabled;
|
|
public ArrayList children; // InotifyData
|
|
public InotifyData data;
|
|
}
|
|
|
|
class InotifyData {
|
|
public FileSystemWatcher FSW;
|
|
public string Directory;
|
|
public int Watch;
|
|
}
|
|
|
|
class InotifyWatcher : IFileWatcher
|
|
{
|
|
static bool failed;
|
|
static InotifyWatcher instance;
|
|
static Hashtable watches; // FSW to ParentInotifyData
|
|
static Hashtable requests; // FSW to InotifyData
|
|
static IntPtr FD;
|
|
static Thread thread;
|
|
static bool stop;
|
|
|
|
private InotifyWatcher ()
|
|
{
|
|
}
|
|
|
|
// Locked by caller
|
|
public static bool GetInstance (out IFileWatcher watcher, bool gamin)
|
|
{
|
|
if (failed == true) {
|
|
watcher = null;
|
|
return false;
|
|
}
|
|
|
|
if (instance != null) {
|
|
watcher = instance;
|
|
return true;
|
|
}
|
|
|
|
FD = GetInotifyInstance ();
|
|
if ((long) FD == -1) {
|
|
failed = true;
|
|
watcher = null;
|
|
return false;
|
|
}
|
|
|
|
watches = Hashtable.Synchronized (new Hashtable ());
|
|
requests = Hashtable.Synchronized (new Hashtable ());
|
|
instance = new InotifyWatcher ();
|
|
watcher = instance;
|
|
return true;
|
|
}
|
|
|
|
public void StartDispatching (FileSystemWatcher fsw)
|
|
{
|
|
ParentInotifyData parent;
|
|
lock (this) {
|
|
if ((long) FD == -1)
|
|
FD = GetInotifyInstance ();
|
|
|
|
if (thread == null) {
|
|
thread = new Thread (new ThreadStart (Monitor));
|
|
thread.IsBackground = true;
|
|
thread.Start ();
|
|
}
|
|
|
|
parent = (ParentInotifyData) watches [fsw];
|
|
}
|
|
|
|
if (parent == null) {
|
|
InotifyData data = new InotifyData ();
|
|
data.FSW = fsw;
|
|
data.Directory = fsw.FullPath;
|
|
|
|
parent = new ParentInotifyData();
|
|
parent.IncludeSubdirs = fsw.IncludeSubdirectories;
|
|
parent.Enabled = true;
|
|
parent.children = new ArrayList();
|
|
parent.data = data;
|
|
|
|
watches [fsw] = parent;
|
|
|
|
try {
|
|
StartMonitoringDirectory (data, false);
|
|
lock (this) {
|
|
AppendRequestData (data);
|
|
stop = false;
|
|
}
|
|
} catch {} // ignore the directory if StartMonitoringDirectory fails.
|
|
}
|
|
}
|
|
|
|
static void AppendRequestData (InotifyData data)
|
|
{
|
|
int wd = data.Watch;
|
|
|
|
object obj = requests [wd];
|
|
ArrayList list = null;
|
|
if (obj == null) {
|
|
requests [data.Watch] = data;
|
|
} else if (obj is InotifyData) {
|
|
list = new ArrayList ();
|
|
list.Add (obj);
|
|
list.Add (data);
|
|
requests [data.Watch] = list;
|
|
} else {
|
|
list = (ArrayList) obj;
|
|
list.Add (data);
|
|
}
|
|
}
|
|
|
|
static bool RemoveRequestData (InotifyData data)
|
|
{
|
|
int wd = data.Watch;
|
|
object obj = requests [wd];
|
|
if (obj == null)
|
|
return true;
|
|
|
|
if (obj is InotifyData) {
|
|
if (obj == data) {
|
|
requests.Remove (wd);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
ArrayList list = (ArrayList) obj;
|
|
list.Remove (data);
|
|
if (list.Count == 0) {
|
|
requests.Remove (wd);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Attempt to match MS and linux behavior.
|
|
static InotifyMask GetMaskFromFilters (NotifyFilters filters)
|
|
{
|
|
InotifyMask mask = InotifyMask.Create | InotifyMask.Delete | InotifyMask.DeleteSelf | InotifyMask.AddMask;
|
|
if ((filters & NotifyFilters.Attributes) != 0)
|
|
mask |= InotifyMask.Attrib;
|
|
|
|
if ((filters & NotifyFilters.Security) != 0)
|
|
mask |= InotifyMask.Attrib;
|
|
|
|
if ((filters & NotifyFilters.Size) != 0) {
|
|
mask |= InotifyMask.Attrib;
|
|
mask |= InotifyMask.Modify;
|
|
}
|
|
|
|
if ((filters & NotifyFilters.LastAccess) != 0) {
|
|
mask |= InotifyMask.Attrib;
|
|
mask |= InotifyMask.Access;
|
|
mask |= InotifyMask.Modify;
|
|
}
|
|
|
|
if ((filters & NotifyFilters.LastWrite) != 0) {
|
|
mask |= InotifyMask.Attrib;
|
|
mask |= InotifyMask.Modify;
|
|
}
|
|
|
|
if ((filters & NotifyFilters.FileName) != 0) {
|
|
mask |= InotifyMask.MovedFrom;
|
|
mask |= InotifyMask.MovedTo;
|
|
}
|
|
|
|
if ((filters & NotifyFilters.DirectoryName) != 0) {
|
|
mask |= InotifyMask.MovedFrom;
|
|
mask |= InotifyMask.MovedTo;
|
|
}
|
|
|
|
return mask;
|
|
}
|
|
|
|
static void StartMonitoringDirectory (InotifyData data, bool justcreated)
|
|
{
|
|
InotifyMask mask = GetMaskFromFilters (data.FSW.NotifyFilter);
|
|
int wd = AddDirectoryWatch (FD, data.Directory, mask);
|
|
if (wd == -1) {
|
|
int error = Marshal.GetLastWin32Error ();
|
|
if (error == 4) { // Too many open watches
|
|
string nr_watches = "(unknown)";
|
|
try {
|
|
using (StreamReader reader = new StreamReader ("/proc/sys/fs/inotify/max_user_watches")) {
|
|
nr_watches = reader.ReadLine ();
|
|
}
|
|
} catch {}
|
|
|
|
string msg = String.Format ("The per-user inotify watches limit of {0} has been reached. " +
|
|
"If you're experiencing problems with your application, increase that limit " +
|
|
"in /proc/sys/fs/inotify/max_user_watches.", nr_watches);
|
|
|
|
throw new Win32Exception (error, msg);
|
|
}
|
|
throw new Win32Exception (error);
|
|
}
|
|
|
|
FileSystemWatcher fsw = data.FSW;
|
|
data.Watch = wd;
|
|
|
|
ParentInotifyData parent = (ParentInotifyData) watches[fsw];
|
|
|
|
if (parent.IncludeSubdirs) {
|
|
foreach (string directory in Directory.GetDirectories (data.Directory)) {
|
|
InotifyData fd = new InotifyData ();
|
|
fd.FSW = fsw;
|
|
fd.Directory = directory;
|
|
|
|
if (justcreated) {
|
|
lock (fsw) {
|
|
RenamedEventArgs renamed = null;
|
|
if (fsw.Pattern.IsMatch (directory)) {
|
|
fsw.DispatchEvents (FileAction.Added, directory, ref renamed);
|
|
if (fsw.Waiting) {
|
|
fsw.Waiting = false;
|
|
System.Threading.Monitor.PulseAll (fsw);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
StartMonitoringDirectory (fd, justcreated);
|
|
AppendRequestData (fd);
|
|
parent.children.Add(fd);
|
|
} catch {} // ignore errors and don't add directory.
|
|
}
|
|
}
|
|
|
|
if (justcreated) {
|
|
foreach (string filename in Directory.GetFiles (data.Directory)) {
|
|
lock (fsw) {
|
|
RenamedEventArgs renamed = null;
|
|
if (fsw.Pattern.IsMatch (filename)) {
|
|
fsw.DispatchEvents (FileAction.Added, filename, ref renamed);
|
|
/* If a file has been created, then it has been written to */
|
|
fsw.DispatchEvents (FileAction.Modified, filename, ref renamed);
|
|
|
|
if (fsw.Waiting) {
|
|
fsw.Waiting = false;
|
|
System.Threading.Monitor.PulseAll(fsw);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void StopDispatching (FileSystemWatcher fsw)
|
|
{
|
|
ParentInotifyData parent;
|
|
lock (this) {
|
|
parent = (ParentInotifyData) watches [fsw];
|
|
if (parent == null)
|
|
return;
|
|
|
|
if (RemoveRequestData (parent.data)) {
|
|
StopMonitoringDirectory (parent.data);
|
|
}
|
|
watches.Remove (fsw);
|
|
if (watches.Count == 0) {
|
|
stop = true;
|
|
IntPtr fd = FD;
|
|
FD = (IntPtr) (-1);
|
|
Close (fd);
|
|
}
|
|
|
|
if (!parent.IncludeSubdirs)
|
|
return;
|
|
|
|
foreach (InotifyData idata in parent.children)
|
|
{
|
|
if (RemoveRequestData (idata)) {
|
|
StopMonitoringDirectory (idata);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void StopMonitoringDirectory (InotifyData data)
|
|
{
|
|
RemoveWatch (FD, data.Watch);
|
|
}
|
|
|
|
void Monitor ()
|
|
{
|
|
byte [] buffer = new byte [4096];
|
|
int nread;
|
|
while (!stop) {
|
|
nread = ReadFromFD (FD, buffer, (IntPtr) buffer.Length);
|
|
if (nread == -1)
|
|
continue;
|
|
|
|
lock (this) {
|
|
ProcessEvents (buffer, nread);
|
|
|
|
}
|
|
}
|
|
|
|
lock (this) {
|
|
thread = null;
|
|
stop = false;
|
|
}
|
|
}
|
|
/*
|
|
struct inotify_event {
|
|
__s32 wd;
|
|
__u32 mask;
|
|
__u32 cookie;
|
|
__u32 len; // Includes any trailing null in 'name'
|
|
char name[0];
|
|
};
|
|
*/
|
|
|
|
static int ReadEvent (byte [] source, int off, int size, out InotifyEvent evt)
|
|
{
|
|
evt = new InotifyEvent ();
|
|
if (size <= 0 || off > size - 16) {
|
|
return -1;
|
|
}
|
|
|
|
int len;
|
|
if (BitConverter.IsLittleEndian) {
|
|
evt.WatchDescriptor = source [off] + (source [off + 1] << 8) +
|
|
(source [off + 2] << 16) + (source [off + 3] << 24);
|
|
evt.Mask = (InotifyMask) (source [off + 4] + (source [off + 5] << 8) +
|
|
(source [off + 6] << 16) + (source [off + 7] << 24));
|
|
// Ignore Cookie -> +4
|
|
len = source [off + 12] + (source [off + 13] << 8) +
|
|
(source [off + 14] << 16) + (source [off + 15] << 24);
|
|
} else {
|
|
evt.WatchDescriptor = source [off + 3] + (source [off + 2] << 8) +
|
|
(source [off + 1] << 16) + (source [off] << 24);
|
|
evt.Mask = (InotifyMask) (source [off + 7] + (source [off + 6] << 8) +
|
|
(source [off + 5] << 16) + (source [off + 4] << 24));
|
|
// Ignore Cookie -> +4
|
|
len = source [off + 15] + (source [off + 14] << 8) +
|
|
(source [off + 13] << 16) + (source [off + 12] << 24);
|
|
}
|
|
|
|
if (len > 0) {
|
|
if (off > size - 16 - len)
|
|
return -1;
|
|
string name = Encoding.UTF8.GetString (source, off + 16, len);
|
|
evt.Name = name.Trim ('\0');
|
|
} else {
|
|
evt.Name = null;
|
|
}
|
|
|
|
return 16 + len;
|
|
}
|
|
|
|
static IEnumerable GetEnumerator (object source)
|
|
{
|
|
if (source == null)
|
|
yield break;
|
|
|
|
if (source is InotifyData)
|
|
yield return source;
|
|
|
|
if (source is ArrayList) {
|
|
ArrayList list = (ArrayList) source;
|
|
for (int i = 0; i < list.Count; i++)
|
|
yield return list [i];
|
|
}
|
|
}
|
|
|
|
/* Interesting events:
|
|
* Modify
|
|
* Attrib
|
|
* MovedFrom
|
|
* MovedTo
|
|
* Create
|
|
* Delete
|
|
* DeleteSelf
|
|
*/
|
|
static InotifyMask Interesting = InotifyMask.Modify | InotifyMask.Attrib | InotifyMask.MovedFrom |
|
|
InotifyMask.MovedTo | InotifyMask.Create | InotifyMask.Delete |
|
|
InotifyMask.DeleteSelf;
|
|
|
|
void ProcessEvents (byte [] buffer, int length)
|
|
{
|
|
ArrayList newdirs = null;
|
|
InotifyEvent evt;
|
|
int nread = 0;
|
|
RenamedEventArgs renamed = null;
|
|
while (length > nread) {
|
|
int bytes_read = ReadEvent (buffer, nread, length, out evt);
|
|
if (bytes_read <= 0)
|
|
break;
|
|
|
|
nread += bytes_read;
|
|
|
|
InotifyMask mask = evt.Mask;
|
|
bool is_directory = (mask & InotifyMask.Directory) != 0;
|
|
mask = (mask & Interesting); // Clear out all the bits that we don't need
|
|
if (mask == 0)
|
|
continue;
|
|
|
|
foreach (InotifyData data in GetEnumerator (requests [evt.WatchDescriptor])) {
|
|
ParentInotifyData parent = (ParentInotifyData) watches[data.FSW];
|
|
|
|
if (data == null || parent.Enabled == false)
|
|
continue;
|
|
|
|
string directory = data.Directory;
|
|
string filename = evt.Name;
|
|
if (filename == null)
|
|
filename = directory;
|
|
|
|
FileSystemWatcher fsw = data.FSW;
|
|
FileAction action = 0;
|
|
if ((mask & (InotifyMask.Modify | InotifyMask.Attrib)) != 0) {
|
|
action = FileAction.Modified;
|
|
} else if ((mask & InotifyMask.Create) != 0) {
|
|
action = FileAction.Added;
|
|
} else if ((mask & InotifyMask.Delete) != 0) {
|
|
action = FileAction.Removed;
|
|
} else if ((mask & InotifyMask.DeleteSelf) != 0) {
|
|
if (data.Watch != parent.data.Watch) {
|
|
// To avoid duplicate events handle DeleteSelf only for the top level directory.
|
|
continue;
|
|
}
|
|
action = FileAction.Removed;
|
|
} else if ((mask & InotifyMask.MoveSelf) != 0) {
|
|
//action = FileAction.Removed;
|
|
continue; // Ignore this one
|
|
} else if ((mask & InotifyMask.MovedFrom) != 0) {
|
|
InotifyEvent to;
|
|
int i = ReadEvent (buffer, nread, length, out to);
|
|
if (i == -1 || (to.Mask & InotifyMask.MovedTo) == 0 || evt.WatchDescriptor != to.WatchDescriptor) {
|
|
action = FileAction.Removed;
|
|
} else {
|
|
nread += i;
|
|
action = FileAction.RenamedNewName;
|
|
renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, data.Directory, to.Name, evt.Name);
|
|
if (evt.Name != data.Directory && !fsw.Pattern.IsMatch (evt.Name))
|
|
filename = to.Name;
|
|
}
|
|
} else if ((mask & InotifyMask.MovedTo) != 0) {
|
|
action = FileAction.Added;
|
|
}
|
|
if (fsw.IncludeSubdirectories) {
|
|
string full = fsw.FullPath;
|
|
string datadir = data.Directory;
|
|
if (datadir != full) {
|
|
int len = full.Length;
|
|
int slash = 1;
|
|
if (len > 1 && full [len - 1] == Path.DirectorySeparatorChar)
|
|
slash = 0;
|
|
string reldir = datadir.Substring (full.Length + slash);
|
|
datadir = Path.Combine (datadir, filename);
|
|
filename = Path.Combine (reldir, filename);
|
|
} else {
|
|
datadir = Path.Combine (full, filename);
|
|
}
|
|
|
|
if (action == FileAction.Added && is_directory) {
|
|
if (newdirs == null)
|
|
newdirs = new ArrayList (2);
|
|
|
|
InotifyData fd = new InotifyData ();
|
|
fd.FSW = fsw;
|
|
fd.Directory = datadir;
|
|
newdirs.Add (fd);
|
|
}
|
|
|
|
if (action == FileAction.RenamedNewName && is_directory) {
|
|
string renamedOldFullPath = renamed.OldFullPath;
|
|
string renamedFullPath = renamed.FullPath;
|
|
int renamedOldFullPathLength = renamedOldFullPath.Length;
|
|
|
|
foreach (InotifyData child in parent.children) {
|
|
|
|
if (child.Directory.StartsWith (renamedOldFullPath
|
|
, StringComparison.Ordinal
|
|
)) {
|
|
child.Directory = renamedFullPath +
|
|
child.Directory.Substring (renamedOldFullPathLength);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (action == FileAction.Removed && filename == data.Directory) {
|
|
int idx = parent.children.IndexOf (data);
|
|
if (idx != -1) {
|
|
parent.children.RemoveAt (idx);
|
|
if (!fsw.Pattern.IsMatch (Path.GetFileName (filename))) {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (filename != data.Directory && !fsw.Pattern.IsMatch (Path.GetFileName (filename))) {
|
|
continue;
|
|
}
|
|
|
|
lock (fsw) {
|
|
fsw.DispatchEvents (action, filename, ref renamed);
|
|
if (action == FileAction.RenamedNewName)
|
|
renamed = null;
|
|
if (fsw.Waiting) {
|
|
fsw.Waiting = false;
|
|
System.Threading.Monitor.PulseAll (fsw);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (newdirs != null) {
|
|
foreach (InotifyData newdir in newdirs) {
|
|
try {
|
|
StartMonitoringDirectory (newdir, true);
|
|
AppendRequestData (newdir);
|
|
((ParentInotifyData) watches[newdir.FSW]).children.Add(newdir);
|
|
} catch {} // ignore the given directory
|
|
}
|
|
newdirs.Clear ();
|
|
}
|
|
}
|
|
|
|
static int AddDirectoryWatch (IntPtr fd, string directory, InotifyMask mask)
|
|
{
|
|
mask |= InotifyMask.Directory;
|
|
return AddWatch (fd, directory, mask);
|
|
}
|
|
|
|
[DllImport ("libc", EntryPoint="close")]
|
|
internal extern static int Close (IntPtr fd);
|
|
|
|
[DllImport ("libc", EntryPoint = "read")]
|
|
extern static int ReadFromFD (IntPtr fd, byte [] buffer, IntPtr length);
|
|
|
|
[MethodImplAttribute(MethodImplOptions.InternalCall)]
|
|
extern static IntPtr GetInotifyInstance ();
|
|
|
|
[MethodImplAttribute(MethodImplOptions.InternalCall)]
|
|
extern static int AddWatch (IntPtr fd, string name, InotifyMask mask);
|
|
|
|
[MethodImplAttribute(MethodImplOptions.InternalCall)]
|
|
extern static IntPtr RemoveWatch (IntPtr fd, int wd);
|
|
}
|
|
}
|
|
|