banshee r3829 - in trunk/banshee: . src/Backends/Banshee.Hal/Banshee.HalBackend src/Core/Banshee.Services/Banshee.Hardware src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod src/Dap/Banshee.Dap.MassStorage/Banshee.Dap.MassStorage src/Dap/Banshee.Dap.Mtp/Banshee.Dap.Mtp src/Dap/Banshee.Dap/Banshee.Dap src/Libraries/Hyena/Hyena



Author: abock
Date: Fri Apr 25 23:08:32 2008
New Revision: 3829
URL: http://svn.gnome.org/viewvc/banshee?rev=3829&view=rev

Log:
2008-04-25  Aaron Bockover  <abock gnome org>

    This commit hopefully addresses remaining hardware and threading issues
    with DAP support and fixes the Mono.Addins dependency issue that people
    have been complaining about for the last couple of days

    * src/Backends/Banshee.Hal/Banshee.HalBackend/Volume.cs: Cache the uuid
    of the volume once it's been resolved so the device doesn't have to
    be queried for it again, which is problematic if the device is removed
    forcefully

    * src/Core/Banshee.Services/Banshee.Hardware/HardwareManager.cs:
    Listen directly on the inner manager for device added/removed and
    raise those events from within the service scope, performing the
    cast to custom device in the device added event

    * src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/IpodSource.cs: Defer the
    loading of the database to the LoadFromDevice threaded call so startup
    is faster; implement eject

    * src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/PodSleuthDeviceProvider.cs:
    Allow the provider to adapt the PodSleuthDevice to either the top level
    iPod storage device or the actual iPod data volume so iPod support works
    when an iPod is plugged in while Banshee was already running

    * src/Dap/Banshee.Dap/Banshee.Dap/DapService.cs: Reworked the DapSource
    instantiation to deal with older Mono.Addins; fully dispose and unmap
    any devices bound to a DAP provider extension if that extension is
    removed at runtime; fully tear down the top level DAP service if it
    is disposed, so DAP support can be fully disabled at runtime as well

    * src/Dap/Banshee.Dap/Banshee.Dap/DapSource.cs: Replaced the IDevice
    ctor with a virtual DeviceInitialized method to cope with older
    Mono.Addins - this should fix the problem people have been complaining
    about for 2 days; store the addin ID on the source as well for
    unmapping in the service

    * src/Dap/Banshee.Dap/Banshee.Dap/RemovableSource.cs: Removed bad
    threading decisions, race issues, and excessive dispose calls; it is
    up to the device implementation to call dispose as needed inside eject

    * src/Dap/Banshee.Dap.MassStorage/Banshee.Dap.MassStorage/MassStorageSource.cs:
    * src/Dap/Banshee.Dap.Mtp/Banshee.Dap.Mtp/MtpSource.cs: Updated to reflect
    minor DapSource API changes due to Mono.Addins

    * src/Libraries/Hyena/Hyena/Log.cs: Walk the exception chain to generate
    a full stack trace printout



Modified:
   trunk/banshee/ChangeLog
   trunk/banshee/src/Backends/Banshee.Hal/Banshee.HalBackend/Volume.cs
   trunk/banshee/src/Core/Banshee.Services/Banshee.Hardware/HardwareManager.cs
   trunk/banshee/src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/IpodSource.cs
   trunk/banshee/src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/PodSleuthDeviceProvider.cs
   trunk/banshee/src/Dap/Banshee.Dap.MassStorage/Banshee.Dap.MassStorage/MassStorageSource.cs
   trunk/banshee/src/Dap/Banshee.Dap.Mtp/Banshee.Dap.Mtp/MtpSource.cs
   trunk/banshee/src/Dap/Banshee.Dap/Banshee.Dap/DapService.cs
   trunk/banshee/src/Dap/Banshee.Dap/Banshee.Dap/DapSource.cs
   trunk/banshee/src/Dap/Banshee.Dap/Banshee.Dap/RemovableSource.cs
   trunk/banshee/src/Libraries/Hyena/Hyena/Log.cs

Modified: trunk/banshee/src/Backends/Banshee.Hal/Banshee.HalBackend/Volume.cs
==============================================================================
--- trunk/banshee/src/Backends/Banshee.Hal/Banshee.HalBackend/Volume.cs	(original)
+++ trunk/banshee/src/Backends/Banshee.Hal/Banshee.HalBackend/Volume.cs	Fri Apr 25 23:08:32 2008
@@ -79,8 +79,17 @@
             get { return HalDevice["volume.fstype"]; }
         }
 
+        private string uuid;
         public override string Uuid {
-            get { return String.IsNullOrEmpty (HalDevice["volume.uuid"]) ? base.Uuid : HalDevice["volume.uuid"]; }
+            get { 
+                if (uuid == null) {
+                    uuid = String.IsNullOrEmpty (HalDevice["volume.uuid"]) 
+                        ? base.Uuid 
+                        : HalDevice["volume.uuid"];
+                }
+                
+                return uuid;
+            }
         }
 
         public bool IsMounted {

Modified: trunk/banshee/src/Core/Banshee.Services/Banshee.Hardware/HardwareManager.cs
==============================================================================
--- trunk/banshee/src/Core/Banshee.Services/Banshee.Hardware/HardwareManager.cs	(original)
+++ trunk/banshee/src/Core/Banshee.Services/Banshee.Hardware/HardwareManager.cs	Fri Apr 25 23:08:32 2008
@@ -36,20 +36,13 @@
 
 namespace Banshee.Hardware
 {
-    public sealed class HardwareManager : IService, IHardwareManager
+    public sealed class HardwareManager : IService, IHardwareManager, IDisposable
     {
         private IHardwareManager manager;
         private List<ICustomDeviceProvider> custom_device_providers = new List<ICustomDeviceProvider> ();
         
-        public event DeviceAddedHandler DeviceAdded {
-            add { manager.DeviceAdded += value; }
-            remove { manager.DeviceAdded -= value; }
-        }
-        
-        public event DeviceRemovedHandler DeviceRemoved {
-            add { manager.DeviceRemoved += value; }
-            remove { manager.DeviceRemoved -= value; }
-        }
+        public event DeviceAddedHandler DeviceAdded;
+        public event DeviceRemovedHandler DeviceRemoved;
         
         public HardwareManager ()
         {
@@ -71,11 +64,65 @@
                 throw new Exception ("No HardwareManager extensions could be loaded. Hardware support will be disabled.");
             }
             
+            manager.DeviceAdded += OnDeviceAdded;
+            manager.DeviceRemoved += OnDeviceRemoved;
+            
             foreach (TypeExtensionNode node in AddinManager.GetExtensionNodes ("/Banshee/Platform/HardwareDeviceProvider")) {
                 custom_device_providers.Add ((ICustomDeviceProvider)node.CreateInstance (typeof (ICustomDeviceProvider)));
             }
         }
         
+        public void Dispose ()
+        {
+            lock (this) {
+                if (manager != null) {
+                    manager.DeviceAdded -= OnDeviceAdded;
+                    manager.DeviceRemoved -= OnDeviceRemoved;
+                    manager.Dispose ();
+                    manager = null;
+                }
+                
+                if (custom_device_providers != null) {
+                    foreach (ICustomDeviceProvider provider in custom_device_providers) {
+                        IDisposable disposable = provider as IDisposable;
+                        if (disposable != null) {
+                            disposable.Dispose ();
+                        }
+                    }
+                    
+                    custom_device_providers.Clear ();
+                    custom_device_providers = null;
+                }
+            }
+        }
+        
+        private void OnDeviceAdded (object o, DeviceAddedArgs args)
+        {
+            lock (this) {
+                DeviceAddedHandler handler = DeviceAdded;
+                if (handler != null) {
+                    DeviceAddedArgs raise_args = args;
+                    IDevice cast_device = CastToCustomDevice<IDevice> (args.Device);
+                    
+                    if (cast_device != args.Device) {
+                        raise_args = new DeviceAddedArgs (cast_device);
+                    }
+                    
+                    handler (this, raise_args);
+                }
+            }
+        }
+        
+        private void OnDeviceRemoved (object o, DeviceRemovedArgs args)
+        {
+            lock (this) {
+                DeviceRemovedHandler handler = DeviceRemoved;
+                if (handler != null) {
+                    handler (this, args);
+                }
+            }
+        }
+        
         private T CastToCustomDevice<T> (T device) where T : class, IDevice
         {
             foreach (ICustomDeviceProvider provider in custom_device_providers) {
@@ -94,11 +141,6 @@
                 yield return CastToCustomDevice<T> (device);
             }
         }
-        
-        public void Dispose ()
-        {
-            manager.Dispose ();
-        }
 
         public IEnumerable<IDevice> GetAllDevices ()
         {

Modified: trunk/banshee/src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/IpodSource.cs
==============================================================================
--- trunk/banshee/src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/IpodSource.cs	(original)
+++ trunk/banshee/src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/IpodSource.cs	Fri Apr 25 23:08:32 2008
@@ -55,16 +55,16 @@
         
 #region Device Setup/Dispose
         
-        public IpodSource (IDevice device) : base (device)
+        public override void DeviceInitialize (IDevice device)
         {
+            base.DeviceInitialize (device);
+            
             ipod_device = device as PodSleuthDevice;
             if (ipod_device == null) {
                 throw new InvalidDeviceException ();
             }
-        
-            if (!LoadIpod ()) {
-                throw new InvalidDeviceException ();
-            }
+            
+            name_path = Path.Combine (Path.GetDirectoryName (ipod_device.TrackDatabasePath), "BansheeIPodName");
             
             Initialize ();
         }
@@ -74,8 +74,17 @@
             base.Dispose ();
         }
 
+        // WARNING: This will be called from a thread!
         protected override void Eject ()
-        {
+        {   
+            if (ipod_device.CanUnmount) {
+                ipod_device.Unmount ();
+            }
+
+            if (ipod_device.CanEject) {
+                ipod_device.Eject ();
+            }
+            
             Dispose ();
         }
         
@@ -83,11 +92,16 @@
 
 #region Database Loading
 
+        // WARNING: This will be called from a thread!
+        protected override void LoadFromDevice ()
+        {
+            LoadIpod ();
+            LoadFromDevice (false);
+        }
+
         private bool LoadIpod ()
         {
             try {
-                name_path = Path.Combine (Path.GetDirectoryName (ipod_device.TrackDatabasePath), "BansheeIPodName");
-                
                 if (File.Exists (ipod_device.TrackDatabasePath)) { 
                     ipod_device.LoadTrackDatabase ();
                 } else {
@@ -126,12 +140,6 @@
             }
         }
         
-        // WARNING: This will be called from a thread!
-        protected override void LoadFromDevice ()
-        {
-            LoadFromDevice (false);
-        }
-        
         private void LoadFromDevice (bool refresh)
         {
             // bool previous_database_supported = database_supported;
@@ -209,10 +217,6 @@
             names[1] = prefix + names[1];
             names[2] = prefix + names[2];
             
-            foreach (string name in names) {
-                Console.WriteLine (name);
-            }
-            
             return names;
         }
         

Modified: trunk/banshee/src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/PodSleuthDeviceProvider.cs
==============================================================================
--- trunk/banshee/src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/PodSleuthDeviceProvider.cs	(original)
+++ trunk/banshee/src/Dap/Banshee.Dap.Ipod/Banshee.Dap.Ipod/PodSleuthDeviceProvider.cs	Fri Apr 25 23:08:32 2008
@@ -37,23 +37,41 @@
         public T GetCustomDevice<T> (T device) where T : class, IDevice
         {
             IDiskDevice disk_device = device as IDiskDevice;
+            IVolume volume = device as IVolume;
             
-            if (disk_device == null || device.MediaCapabilities == null || !device.MediaCapabilities.IsType ("ipod")) {
+            if (volume != null && CheckStorageCaps (volume.Parent) && CheckVolume (volume)) {
+                return AsPodSleuthDevice<T> (volume, device);
+            } else if (disk_device == null || !CheckStorageCaps (device)) {
                 return device;
             }
             
-            foreach (IVolume volume in disk_device.Volumes) {
-                if (volume.PropertyExists ("org.podsleuth.version")) {
-                    try {
-                        return (T)((IDevice)(new PodSleuthDevice (volume)));
-                    } catch (Exception e) {
-                        Hyena.Log.Exception (e);
-                        return device;
-                    }
+            foreach (IVolume child_volume in disk_device.Volumes) {
+                if (CheckVolume (child_volume)) {
+                    return AsPodSleuthDevice<T> (child_volume, device);
                 }
             }
             
             return device;
         }
+        
+        private T AsPodSleuthDevice<T> (IVolume volume, T original)
+        {
+            try {
+                return (T)((IDevice)(new PodSleuthDevice (volume)));
+            } catch (Exception e) {
+                Hyena.Log.Exception (e);
+                return original;
+            }
+        }
+        
+        private bool CheckStorageCaps (IDevice device)
+        {
+            return device != null && device.MediaCapabilities != null && device.MediaCapabilities.IsType ("ipod");
+        }
+        
+        private bool CheckVolume (IVolume volume)
+        {
+            return volume.PropertyExists ("org.podsleuth.version");
+        }
     }
 }

Modified: trunk/banshee/src/Dap/Banshee.Dap.MassStorage/Banshee.Dap.MassStorage/MassStorageSource.cs
==============================================================================
--- trunk/banshee/src/Dap/Banshee.Dap.MassStorage/Banshee.Dap.MassStorage/MassStorageSource.cs	(original)
+++ trunk/banshee/src/Dap/Banshee.Dap.MassStorage/Banshee.Dap.MassStorage/MassStorageSource.cs	Fri Apr 25 23:08:32 2008
@@ -49,8 +49,10 @@
     {
         protected IVolume volume;
 
-        public MassStorageSource (IDevice device) : base (device)
+        public override void DeviceInitialize (IDevice device)
         {
+            base.DeviceInitialize (device);
+            
             this.volume = device as IVolume;
             if (volume == null)
                 throw new InvalidDeviceException ();

Modified: trunk/banshee/src/Dap/Banshee.Dap.Mtp/Banshee.Dap.Mtp/MtpSource.cs
==============================================================================
--- trunk/banshee/src/Dap/Banshee.Dap.Mtp/Banshee.Dap.Mtp/MtpSource.cs	(original)
+++ trunk/banshee/src/Dap/Banshee.Dap.Mtp/Banshee.Dap.Mtp/MtpSource.cs	Fri Apr 25 23:08:32 2008
@@ -55,8 +55,10 @@
         //private bool supports_jpegs = false;
         private Dictionary<int, Track> track_map;
 
-        public MtpSource (IDevice device) : base (device)
+        public override void DeviceInitialize (IDevice device)
         {
+            base.DeviceInitialize (device);
+            
             if (MediaCapabilities == null || !MediaCapabilities.IsType ("mtp")) {
                 throw new InvalidDeviceException ();
             }

Modified: trunk/banshee/src/Dap/Banshee.Dap/Banshee.Dap/DapService.cs
==============================================================================
--- trunk/banshee/src/Dap/Banshee.Dap/Banshee.Dap/DapService.cs	(original)
+++ trunk/banshee/src/Dap/Banshee.Dap/Banshee.Dap/DapService.cs	Fri Apr 25 23:08:32 2008
@@ -30,7 +30,6 @@
 
 using System;
 using System.Collections.Generic;
-using System.Threading;
 using Mono.Unix;
 using Mono.Addins;
 
@@ -48,53 +47,66 @@
     {
         private Dictionary<string, DapSource> sources;
         private List<TypeExtensionNode> supported_dap_types = new List<TypeExtensionNode> ();
-        
-        public DapService ()
-        {
-        }
-        
+
         public void Initialize ()
         {
-            sources = new Dictionary<string, DapSource> ();
-            AddinManager.AddExtensionNodeHandler ("/Banshee/Dap/DeviceClass", OnExtensionChanged);
-            ServiceManager.HardwareManager.DeviceAdded += OnHardwareDeviceAdded;
-            ServiceManager.HardwareManager.DeviceRemoved += OnHardwareDeviceRemoved;
-            ServiceManager.SourceManager.SourceRemoved += OnSourceRemoved;
+            lock (this) {
+                sources = new Dictionary<string, DapSource> ();
+                
+                AddinManager.AddExtensionNodeHandler ("/Banshee/Dap/DeviceClass", OnExtensionChanged);
+                
+                ServiceManager.HardwareManager.DeviceAdded += OnHardwareDeviceAdded;
+                ServiceManager.HardwareManager.DeviceRemoved += OnHardwareDeviceRemoved;
+                ServiceManager.SourceManager.SourceRemoved += OnSourceRemoved;
+            }
         }
 
         private void OnExtensionChanged (object o, ExtensionNodeEventArgs args) 
         {
-            TypeExtensionNode node = (TypeExtensionNode) args.ExtensionNode;
-            if (args.Change == ExtensionChange.Add) {
-                Log.DebugFormat ("Dap support extension loaded: {0}", node.Addin.Id);
-                supported_dap_types.Add (node);
-
-                // See if any existing devices are handled by this new DAP support
-                foreach (IDevice device in ServiceManager.HardwareManager.GetAllDevices ()) {
-                    MapDevice (device);
-                }
-            } else {
-                // TODO remove/dispose all loaded DAPs of this type?
-                supported_dap_types.Remove (node);
+            lock (this) {
+                TypeExtensionNode node = (TypeExtensionNode)args.ExtensionNode;
+                
+                if (args.Change == ExtensionChange.Add) {
+                    Log.DebugFormat ("Dap support extension loaded: {0}", node.Addin.Id);
+                    supported_dap_types.Add (node);
+    
+                    // See if any existing devices are handled by this new DAP support
+                    foreach (IDevice device in ServiceManager.HardwareManager.GetAllDevices ()) {
+                        MapDevice (device);
+                    }
+                } else if (args.Change == ExtensionChange.Remove) {
+                    supported_dap_types.Remove (node);
+                    
+                    Queue<DapSource> to_remove = new Queue<DapSource> ();
+                    foreach (DapSource source in sources.Values) {
+                        if (source.AddinId == node.Addin.Id) {
+                            to_remove.Enqueue (source);
+                        }
+                    }
+                    
+                    while (to_remove.Count > 0) {
+                        UnmapDevice (to_remove.Dequeue ().Device.Uuid);
+                    }
+                }
             }
         }
 
         public void Dispose ()
         {
             lock (this) {
+                AddinManager.RemoveExtensionNodeHandler ("/Banshee/Dap/DeviceClass", OnExtensionChanged);
+                
                 ServiceManager.HardwareManager.DeviceAdded -= OnHardwareDeviceAdded;
                 ServiceManager.HardwareManager.DeviceRemoved -= OnHardwareDeviceRemoved;
                 ServiceManager.SourceManager.SourceRemoved -= OnSourceRemoved;
                 
-                ThreadPool.QueueUserWorkItem (delegate {
-                    List<DapSource> dap_sources = new List<DapSource> (sources.Values);
-                    foreach (DapSource source in dap_sources) {
-                        UnmapDevice (source.Device.Uuid);
-                    }
-                    
-                    sources.Clear ();
-                    sources = null;
-                });
+                List<DapSource> dap_sources = new List<DapSource> (sources.Values);
+                foreach (DapSource source in dap_sources) {
+                    UnmapDevice (source.Device.Uuid);
+                }
+                
+                sources.Clear ();
+                sources = null;
             }
         }
         
@@ -102,15 +114,16 @@
         {
             foreach (TypeExtensionNode node in supported_dap_types) {
                 try {
-                    DapSource source = (DapSource)Activator.CreateInstance (node.Type, new object [] { device });
+                    DapSource source = (DapSource)node.CreateInstance ();
+                    source.DeviceInitialize (device);
                     source.LoadDeviceContents ();
+                    source.AddinId = node.Addin.Id;
                     return source;
-                } catch (System.Reflection.TargetInvocationException e) {
-                    if (!(e.InnerException is InvalidDeviceException)) {
-                        Log.Exception (e);
-                    }
+                } catch (InvalidDeviceException) {
                 } catch (InvalidCastException e) {
                     Log.Exception ("Extension is not a DapSource as required", e);
+                } catch (Exception e) {
+                    Log.Exception (e);
                 }
             }
             

Modified: trunk/banshee/src/Dap/Banshee.Dap/Banshee.Dap/DapSource.cs
==============================================================================
--- trunk/banshee/src/Dap/Banshee.Dap/Banshee.Dap/DapSource.cs	(original)
+++ trunk/banshee/src/Dap/Banshee.Dap/Banshee.Dap/DapSource.cs	Fri Apr 25 23:08:32 2008
@@ -50,8 +50,18 @@
         internal IDevice Device {
             get { return device; }
         }
+        
+        private string addin_id;
+        internal string AddinId {
+            get { return addin_id; }
+            set { addin_id = value; }
+        }
+        
+        protected DapSource ()
+        {
+        }
 
-        protected DapSource (IDevice device)
+        public virtual void DeviceInitialize (IDevice device)
         {
             this.device = device;
             type_unique_id = device.Uuid;

Modified: trunk/banshee/src/Dap/Banshee.Dap/Banshee.Dap/RemovableSource.cs
==============================================================================
--- trunk/banshee/src/Dap/Banshee.Dap/Banshee.Dap/RemovableSource.cs	(original)
+++ trunk/banshee/src/Dap/Banshee.Dap/Banshee.Dap/RemovableSource.cs	Fri Apr 25 23:08:32 2008
@@ -33,6 +33,7 @@
 
 using Hyena;
 using Banshee.Base;
+using Banshee.Library;
 using Banshee.ServiceStack;
 using Banshee.Sources;
 using Banshee.Collection;
@@ -41,7 +42,7 @@
 
 namespace Banshee.Dap
 {
-    public abstract class RemovableSource : PrimarySource, IUnmapableSource, Banshee.Library.IImportSource, IDiskUsageReporter
+    public abstract class RemovableSource : PrimarySource, IUnmapableSource, IImportSource, IDiskUsageReporter
     {
         protected RemovableSource () : base ()
         {
@@ -96,13 +97,10 @@
             ThreadPool.QueueUserWorkItem (delegate {
                 try {
                     Eject ();
-
-                    ThreadAssist.ProxyToMain (delegate {
-                        Dispose ();
-                    });
                 } catch (Exception e) {
                     ThreadAssist.ProxyToMain (delegate {
-                        SetStatus (String.Format (Catalog.GetString ("Could not eject {0}: {1}"), GenericName, e.Message), true);
+                        SetStatus (String.Format (Catalog.GetString ("Could not eject {0}: {1}"),
+                            GenericName, e.Message), true);
                     });
                     
                     Log.Exception (e);
@@ -136,7 +134,7 @@
 
         public abstract void Import ();
 
-        public virtual string [] IconNames {
+        string [] IImportSource.IconNames {
             get { return Properties.GetStringList ("Icon.Name"); }
         }
 

Modified: trunk/banshee/src/Libraries/Hyena/Hyena/Log.cs
==============================================================================
--- trunk/banshee/src/Libraries/Hyena/Hyena/Log.cs	(original)
+++ trunk/banshee/src/Libraries/Hyena/Hyena/Log.cs	Fri Apr 25 23:08:32 2008
@@ -27,6 +27,7 @@
 //
 
 using System;
+using System.Text;
 using System.Collections.Generic;
 
 namespace Hyena
@@ -366,8 +367,25 @@
         
         public static void Exception (string message, Exception e)
         {
+            Stack<Exception> exception_chain = new Stack<Exception> ();
+            StringBuilder builder = new StringBuilder ();
+            
+            while (e != null) {
+                exception_chain.Push (e);
+                e = e.InnerException;
+            }
+            
+            while (exception_chain.Count > 0) {
+                e = exception_chain.Pop ();
+                builder.AppendFormat ("{0} (in `{1}')", e.Message, e.Source).AppendLine ();
+                builder.Append (e.StackTrace);
+                if (exception_chain.Count > 0) {
+                    builder.AppendLine ();
+                }
+            }
+        
             // FIXME: We should save these to an actual log file
-            Log.Warning (message ?? "Caught an exception", e.ToString (), false);
+            Log.Warning (message ?? "Caught an exception", builder.ToString (), false);
         }
         
         #endregion



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]