[banshee/watcher] Patch 144873 from bgo#385965
- From: Alexander Kojevnikov <alexk src gnome org>
- To: svn-commits-list gnome org
- Cc:
- Subject: [banshee/watcher] Patch 144873 from bgo#385965
- Date: Mon, 16 Nov 2009 23:36:36 +0000 (UTC)
commit b7dc2f9fa4c37368c07a585038d398efce2b1947
Author: Alexander Kojevnikov <alexander kojevnikov com>
Date: Tue Nov 17 10:35:46 2009 +1100
Patch 144873 from bgo#385965
Banshee.sln | 7 +
build/build.environment.mk | 1 +
configure.ac | 1 +
.../Banshee.Sources/PrimarySource.cs | 2 +-
.../Banshee.LibraryWatcher.addin.xml | 22 +
.../Banshee.LibraryWatcher.csproj | 85 +++
.../FileSystemWatcherProxy.cs | 146 +++++
.../Banshee.LibraryWatcher/IO/FileAction.cs | 39 ++
.../Banshee.LibraryWatcher/IO/FileSystemWatcher.cs | 541 +++++++++++++++++
.../Banshee.LibraryWatcher/IO/IFileWatcher.cs | 37 ++
.../Banshee.LibraryWatcher/IO/InotifyWatcher.cs | 624 ++++++++++++++++++++
.../Banshee.LibraryWatcher/IO/SearchPattern.cs | 225 +++++++
.../LibraryWatcherService.cs | 120 ++++
.../Banshee.LibraryWatcher/SourceWatcher.cs | 293 +++++++++
src/Extensions/Banshee.LibraryWatcher/Makefile.am | 19 +
src/Extensions/Makefile.am | 2 +
.../Hyena/Hyena.Query/StringQueryValue.cs | 4 +-
src/Libraries/Hyena/Hyena/StringUtil.cs | 9 +
18 files changed, 2173 insertions(+), 4 deletions(-)
---
diff --git a/Banshee.sln b/Banshee.sln
index 3fb6a39..264e065 100644
--- a/Banshee.sln
+++ b/Banshee.sln
@@ -63,6 +63,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.NowPlaying", "src\E
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.Lastfm", "src\Extensions\Banshee.Lastfm\Banshee.Lastfm.csproj", "{02FD8195-9796-4AF5-A9D2-D310721963F4}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.LibraryWatcher", "src\Extensions\Banshee.LibraryWatcher\Banshee.LibraryWatcher.csproj", "{49CA3F27-0BB6-428d-8B3A-20232493652E}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.Wikipedia", "src\Extensions\Banshee.Wikipedia\Banshee.Wikipedia.csproj", "{BF5D1722-269B-452E-B577-AEBA0CB894BA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.PlayerMigration", "src\Extensions\Banshee.PlayerMigration\Banshee.PlayerMigration.csproj", "{0AB92BF8-3A25-46AD-9748-1236471E9408}"
@@ -241,6 +243,8 @@ Global
{EABA3019-7539-4430-9935-D36CEA96F250}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F38B53BA-8F85-4DC6-9B94-029C1CF96F24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F38B53BA-8F85-4DC6-9B94-029C1CF96F24}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {49CA3F27-0BB6-428d-8B3A-20232493652E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {49CA3F27-0BB6-428d-8B3A-20232493652E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FCC1AE87-E10B-4B47-8ADE-D5F447E48518}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FCC1AE87-E10B-4B47-8ADE-D5F447E48518}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BB1D1D81-7A74-4183-B7B1-3E78B32D42F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
@@ -309,6 +313,8 @@ Global
{0130499B-8A93-4CD9-8F3C-593B231609C7}.Windows|Any CPU.ActiveCfg = Windows|Any CPU
{F38B53BA-8F85-4DC6-9B94-029C1CF96F24}.Windows|Any CPU.Build.0 = Windows|Any CPU
{F38B53BA-8F85-4DC6-9B94-029C1CF96F24}.Windows|Any CPU.ActiveCfg = Windows|Any CPU
+ {49CA3F27-0BB6-428d-8B3A-20232493652E}.Windows|Any CPU.Build.0 = Windows|Any CPU
+ {49CA3F27-0BB6-428d-8B3A-20232493652E}.Windows|Any CPU.ActiveCfg = Windows|Any CPU
{FCC1AE87-E10B-4B47-8ADE-D5F447E48518}.Windows|Any CPU.Build.0 = Windows|Any CPU
{FCC1AE87-E10B-4B47-8ADE-D5F447E48518}.Windows|Any CPU.ActiveCfg = Windows|Any CPU
{46AD1892-C5D3-4696-BA40-FBF7F4CE2B39}.Windows|Any CPU.Build.0 = Windows|Any CPU
@@ -391,6 +397,7 @@ Global
{16FB0D3A-53FA-4B8E-B02B-4AF66E87829A} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
{0130499B-8A93-4CD9-8F3C-593B231609C7} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
{F38B53BA-8F85-4DC6-9B94-029C1CF96F24} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
+ {49CA3F27-0BB6-428d-8B3A-20232493652E} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
{FCC1AE87-E10B-4B47-8ADE-D5F447E48518} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
{46AD1892-C5D3-4696-BA40-FBF7F4CE2B39} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
{6FF6F049-9DAB-48A7-BC4B-F7F3ED0EBA63} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
diff --git a/build/build.environment.mk b/build/build.environment.mk
index b4d71ae..e766618 100644
--- a/build/build.environment.mk
+++ b/build/build.environment.mk
@@ -126,6 +126,7 @@ REF_EXTENSION_DAAP = $(LINK_BANSHEE_THICKCLIENT_DEPS) $(LINK_ICSHARP_ZIP_LIB) $(
REF_EXTENSION_FILESYSTEMQUEUE = $(LINK_BANSHEE_THICKCLIENT_DEPS)
REF_EXTENSION_INTERNETRADIO = $(LINK_BANSHEE_THICKCLIENT_DEPS)
REF_EXTENSION_INTERNETARCHIVE = $(LINK_BANSHEE_THICKCLIENT_DEPS)
+REF_EXTENSION_LIBRARYWATCHER = $(LINK_BANSHEE_SERVICES_DEPS)
REF_EXTENSION_MINIMODE = $(LINK_BANSHEE_THICKCLIENT_DEPS)
REF_EXTENSION_MOBLIN = $(LINK_BANSHEE_THICKCLIENT_DEPS)
REF_EXTENSION_MULTIMEDIAKEYS = $(LINK_BANSHEE_SERVICES_DEPS)
diff --git a/configure.ac b/configure.ac
index 5b78f4e..dd242d1 100644
--- a/configure.ac
+++ b/configure.ac
@@ -287,6 +287,7 @@ src/Extensions/Banshee.FileSystemQueue/Makefile
src/Extensions/Banshee.InternetArchive/Makefile
src/Extensions/Banshee.InternetRadio/Makefile
src/Extensions/Banshee.Lastfm/Makefile
+src/Extensions/Banshee.LibraryWatcher/Makefile
src/Extensions/Banshee.MiniMode/Makefile
src/Extensions/Banshee.Moblin/Makefile
src/Extensions/Banshee.MultimediaKeys/Makefile
diff --git a/src/Core/Banshee.Services/Banshee.Sources/PrimarySource.cs b/src/Core/Banshee.Services/Banshee.Sources/PrimarySource.cs
index bdab989..b9f43a1 100644
--- a/src/Core/Banshee.Services/Banshee.Sources/PrimarySource.cs
+++ b/src/Core/Banshee.Services/Banshee.Sources/PrimarySource.cs
@@ -335,7 +335,7 @@ namespace Banshee.Sources
OnTracksChanged ();
}
- internal void NotifyTracksDeleted ()
+ public void NotifyTracksDeleted ()
{
OnTracksDeleted ();
}
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher.addin.xml b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher.addin.xml
new file mode 100644
index 0000000..421c7d8
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher.addin.xml
@@ -0,0 +1,22 @@
+<Addin
+ id="Banshee.LibraryWatcher"
+ version="1.0"
+ compatVersion="1.0"
+ copyright="© 2008-2009 Christian Martellini, Alexander Hixon, Alexander Kojevnikov. Licensed under the MIT X11 license."
+ name="Library Watcher"
+ category="Hardware"
+ description="Automatically update music and video libraries"
+ author="Christian Martellini, Alexander Hixon, Alexander Kojevnikov"
+ url="http://banshee-project.org/"
+ defaultEnabled="true">
+
+ <Dependencies>
+ <Addin id="Banshee.Services" version="1.0"/>
+ <Addin id="Banshee.ThickClient" version="1.0"/>
+ </Dependencies>
+
+ <Extension path="/Banshee/ServiceManager/Service">
+ <Service class="Banshee.LibraryWatcher.LibraryWatcherService"/>
+ </Extension>
+
+</Addin>
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher.csproj b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher.csproj
new file mode 100644
index 0000000..ae55b3a
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher.csproj
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>8.0.50727</ProductVersion>
+ <ProjectGuid>{49CA3F27-0BB6-428d-8B3A-20232493652E}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <UseParentDirectoryAsNamespace>true</UseParentDirectoryAsNamespace>
+ <AssemblyName>Banshee.LibraryWatcher</AssemblyName>
+ <SchemaVersion>2.0</SchemaVersion>
+ <TargetFrameworkVersion>v2.0</TargetFrameworkVersion>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\..\bin</OutputPath>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
+ <CustomCommands>
+ <CustomCommands>
+ <Command type="Build" command="make" workingdir="${SolutionDir}" />
+ <Command type="Execute" command="make run" workingdir="${SolutionDir}" />
+ </CustomCommands>
+ </CustomCommands>
+ <AssemblyKeyFile>.</AssemblyKeyFile>
+ </PropertyGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\Core\Banshee.Core\Banshee.Core.csproj">
+ <Project>{2ADB831A-A050-47D0-B6B9-9C19D60233BB}</Project>
+ <Name>Banshee.Core</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\Core\Banshee.Services\Banshee.Services.csproj">
+ <Project>{B28354F0-BA87-44E8-989F-B864A3C7C09F}</Project>
+ <Name>Banshee.Services</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\Libraries\Hyena\Hyena.csproj">
+ <Project>{95374549-9553-4C1E-9D89-667755F90E12}</Project>
+ <Name>Hyena</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\Core\Banshee.ThickClient\Banshee.ThickClient.csproj">
+ <Project>{AC839523-7BDF-4AB6-8115-E17921B96EC6}</Project>
+ <Name>Banshee.ThickClient</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="Mono.Addins, Version=0.4.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756" />
+ <Reference Include="gtk-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="Banshee.LibraryWatcher.addin.xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Banshee.LibraryWatcher\LibraryWatcherService.cs" />
+ <Compile Include="Banshee.LibraryWatcher\SourceWatcher.cs" />
+ <Compile Include="Banshee.LibraryWatcher\IO\FileSystemWatcher.cs" />
+ <Compile Include="Banshee.LibraryWatcher\IO\InotifyWatcher.cs" />
+ <Compile Include="Banshee.LibraryWatcher\IO\IFileWatcher.cs" />
+ <Compile Include="Banshee.LibraryWatcher\IO\FileAction.cs" />
+ <Compile Include="Banshee.LibraryWatcher\IO\SearchPattern.cs" />
+ <Compile Include="Banshee.LibraryWatcher\FileSystemWatcherProxy.cs" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+ <ProjectExtensions>
+ <MonoDevelop>
+ <Properties>
+ <MonoDevelop.Autotools.MakefileInfo IntegrationEnabled="true" RelativeMakefileName="./Makefile.am">
+ <BuildFilesVar Sync="true" Name="SOURCES" />
+ <DeployFilesVar />
+ <ResourcesVar Sync="true" Name="RESOURCES" />
+ <OthersVar />
+ <GacRefVar />
+ <AsmRefVar />
+ <ProjectRefVar />
+ </MonoDevelop.Autotools.MakefileInfo>
+ </Properties>
+ </MonoDevelop>
+ </ProjectExtensions>
+ <ItemGroup>
+ <Folder Include="Banshee.LibraryWatcher\IO\" />
+ </ItemGroup>
+</Project>
\ No newline at end of file
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/FileSystemWatcherProxy.cs b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/FileSystemWatcherProxy.cs
new file mode 100644
index 0000000..2193702
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/FileSystemWatcherProxy.cs
@@ -0,0 +1,146 @@
+//
+// FileSystemWatcherProxy.cs
+//
+// Authors:
+// Alexander Kojevnikov <alexander kojevnikov com>
+//
+// Copyright (C) 2009 Alexander Kojevnikov
+//
+// 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;
+
+namespace Banshee.LibraryWatcher
+{
+ public class FileSystemWatcherProxy : IDisposable
+ {
+ private readonly Banshee.LibraryWatcher.IO.FileSystemWatcher watcher_fixed;
+ private readonly System.IO.FileSystemWatcher watcher;
+
+ public FileSystemWatcherProxy(string path)
+ {
+ // FIXME: There must be a better way to check if we are running under Linux.
+ if (Environment.OSVersion.Platform == PlatformID.Unix &&
+ Environment.OSVersion.Version.ToString ().StartsWith ("2.6.")) {
+ watcher_fixed = new Banshee.LibraryWatcher.IO.FileSystemWatcher (path);
+ } else {
+ watcher = new System.IO.FileSystemWatcher (path);
+ }
+ }
+
+ public void Dispose ()
+ {
+ if (watcher_fixed != null) {
+ watcher_fixed.Dispose ();
+ } else {
+ watcher.Dispose ();
+ }
+ }
+
+ public bool IncludeSubdirectories {
+ set {
+ if (watcher_fixed != null) {
+ watcher_fixed.IncludeSubdirectories = value;
+ } else {
+ watcher.IncludeSubdirectories = value;
+ }
+ }
+ }
+
+ public event System.IO.FileSystemEventHandler Changed {
+ add {
+ if (watcher_fixed != null) {
+ watcher_fixed.Changed += value;
+ } else {
+ watcher.Changed += value;
+ }
+ }
+ remove {
+ if (watcher_fixed != null) {
+ watcher_fixed.Changed -= value;
+ } else {
+ watcher.Changed -= value;
+ }
+ }
+ }
+
+ public event System.IO.FileSystemEventHandler Created {
+ add {
+ if (watcher_fixed != null) {
+ watcher_fixed.Created += value;
+ } else {
+ watcher.Created += value;
+ }
+ }
+ remove {
+ if (watcher_fixed != null) {
+ watcher_fixed.Created -= value;
+ } else {
+ watcher.Created -= value;
+ }
+ }
+ }
+
+ public event System.IO.FileSystemEventHandler Deleted {
+ add {
+ if (watcher_fixed != null) {
+ watcher_fixed.Deleted += value;
+ } else {
+ watcher.Deleted += value;
+ }
+ }
+ remove {
+ if (watcher_fixed != null) {
+ watcher_fixed.Deleted -= value;
+ } else {
+ watcher.Deleted -= value;
+ }
+ }
+ }
+
+ public event System.IO.RenamedEventHandler Renamed {
+ add {
+ if (watcher_fixed != null) {
+ watcher_fixed.Renamed += value;
+ } else {
+ watcher.Renamed += value;
+ }
+ }
+ remove {
+ if (watcher_fixed != null) {
+ watcher_fixed.Renamed -= value;
+ } else {
+ watcher.Renamed -= value;
+ }
+ }
+ }
+
+ public bool EnableRaisingEvents {
+ set {
+ if (watcher_fixed != null) {
+ watcher_fixed.EnableRaisingEvents = value;
+ } else {
+ watcher.EnableRaisingEvents = value;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/FileAction.cs b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/FileAction.cs
new file mode 100644
index 0000000..8d64a87
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/FileAction.cs
@@ -0,0 +1,39 @@
+//
+// System.IO.FileAction.cs
+//
+// Authors:
+// Gonzalo Paniagua Javier (gonzalo ximian com)
+//
+// (c) 2004 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.
+//
+
+namespace Banshee.LibraryWatcher.IO {
+ enum FileAction {
+ Added = 1,
+ Removed = 2,
+ Modified = 3,
+ RenamedOldName = 4,
+ RenamedNewName = 5
+ }
+}
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/FileSystemWatcher.cs b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/FileSystemWatcher.cs
new file mode 100644
index 0000000..f4eba39
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/FileSystemWatcher.cs
@@ -0,0 +1,541 @@
+//
+// System.IO.FileSystemWatcher.cs
+//
+// Authors:
+// Tim Coleman (tim timcoleman com)
+// Gonzalo Paniagua Javier (gonzalo ximian com)
+//
+// Copyright (C) Tim Coleman, 2002
+// (c) 2003 Ximian, Inc. (http://www.ximian.com)
+// Copyright (C) 2004, 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.IO;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Security.Permissions;
+using System.Threading;
+
+namespace Banshee.LibraryWatcher.IO {
+ [DefaultEvent("Changed")]
+#if NET_2_0
+ [IODescription ("")]
+#endif
+ public class FileSystemWatcher : Component, ISupportInitialize {
+
+ #region Fields
+
+ bool enableRaisingEvents;
+ string filter;
+ bool includeSubdirectories;
+ int internalBufferSize;
+ NotifyFilters notifyFilter;
+ string path;
+ string fullpath;
+ ISynchronizeInvoke synchronizingObject;
+ WaitForChangedResult lastData;
+ bool waiting;
+ SearchPattern2 pattern;
+ bool disposed;
+ string mangledFilter;
+ static IFileWatcher watcher;
+ static object lockobj = new object ();
+
+ #endregion // Fields
+
+ #region Constructors
+
+ public FileSystemWatcher ()
+ {
+ this.notifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName;
+ this.enableRaisingEvents = false;
+ this.filter = "*.*";
+ this.includeSubdirectories = false;
+ this.internalBufferSize = 8192;
+ this.path = "";
+ InitWatcher ();
+ }
+
+ public FileSystemWatcher (string path)
+ : this (path, "*.*")
+ {
+ }
+
+ public FileSystemWatcher (string path, string filter)
+ {
+ if (path == null)
+ throw new ArgumentNullException ("path");
+
+ if (filter == null)
+ throw new ArgumentNullException ("filter");
+
+ if (path == String.Empty)
+ throw new ArgumentException ("Empty path", "path");
+
+ if (!Directory.Exists (path))
+ throw new ArgumentException ("Directory does not exists", "path");
+
+ this.enableRaisingEvents = false;
+ this.filter = filter;
+ this.includeSubdirectories = false;
+ this.internalBufferSize = 8192;
+ this.notifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName;
+ this.path = path;
+ this.synchronizingObject = null;
+ InitWatcher ();
+ }
+
+ [EnvironmentPermission (SecurityAction.Assert, Read="MONO_MANAGED_WATCHER")]
+ void InitWatcher ()
+ {
+ lock (lockobj) {
+ if (watcher != null)
+ return;
+
+ /*string managed = Environment.GetEnvironmentVariable ("MONO_MANAGED_WATCHER");
+ int mode = 0;
+ if (managed == null)
+ mode = InternalSupportsFSW ();
+
+ bool ok = false;
+ switch (mode) {
+ case 1: // windows
+ ok = DefaultWatcher.GetInstance (out watcher);
+ //ok = WindowsWatcher.GetInstance (out watcher);
+ break;
+ case 2: // libfam
+ ok = FAMWatcher.GetInstance (out watcher, false);
+ break;
+ case 3: // kevent
+ ok = KeventWatcher.GetInstance (out watcher);
+ break;
+ case 4: // libgamin
+ ok = FAMWatcher.GetInstance (out watcher, true);
+ break;
+ case 5: // inotify
+ ok = InotifyWatcher.GetInstance (out watcher, true);
+ break;
+ }
+
+ if (mode == 0 || !ok) {
+ if (String.Compare (managed, "disabled", true) == 0)
+ NullFileWatcher.GetInstance (out watcher);
+ else
+ DefaultWatcher.GetInstance (out watcher);
+ }*/
+ InotifyWatcher.GetInstance (out watcher, true);
+
+ ShowWatcherInfo ();
+ }
+ }
+
+ [Conditional ("DEBUG"), Conditional ("TRACE")]
+ void ShowWatcherInfo ()
+ {
+ Console.WriteLine ("Watcher implementation: {0}", watcher != null ? watcher.GetType ().ToString () : "<none>");
+ }
+
+ #endregion // Constructors
+
+ #region Properties
+
+ /* If this is enabled, we Pulse this instance */
+ internal bool Waiting {
+ get { return waiting; }
+ set { waiting = value; }
+ }
+
+ internal string MangledFilter {
+ get {
+ if (filter != "*.*")
+ return filter;
+
+ if (mangledFilter != null)
+ return mangledFilter;
+
+ string filterLocal = "*.*";
+ //if (!(watcher.GetType () == typeof (WindowsWatcher)))
+ filterLocal = "*";
+
+ return filterLocal;
+ }
+ }
+
+ internal SearchPattern2 Pattern {
+ get {
+ if (pattern == null) {
+ pattern = new SearchPattern2 (MangledFilter);
+ }
+ return pattern;
+ }
+ }
+
+ internal string FullPath {
+ get {
+ if (fullpath == null) {
+ if (path == null || path == "")
+ fullpath = Environment.CurrentDirectory;
+ else
+ fullpath = System.IO.Path.GetFullPath (path);
+ }
+
+ return fullpath;
+ }
+ }
+
+ [DefaultValue(false)]
+ [IODescription("Flag to indicate if this instance is active")]
+ public bool EnableRaisingEvents {
+ get { return enableRaisingEvents; }
+ set {
+ if (value == enableRaisingEvents)
+ return; // Do nothing
+
+ enableRaisingEvents = value;
+ if (value) {
+ Start ();
+ } else {
+ Stop ();
+ }
+ }
+ }
+
+ [DefaultValue("*.*")]
+ [IODescription("File name filter pattern")]
+ //[RecommendedAsConfigurable(true)]
+ //[TypeConverter ("System.Diagnostics.Design.StringValueConverter, " + Consts.AssemblySystem_Design)]
+ public string Filter {
+ get { return filter; }
+ set {
+ if (value == null || value == "")
+ value = "*.*";
+
+ if (filter != value) {
+ filter = value;
+ pattern = null;
+ mangledFilter = null;
+ }
+ }
+ }
+
+ [DefaultValue(false)]
+ [IODescription("Flag to indicate we want to watch subdirectories")]
+ public bool IncludeSubdirectories {
+ get { return includeSubdirectories; }
+ set {
+ if (includeSubdirectories == value)
+ return;
+
+ includeSubdirectories = value;
+ if (value && enableRaisingEvents) {
+ Stop ();
+ Start ();
+ }
+ }
+ }
+
+ [Browsable(false)]
+ [DefaultValue(8192)]
+ public int InternalBufferSize {
+ get { return internalBufferSize; }
+ set {
+ if (internalBufferSize == value)
+ return;
+
+ if (value < 4196)
+ value = 4196;
+
+ internalBufferSize = value;
+ if (enableRaisingEvents) {
+ Stop ();
+ Start ();
+ }
+ }
+ }
+
+ [DefaultValue(NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite)]
+ [IODescription("Flag to indicate which change event we want to monitor")]
+ public NotifyFilters NotifyFilter {
+ get { return notifyFilter; }
+ set {
+ if (notifyFilter == value)
+ return;
+
+ notifyFilter = value;
+ if (enableRaisingEvents) {
+ Stop ();
+ Start ();
+ }
+ }
+ }
+
+ [DefaultValue("")]
+ [IODescription("The directory to monitor")]
+ //[RecommendedAsConfigurable(true)]
+ //[TypeConverter ("System.Diagnostics.Design.StringValueConverter, " + Consts.AssemblySystem_Design)]
+ //[Editor ("System.Diagnostics.Design.FSWPathEditor, " + Consts.AssemblySystem_Design, "System.Drawing.Design.UITypeEditor, " + Consts.AssemblySystem_Drawing)]
+ public string Path {
+ get { return path; }
+ set {
+ if (path == value)
+ return;
+
+ bool exists = false;
+ Exception exc = null;
+
+ try {
+ exists = Directory.Exists (value);
+ } catch (Exception e) {
+ exc = e;
+ }
+
+ if (exc != null)
+ throw new ArgumentException ("Invalid directory name", "value", exc);
+
+ if (!exists)
+ throw new ArgumentException ("Directory does not exists", "value");
+
+ path = value;
+ fullpath = null;
+ if (enableRaisingEvents) {
+ Stop ();
+ Start ();
+ }
+ }
+ }
+
+ [Browsable(false)]
+ public override ISite Site {
+ get { return base.Site; }
+ set { base.Site = value; }
+ }
+
+ [DefaultValue(null)]
+ [IODescription("The object used to marshal the event handler calls resulting from a directory change")]
+#if NET_2_0
+ [Browsable (false)]
+#endif
+ public ISynchronizeInvoke SynchronizingObject {
+ get { return synchronizingObject; }
+ set { synchronizingObject = value; }
+ }
+
+ #endregion // Properties
+
+ #region Methods
+
+ public void BeginInit ()
+ {
+ // Not necessary in Mono
+ }
+
+ protected override void Dispose (bool disposing)
+ {
+ if (!disposed) {
+ disposed = true;
+ Stop ();
+ }
+
+ base.Dispose (disposing);
+ }
+
+ ~FileSystemWatcher ()
+ {
+ disposed = true;
+ Stop ();
+ }
+
+ public void EndInit ()
+ {
+ // Not necessary in Mono
+ }
+
+ enum EventType {
+ FileSystemEvent,
+ ErrorEvent,
+ RenameEvent
+ }
+ private void RaiseEvent (Delegate ev, EventArgs arg, EventType evtype)
+ {
+ if (ev == null)
+ return;
+
+ if (synchronizingObject == null) {
+ switch (evtype) {
+ case EventType.RenameEvent:
+ ((RenamedEventHandler)ev).BeginInvoke (this, (RenamedEventArgs) arg, null, null);
+ break;
+ case EventType.ErrorEvent:
+ ((ErrorEventHandler)ev).BeginInvoke (this, (ErrorEventArgs) arg, null, null);
+ break;
+ case EventType.FileSystemEvent:
+ ((FileSystemEventHandler)ev).BeginInvoke (this, (FileSystemEventArgs) arg, null, null);
+ break;
+ }
+ return;
+ }
+
+ synchronizingObject.BeginInvoke (ev, new object [] {this, arg});
+ }
+
+ protected void OnChanged (FileSystemEventArgs e)
+ {
+ RaiseEvent (Changed, e, EventType.FileSystemEvent);
+ }
+
+ protected void OnCreated (FileSystemEventArgs e)
+ {
+ RaiseEvent (Created, e, EventType.FileSystemEvent);
+ }
+
+ protected void OnDeleted (FileSystemEventArgs e)
+ {
+ RaiseEvent (Deleted, e, EventType.FileSystemEvent);
+ }
+
+ protected void OnError (ErrorEventArgs e)
+ {
+ RaiseEvent (Error, e, EventType.ErrorEvent);
+ }
+
+ protected void OnRenamed (RenamedEventArgs e)
+ {
+ RaiseEvent (Renamed, e, EventType.RenameEvent);
+ }
+
+ public WaitForChangedResult WaitForChanged (WatcherChangeTypes changeType)
+ {
+ return WaitForChanged (changeType, Timeout.Infinite);
+ }
+
+ public WaitForChangedResult WaitForChanged (WatcherChangeTypes changeType, int timeout)
+ {
+ WaitForChangedResult result = new WaitForChangedResult ();
+ bool prevEnabled = EnableRaisingEvents;
+ if (!prevEnabled)
+ EnableRaisingEvents = true;
+
+ bool gotData;
+ lock (this) {
+ waiting = true;
+ gotData = Monitor.Wait (this, timeout);
+ if (gotData)
+ result = this.lastData;
+ }
+
+ EnableRaisingEvents = prevEnabled;
+ if (!gotData)
+ result.TimedOut = true;
+
+ return result;
+ }
+
+ internal void DispatchEvents (FileAction act, string filename, ref RenamedEventArgs renamed)
+ {
+ if (waiting) {
+ lastData = new WaitForChangedResult ();
+ }
+
+ switch (act) {
+ case FileAction.Added:
+ lastData.Name = filename;
+ lastData.ChangeType = WatcherChangeTypes.Created;
+ OnCreated (new FileSystemEventArgs (WatcherChangeTypes.Created, path, filename));
+ break;
+ case FileAction.Removed:
+ lastData.Name = filename;
+ lastData.ChangeType = WatcherChangeTypes.Deleted;
+ OnDeleted (new FileSystemEventArgs (WatcherChangeTypes.Deleted, path, filename));
+ break;
+ case FileAction.Modified:
+ lastData.Name = filename;
+ lastData.ChangeType = WatcherChangeTypes.Changed;
+ OnChanged (new FileSystemEventArgs (WatcherChangeTypes.Changed, path, filename));
+ break;
+ case FileAction.RenamedOldName:
+ if (renamed != null) {
+ OnRenamed (renamed);
+ }
+ lastData.OldName = filename;
+ lastData.ChangeType = WatcherChangeTypes.Renamed;
+ renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, path, filename, "");
+ break;
+ case FileAction.RenamedNewName:
+ lastData.Name = filename;
+ lastData.ChangeType = WatcherChangeTypes.Renamed;
+ if (renamed == null) {
+ renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, path, "", filename);
+ }
+ OnRenamed (renamed);
+ renamed = null;
+ break;
+ default:
+ break;
+ }
+ }
+
+ void Start ()
+ {
+ watcher.StartDispatching (this);
+ }
+
+ void Stop ()
+ {
+ watcher.StopDispatching (this);
+ }
+ #endregion // Methods
+
+ #region Events and Delegates
+
+ [IODescription("Occurs when a file/directory change matches the filter")]
+ public event FileSystemEventHandler Changed;
+
+ [IODescription("Occurs when a file/directory creation matches the filter")]
+ public event FileSystemEventHandler Created;
+
+ [IODescription("Occurs when a file/directory deletion matches the filter")]
+ public event FileSystemEventHandler Deleted;
+
+ [Browsable(false)]
+ public event ErrorEventHandler Error;
+
+ [IODescription("Occurs when a file/directory rename matches the filter")]
+ public event RenamedEventHandler Renamed;
+
+ #endregion // Events and Delegates
+
+ /* 0 -> not supported */
+ /* 1 -> windows */
+ /* 2 -> FAM */
+ /* 3 -> Kevent */
+ /* 4 -> gamin */
+ /* 5 -> inotify */
+ //[MethodImplAttribute(MethodImplOptions.InternalCall)]
+ //static extern int InternalSupportsFSW ();
+ }
+}
+
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/IFileWatcher.cs b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/IFileWatcher.cs
new file mode 100644
index 0000000..b761016
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/IFileWatcher.cs
@@ -0,0 +1,37 @@
+//
+// System.IO.IFileWatcher.cs
+//
+// Authors:
+// Gonzalo Paniagua Javier (gonzalo ximian com)
+//
+// (c) 2004 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.
+//
+
+namespace Banshee.LibraryWatcher.IO {
+ interface IFileWatcher {
+ void StartDispatching (FileSystemWatcher fsw);
+ void StopDispatching (FileSystemWatcher fsw);
+ }
+}
+
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/InotifyWatcher.cs b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/InotifyWatcher.cs
new file mode 100644
index 0000000..d1af099
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/InotifyWatcher.cs
@@ -0,0 +1,624 @@
+//
+// 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.IO;
+using System.Collections;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+
+namespace Banshee.LibraryWatcher.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) {
+ foreach (InotifyData child in parent.children) {
+ if (child.Directory.StartsWith (renamed.OldFullPath)) {
+ child.Directory = renamed.FullPath +
+ child.Directory.Substring (renamed.OldFullPath.Length);
+ }
+ }
+ }
+ }
+
+ 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)]
+ [DllImport ("libc", EntryPoint = "inotify_init")]
+ extern static IntPtr GetInotifyInstance ();
+
+ //[MethodImplAttribute(MethodImplOptions.InternalCall)]
+ [DllImport ("libc", EntryPoint = "inotify_add_watch")]
+ extern static int AddWatch (IntPtr fd, string name, InotifyMask mask);
+
+ //[MethodImplAttribute(MethodImplOptions.InternalCall)]
+ [DllImport ("libc", EntryPoint = "inotify_rm_watch")]
+ extern static IntPtr RemoveWatch (IntPtr fd, int wd);
+ }
+}
+
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/SearchPattern.cs b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/SearchPattern.cs
new file mode 100644
index 0000000..cb3adf4
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/SearchPattern.cs
@@ -0,0 +1,225 @@
+//
+// System.IO.SearchPattern2.cs: Filename glob support.
+//
+// Author:
+// Dan Lewis (dihlewis yahoo co uk)
+//
+// (C) 2002
+//
+
+//
+// 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.
+//
+
+// Copied from corlib/System.IO/SearchPatter.cs
+using System;
+using System.IO;
+
+namespace Banshee.LibraryWatcher.IO {
+
+ // FIXME: there's a complication with this algorithm under windows.
+ // the pattern '*.*' matches all files (i think . matches the extension),
+ // whereas under UNIX it should only match files containing the '.' character.
+
+ class SearchPattern2 {
+ public SearchPattern2 (string pattern) : this (pattern, false) { }
+
+ public SearchPattern2 (string pattern, bool ignore)
+ {
+ this.ignore = ignore;
+ this.pattern = pattern;
+ Compile (pattern);
+ }
+
+ // OSX has a retarded case-insensitive yet case-aware filesystem
+ // so we need a overload in here for the Kqueue watcher
+ public bool IsMatch (string text, bool ignorecase)
+ {
+ if (!hasWildcard) {
+ bool match = String.Compare (pattern, text, ignorecase) == 0;
+ if (match)
+ return true;
+
+ // This is a special case for FSW. It needs to match e.g. subdir/file.txt
+ // when the pattern is "file.txt"
+ int idx = text.LastIndexOf ('/');
+ if (idx == -1)
+ return false;
+ idx++;
+ if (idx == text.Length)
+ return false;
+
+ return (String.Compare (pattern, text.Substring (idx), ignorecase) == 0);
+ }
+
+ return Match (ops, text, 0);
+ }
+
+ public bool IsMatch (string text)
+ {
+ return IsMatch (text, ignore);
+ }
+
+ public bool HasWildcard {
+ get { return hasWildcard; }
+ }
+ // private
+
+ Op ops; // the compiled pattern
+ bool ignore; // ignore case
+ bool hasWildcard;
+ string pattern;
+
+ private void Compile (string pattern)
+ {
+ if (pattern == null || pattern.IndexOfAny (InvalidChars) >= 0)
+ throw new ArgumentException ("Invalid search pattern: '" + pattern + "'");
+
+ if (pattern == "*") { // common case
+ ops = new Op (OpCode.True);
+ hasWildcard = true;
+ return;
+ }
+
+ ops = null;
+
+ int ptr = 0;
+ Op last_op = null;
+ while (ptr < pattern.Length) {
+ Op op;
+
+ switch (pattern [ptr]) {
+ case '?':
+ op = new Op (OpCode.AnyChar);
+ ++ ptr;
+ hasWildcard = true;
+ break;
+
+ case '*':
+ op = new Op (OpCode.AnyString);
+ ++ ptr;
+ hasWildcard = true;
+ break;
+
+ default:
+ op = new Op (OpCode.ExactString);
+ int end = pattern.IndexOfAny (WildcardChars, ptr);
+ if (end < 0)
+ end = pattern.Length;
+
+ op.Argument = pattern.Substring (ptr, end - ptr);
+ if (ignore)
+ op.Argument = op.Argument.ToLower ();
+
+ ptr = end;
+ break;
+ }
+
+ if (last_op == null)
+ ops = op;
+ else
+ last_op.Next = op;
+
+ last_op = op;
+ }
+
+ if (last_op == null)
+ ops = new Op (OpCode.End);
+ else
+ last_op.Next = new Op (OpCode.End);
+ }
+
+ private bool Match (Op op, string text, int ptr)
+ {
+ while (op != null) {
+ switch (op.Code) {
+ case OpCode.True:
+ return true;
+
+ case OpCode.End:
+ if (ptr == text.Length)
+ return true;
+
+ return false;
+
+ case OpCode.ExactString:
+ int length = op.Argument.Length;
+ if (ptr + length > text.Length)
+ return false;
+
+ string str = text.Substring (ptr, length);
+ if (ignore)
+ str = str.ToLower ();
+
+ if (str != op.Argument)
+ return false;
+
+ ptr += length;
+ break;
+
+ case OpCode.AnyChar:
+ if (++ ptr > text.Length)
+ return false;
+ break;
+
+ case OpCode.AnyString:
+ while (ptr <= text.Length) {
+ if (Match (op.Next, text, ptr))
+ return true;
+
+ ++ ptr;
+ }
+
+ return false;
+ }
+
+ op = op.Next;
+ }
+
+ return true;
+ }
+
+ // private static
+
+ internal static readonly char [] WildcardChars = { '*', '?' };
+ internal static readonly char [] InvalidChars = { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
+
+ private class Op {
+ public Op (OpCode code)
+ {
+ this.Code = code;
+ this.Argument = null;
+ this.Next = null;
+ }
+
+ public OpCode Code;
+ public string Argument;
+ public Op Next;
+ }
+
+ private enum OpCode {
+ ExactString, // literal
+ AnyChar, // ?
+ AnyString, // *
+ End, // end of pattern
+ True // always succeeds
+ };
+ }
+}
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/LibraryWatcherService.cs b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/LibraryWatcherService.cs
new file mode 100644
index 0000000..fee1aed
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/LibraryWatcherService.cs
@@ -0,0 +1,120 @@
+//
+// LibraryWatcherService.cs
+//
+// Authors:
+// Alexander Hixon <hixon alexander mediati org>
+// Christian Martellini <christian martellini gmail com>
+//
+// Copyright (C) 2008 Alexander Hixon
+// Copyright (C) 2009 Christian Martellini
+//
+// 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 Banshee.Base ;
+using Banshee.Sources;
+using Banshee.ServiceStack;
+using Banshee.Library;
+
+namespace Banshee.LibraryWatcher
+{
+ public class LibraryWatcherService : IExtensionService, IDisposable, IDelayedInitializeService
+ {
+ private readonly Dictionary<LibrarySource, SourceWatcher> watchers =
+ new Dictionary<LibrarySource, SourceWatcher> ();
+
+ string IService.ServiceName {
+ get { return "LibraryWatcherService"; }
+ }
+
+ void IExtensionService.Initialize ()
+ {
+ }
+
+ public void DelayedInitialize ()
+ {
+ ServiceManager.SourceManager.SourceAdded += OnSourceAdded;
+ ServiceManager.SourceManager.SourceRemoved += OnSourceRemoved;
+
+ foreach (var library in ServiceManager.SourceManager.FindSources<LibrarySource> ()) {
+ AddLibrary (library);
+ }
+ }
+
+ public void Dispose ()
+ {
+ lock (watchers) {
+ foreach (var watcher in watchers.Values) {
+ watcher.Dispose ();
+ }
+ watchers.Clear ();
+ }
+ }
+
+ private void OnSourceAdded (SourceAddedArgs args)
+ {
+ var library = args.Source as LibrarySource;
+ if (library != null) {
+ AddLibrary (library);
+ }
+ }
+
+ private void OnSourceRemoved (SourceEventArgs args)
+ {
+ var library = args.Source as LibrarySource;
+ if (library != null) {
+ RemoveLibrary (library);
+ }
+ }
+
+ private void AddLibrary (LibrarySource library)
+ {
+ if (!Banshee.IO.Directory.Exists(library.BaseDirectoryWithSeparator)) {
+ Hyena.Log.DebugFormat ("Will not watch library {0} because its folder doesn't exist: {1}",
+ library.Name, library.BaseDirectoryWithSeparator);
+ return;
+ }
+ lock (watchers) {
+ if (!watchers.ContainsKey (library)) {
+ try {
+ watchers[library] = new SourceWatcher (library);
+ Hyena.Log.DebugFormat ("Started LibraryWatcher for {0} ({1})",
+ library.Name, library.BaseDirectoryWithSeparator);
+ } catch (Exception e) {
+ Hyena.Log.Exception (e);
+ }
+ }
+ }
+ }
+
+ private void RemoveLibrary (LibrarySource library)
+ {
+ lock (watchers) {
+ if (watchers.ContainsKey (library)) {
+ watchers[library].Dispose ();
+ watchers.Remove (library);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/SourceWatcher.cs b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/SourceWatcher.cs
new file mode 100644
index 0000000..b2d1b35
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/SourceWatcher.cs
@@ -0,0 +1,293 @@
+//
+// SourceWatcher.cs
+//
+// Authors:
+// Christian Martellini <christian martellini gmail com>
+// Alexander Kojevnikov <alexander kojevnikov com>
+//
+// Copyright (C) 2009 Christian Martellini
+// Copyright (C) 2009 Alexander Kojevnikov
+//
+// 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.IO;
+using System.Linq;
+using System.Data;
+using System.Threading;
+using System.Collections.Generic;
+
+using Hyena;
+using Hyena.Data.Sqlite;
+
+using Banshee.Base;
+using Banshee.Collection;
+using Banshee.Collection.Database;
+using Banshee.Library;
+using Banshee.ServiceStack;
+using Banshee.Sources;
+using Banshee.Streaming;
+
+namespace Banshee.LibraryWatcher
+{
+ public class SourceWatcher : IDisposable
+ {
+ private readonly LibraryImportManager import_manager;
+ private readonly LibrarySource library;
+ private readonly FileSystemWatcherProxy watcher;
+ private readonly ManualResetEvent handle;
+ private readonly Thread watch_thread;
+
+ private readonly Queue<QueueItem> queue = new Queue<QueueItem> ();
+ private readonly TimeSpan delay = TimeSpan.FromMilliseconds (1000);
+
+ private bool active;
+ private bool disposed;
+
+ private class QueueItem
+ {
+ public DateTime When;
+ public WatcherChangeTypes ChangeType;
+ public string OldFullPath;
+ public string FullPath;
+ public string MetadataHash;
+ }
+
+ public SourceWatcher (LibrarySource library)
+ {
+ this.library = library;
+ handle = new ManualResetEvent(false);
+ string path = library.BaseDirectoryWithSeparator;
+ string home = Environment.GetFolderPath (Environment.SpecialFolder.Personal) + Path.DirectorySeparatorChar;
+ if (path == home) {
+ throw new Exception ("Will not create LibraryWatcher for the entire home directory");
+ }
+
+ import_manager = ServiceManager.Get<LibraryImportManager> ();
+
+ watcher = new FileSystemWatcherProxy (path);
+ watcher.IncludeSubdirectories = true;
+ watcher.Changed += OnChanged;
+ watcher.Created += OnChanged;
+ watcher.Deleted += OnChanged;
+ watcher.Renamed += OnChanged;
+
+ active = true;
+ watch_thread = new Thread (new ThreadStart (Watch));
+ watch_thread.Name = String.Format ("LibraryWatcher for {0}", library.Name);
+ watch_thread.IsBackground = true;
+ watch_thread.Start ();
+ }
+
+#region Public Methods
+
+ public void Dispose ()
+ {
+ if (!disposed) {
+ watcher.EnableRaisingEvents = false;
+ active = false;
+ watcher.Changed -= OnChanged;
+ watcher.Created -= OnChanged;
+ watcher.Deleted -= OnChanged;
+ watcher.Renamed -= OnChanged;
+
+ watcher.Dispose ();
+ disposed = true;
+ }
+ }
+
+#endregion
+
+#region Private Methods
+
+ private void OnChanged (object source, FileSystemEventArgs args)
+ {
+ var item = new QueueItem {
+ When = DateTime.Now,
+ ChangeType = args.ChangeType,
+ FullPath = args.FullPath,
+ OldFullPath = args is RenamedEventArgs ? ((RenamedEventArgs)args).OldFullPath : args.FullPath
+ };
+
+ lock (queue) {
+ queue.Enqueue (item);
+ }
+ handle.Set ();
+
+ if (args.ChangeType != WatcherChangeTypes.Changed) {
+ Hyena.Log.DebugFormat ("Watcher: {0} {1}{2}",
+ item.ChangeType, args is RenamedEventArgs ? item.OldFullPath + " => " : "", item.FullPath);
+ }
+ }
+
+ private void Watch ()
+ {
+ watcher.EnableRaisingEvents = true;
+
+ while (active) {
+ WatcherChangeTypes change_types = 0;
+ while (queue.Count > 0) {
+ QueueItem item;
+ lock (queue) {
+ item = queue.Dequeue ();
+ }
+
+ int sleep = (int) (item.When + delay - DateTime.Now).TotalMilliseconds;
+ if (sleep > 0) {
+ Hyena.Log.DebugFormat ("Watcher: sleeping {0}ms", sleep);
+ Thread.Sleep (sleep);
+ }
+
+ if (item.ChangeType == WatcherChangeTypes.Changed) {
+ UpdateTrack (item.FullPath);
+ } else if (item.ChangeType == WatcherChangeTypes.Created) {
+ AddTrack (item.FullPath);
+ } else if (item.ChangeType == WatcherChangeTypes.Deleted) {
+ RemoveTrack (item.FullPath);
+ } else if (item.ChangeType == WatcherChangeTypes.Renamed) {
+ RenameTrack (item.OldFullPath, item.FullPath);
+ }
+
+ change_types |= item.ChangeType;
+ }
+
+ if ((change_types & WatcherChangeTypes.Deleted) > 0) {
+ library.NotifyTracksDeleted ();
+ }
+ if ((change_types & (WatcherChangeTypes.Renamed |
+ WatcherChangeTypes.Created | WatcherChangeTypes.Changed)) > 0) {
+ library.NotifyTracksChanged ();
+ }
+
+ handle.WaitOne ();
+ handle.Reset ();
+ }
+ }
+
+ private void UpdateTrack (string track)
+ {
+ using (var reader = ServiceManager.DbConnection.Query (
+ DatabaseTrackInfo.Provider.CreateFetchCommand (
+ "CoreTracks.Uri = ? LIMIT 1"), new SafeUri (track).AbsoluteUri)) {
+ if (reader.Read ()) {
+ var track_info = DatabaseTrackInfo.Provider.Load (reader);
+ if (Banshee.IO.File.GetModifiedTime (track_info.Uri) > track_info.FileModifiedStamp) {
+ var file = StreamTagger.ProcessUri (track_info.Uri);
+ StreamTagger.TrackInfoMerge (track_info, file, false);
+ track_info.LastSyncedStamp = DateTime.Now;
+ track_info.Save (false);
+ }
+ }
+ }
+ }
+
+ private void AddTrack (string track)
+ {
+ import_manager.ImportTrack (track);
+
+ // Trigger file rename.
+ string uri = new SafeUri(track).AbsoluteUri;
+ HyenaSqliteCommand command = new HyenaSqliteCommand (@"
+ UPDATE CoreTracks
+ SET DateUpdatedStamp = LastSyncedStamp + 1
+ WHERE Uri = ?", uri);
+ ServiceManager.DbConnection.Execute (command);
+ }
+
+ private void RemoveTrack (string track)
+ {
+ string uri = new SafeUri(track).AbsoluteUri;
+ const string hash_sql = @"SELECT TrackID, MetadataHash FROM CoreTracks WHERE Uri = ? LIMIT 1";
+ int track_id = 0;
+ string hash = null;
+ using (var reader = new HyenaDataReader (ServiceManager.DbConnection.Query (hash_sql, uri))) {
+ if (reader.Read ()) {
+ track_id = reader.Get<int> (0);
+ hash = reader.Get<string> (1);
+ }
+ }
+
+ if (hash != null) {
+ QueueItem item;
+ lock (queue) {
+ item = queue.FirstOrDefault (
+ i => i.ChangeType == WatcherChangeTypes.Created && GetMetadataHash(i) == hash);
+ }
+ if (item != null) {
+ item.ChangeType = WatcherChangeTypes.Renamed;
+ item.OldFullPath = track;
+ return;
+ }
+ }
+
+ const string delete_sql = @"
+ INSERT INTO CoreRemovedTracks (DateRemovedStamp, TrackID, Uri)
+ SELECT ?, TrackID, Uri FROM CoreTracks WHERE TrackID IN ({0})
+ ;
+ DELETE FROM CoreTracks WHERE TrackID IN ({0})";
+
+ // If track_id is 0, it's a directory.
+ HyenaSqliteCommand delete_command;
+ if (track_id > 0) {
+ delete_command = new HyenaSqliteCommand (String.Format (delete_sql,
+ "?"), DateTime.Now, track_id, track_id);
+ } else {
+ string pattern = StringUtil.EscapeLike (uri) + "/_%";
+ delete_command = new HyenaSqliteCommand (String.Format (delete_sql,
+ @"SELECT TrackID FROM CoreTracks WHERE Uri LIKE ? ESCAPE '\'"), DateTime.Now, pattern, pattern);
+ }
+
+ ServiceManager.DbConnection.Execute (delete_command);
+ }
+
+ private void RenameTrack(string oldFullPath, string fullPath)
+ {
+ if (oldFullPath == fullPath) {
+ // FIXME: bug in Mono, see bnc#322330
+ return;
+ }
+ string old_uri = new SafeUri (oldFullPath).AbsoluteUri;
+ string new_uri = new SafeUri (fullPath).AbsoluteUri;
+ string pattern = StringUtil.EscapeLike (old_uri) + "%";
+ HyenaSqliteCommand rename_command = new HyenaSqliteCommand (@"
+ UPDATE CoreTracks
+ SET Uri = REPLACE(Uri, ?, ?), DateUpdatedStamp = ?
+ WHERE Uri LIKE ? ESCAPE '\'",
+ old_uri, new_uri, DateTime.Now, pattern);
+ ServiceManager.DbConnection.Execute (rename_command);
+ }
+
+ private string GetMetadataHash (QueueItem item)
+ {
+ if (item.ChangeType == WatcherChangeTypes.Created && item.MetadataHash == null) {
+ var uri = new SafeUri (item.FullPath);
+ if (DatabaseImportManager.IsWhiteListedFile (item.FullPath) && Banshee.IO.File.Exists (uri)) {
+ var track = new TrackInfo ();
+ StreamTagger.TrackInfoMerge (track, StreamTagger.ProcessUri (uri));
+ item.MetadataHash = track.MetadataHash;
+ }
+ }
+ return item.MetadataHash;
+ }
+
+#endregion
+ }
+}
diff --git a/src/Extensions/Banshee.LibraryWatcher/Makefile.am b/src/Extensions/Banshee.LibraryWatcher/Makefile.am
new file mode 100644
index 0000000..64fbd62
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Makefile.am
@@ -0,0 +1,19 @@
+ASSEMBLY = Banshee.LibraryWatcher
+TARGET = library
+LINK = $(REF_EXTENSION_LIBRARYWATCHER)
+INSTALL_DIR = $(EXTENSIONS_INSTALL_DIR)
+
+SOURCES = \
+ Banshee.LibraryWatcher/FileSystemWatcherProxy.cs \
+ Banshee.LibraryWatcher/IO/FileAction.cs \
+ Banshee.LibraryWatcher/IO/FileSystemWatcher.cs \
+ Banshee.LibraryWatcher/IO/IFileWatcher.cs \
+ Banshee.LibraryWatcher/IO/InotifyWatcher.cs \
+ Banshee.LibraryWatcher/IO/SearchPattern.cs \
+ Banshee.LibraryWatcher/LibraryWatcherService.cs \
+ Banshee.LibraryWatcher/SourceWatcher.cs
+
+RESOURCES = Banshee.LibraryWatcher.addin.xml
+
+include $(top_srcdir)/build/build.mk
+
diff --git a/src/Extensions/Makefile.am b/src/Extensions/Makefile.am
index c38928e..eae043e 100644
--- a/src/Extensions/Makefile.am
+++ b/src/Extensions/Makefile.am
@@ -9,6 +9,7 @@ SUBDIRS = \
Banshee.InternetArchive \
Banshee.InternetRadio \
Banshee.Lastfm \
+ Banshee.LibraryWatcher \
Banshee.MiniMode \
Banshee.MultimediaKeys \
Banshee.NotificationArea \
@@ -23,4 +24,5 @@ SUBDIRS = \
Banshee.RemoteAudio \
Banshee.Wikipedia
+
MAINTAINERCLEANFILES = Makefile.in
diff --git a/src/Libraries/Hyena/Hyena.Query/StringQueryValue.cs b/src/Libraries/Hyena/Hyena.Query/StringQueryValue.cs
index a707283..2572e91 100644
--- a/src/Libraries/Hyena/Hyena.Query/StringQueryValue.cs
+++ b/src/Libraries/Hyena/Hyena.Query/StringQueryValue.cs
@@ -90,9 +90,7 @@ namespace Hyena.Query
if (op == Contains || op == DoesNotContain ||
op == StartsWith || op == EndsWith) {
- return orig.Replace ("\\", "\\\\")
- .Replace ("%", "\\%")
- .Replace ("_", "\\_");
+ return StringUtil.EscapeLike (orig);
}
return orig;
diff --git a/src/Libraries/Hyena/Hyena/StringUtil.cs b/src/Libraries/Hyena/Hyena/StringUtil.cs
index 9070b5c..dbf03ac 100644
--- a/src/Libraries/Hyena/Hyena/StringUtil.cs
+++ b/src/Libraries/Hyena/Hyena/StringUtil.cs
@@ -328,5 +328,14 @@ namespace Hyena
position = index + 1;
}
}
+
+ private static readonly char[] escaped_like_chars = new char[] {'\\', '%', '_'};
+ public static string EscapeLike (string s)
+ {
+ if (s.IndexOfAny (escaped_like_chars) != -1) {
+ return s.Replace (@"\", @"\\").Replace ("%", @"\%").Replace ("_", @"\_");
+ }
+ return s;
+ }
}
}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]