sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/
commit 7edbb7ee676b4789296bd733b28b60f669724f88
parent e624fea237e262a3b22e944b0eb343e028b02272
Author: Rich Lane <rlane@club.cc.cmu.edu>
Date:   Sat, 10 Apr 2010 15:57:40 -0700

source error handling rework

Diffstat:
M lib/sup/draft.rb | 2 +-
M lib/sup/index.rb | 4 ++--
M lib/sup/maildir.rb | 4 ++++
M lib/sup/message.rb | 126 ++++++++++++++++++++++++++++++++++++++++++-------------------------------------
M lib/sup/modes/thread-index-mode.rb | 2 +-
M lib/sup/poll.rb | 17 ++++-------------
M lib/sup/sent.rb | 2 +-
M lib/sup/source.rb | 6 +++++-
M lib/sup/util.rb | 34 ----------------------------------
9 files changed, 85 insertions(+), 112 deletions(-)
diff --git a/lib/sup/draft.rb b/lib/sup/draft.rb
@@ -11,7 +11,7 @@ class DraftManager
 
   def self.source_name; "sup://drafts"; end
   def self.source_id; 9999; end
-  def new_source; @source = Recoverable.new DraftLoader.new; end
+  def new_source; @source = DraftLoader.new; end
 
   def write_draft
     offset = @source.gen_offset
diff --git a/lib/sup/index.rb b/lib/sup/index.rb
@@ -197,7 +197,7 @@ EOS
     locations = entry[:locations].map do |source_id,source_info|
       source = SourceManager[source_id]
       raise "invalid source #{source_id}" unless source
-      [source, source_info]
+      Location.new source, source_info
     end
 
     m = Message.new :locations => locations,
@@ -603,7 +603,7 @@ EOS
 
     entry = {
       :message_id => m.id,
-      :locations => m.locations.map { |source,source_info| [source.id, source_info] },
+      :locations => m.locations.map { |x| [x.source.id, x.info] },
       :date => truncate_date(m.date),
       :snippet => snippet,
       :labels => m.labels.to_a,
diff --git a/lib/sup/maildir.rb b/lib/sup/maildir.rb
@@ -138,6 +138,10 @@ class Maildir < Source
   def mark_seen id; maildir_mark_file id, "S" unless seen? id; end
   def mark_trashed id; maildir_mark_file id, "T" unless trashed? id; end
 
+  def valid? id
+    File.exists? File.join(@dir, id)
+  end
+
 private
 
   def new_maildir_basefn
diff --git a/lib/sup/message.rb b/lib/sup/message.rb
@@ -9,9 +9,6 @@ module Redwood
 ## i would like, for example, to be able to add in a ruby-talk
 ## specific module that would detect and link to /ruby-talk:\d+/
 ## sequences in the text of an email. (how sweet would that be?)
-##
-## this class catches all source exceptions. if the underlying source
-## throws an error, it is caught and handled.
 
 class Message
   SNIPPET_LEN = 80
@@ -171,7 +168,7 @@ class Message
 
   attr_reader :snippet
   def is_list_message?; !@list_address.nil?; end
-  def is_draft?; source.is_a? DraftLoader; end
+  def is_draft?; @labels.member? :draft; end
   def draft_filename
     raise "not a draft" unless is_draft?
     source.fn_for_offset source_info
@@ -225,87 +222,59 @@ class Message
     @chunks
   end
 
+  def location
+    @locations.find { |x| x.valid? } || raise(OutOfSyncSourceError.new)
+  end
+
   def source
-    fail if @locations.empty?
-    @locations.last[0]
+    location.source
   end
 
   def source_info
-    fail if @locations.empty?
-    @locations.last[1]
+    location.info
   end
 
   ## this is called when the message body needs to actually be loaded.
   def load_from_source!
     @chunks ||=
-      if source.respond_to?(:has_errors?) && source.has_errors?
-        [Chunk::Text.new(error_message(source.error.message).split("\n"))]
-      else
-        begin
-          ## we need to re-read the header because it contains information
-          ## that we don't store in the index. actually i think it's just
-          ## the mailing list address (if any), so this is kinda overkill.
-          ## i could just store that in the index, but i think there might
-          ## be other things like that in the future, and i'd rather not
-          ## bloat the index.
-          ## actually, it's also the differentiation between to/cc/bcc,
-          ## so i will keep this.
-          rmsg = source.load_message(source_info)
-          parse_header rmsg.header
-          message_to_chunks rmsg
-        rescue SourceError, SocketError => e
-          warn "problem getting messages from #{source}: #{e.message}"
-          ## we need force_to_top here otherwise this window will cover
-          ## up the error message one
-          source.error ||= e
-          Redwood::report_broken_sources :force_to_top => true
-          [Chunk::Text.new(error_message(e.message).split("\n"))]
-        end
+      begin
+        ## we need to re-read the header because it contains information
+        ## that we don't store in the index. actually i think it's just
+        ## the mailing list address (if any), so this is kinda overkill.
+        ## i could just store that in the index, but i think there might
+        ## be other things like that in the future, and i'd rather not
+        ## bloat the index.
+        ## actually, it's also the differentiation between to/cc/bcc,
+        ## so i will keep this.
+        rmsg = location.parsed_message
+        parse_header rmsg.header
+        message_to_chunks rmsg
+      rescue SourceError, SocketError => e
+        warn "problem reading message #{id}"
+        [Chunk::Text.new(error_message.split("\n"))]
       end
   end
 
-  def error_message msg
+  def error_message
     <<EOS
 #@snippet...
 
 ***********************************************************************
- An error occurred while loading this message. It is possible that
- the source has changed, or (in the case of remote sources) is down.
- You can check the log for errors, though hopefully an error window
- should have popped up at some point.
-
- The message location was:
- #{source}##{source_info}
+ An error occurred while loading this message.
 ***********************************************************************
-
-The error message was:
-  #{msg}
 EOS
   end
 
-  ## wrap any source methods that might throw sourceerrors
-  def with_source_errors_handled
-    begin
-      yield
-    rescue SourceError => e
-      warn "problem getting messages from #{source}: #{e.message}"
-      source.error ||= e
-      Redwood::report_broken_sources :force_to_top => true
-      error_message e.message
-    end
-  end
-
   def raw_header
-    with_source_errors_handled { source.raw_header source_info }
+    location.raw_header
   end
 
   def raw_message
-    with_source_errors_handled { source.raw_message source_info }
+    location.raw_message
   end
 
-  ## much faster than raw_message
   def each_raw_message_line &b
-    with_source_errors_handled { source.each_raw_message_line(source_info, &b) }
+    location.each_raw_message_line &b
   end
 
   ## returns all the content from a message that will be indexed
@@ -347,7 +316,7 @@ EOS
   end
 
   def self.build_from_source source, source_info
-    m = Message.new :locations => [[source, source_info]]
+    m = Message.new :locations => [Location.new(source, source_info)]
     m.load_from_source!
     m
   end
@@ -611,4 +580,43 @@ private
   end
 end
 
+class Location
+  attr_reader :source
+  attr_reader :info
+
+  def initialize source, info
+    @source = source
+    @info = info
+  end
+
+  def raw_header
+    source.raw_header info
+  end
+
+  def raw_message
+    source.raw_message info
+  end
+
+  ## much faster than raw_message
+  def each_raw_message_line &b
+    source.each_raw_message_line info, &b
+  end
+
+  def parsed_message
+    source.load_message info
+  end
+
+  def valid?
+    source.valid? info
+  end
+
+  def == o
+    o.source.id == source.id and o.info == info
+  end
+
+  def hash
+    [source.id, info].hash
+  end
+end
+
 end
diff --git a/lib/sup/modes/thread-index-mode.rb b/lib/sup/modes/thread-index-mode.rb
@@ -231,7 +231,7 @@ EOS
     old_cursor_thread = cursor_thread
     @mutex.synchronize do
       ## let's see you do THIS in python
-      @threads = @ts.threads.select { |t| !@hidden_threads[t] }.sort_by { |t| [t.date, t.first.id] }.reverse
+      @threads = @ts.threads.select { |t| !@hidden_threads[t] }.select(&:first).sort_by { |t| [t.date, t.first.id] }.reverse
       @size_widgets = @threads.map { |t| size_widget_for_thread t }
       @size_widget_width = @size_widgets.max_of { |w| w.display_length }
       @date_widgets = @threads.map { |t| date_widget_for_thread t }
diff --git a/lib/sup/poll.rb b/lib/sup/poll.rb
@@ -103,10 +103,9 @@ EOS
     @mutex.synchronize do
       @poll_sources.each do |source|
         begin
-          yield "Loading from #{source}... " unless source.has_errors?
+          yield "Loading from #{source}... "
         rescue SourceError => e
           warn "problem getting messages from #{source}: #{e.message}"
-          Redwood::report_broken_sources :force_to_top => true
           next
         end
 
@@ -117,7 +116,7 @@ EOS
             yield "Deleting #{m.id}"
           elsif action == :add
             if old_m
-              if not old_m.locations.member? [source, m.source_info]
+              if not old_m.locations.member? m.location
                 yield "Message at #{m.source_info} is an updated of an old message. Updating labels from #{old_m.labels.to_a * ','} => #{m.labels.to_a * ','}"
               else
                 yield "Skipping already-imported message at #{m.source_info}"
@@ -153,14 +152,7 @@ EOS
   ## from the index after being yielded.
   def poll_from source, opts={}
     begin
-      return if source.has_errors?
-
       source.poll do |sym, args|
-        if source.has_errors?
-          warn "error loading messages from #{source}: #{source.error.message}"
-          return
-        end
-
         case sym
         when :add
           m = Message.build_from_source source, args[:info]
@@ -177,16 +169,15 @@ EOS
           UpdateManager.relay self, :added, m
         when :delete
           Index.each_message :location => [source.id, args[:info]] do |m|
-            m.locations.delete [source,args[:info]]
+            m.locations.delete Location.new(source, args[:info])
             yield :delete, m, [source,args[:info]] if block_given?
             Index.sync_message m, false
-            UpdateManager.relay self, :deleted, m
+            #UpdateManager.relay self, :deleted, m
           end
         end
       end
     rescue SourceError => e
       warn "problem getting messages from #{source}: #{e.message}"
-      Redwood::report_broken_sources :force_to_top => true
     end
   end
 
diff --git a/lib/sup/sent.rb b/lib/sup/sent.rb
@@ -19,7 +19,7 @@ class SentManager
   end
 
   def default_source
-    @source = Recoverable.new SentLoader.new
+    @source = SentLoader.new
     @source_uri = @source.uri
     @source
   end
diff --git a/lib/sup/source.rb b/lib/sup/source.rb
@@ -88,6 +88,10 @@ class Source
     unimplemented
   end
 
+  def valid? info
+    true
+  end
+
   ## utility method to read a raw email header from an IO stream and turn it
   ## into a hash of key-value pairs. minor special semantics for certain headers.
   ##
@@ -188,7 +192,7 @@ class SourceManager
   def unusual_sources; sources.find_all { |s| !s.usual? }; end
 
   def load_sources fn=Redwood::SOURCE_FN
-    source_array = (Redwood::load_yaml_obj(fn) || []).map { |o| Recoverable.new o }
+    source_array = Redwood::load_yaml_obj(fn) || []
     @source_mutex.synchronize do
       @sources = Hash[*(source_array).map { |s| [s.id, s] }.flatten]
       @sources_dirty = false
diff --git a/lib/sup/util.rb b/lib/sup/util.rb
@@ -594,40 +594,6 @@ module Singleton
   end
 end
 
-## wraps an object. if it throws an exception, keeps a copy.
-class Recoverable
-  def initialize o
-    @o = o
-    @error = nil
-    @mutex = Mutex.new
-  end
-
-  attr_accessor :error
-
-  def clear_error!; @error = nil; end
-  def has_errors?; !@error.nil?; end
-
-  def method_missing m, *a, &b; __pass m, *a, &b end
-
-  def id; __pass :id; end
-  def to_s; __pass :to_s; end
-  def to_yaml x; __pass :to_yaml, x; end
-  def is_a? c; @o.is_a? c; end
-
-  def respond_to?(m, include_private=false)
-    @o.respond_to?(m, include_private)
-  end
-
-  def __pass m, *a, &b
-    begin
-      @o.send(m, *a, &b)
-    rescue Exception => e
-      @error ||= e
-      raise
-    end
-  end
-end
-
 ## acts like a hash with an initialization block, but saves any
 ## newly-created value even upon lookup.
 ##