mac: Use native OS X FSEvents API to detect changes. Closes #472

This commit is contained in:
Hylke Bons 2012-02-11 14:00:16 +01:00
parent f38950d91c
commit e7abaeac42
2 changed files with 205 additions and 64 deletions

View file

@ -68,7 +68,13 @@ namespace SparkleShare {
{
base.Initialize ();
this.watcher.Changed += delegate (string path) {
this.watcher.Changed += delegate (object sender, SparkleMacWatcherEventArgs args) {
string path = args.Path;
// Don't even bother with paths in .git/
if (path.Contains (".git"))
return;
string repo_name;
if (path.Contains ("/"))
@ -77,17 +83,24 @@ namespace SparkleShare {
repo_name = path;
// Ignore changes in the root of each subfolder, these
// are already handled bu the repository
// are already handled by the repository
if (Path.GetFileNameWithoutExtension (path).Equals (repo_name))
return;
repo_name = repo_name.Trim ("/".ToCharArray ());
FileSystemEventArgs args = new FileSystemEventArgs (WatcherChangeTypes.Changed,
Path.Combine (SparkleConfig.DefaultConfig.FoldersPath, path), Path.GetFileName (path));
FileSystemEventArgs fse_args = new FileSystemEventArgs (
WatcherChangeTypes.Changed,
Path.Combine (SparkleConfig.DefaultConfig.FoldersPath, path),
Path.GetFileName (path)
);
foreach (SparkleRepoBase repo in Repositories) {
if (repo.Name.Equals (repo_name))
repo.OnFileActivity (args);
if (repo.Name.Equals (repo_name)) {
repo.OnFileActivity (fse_args);
Console.WriteLine (">>> " + repo_name);
}
}
};
}

View file

@ -14,88 +14,216 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Originally taken from:
// https://github.com/jesse99/Continuum/blob/master/source/shared/DirectoryWatcher.cs
// Modified to use MonoMac and integrate into SparkleShare
// Copyright (C) 2008 Jesse Jones
//
// 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.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.IO;
using System.Threading;
using System.Timers;
using MonoMac.AppKit;
using MonoMac.Foundation;
namespace SparkleShare {
public class SparkleMacWatcher {
[Serializable]
public sealed class SparkleMacWatcherEventArgs : EventArgs {
public delegate void ChangedEventHandler (string path);
public event ChangedEventHandler Changed;
public string Path { get; private set; }
private FileSystemInfo last_changed;
private Thread thread;
private int poll_count = 0;
public SparkleMacWatcherEventArgs (string path)
{
Path = path;
}
}
public sealed class SparkleMacWatcher : IDisposable
{
public event EventHandler<SparkleMacWatcherEventArgs> Changed;
public string Path { get; private set; }
[Flags]
[Serializable]
private enum FSEventStreamCreateFlags : uint
{
kFSEventStreamCreateFlagNone = 0x00000000,
kFSEventStreamCreateFlagUseCFTypes = 0x00000001,
kFSEventStreamCreateFlagNoDefer = 0x00000002,
kFSEventStreamCreateFlagWatchRoot = 0x00000004,
}
private DateTime last_found_timestamp;
private IntPtr m_stream;
private FSEventStreamCallback m_callback; // need to keep a reference around so that it isn't GC'ed
private static readonly IntPtr kCFRunLoopDefaultMode = (new NSString ("kCFRunLoopDefaultMode")).Handle;
private ulong kFSEventStreamEventIdSinceNow = 0xFFFFFFFFFFFFFFFFUL;
private delegate void FSEventStreamCallback (
IntPtr streamRef,
IntPtr clientCallBackInfo,
int numEvents,
IntPtr eventPaths,
IntPtr eventFlags,
IntPtr eventIds);
~SparkleMacWatcher ()
{
Dispose (false);
}
public SparkleMacWatcher (string path)
{
this.thread = new Thread (new ThreadStart (delegate {
DateTime timestamp;
DirectoryInfo parent = new DirectoryInfo (path);
this.last_changed = new DirectoryInfo (path);
Path = path;
m_callback = DoCallback;
while (true) {
timestamp = this.last_changed.LastWriteTime;
GetLastChange (parent);
NSString [] s = new NSString [1];
s [0] = new NSString (path);
NSArray path_p = NSArray.FromNSObjects (s);
if (DateTime.Compare (this.last_changed.LastWriteTime, timestamp) != 0) {
string relative_path = this.last_changed.FullName.Substring (path.Length + 1);
m_stream = FSEventStreamCreate ( // note that the stream will always be valid
IntPtr.Zero, // allocator
m_callback, // callback
IntPtr.Zero, // context
path_p.Handle, // pathsToWatch
kFSEventStreamEventIdSinceNow, // sinceWhen
2, // latency (in seconds)
FSEventStreamCreateFlags.kFSEventStreamCreateFlagNone); // flags
if (Changed != null)
Changed (relative_path);
}
FSEventStreamScheduleWithRunLoop (
m_stream, // streamRef
CFRunLoopGetMain(), // runLoop
kCFRunLoopDefaultMode); // runLoopMode
Thread.Sleep (7500);
this.poll_count++;
}
}));
this.thread.Start ();
}
private void GetLastChange (DirectoryInfo parent)
{
try {
if (DateTime.Compare (parent.LastWriteTime, this.last_changed.LastWriteTime) > 0)
this.last_changed = parent;
foreach (DirectoryInfo info in parent.GetDirectories ()) {
if (!info.FullName.Contains ("/.")) {
if (DateTime.Compare (info.LastWriteTime, this.last_changed.LastWriteTime) > 0)
this.last_changed = info;
GetLastChange (info);
}
}
if (this.poll_count >= 8) {
foreach (FileInfo info in parent.GetFiles ()) {
if (!info.FullName.Contains ("/.")) {
if (DateTime.Compare (info.LastWriteTime, this.last_changed.LastWriteTime) > 0)
this.last_changed = info;
}
}
this.poll_count = 0;
}
} catch (Exception) {
// Don't care...
bool started = FSEventStreamStart (m_stream);
if (!started) {
GC.SuppressFinalize (this);
throw new InvalidOperationException ("Failed to start FSEvent stream for " + path);
}
}
public void Dispose ()
{
this.thread.Join ();
this.thread.Abort ();
Dispose (true);
GC.SuppressFinalize (this);
}
private void Dispose (bool disposing)
{
if (m_stream != IntPtr.Zero) {
FSEventStreamStop (m_stream);
FSEventStreamInvalidate (m_stream);
FSEventStreamRelease (m_stream);
m_stream = IntPtr.Zero;
}
}
private void checkDirectory (string dir)
{
DirectoryInfo parent = new DirectoryInfo (dir);
if (!parent.FullName.Contains ("/.") &&
DateTime.Compare (parent.LastWriteTime, this.last_found_timestamp) > 0) {
last_found_timestamp = parent.LastWriteTime;
}
}
private void DoCallback (IntPtr streamRef, IntPtr clientCallBackInfo,
int numEvents, IntPtr eventPaths, IntPtr eventFlags, IntPtr eventIds)
{
int bytes = Marshal.SizeOf (typeof (IntPtr));
string [] paths = new string [numEvents];
for (int i = 0; i < numEvents; ++i) {
IntPtr p = Marshal.ReadIntPtr (eventPaths, i * bytes);
paths [i] = Marshal.PtrToStringAnsi (p);
checkDirectory (paths [i]);
}
var handler = Changed;
if (handler != null) {
string path = paths [0];
path = path.Substring (Path.Length);
path = path.Trim ("/".ToCharArray ());
handler (this, new SparkleMacWatcherEventArgs (path));
}
GC.KeepAlive (this);
}
[DllImport("/System/Library/Frameworks/CoreServices.framework/CoreServices")]
private extern static IntPtr CFRunLoopGetMain ();
[DllImport("/System/Library/Frameworks/CoreServices.framework/CoreServices")]
private extern static IntPtr FSEventStreamCreate (
IntPtr allocator,
FSEventStreamCallback callback,
IntPtr context,
IntPtr pathsToWatch,
ulong sinceWhen,
double latency,
FSEventStreamCreateFlags flags);
[DllImport("/System/Library/Frameworks/CoreServices.framework/CoreServices")]
private extern static void FSEventStreamScheduleWithRunLoop (
IntPtr streamRef,
IntPtr runLoop,
IntPtr runLoopMode);
[DllImport("/System/Library/Frameworks/CoreServices.framework/CoreServices")]
[return: MarshalAs (UnmanagedType.U1)]
private extern static bool FSEventStreamStart (
IntPtr streamRef);
[DllImport("/System/Library/Frameworks/CoreServices.framework/CoreServices")]
private extern static void FSEventStreamStop (
IntPtr streamRef);
[DllImport("/System/Library/Frameworks/CoreServices.framework/CoreServices")]
private extern static void FSEventStreamInvalidate (
IntPtr streamRef);
[DllImport("/System/Library/Frameworks/CoreServices.framework/CoreServices")]
private extern static void FSEventStreamRelease (
IntPtr streamRef);
}
}