Configuration system for beagle (XmlSerializer-based)

Designed to give the users more control.

Currently this just gives access to add more roots.
Possible ideas for future configuration include enabling/disabling backends,
setting default query domain to local/global, setting default number of hits
per backend, see the wiki for other ideas.

Here's how it works:

- Beagle.Util.Conf is a new class which provides access to read and modify
  the current configuration.

- Beagle.Util.Conf is split up into "sections". There is currently only one
  section: indexing.

- Each section has a series of "options" which can be invoked by the user or by
  other areas of beagle code. For example, BU.Conf.Indexing has an "AddRoot"
  section option.

- On startup, the config is loaded. Each "subsystem" which will obey user
  configuration has to subscribe a callback to the section(s) that they use.

- At the end of the startup sequence, the config is loaded, and the callbacks
  mentioned above are executed.

- If inotify is enabled, config changes will happen right away. If not, the
  config files will be polled for changes every 60 seconds.

- When a config change is detected, the subscribed callbacks are executed. This
  means that every callback needs to be prepared to read duplicated data back
  from BU.Conf.

- There is a command-line interface to Beagle.Util.Conf, called beagle-config.
  beagle-config allows execution of "section options" in Beagle.Util.Conf. 
  beagle-config discovers everything at runtime frmo BU.Conf, no hard-coding
  inside beagle-config is present.

- Each section is represented by an object

- Config settings are stored as serialized XML objects, in ~/.beagle/config/
  There is one file per config section.

- .noindex/.neverindex functionality has been removed, replaced by
   user-configurable ignore patterns.

diff -urNpX dontdiff beagle/BeagleClient/RemoteControl.cs beagle-xmlser/BeagleClient/RemoteControl.cs
--- beagle/BeagleClient/RemoteControl.cs	2005-05-18 23:29:49.000000000 +0100
+++ beagle-xmlser/BeagleClient/RemoteControl.cs	2005-05-21 23:32:02.000000000 +0100
@@ -29,6 +29,7 @@ namespace Beagle {
 	// These requests have no interesting client-side state
 	public class DaemonInformationRequest : RequestMessage { }
 	public class ShutdownRequest : RequestMessage { }
+	public class ReloadConfigRequest : RequestMessage { }
 
 	public class DaemonInformationResponse : ResponseMessage {
 		public string Version;
diff -urNpX dontdiff beagle/beagled/BeagleDaemon.cs beagle-xmlser/beagled/BeagleDaemon.cs
--- beagle/beagled/BeagleDaemon.cs	2005-05-23 20:15:32.000000000 +0100
+++ beagle-xmlser/beagled/BeagleDaemon.cs	2005-05-23 19:56:42.000000000 +0100
@@ -312,6 +312,10 @@ namespace Beagle.Daemon {
 #endif
 			Shutdown.ShutdownEvent += OnShutdown;
 
+			// Load user configuration
+			Conf.Load ();
+			Conf.WatchForUpdates ();
+
 			stopwatch.Stop ();
 
 			Logger.Log.Debug ("Daemon initialization finished after {0}", stopwatch);
diff -urNpX dontdiff beagle/beagled/FileSystemQueryable/FileNameFilter.cs beagle-xmlser/beagled/FileSystemQueryable/FileNameFilter.cs
--- beagle/beagled/FileSystemQueryable/FileNameFilter.cs	2005-05-21 22:49:35.000000000 +0100
+++ beagle-xmlser/beagled/FileSystemQueryable/FileNameFilter.cs	2005-05-22 17:41:21.000000000 +0100
@@ -36,13 +36,13 @@ namespace Beagle.Daemon.FileSystemQuerya
 	public class FileNameFilter {
 		
 		static string home_dir;
+		private FileSystemModel model = null;
 
 		static FileNameFilter ()
 		{
 			home_dir = PathFinder.HomeDir;
 		}
 
-
 		private class Pattern {
 			private string exactMatch;
 			private string prefix;
@@ -96,36 +96,16 @@ namespace Beagle.Daemon.FileSystemQuerya
 			}
 		}
 
-		static ArrayList LoadPatterns (string filename)
-		{
-			if (! File.Exists (filename))
-				return null;
-			
-			ArrayList array = new ArrayList ();
-			StreamReader sr = new StreamReader (filename);
-			string line;
-			while ((line = sr.ReadLine ()) != null) {
-				line = line.Trim ();
-				if (line.Length > 0) {
-					Pattern pattern = new Pattern (line);
-					array.Add (pattern);
-				}
-			}
-			
-			return array;
-		}
-
 		//////////////////////////////////////////////////////////////////////
 		
-		private ArrayList defaultPatternsToIgnore;
+		private Hashtable patterns_to_ignore;
 
 		private void SetupDefaultPatternsToIgnore ()
 		{
-			defaultPatternsToIgnore = new ArrayList ();
-
 			// Add our default skip patterns.
 			// FIXME: This probably shouldn't be hard-wired.  Or should it?
-			AddDefaultPatternToIgnore (".*",
+			AddPatternToIgnore (new string [] {
+						   ".*",
 						   "*~",
 						   "#*#",
 						   "*.o",
@@ -156,6 +136,7 @@ namespace Beagle.Daemon.FileSystemQuerya
 						   "Makefile.am",
 						   "Makefile.in",
 						   "CVS",
+						   "SCCS",
 						   // Garbage generated by the autotools
 						   "conftest",
 						   "confdefs.h",
@@ -163,113 +144,32 @@ namespace Beagle.Daemon.FileSystemQuerya
 						   "confstat*",
 						   "/conf[0-9]+.sh/",
 						   "/conf[0-9]+.file/"
-				);
-			
-			// Read the ~/.neverindex file, which contains patterns
-			// for files that should always be ignored.
-			string neverindex = Path.Combine (home_dir, ".neverindex");
-			ArrayList patterns = LoadPatterns (neverindex);
-			if (patterns != null) {
-				foreach (Pattern pattern in patterns)
-					defaultPatternsToIgnore.Add (pattern);
-			}
+			});
 		}
 
-		public void AddDefaultPatternToIgnore (string pattern)
+		public void AddPatternToIgnore (string pattern)
 		{
-			defaultPatternsToIgnore.Add (new Pattern (pattern));
+			if (! patterns_to_ignore.ContainsKey (pattern))
+				patterns_to_ignore.Add (pattern, new Pattern (pattern));
 		}
 		
-		public void AddDefaultPatternToIgnore (params string[] patterns)
+		public void AddPatternToIgnore (IEnumerable patterns)
 		{
 			foreach (string pattern in patterns)
-				AddDefaultPatternToIgnore (pattern);
+				AddPatternToIgnore (pattern);
 		}
 		
 		/////////////////////////////////////////////////////////////
 
-		public FileNameFilter ()
+		public FileNameFilter (FileSystemModel model)
 		{
+			this.model = model;
+			patterns_to_ignore = new Hashtable ();
 			SetupDefaultPatternsToIgnore ();
 		}
 
 		/////////////////////////////////////////////////////////////
 
-		private class PerDirectoryInfo {
-			string dir;
-			DateTime noindexTimestamp;
-			DateTime noindexLastCheck;
-			ArrayList patternsToIgnore;
-
-			public PerDirectoryInfo (string _dir)
-			{
-				dir = _dir;
-				ConditionallyLoad ();
-			}
-
-			public bool IsEmpty {
-				get { return patternsToIgnore == null; }
-			}
-
-			// If the directory's .noindex file has changed, load it.
-			// FIXME: This should be done with inotify instead of using
-			// this sort of seek-happy polling.
-			private void ConditionallyLoad ()
-			{
-				// Only re-check the .noindex file every 11 seconds.
-				DateTime now = DateTime.Now;
-				if ((now - noindexLastCheck).TotalSeconds < 11)
-					return;
-				noindexLastCheck = now;
-
-				string noindex = Path.Combine (dir, ".noindex");
-
-				if (File.Exists (noindex)) {
-					DateTime timestamp = File.GetLastWriteTime (noindex);
-					if (timestamp != noindexTimestamp) {
-						patternsToIgnore = LoadPatterns (noindex);
-						noindexTimestamp = timestamp;
-					}
-				} else {
-					patternsToIgnore = null;
-				}
-			}
-
-			public bool Ignore (string name)
-			{
-				ConditionallyLoad ();
-				if (patternsToIgnore == null)
-					return false;
-				if (patternsToIgnore.Count == 0) // i.e. IgnoreAll
-					return true;
-				foreach (Pattern pattern in patternsToIgnore)
-					if (pattern.IsMatch (name))
-						return true;
-
-				return false;
-			}
-		}
-
-		
-		private Hashtable perDirectoryCache = new Hashtable ();
-
-		private PerDirectoryInfo GetPerDirectoryInfo (string dir)
-		{
-			if (dir == null)
-				return null;
-
-			// Only cache the non-trivial per-directory information
-			PerDirectoryInfo info;
-			lock (perDirectoryCache) {
-				if (! perDirectoryCache.Contains (dir)) {
-					info = new PerDirectoryInfo (dir);
-					perDirectoryCache [dir] = info.IsEmpty ? null : info;
-				}
-				info = perDirectoryCache [dir] as PerDirectoryInfo;
-			}
-			return info;
-		}
-
 		// FIXME: We could make this more efficient by storing more information.
 		// In particular, if ~/foo is an IgnoreAll directory, we could store
 		// that info in the PerDirectoryInfo of subdir ~/foo/bar so that
@@ -278,23 +178,19 @@ namespace Beagle.Daemon.FileSystemQuerya
 		{
 			if (! Path.IsPathRooted (path))
 				path = Path.GetFullPath (path);
-			
-			if (path == home_dir)
-				return false;
+
+			foreach (FileSystemModel.Directory root in model.Roots)
+				if (root.FullName == path)
+					return false;
 
 			string name = Path.GetFileName (path);
 
-			foreach (Pattern pattern in defaultPatternsToIgnore) {
+			foreach (Pattern pattern in patterns_to_ignore.Values)
 				if (pattern.IsMatch (name))
 					return true;
-			}
 
 			string dir = Path.GetDirectoryName (path);
-			PerDirectoryInfo perDir = GetPerDirectoryInfo (dir);
 
-			if (perDir != null && perDir.Ignore (name))
-				return true;
-			
 			// A file should be ignored if any of its parent directories
 			// is ignored.
 			return Ignore (dir);
diff -urNpX dontdiff beagle/beagled/FileSystemQueryable/FileSystemModel.cs beagle-xmlser/beagled/FileSystemQueryable/FileSystemModel.cs
--- beagle/beagled/FileSystemQueryable/FileSystemModel.cs	2005-05-21 22:49:35.000000000 +0100
+++ beagle-xmlser/beagled/FileSystemQueryable/FileSystemModel.cs	2005-05-22 17:34:39.000000000 +0100
@@ -413,7 +413,7 @@ namespace Beagle.Daemon.FileSystemQuerya
 		Hashtable path_cache = new Hashtable ();
 		Hashtable by_unique_id = new Hashtable ();
 		Queue to_be_scanned = new Queue ();
-		FileNameFilter filter = new FileNameFilter ();
+		FileNameFilter filter;
 
 		IFileEventBackend event_backend;
 		int needs_crawl_count = 0;
@@ -432,6 +432,7 @@ namespace Beagle.Daemon.FileSystemQuerya
 
 			unique_id_store = new UniqueIdStore (index_directory, index_fingerprint);
 			name_index = new NameIndex (index_directory, index_fingerprint);
+			filter = new FileNameFilter (this);
 
 			IFileAttributesStore backing_store_i;
 			backing_store_i = new FileAttributesStore_Mixed (index_directory, index_fingerprint);
@@ -442,6 +443,17 @@ namespace Beagle.Daemon.FileSystemQuerya
 			fa_store = new FileAttributesStore (this);
 		}
 
+		public void LoadConf (Conf.Section section)
+		{
+			if (Conf.Indexing.IndexHomeDir)
+				AddRoot (PathFinder.HomeDir);
+
+			foreach (string root in Conf.Indexing.Roots)
+				AddRoot (root);
+
+			filter.AddPatternToIgnore (Conf.Indexing.IgnorePatterns);
+		}
+
 		// I'd rather not expose these, but we really can't avoid it.
 		public UniqueIdStore UniqueIdStore {
 			get { return unique_id_store; }
@@ -468,8 +480,6 @@ namespace Beagle.Daemon.FileSystemQuerya
 			while (path.Length > 0 && path [path.Length-1] == System.IO.Path.DirectorySeparatorChar)
 				path = path.Substring (0, path.Length-1);
 
-			Logger.Log.Debug ("Adding root {0}", path);
-				
 			DirectoryPrivate root = new DirectoryPrivate (big_lock);
 			root.InitRoot (System.IO.Path.GetDirectoryName (path), 
 				       System.IO.Path.GetFileName (path));
@@ -483,13 +493,11 @@ namespace Beagle.Daemon.FileSystemQuerya
 			lock (big_lock) {
 				// FIXME: We also should make sure the path is not a parent or child
 				// of any existing root.
-				foreach (Directory existing_root in roots) {
-					if (existing_root.FullName == root.FullName) {
-						throw new Exception ("Attempt to re-add root " + root.FullName);
-						// FIXME: Is there a case where we would
-						// want to just return existing_root?
-					}
-				}
+				foreach (Directory existing_root in roots)
+					if (existing_root.FullName == root.FullName)
+						return existing_root;
+
+				Logger.Log.Debug ("Adding root {0}", path);
 				roots.Add (root);
 				to_be_scanned.Enqueue (root);
 				if (to_be_scanned.Count == 1)
diff -urNpX dontdiff beagle/beagled/FileSystemQueryable/FileSystemQueryable.cs beagle-xmlser/beagled/FileSystemQueryable/FileSystemQueryable.cs
--- beagle/beagled/FileSystemQueryable/FileSystemQueryable.cs	2005-05-21 22:49:35.000000000 +0100
+++ beagle-xmlser/beagled/FileSystemQueryable/FileSystemQueryable.cs	2005-05-22 16:03:02.000000000 +0100
@@ -271,12 +271,8 @@ namespace Beagle.Daemon.FileSystemQuerya
 		public void StartWorker ()
 		{
 			event_backend.Start (this);
-
-			// FIXME: Shouldn't be hard-wired
-			model.AddRoot (PathFinder.HomeDir);
-
+			Conf.Subscribe (typeof (Conf.IndexingConfig), model.LoadConf);
 			log.Info ("FileSystemQueryable start-up thread finished");
-			
 			// FIXME: Do we need to re-run queries when we are fully started?
 		}
 
diff -urNpX dontdiff beagle/beagled/RemoteControlExecutors.cs beagle-xmlser/beagled/RemoteControlExecutors.cs
--- beagle/beagled/RemoteControlExecutors.cs	2005-05-16 17:05:49.000000000 +0100
+++ beagle-xmlser/beagled/RemoteControlExecutors.cs	2005-05-22 13:28:40.000000000 +0100
@@ -65,4 +65,14 @@ namespace Beagle.Daemon {
 			return new EmptyResponse ();
 		}
 	}
-}
\ No newline at end of file
+
+	[RequestMessage (typeof (ReloadConfigRequest))]
+	public class ReloadConfigExecutor : RequestMessageExecutor {
+		
+		public override ResponseMessage Execute (RequestMessage req)
+		{
+			Conf.Load (true);
+			return new EmptyResponse ();
+		}
+	}
+}
diff -urNpX dontdiff beagle/doc/TODO beagle-xmlser/doc/TODO
--- beagle/doc/TODO	2005-05-21 22:49:35.000000000 +0100
+++ beagle-xmlser/doc/TODO	2005-05-22 18:03:09.000000000 +0100
@@ -55,3 +55,7 @@ Mail
   text/plain parts of a mail; at the very least it could index the
   HTML parts of a mail message.
 
+Configuration
+-------------
+
+beagle-config GUI (fredrik?)
diff -urNpX dontdiff beagle/tools/beagle-config.1 beagle-xmlser/tools/beagle-config.1
--- beagle/tools/beagle-config.1	1970-01-01 01:00:00.000000000 +0100
+++ beagle-xmlser/tools/beagle-config.1	2005-05-22 16:34:19.000000000 +0100
@@ -0,0 +1,55 @@
+.\" beagle-config(1) manpage
+.\"
+.\" Copyright (C) 2005 Novell, Inc.
+.\"
+.TH BEAGLE-CONFIG "1" "May 2005" "beagle" "Linux User's Manual"
+.SH NAME
+beagle-config \- command-line interface to the Beagle configuration file
+.SH SYNOPSIS
+.B beagle-config
+\fIoptions\fR
+.br
+.B beagle-config
+\fIsections\fR
+.br
+.B beagle-config
+\fIsections\fR \fIsection-options\fR [\fIparams\fR]
+.SH DESCRIPTION
+.BR beagle-config
+is a command line tool designed to manipulate the Beagle configuration.
+.PP
+The configuration is stored in a series of files usually located in the \fB~/.beagle/config\fR directory. The configuration is split up into \fIsections\fR, each section being represented by 1 file.
+.PP
+The format used is the result of serialized objects. Manually modifying the configuration files is not recommended, you should use this utility instead.
+.PP
+Each \fIsection\fR configures a different area of the Beagle system. Each section has a number of preset \fIsection-options\fR (commands) which you can perform on it via this command-line application.
+.PP
+The available sections, and available options for each section, are discovered at runtime. This manual does not attempt to document the sections in detail. This manual demonstrates how you can retrieve a list of available sections and their options.
+.PP
+\fIbeagle-config\fR can be used "offline", it does not require a Beagle daemon to be running for most operations. If a Beagle daemon is running, it will immediately be notified of any configuration changes, and your new settings will come into effect almost immediately. Alternatively, you can force a re-load with the \fB--beagled-reload-config\fR argument.
+.SH OPTIONS
+.TP
+.B --help 
+Print a brief copyright and usage message.
+.TP
+.B --beagled-reload-config
+Asks the beagle daemon to reload the configuration files.
+.TP
+.B --list-sections
+Lists all of the available sections for use as the \fIsections\fR parameter.
+.SH SECTION OPTIONS
+Each section has its own unique set of options available. To see a list of supported options for a particular section, run \fBbeagle-config section-name\fR with no additional parameters.
+.SH PARAMS
+Each option may take a number of parameters (or none). Please refer to online documentation if you require explanations of every available command.
+.SH AUTHOR
+Originally written by Daniel Drake <dsd@gentoo.org>.
+.SH "REPORTING BUGS"
+Report bugs to <dashboard-hackers@gnome.org>.
+.SH COPYRIGHT
+Copyright \(co 2005 Novell, Inc.
+.sp
+This is free software; see the source for copying conditions.  There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+.BR beagled (1),
+.BR best (1)
diff -urNpX dontdiff beagle/tools/Config.cs beagle-xmlser/tools/Config.cs
--- beagle/tools/Config.cs	1970-01-01 01:00:00.000000000 +0100
+++ beagle-xmlser/tools/Config.cs	2005-05-22 14:43:12.000000000 +0100
@@ -0,0 +1,188 @@
+//
+// Config.cs
+//
+// Copyright (C) 2005 Novell, Inc.
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.Collections;
+using System.Collections.Specialized;
+using System.Reflection;
+
+using Beagle;
+using Beagle.Util;
+
+using Gtk;
+
+public static class ConfigTool {
+
+	private static void PrintUsageAndExit ()
+	{
+		string usage =
+			"beagle-config: Command-line interface to the Beagle config file.\n" +
+			"Web page: http://www.gnome.org/projects/beagle\n" +
+			"Copyright (C) 2005 Novell, Inc.\n\n";
+		usage +=
+			"Usage: beagle-config [OPTIONS]\n" +
+			"   or: beagle-config <SECTION>\n" +
+			"   or: beagle-config <SECTION> <SECTIONOPTION> [PARAMS]\n\n" +
+			"Options:\n" +
+			"  --beagled-reload-config\tAsk the beagle daemon to reload\n" +
+			"                         \tthe configuration file.\n" +
+			"  --list-sections\t\tList all available configuration sections.\n" +
+			"  --help\t\t\tPrint this usage message.\n\n" +
+			"If a section is specified with no options, then a list of the available commands for that section is shown.\n";
+
+		Console.WriteLine (usage);
+
+		System.Environment.Exit (0);
+	}
+
+	private static void ListSectionsAndExit ()
+	{
+		Console.WriteLine ("Available configuration sections: ");
+		foreach (string key in Conf.Sections.Keys)
+			Console.WriteLine (" - {0}", key);
+
+		System.Environment.Exit (0);
+	}
+	
+	private static void ListSectionOptionsAndExit (string sectionname, Hashtable options)
+	{
+		Console.WriteLine ("Available options for section '{0}':", sectionname);
+		foreach (string key in options.Keys) {
+			Console.Write (" - {0}", key);
+			if (options [key] != null)
+				Console.Write (" ({0})", options [key]);
+
+			Console.WriteLine ();
+		}
+		
+		System.Environment.Exit (0);
+	}
+
+	private static void ReloadConfigAndExit ()
+	{
+		try {
+			ReloadConfigRequest request = new ReloadConfigRequest ();
+			request.Send ();
+			Console.WriteLine ("ReloadConfig request was sent successfully.");
+			System.Environment.Exit (0);
+		} catch (Exception e) {
+			Console.Error.WriteLine ("ERROR: Could not send ReloadConfig request: {0}", e.Message);
+			System.Environment.Exit (-1);
+		}
+	}
+		
+	public static void Main (string [] args)
+	{
+		Application.InitCheck ("beagle-config", ref args);
+
+		if (args.Length == 0)
+			PrintUsageAndExit ();
+
+		int i = 0;
+		while (i < args.Length) {
+			switch (args [i]) {
+			case "--list-sections":
+				Conf.Load ();
+				ListSectionsAndExit ();
+				return;
+
+			case "--reload":
+			case "--beagled-reload-config":
+				ReloadConfigAndExit ();
+				return;
+
+			case "--help":
+			case "--usage":
+				PrintUsageAndExit ();
+				return;
+
+			default:
+				break;
+			}
+			++i;
+		}
+
+		Conf.Load ();
+
+		string sectionname = args [0];
+
+		if (! Conf.Sections.ContainsKey (sectionname)) {
+			Console.Error.WriteLine ("ERROR: Invalid section name '{0}'", sectionname);
+			Environment.Exit (-1);
+		}
+
+		Conf.Section section = (Conf.Section) Conf.Sections [sectionname];
+		Hashtable options = Conf.GetOptions (section);
+
+		// No option specified?
+		if (args.Length == 1)
+			ListSectionOptionsAndExit (sectionname, options);
+		
+		string optionname = args [1];
+		if (! options.ContainsKey (optionname)) {
+			Console.Error.WriteLine ("ERROR: Invalid option name '{0}'", optionname);
+			Environment.Exit (-2);
+		}
+
+		//
+		// Invoke the method the user has chosen
+		//
+
+		// Pack the remaining command line params into an array used for
+		// params of the method.
+		string [] optionparams = new string [args.Length - 2];
+		int j, k;
+		for (j = 0, k = 2; k < args.Length; j++, k++)
+			optionparams [j] = args [k];
+
+		// Invoke the method
+		string output = null;
+		bool result = false;
+
+		try {
+			result = Conf.InvokeOption (section, optionname, optionparams, out output);
+		} catch (Exception e) {
+			Console.Error.WriteLine("ERROR: Command execution failed - caught exception.");
+			Console.Error.WriteLine(e.Message);
+			Environment.Exit (-3);
+		}
+
+		// Read the result and show the output
+		if (result == true)
+			Console.WriteLine (output);
+		else {
+			Console.Error.WriteLine ("ERROR: Command execution failed.");
+			Console.Error.WriteLine (output);
+			Environment.Exit (-4);
+		}
+
+		Conf.Save ();
+		Environment.Exit (0);
+
+	}
+
+
+}
Files beagle/tools/Config.exe and beagle-xmlser/tools/Config.exe differ
Files beagle/tools/Config.exe.mdb and beagle-xmlser/tools/Config.exe.mdb differ
diff -urNpX dontdiff beagle/tools/Makefile.am beagle-xmlser/tools/Makefile.am
--- beagle/tools/Makefile.am	2005-05-21 22:49:35.000000000 +0100
+++ beagle-xmlser/tools/Makefile.am	2005-05-22 16:15:15.000000000 +0100
@@ -64,17 +64,30 @@ $(EXERCISE_FILE_SYSTEM_WRAPPER): $(WRAPP
 	$(WRAPPER_SED) -e "s|\@target\@|$(EXERCISE_FILE_SYSTEM_TARGET)|g" < $(srcdir)/$(WRAPPER_IN) > $@
 	chmod +x $(EXERCISE_FILE_SYSTEM_WRAPPER)
 
+CONFIG_TARGET = Config.exe
+CONFIG_WRAPPER = beagle-config
+CONFIG_CSFILES = $(srcdir)/Config.cs
+
+$(CONFIG_TARGET): $(CONFIG_CSFILES) $(LOCAL_ASSEMBLIES)
+	$(CSC) -out:$@ $(CSFLAGS) $(CONFIG_CSFILES) $(ASSEMBLIES)
+
+$(CONFIG_WRAPPER): $(WRAPPER_IN)
+	$(WRAPPER_SED) -e "s|\@target\@|$(CONFIG_TARGET)|g" < $(srcdir)/$(WRAPPER_IN) > $@
+	chmod +x $(CONFIG_WRAPPER)
+
 TARGETS = 				\
 	$(INFO_TARGET)			\
 	$(SHUTDOWN_TARGET)		\
 	$(QUERY_TARGET)			\
-	$(EXERCISE_FILE_SYSTEM_TARGET)
+	$(EXERCISE_FILE_SYSTEM_TARGET)	\
+	$(CONFIG_TARGET)
 
 WRAPPERS = 				\
 	$(INFO_WRAPPER)			\
 	$(SHUTDOWN_WRAPPER)		\
 	$(QUERY_WRAPPER)		\
-	$(EXERCISE_FILE_SYSTEM_WRAPPER)
+	$(EXERCISE_FILE_SYSTEM_WRAPPER)	\
+	$(CONFIG_WRAPPER)
 
 bin_SCRIPTS = 			\
 	$(WRAPPERS) 		\
@@ -85,7 +98,8 @@ bin_SCRIPTS = 			\
 man_MANS = 			\
 	beagle-query.1		\
 	beagle-shutdown.1	\
-	beagle-status.1
+	beagle-status.1		\
+	beagle-config.1
 
 all: $(TARGETS) $(WRAPPERS)
 
diff -urNpX dontdiff beagle/Util/Conf.cs beagle-xmlser/Util/Conf.cs
--- beagle/Util/Conf.cs	1970-01-01 01:00:00.000000000 +0100
+++ beagle-xmlser/Util/Conf.cs	2005-05-23 20:38:45.000000000 +0100
@@ -0,0 +1,395 @@
+//
+// Conf.cs
+//
+// Copyright (C) 2005 Novell, Inc.
+//
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+using System;
+using System.Collections;
+using System.IO;
+using System.Reflection;
+using System.Xml.Serialization;
+
+using Beagle.Util;
+namespace Beagle.Util {
+
+	public static class Conf {
+
+		public static Hashtable Sections;
+		public static IndexingConfig Indexing = null;
+		private static string configs_dir;
+		private static Hashtable mtimes;
+		private static Hashtable subscriptions;
+		private static bool watching_for_updates;
+		private static int update_wd;
+		private static BindingFlags method_search_flags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod;
+
+		public delegate void ConfigUpdateHandler (Section section);
+
+		static Conf ()
+		{
+			Sections = new Hashtable (1);
+			mtimes = new Hashtable (1);
+			subscriptions = new Hashtable (1);
+
+			configs_dir = Path.Combine (PathFinder.StorageDir, "config");
+			if (!Directory.Exists (configs_dir))
+				Directory.CreateDirectory (configs_dir);
+
+			// We'll start processing file update notifications after we've loaded the
+			// configuration for the first time.
+			watching_for_updates = false;
+		}
+
+		public static void WatchForUpdates ()
+		{
+			if (update_wd > 0)
+				return;
+
+			if (Inotify.Enabled) {
+				Inotify.Event += OnInotifyEvent;
+				update_wd = Inotify.Watch (configs_dir, Inotify.EventType.Create | Inotify.EventType.Modify);
+			} else {
+				// Poll for updates every 60 secs
+				GLib.Timeout.Add (60000, new GLib.TimeoutHandler (CheckForUpdates));
+				update_wd = 1;
+			}
+		}
+
+		private static void OnInotifyEvent (int wd, string path, string subitem, string srcpath, Inotify.EventType type)
+		{
+			if (wd != update_wd || subitem == "" || watching_for_updates == false)
+				return;
+
+			Load ();
+		}
+
+		private static bool CheckForUpdates ()
+		{
+			if (watching_for_updates)
+				Load ();
+			return true;
+		}
+
+		public static void Subscribe (Type type, ConfigUpdateHandler callback)
+		{
+			if (!subscriptions.ContainsKey (type))
+				subscriptions.Add (type, new ArrayList (1));
+
+			ArrayList callbacks = (ArrayList) subscriptions [type];
+			callbacks.Add (callback);
+		}
+
+		private static void NotifySubscribers (Section section)
+		{
+			Type type = section.GetType ();
+			ArrayList callbacks = (ArrayList) subscriptions [type];
+
+			if (callbacks == null)
+				return;
+
+			foreach (ConfigUpdateHandler callback in callbacks)
+				callback (section);
+		}
+
+		public static void Load ()
+		{
+			Load (false);
+		}
+
+		public static void Load (bool force)
+		{
+			Section temp;
+
+			LoadFile (typeof (IndexingConfig), Indexing, out temp, force);
+			Indexing = (IndexingConfig) temp;
+			NotifySubscribers (Indexing);
+
+			watching_for_updates = true;
+		}
+
+		public static void Save ()
+		{
+			Save (false);
+		}
+
+		public static void Save (bool force)
+		{
+			foreach (Section section in Sections.Values)
+				if (force || section.SaveNeeded)
+					SaveFile (section);
+		}
+
+		private static bool LoadFile (Type type, Section current, out Section section, bool force)
+		{
+			section = current;
+			object [] attrs = Attribute.GetCustomAttributes (type, typeof (ConfigSection));
+			if (attrs.Length == 0)
+				throw new ConfigException ("Could not find ConfigSection attribute on " + type);
+
+			string sectionname = ((ConfigSection) attrs [0]).Name;
+			string filename = sectionname + ".xml";
+			string filepath = Path.Combine (configs_dir, filename);
+			if (!File.Exists (filepath)) {
+				if (current == null)
+					ConstructDefaultSection (type, sectionname, out section);
+				return false;
+			}
+			if (!force && current != null && mtimes.ContainsKey (sectionname) &&
+					File.GetLastWriteTimeUtc (filepath).CompareTo ((DateTime) mtimes [sectionname]) < 0)
+				return false;
+
+			Logger.Log.Debug ("Loading {0} from {1}", type, filename);
+			FileStream fs = null;
+
+			try {
+				fs = File.OpenRead (filepath);
+				XmlSerializer serializer = new XmlSerializer (type);
+				section = (Section) serializer.Deserialize (fs);
+			} catch (Exception e) {
+				Logger.Log.Error ("Could not load configuration from {0}: {1}", filename, e.Message);
+				if (fs != null)
+					fs.Close ();
+				if (current == null)
+					ConstructDefaultSection (type, sectionname, out section);
+				return false;
+			}
+
+			fs.Close ();
+			Sections.Remove (sectionname);
+			Sections.Add (sectionname, section);
+			mtimes.Remove (sectionname);
+			mtimes.Add (sectionname, File.GetLastWriteTimeUtc (filepath));
+			return true;
+		}
+
+		private static bool SaveFile (Section section)
+		{
+			Type type = section.GetType ();
+			object [] attrs = Attribute.GetCustomAttributes (type, typeof (ConfigSection));
+			if (attrs.Length == 0)
+				throw new ConfigException ("Could not find ConfigSection attribute on " + type);
+
+			string sectionname = ((ConfigSection) attrs [0]).Name;
+			string filename = sectionname + ".xml";
+			string filepath = Path.Combine (configs_dir, filename);
+
+			Logger.Log.Debug ("Saving {0} to {1}", type, filename);
+			FileStream fs = null;
+
+			try {
+				watching_for_updates = false;
+				fs = new FileStream (filepath, FileMode.Create);
+				XmlSerializer serializer = new XmlSerializer (type);
+				serializer.Serialize (fs, section);
+			} catch (Exception e) {
+				if (fs != null)
+					fs.Close ();
+				Logger.Log.Error ("Could not save configuration to {0}: {1}", filename, e);
+				watching_for_updates = true;
+				return false;
+			}
+
+			fs.Close ();
+			mtimes.Remove (sectionname);
+			mtimes.Add (sectionname, File.GetLastWriteTimeUtc (filepath));
+			watching_for_updates = true;
+			return true;
+		}
+
+		private static void ConstructDefaultSection (Type type, string sectionname, out Section section)
+		{
+			ConstructorInfo ctor = type.GetConstructor (Type.EmptyTypes);
+			section = (Section) ctor.Invoke (null);
+			Sections.Remove (sectionname);
+			Sections.Add (sectionname, section);
+		}
+
+		// Lists all config file options in a hash table where key is option name,
+		// and value is description.
+		public static Hashtable GetOptions (Section section)
+		{
+			Hashtable options = new Hashtable ();
+			MemberInfo [] members = section.GetType ().GetMembers (method_search_flags);
+
+			// Find all of the methods ("options") inside the specified section
+			// object which have the ConfigOption attribute.
+			foreach (MemberInfo member in members) {
+				object [] attrs = member.GetCustomAttributes (typeof (ConfigOption), false);
+				if (attrs.Length > 0)
+					options.Add (member.Name, ((ConfigOption) attrs [0]).Description);
+			}
+
+			return options;
+		}
+
+		public static bool InvokeOption (Section section, string option, string [] args, out string output)
+		{
+			MethodInfo method = section.GetType ().GetMethod (option, method_search_flags);
+			if (method == null) {
+				string msg = String.Format ("No such method '{0}' for section '{1}'", option, section);
+				throw new ConfigException(msg);
+			}
+			object [] attrs = method.GetCustomAttributes (typeof (ConfigOption), false);
+			if (attrs.Length == 0) {
+				string msg = String.Format ("Method '{0}' is not a configurable option", option);
+				throw new ConfigException (msg);
+			}
+
+			// Check the required number of parameters have been provided
+			ConfigOption attr = (ConfigOption) attrs [0];
+			if (attr.Params > 0 && args.Length < attr.Params) {
+				string msg = String.Format ("Option '{0}' requires {1} parameter(s): {2}", option, attr.Params, attr.ParamsDescription);
+				throw new ConfigException (msg);
+			}
+
+			object [] methodparams = { null, args };
+			bool result = (bool) method.Invoke (section, methodparams);
+			output = (string) methodparams [0];
+
+			// Mark the section as save-needed if we just changed something
+			if (result && attr.IsMutator)
+				section.SaveNeeded = true;
+
+			return result;
+		}
+
+		[ConfigSection (Name="indexing")]
+		public class IndexingConfig : Section {
+
+			public IndexingConfig ()
+			{
+				roots = new ArrayList ();
+				ignore_patterns = new ArrayList ();
+				index_home_dir = true;
+			}
+
+			private ArrayList roots;
+			public ArrayList Roots {
+				get { return ArrayList.ReadOnly (roots); }
+				set { roots = value; }
+			}
+
+			private bool index_home_dir;
+			public bool IndexHomeDir {
+				get { return index_home_dir; }
+				set { index_home_dir = value; }
+			}
+
+			private ArrayList ignore_patterns;
+			public ArrayList IgnorePatterns {
+				get { return ArrayList.ReadOnly (ignore_patterns); }
+				set { ignore_patterns = value; }
+			}
+
+			[ConfigOption (Description="List the indexing roots", IsMutator=false)]
+			internal bool ListRoots (out string output, string [] args)
+			{
+				output = "Current roots:\n";
+				if (this.index_home_dir == true)
+					output += " - Your home directory\n";
+				foreach (string root in roots)
+					output += " - " + root + "\n";
+
+				return true;
+			}
+
+			[ConfigOption (Description="Toggles whether your home directory is to be indexed as a root")]
+			internal bool IndexHome (out string output, string [] args)
+			{
+				if (index_home_dir)
+					output = "Your home directory will not be indexed.";
+				else
+					output = "Your home directory will be indexed.";
+				index_home_dir = !index_home_dir;
+				return true;
+			}
+
+			[ConfigOption (Description="Add a root path to be indexed", Params=1, ParamsDescription="A path")]
+			internal bool AddRoot (out string output, string [] args)
+			{
+				roots.Add (args [0]);
+				output = "Root added.";
+				return true;
+			}
+
+			[ConfigOption (Description="Remove an indexing root", Params=1, ParamsDescription="A path")]
+			internal bool DelRoot (out string output, string [] args)
+			{
+				roots.Remove (args [0]);
+				output = "Root removed.";
+				return true;
+			}
+
+			[ConfigOption (Description="List user-specified filename patterns to be ignored", IsMutator=false)]
+			internal bool ListIgnorePatterns (out string output, string [] args)
+			{
+				output = "User-specified ignore patterns:\n";
+				foreach (string pattern in ignore_patterns)
+					output += " - " + pattern + "\n";
+				return true;
+			}
+
+			[ConfigOption (Description="Add a filename pattern to be ignored", Params=1, ParamsDescription="A pattern")]
+			internal bool AddIgnorePattern (out string output, string [] args)
+			{
+				ignore_patterns.Add (args [0]);
+				output = "Pattern added.";
+				return true;
+			}
+
+			[ConfigOption (Description="Remove an ignored filename pattern", Params=1, ParamsDescription="A pattern")]
+			internal bool DelIgnorePattern (out string output, string [] args)
+			{
+				ignore_patterns.Remove (args [0]);
+				output = "Pattern removed.";
+				return true;
+			}
+
+		}
+
+		public class Section {
+			[XmlIgnore]
+			public bool SaveNeeded = false;
+		}
+
+		private class ConfigOption : Attribute {
+			public string Description;
+			public int Params;
+			public string ParamsDescription;
+			public bool IsMutator = true;
+		}
+
+		private class ConfigSection : Attribute {
+			public string Name;
+		}
+
+		public class ConfigException : Exception {
+			public ConfigException (string msg) : base (msg) { }
+		}
+
+	}
+
+
+
+}
diff -urNpX dontdiff beagle/Util/Makefile.am beagle-xmlser/Util/Makefile.am
--- beagle/Util/Makefile.am	2005-05-21 22:49:35.000000000 +0100
+++ beagle-xmlser/Util/Makefile.am	2005-05-21 22:49:22.000000000 +0100
@@ -30,6 +30,7 @@ $(EXTSTR): $(EXTSTR_IN)
 CSFILES = 			        	\
 	$(srcdir)/camel.cs              	\
 	$(srcdir)/CommandLineFu.cs		\
+	$(srcdir)/Conf.cs			\
 	$(srcdir)/DirectoryWalker.cs		\
 	$(srcdir)/ExceptionHandlingThread.cs	\
 	$(srcdir)/ExifData.cs			\
