]> git.cworth.org Git - sup/commitdiff
many changes (while on the airplane).
authorwmorgan <wmorgan@5c8cc53c-5e98-4d25-b20a-d8db53a31250>
Tue, 2 Jan 2007 16:00:44 +0000 (16:00 +0000)
committerwmorgan <wmorgan@5c8cc53c-5e98-4d25-b20a-d8db53a31250>
Tue, 2 Jan 2007 16:00:44 +0000 (16:00 +0000)
- reworked source error handling so that it finally works well.
- reworked ssh and imap to conform to that.
- cleaned up logging and comments all over.
- made sup-import use highline and allow the user to use previously-defined
  account information.
- made the buffer line expandable

git-svn-id: svn://rubyforge.org/var/svn/sup/trunk@135 5c8cc53c-5e98-4d25-b20a-d8db53a31250

14 files changed:
bin/sup
bin/sup-import
lib/sup/buffer.rb
lib/sup/imap.rb
lib/sup/index.rb
lib/sup/label.rb
lib/sup/mbox/ssh-file.rb
lib/sup/mbox/ssh-loader.rb
lib/sup/message.rb
lib/sup/modes/scroll-mode.rb
lib/sup/modes/thread-index-mode.rb
lib/sup/modes/thread-view-mode.rb
lib/sup/source.rb
lib/sup/util.rb

diff --git a/bin/sup b/bin/sup
index 4cc1ecf90285253a20fc296f7267343fe0de8fe3..3066d6d79d986ed3c28d64603f8afca33cb6169e 100644 (file)
--- a/bin/sup
+++ b/bin/sup
@@ -224,8 +224,8 @@ EOS
 I'm very sorry, but it seems that an error occurred in Sup. 
 Please accept my sincere apologies. If you don't mind, please
 send the backtrace below and a brief report of the circumstances
-to user wmorgan-sup at site masanjin dot net so that I might
-address this problem. Thank you!
+to wmorgan-sup at masanjin dot nets so that I might address this
+problem. Thank you!
 
 Sincerely,
 William
index a31387d830997f2e70c17cb937ce48bdec700dcc..d4fbb278ba4771a3371283befa253a9ae06374a3 100644 (file)
@@ -1,9 +1,11 @@
 #!/usr/bin/env ruby
 
+require 'uri'
 require 'rubygems'
-require 'highline'
+require 'highline/import'
 require "sup"
 
+
 Thread.abort_on_exception = true # make debugging possible
 
 class Float
@@ -61,6 +63,37 @@ EOS
   exit
 end
 
+## for sources that require login information, prompt the user for
+## that. also provide a list of previously-defined login info to
+## choose from, if any.
+def get_login_info uri, sources
+  uri = URI(uri)
+  accounts = sources.map do |s|
+    next unless s.respond_to?(:username)
+    suri = URI(s.uri)
+    [suri.host, s.username, s.password]
+  end.compact.uniq.sort_by { |h, u, p| h == uri.host ? 0 : 1 }
+
+  username, password = nil, nil
+  unless accounts.empty?
+    say "Would you like to use the same account as for a previous source?"
+    choose do |menu|
+      accounts.each do |host, olduser, oldpw|
+        menu.choice("Use the account info for #{olduser}@#{host}") { username, password = olduser, oldpw }
+      end
+      menu.choice("Use a new account") { }
+    end
+  end
+
+  unless username && password
+    username = ask("Username for #{uri.host}: ");
+    password = ask("Password for #{uri.host}: ") { |q| q.echo = false }
+  end
+
+  [username, password]
+end
+
+
 educate_user if ARGV.member? '--help'
 
 archive = ARGV.delete "--archive"
@@ -83,31 +116,27 @@ if(o = ARGV.find { |x| x =~ /^--/ })
   educate_user
 end
 
+$terminal.wrap_at = :auto
 Redwood::start
-
 index = Redwood::Index.new
 index.load
 
-h = HighLine.new
-
-sources = ARGV.map do |fn|
-  fn = "mbox://#{fn}" unless fn =~ %r!://!
-  source = index.source_for fn
+sources = ARGV.map do |uri|
+  uri = "mbox://#{uri}" unless uri =~ %r!://!
+  source = index.source_for uri
   unless source
     source = 
-      case fn
+      case uri
       when %r!^mbox\+ssh://!
-        username = h.ask("Username for #{fn}: ");
-        password = h.ask("Password for #{fn}: ") { |q| q.echo = false }
-        puts # why?
-        Redwood::MBox::SSHLoader.new(fn, username, password, nil, !unusual, !!archive)
+        say "For SSH connections, if you will use public key authentication, you may leave the username and password blank."
+        say "\n"
+        username, password = get_login_info uri, index.sources
+        Redwood::MBox::SSHLoader.new(uri, username, password, nil, !unusual, !!archive)
       when %r!^imaps?://!
-        username = h.ask("Username for #{fn}: ");
-        password = h.ask("Password for #{fn}: ") { |q| q.echo = false }
-        puts # why?
-        Redwood::IMAP.new(fn, username, password, nil, !unusual, !!archive)
+        username, password = get_login_info uri, sources
+        Redwood::IMAP.new(uri, username, password, nil, !unusual, !!archive)
       else
-        Redwood::MBox::Loader.new(fn, nil, !unusual, !!archive)
+        Redwood::MBox::Loader.new(uri, nil, !unusual, !!archive)
       end
     index.add_source source
   end
@@ -128,7 +157,7 @@ start = Time.now
 begin
   sources.each do |source|
     if source.broken?
-      puts "error loading messages from #{source}: #{source.broken_msg}"
+      $stderr.puts "error loading messages from #{source}: #{source.broken_msg}"
       next
     end
     next if source.done?
index 1d5ec22faa47a0117f33cb6a90412965df0fd02b..d672bfebcc2e01a85d158fe1516acfb523326e46 100644 (file)
@@ -44,6 +44,16 @@ end
 
 module Redwood
 
+## could be a bottleneck, but doesn't seem to significantly slow
+## things down.
+
+class SafeNcurses
+  def self.method_missing meth, *a, &b
+    @mutex ||= Mutex.new
+    @mutex.synchronize { Ncurses.send meth, *a, &b }
+  end
+end
+
 class Buffer
   attr_reader :mode, :x, :y, :width, :height, :title
   bool_reader :dirty
@@ -60,7 +70,8 @@ class Buffer
   def content_height; @height - 1; end
   def content_width; @width; end
 
-  def resize rows, cols
+  def resize rows, cols 
+    return if rows == @width && cols == @height
     @width = cols
     @height = rows
     mode.resize rows, cols
@@ -179,15 +190,15 @@ class BufferManager
 
   def completely_redraw_screen
     return if @freeze
-    Ncurses.clear
+    SafeNcurses.clear
     @dirty = true
     draw_screen
   end
 
   def handle_resize
     return if @freeze
-    rows, cols = Ncurses.rows, Ncurses.cols
-    @buffers.each { |b| b.resize rows - 1, cols }
+    rows, cols = SafeNcurses.rows, SafeNcurses.cols
+    @buffers.each { |b| b.resize rows - minibuf_lines, cols }
     completely_redraw_screen
     flash "resized to #{rows}x#{cols}"
   end
@@ -199,15 +210,19 @@ class BufferManager
     ## (currently we only have one buffer visible at a time).
     ## TODO: reenable this if we allow multiple buffers
     false && @buffers.inject(@dirty) do |dirty, buf|
-      dirty ? buf.draw : buf.redraw
-      dirty || buf.dirty?
+      buf.resize SafeNcurses.rows - minibuf_lines, SafeNcurses.cols
+      @dirty ? buf.draw : buf.redraw
     end
     ## quick hack
-    true && (@dirty ? @buffers.last.draw : @buffers.last.redraw)
-    
+    if true
+      buf = @buffers.last
+      buf.resize SafeNcurses.rows - minibuf_lines, SafeNcurses.cols
+      @dirty ? buf.draw : buf.redraw
+    end
+
     draw_minibuf unless skip_minibuf
     @dirty = false
-    Ncurses.doupdate
+    SafeNcurses.doupdate
   end
 
   ## gets the mode from the block, which is only called if the buffer
@@ -228,13 +243,13 @@ class BufferManager
     realtitle = title
     num = 2
     while @name_map.member? realtitle
-      realtitle = "#{title} #{num}"
+      realtitle = "#{title} <#{num}>"
       num += 1
     end
 
     Redwood::log "spawning buffer \"#{realtitle}\""
-    width = opts[:width] || Ncurses.cols
-    height = opts[:height] || Ncurses.rows - 1
+    width = opts[:width] || SafeNcurses.cols
+    height = opts[:height] || SafeNcurses.rows - 1
 
     ## since we are currently only doing multiple full-screen modes,
     ## use stdscr for each window. once we become more sophisticated,
@@ -242,9 +257,7 @@ class BufferManager
     ##
     ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
     ## (opts[:left] || 0))
-    w = Ncurses.stdscr
-    raise "nil window" unless w
-    
+    w = SafeNcurses.stdscr
     b = Buffer.new w, mode, width, height, :title => realtitle
     mode.buffer = b
     @name_map[realtitle] = b
@@ -279,8 +292,8 @@ class BufferManager
   end
 
   def ask domain, question, default=nil
-    @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0,
-                            Ncurses.cols
+    @textfields[domain] ||= TextField.new SafeNcurses.stdscr, SafeNcurses.rows - 1, 0,
+                            SafeNcurses.cols
     tf = @textfields[domain]
 
     ## this goddamn ncurses form shit is a fucking 1970's
@@ -295,8 +308,8 @@ class BufferManager
     ret = nil
     @freeze = true
     tf.position_cursor
-    Ncurses.refresh
-    while tf.handle_input(Ncurses.nonblocking_getch); end
+    SafeNcurses.refresh
+    while tf.handle_input(SafeNcurses.nonblocking_getch); end
     @freeze = false
 
     ret = tf.value
@@ -311,15 +324,15 @@ class BufferManager
     accept = accept.split(//).map { |x| x[0] } if accept
 
     flash question
-    Ncurses.curs_set 1
-    Ncurses.move Ncurses.rows - 1, question.length + 1
-    Ncurses.refresh
+    SafeNcurses.curs_set 1
+    SafeNcurses.move SafeNcurses.rows - 1, question.length + 1
+    SafeNcurses.refresh
 
     ret = nil
     done = false
     @freeze = true
     until done
-      key = Ncurses.nonblocking_getch
+      key = SafeNcurses.nonblocking_getch
       if key == Ncurses::KEY_CANCEL
         done = true
       elsif (accept && accept.member?(key)) || !accept
@@ -328,10 +341,10 @@ class BufferManager
       end
     end
     @freeze = false
-    Ncurses.curs_set 0
+    SafeNcurses.curs_set 0
     erase_flash
     draw_screen
-    Ncurses.curs_set 0
+    SafeNcurses.curs_set 0
 
     ret
   end
@@ -348,12 +361,16 @@ class BufferManager
     end
   end
 
+  def minibuf_lines; [(@flash ? 1 : 0) + @minibuf_stack.compact.size, 1].max; end
+  
   def draw_minibuf
-    s = @flash || @minibuf_stack.reverse.find { |x| x } || ""
-
-    Ncurses.attrset Colormap.color_for(:none)
-    Ncurses.mvaddstr Ncurses.rows - 1, 0, s + (" " * [Ncurses.cols - s.length,
-                                                      0].max)
+    SafeNcurses.attrset Colormap.color_for(:none)
+    m = @minibuf_stack.compact
+    m << @flash if @flash
+    m << "" if m.empty?
+    m.each_with_index do |s, i|
+      SafeNcurses.mvaddstr SafeNcurses.rows - i - 1, 0, s + (" " * [SafeNcurses.cols - s.length, 0].max)
+    end
   end
 
   def say s, id=nil
@@ -361,12 +378,14 @@ class BufferManager
     @minibuf_stack[id] = s
     unless @freeze
       draw_screen
-      Ncurses.refresh
+      SafeNcurses.refresh
     end
     if block_given?
-      yield
-      clear id
-      return
+      begin
+        yield
+      ensure
+        clear id
+      end
     end
     id
   end
@@ -377,10 +396,12 @@ class BufferManager
     @flash = s
     unless @freeze
       draw_screen
-      Ncurses.refresh
+      SafeNcurses.refresh
     end
   end
 
+  ## a little tricky because we can't just delete_at id because ids
+  ## are relative (they're positions into the array).
   def clear id
     @minibuf_stack[id] = nil
     if id == @minibuf_stack.length - 1
@@ -389,18 +410,19 @@ class BufferManager
         @minibuf_stack.delete_at i
       end
     end
+
     unless @freeze
       draw_screen
-      Ncurses.refresh
+      SafeNcurses.refresh
     end
   end
 
   def shell_out command
     @freeze = true
-    Ncurses.endwin
+    SafeNcurses.endwin
     system command
-    Ncurses.refresh
-    Ncurses.curs_set 0
+    SafeNcurses.refresh
+    SafeNcurses.curs_set 0
     @freeze = false
   end
 end
index d1c6b04a67a114d5a932dcafdd79840e58e649ae..06ade3184803cf4f510b8fe81fb50e9dd875627a 100644 (file)
@@ -1,37 +1,39 @@
 require 'uri'
 require 'net/imap'
 require 'stringio'
+require 'time'
 
 ## fucking imap fucking sucks. what the FUCK kind of committee of
 ## dunces designed this shit.
 
-## you see, imap touts 'unique ids' for messages, which are to be used
-## for cross-session identification. great, just what sup needs! only,
-## it turns out the uids can be invalidated every time some arbitrary
-## 'uidvalidity' value changes on the server, and 'uidvalidity' has no
-## restrictions. it can change any time you log in. it can change
-## EVERY time you log in. of course the imap spec "strongly
+## imap talks about 'unique ids' for messages, to be used for
+## cross-session identification. great---just what sup needs! except
+## it turns out the uids can be invalidated every time the
+## 'uidvalidity' value changes on the server, and 'uidvalidity' can
+## change without restriction. it can change any time you log in. it
+## can change EVERY time you log in. of course the imap spec "strongly
 ## recommends" that it never change, but there's nothing to stop
-## people from just setting it to the current time, and in fact that's
-## exactly what the one imap server i have at my disposal does. thus
-## the so-called uids are absolutely useless and imap provides no
-## cross-session way of uniquely identifying a message. but thanks for
-## the "strong recommendation", guys!
-
-## right now i'm using the 'internal date' and the size of each
-## message to uniquely identify it, and i have to scan over the entire
-## mailbox each time i open it to map those things to message ids, and
-## we'll just hope that there are no collisions. ho ho! that's a
-## perfectly reasonable solution!
-
-## fuck you imap committee. you managed to design something as shitty
+## people from just setting it to the current timestamp, and in fact
+## that's exactly what the one imap server i have at my disposal
+## does. thus the so-called uids are absolutely useless and imap
+## provides no cross-session way of uniquely identifying a
+## message. but thanks for the "strong recommendation", guys!
+
+## so right now i'm using the 'internal date' and the size of each
+## message to uniquely identify it, and i scan over the entire mailbox
+## each time i open it to map those things to message ids. that can be
+## slow for large mailboxes, and we'll just have to hope that there
+## are no collisions. ho ho! a perfectly reasonable solution!
+
+## fuck you, imap committee. you managed to design something as shitty
 ## as mbox but goddamn THIRTY YEARS LATER.
 
 module Redwood
 
 class IMAP < Source
   attr_reader_cloned :labels
-  
+  attr_accessor :username, :password
+
   def initialize uri, username, password, last_idate=nil, usual=true, archived=false, id=nil
     raise ArgumentError, "username and password must be specified" unless username && password
     raise ArgumentError, "not an imap uri" unless uri =~ %r!imaps?://!
@@ -47,6 +49,7 @@ class IMAP < Source
     @labels = [:unread]
     @labels << :inbox unless archived?
     @labels << mailbox.intern unless mailbox =~ /inbox/i || mailbox.nil?
+    @mutex = Mutex.new
   end
 
   def connect
@@ -69,24 +72,28 @@ class IMAP < Source
     Redwood::log "connecting to #{@parsed_uri.host} port #{ssl? ? 993 : 143}, ssl=#{ssl?} ..."
     sid = BufferManager.say "Connecting to IMAP server #{host}..." if BufferManager.instantiated?
 
-    ::Thread.new do
+    Redwood::reporting_thread do
       begin
         #raise Net::IMAP::ByeResponseError, "simulated imap failure"
-        @imap = Net::IMAP.new host, ssl? ? 993 : 143, ssl?
+        # @imap = Net::IMAP.new host, ssl? ? 993 : 143, ssl?
+        sleep 3
         BufferManager.say "Logging in...", sid if BufferManager.instantiated?
-        @imap.authenticate 'LOGIN', @username, @password
+        # @imap.authenticate 'LOGIN', @username, @password
+        sleep 3
         BufferManager.say "Sizing mailbox...", sid if BufferManager.instantiated?
-        @imap.examine mailbox
-        last_id = @imap.responses["EXISTS"][-1]
-
+        # @imap.examine mailbox
+        # last_id = @imap.responses["EXISTS"][-1]
+        sleep 1
+        
         BufferManager.say "Reading headers (because IMAP sucks)...", sid if BufferManager.instantiated?
-        values = @imap.fetch(1 .. last_id, ['RFC822.SIZE', 'INTERNALDATE'])
-
+        # values = @imap.fetch(1 .. last_id, ['RFC822.SIZE', 'INTERNALDATE'])
+        sleep 3
+        
+        raise Net::IMAP::ByeResponseError, "simulated imap failure"
         Redwood::log "successfully connected to #{@parsed_uri}"
-
+        
         values.each do |v|
-          msize, mdate = v.attr['RFC822.SIZE'], Time.parse(v.attr["INTERNALDATE"])
-          id = sprintf("%d.%07d", mdate.to_i, msize).to_i
+          id = make_id v
           @ids << id
           @imap_ids[id] = v.seqno
         end
@@ -99,10 +106,17 @@ class IMAP < Source
       end
     end.join
 
+    @mutex.unlock
     !!@imap
   end
   private :connect
 
+  def make_id imap_stuff
+    msize, mdate = imap_stuff.attr['RFC822.SIZE'], Time.parse(imap_stuff.attr["INTERNALDATE"])
+    sprintf("%d.%07d", mdate.to_i, msize).to_i
+  end
+  private :make_id
+
   def host; @parsed_uri.host; end
   def mailbox; @parsed_uri.path[1..-1] end ##XXXX TODO handle nil
   def ssl?; @parsed_uri.scheme == 'imaps' end
@@ -117,31 +131,38 @@ class IMAP < Source
 
   ## load the full header text
   def raw_header id
-    connect or raise SourceError, broken_msg
-    get_imap_field(id, 'RFC822.HEADER').gsub(/\r\n/, "\n")
+    @mutex.synchronize do
+      connect or raise SourceError, broken_msg
+      get_imap_field(id, 'RFC822.HEADER').gsub(/\r\n/, "\n")
+    end
   end
 
   def raw_full_message id
-    connect or raise SourceError, broken_msg
-    get_imap_field(id, 'RFC822').gsub(/\r\n/, "\n")
+    @mutex.synchronize do
+      connect or raise SourceError, broken_msg
+      get_imap_field(id, 'RFC822').gsub(/\r\n/, "\n")
+    end
   end
 
   def get_imap_field id, field
-    imap_id = @imap_ids[id] or raise SourceError, "Unknown message id #{id}. It is likely that messages have been deleted from this IMAP mailbox. Please run sup-import --rebuild #{to_s} in order to correct this problem."
-
-    f = 
+    f = nil
+    @mutex.synchronize do
+      imap_id = @imap_ids[id] or raise SourceError, "Unknown message id #{id}. It is likely that messages have been deleted from this IMAP mailbox."
       begin
-        @imap.fetch imap_id, field
+        f = @imap.fetch imap_id, [field, 'RFC822.SIZE', 'INTERNALDATE']
+        got_id = make_id f
+        raise SourceError, "IMAP message mismatch: requested #{id}, got #{got_id}. It is likely the IMAP mailbox has been modified." unless got_id == id
       rescue Net::IMAP::Error => e
         raise SourceError, e.message
       end
-    raise SourceError, "null IMAP field '#{field}' for message with id #{id} imap id #{imap_id}" if f.nil?
+      raise SourceError, "null IMAP field '#{field}' for message with id #{id} imap id #{imap_id}" if f.nil?
+    end
     f[0].attr[field]
   end
   private :get_imap_field
   
   def each
-    connect or raise SourceError, broken_msg
+    @mutex.synchronize { connect or raise SourceError, broken_msg }
 
     start = @ids.index(cur_offset || start_offset)
     start.upto(@ids.length - 1) do |i|
@@ -152,11 +173,11 @@ class IMAP < Source
   end
 
   def start_offset
-    connect or raise SourceError, broken_msg
+    @mutex.synchronize { connect or raise SourceError, broken_msg }
     @ids.first
   end
   def end_offset
-    connect or raise SourceError, broken_msg
+    @mutex.synchronize { connect or raise SourceError, broken_msg }
     @ids.last
   end
 end
index 9c0f056804b55c6c66f61333c00a24fc486007c2..c4078941a1e067266ddcbb06d89344f86bc6cfd6 100644 (file)
@@ -19,8 +19,7 @@ end
 class Index
   include Singleton
 
-  attr_reader :index # debugging only
-  
+  attr_reader :index
   def initialize dir=BASE_DIR
     @dir = dir
     @sources = {}
@@ -57,6 +56,7 @@ class Index
 
   def source_for name; @sources.values.find { |s| s.is_source_for? name }; end
   def usual_sources; @sources.values.find_all { |s| s.usual? }; end
+  def sources; @sources.values; end
 
   def load_index dir=File.join(@dir, "ferret")
     if File.exists? dir
index 95c71fb764254187523986aaa1fea2147ac8c6bd..7b0bbdfb55c52b46a8fcde38d2d5305576906e5b 100644 (file)
@@ -28,11 +28,8 @@ class LabelManager
   end
 
   def user_labels; @labels.keys; end
-
   def << t; @labels[t] = true unless @labels.member?(t) || RESERVED_LABELS.member?(t); end
-
   def delete t; @labels.delete t; end
-
   def save
     File.open(@fn, "w") { |f| f.puts @labels.keys }
   end
index e8290a6d2c27832f78608fa86095bfafa4c3394b..9e91d709101d59492248dba45184ab6a87d883e6 100644 (file)
@@ -13,11 +13,8 @@ class SSHFileError < StandardError; end
 ## straight through the mbox (an import) or we're reading a few
 ## messages at a time (viewing messages) so the latency is not a problem.
 
-## all of the methods here catch SSHFileErrors, SocketErrors, and
-## Net::SSH::Exceptions and reraise them as SourceErrors. due to this
-## and to the logging, this class is somewhat tied to Sup, but it
-## wouldn't be too difficult to remove those bits and make it more
-## general-purpose.
+## all of the methods here can throw SSHFileErrors, SocketErrors,
+## Net::SSH::Exceptions and Errno::ENOENTs.
 
 ## debugging TODO: remove me
 def debug s
@@ -92,30 +89,48 @@ class SSHFile
     @ssh_opts = ssh_opts
     @file_size = nil
     @offset = 0
+    @say_id = nil
+    @broken_msg = nil
+  end
+
+  def broken?; !@broken_msg.nil?; end
+
+  def say s
+    @say_id = BufferManager.say s, @say_id if BufferManager.instantiated?
+    Redwood::log s
+  end
+  private :say
+
+  def shutup
+    BufferManager.clear @say_id if BufferManager.instantiated?
+    @say_id = nil
   end
 
   def connect
     return if @session
+    raise SSHFileError, @broken_msg if broken?
 
-    Redwood::log "starting SSH session to #@host for #@fn..."
-    sid = BufferManager.say "Connecting to SSH host #{@host}..." if BufferManager.instantiated?
+    say "Opening SSH connection to #{@host}..."
 
     begin
-      @session = Net::SSH.start @host, @ssh_opts
-      MBox::debug "starting SSH shell..."
-      BufferManager.say "Starting SSH shell...", sid if BufferManager.instantiated?
-      @shell = @session.shell.sync
-      MBox::debug "checking for file existence..."
+      #raise SSHFileError, "simulated SSH file error"
+      #@session = Net::SSH.start @host, @ssh_opts
+      sleep 3
+      say "Starting SSH shell..."
+      # @shell = @session.shell.sync
+      sleep 3
+      say "Checking for #@fn..."
+      sleep 1
+      raise Errno::ENOENT, @fn
       raise Errno::ENOENT, @fn unless @shell.test("-e #@fn").status == 0
-      MBox::debug "SSH is ready"
-    ensure 
-      BufferManager.clear sid if BufferManager.instantiated?
+    ensure
+      shutup
     end
   end
 
-  def eof?; raise "offset #@offset size #{size}" unless @offset && size; @offset >= size; end
-  def eof; eof?; end # lame but IO does this and rmail depends on it
-  def seek loc; raise "nil" unless loc; @offset = loc; end
+  def eof?; @offset >= size; end
+  def eof; eof?; end # lame but IO's method is named this and rmail calls that
+  def seek loc; @offset = loc; end
   def tell; @offset; end
   def total; size; end
 
@@ -129,11 +144,9 @@ class SSHFile
 
   def gets
     return nil if eof?
-
     make_buf_include @offset
     expand_buf_forward while @buf.index("\n", @offset).nil? && @buf.endd < size
-
-    with(@buf[@offset .. (@buf.index("\n", @offset) || -1)]) { |line| @offset += line.length }
+    returning(@buf[@offset .. (@buf.index("\n", @offset) || -1)]) { |line| @offset += line.length }
   end
 
   def read n
@@ -152,22 +165,18 @@ private
       begin
         result = @shell.send_command cmd
         raise SSHFileError, "Failure during remote command #{cmd.inspect}: #{result.stderr[0 .. 100]}" unless result.status == 0
-
       rescue Net::SSH::Exception # these happen occasionally for no apparent reason. gotta love that nondeterminism!
         retry if (retries += 1) < 3
         raise
       end
-      result.stdout
-    rescue Net::SSH::Exception, SocketError, Errno::ENOENT => e
-      @session = nil
-      Redwood::log "error connecting to SSH server: #{e.message}"
-      raise SourceError, "error connecting to SSH server: #{e.message}"
+    rescue Net::SSH::Exception, SSHFileError, Errno::ENOENT => e
+      @broken_msg = e.message
+      raise
     end
+    result.stdout
   end
 
   def get_bytes offset, size
-    #MBox::debug "! request for [#{offset}, #{offset + size}); buf is #@buf"
-    raise "wtf: offset #{offset} size #{size}" if size == 0 || offset < 0
     do_remote "tail -c +#{offset + 1} #@fn | head -c #{size}", size
   end
 
index bfbce0e7a471daaeca453c41c9e3e8869a011f07..33aac121f7cb32788ee2df9743e3cde197ac3a40 100644 (file)
@@ -3,8 +3,13 @@ require 'net/ssh'
 module Redwood
 module MBox
 
+## this is slightly complicated because SSHFile (and thus @f or
+## @loader) can throw a variety of exceptions, and we need to catch
+## those, reraise them as SourceErrors, and set ourselves as broken.
+
 class SSHLoader < Source
   attr_reader_cloned :labels
+  attr_accessor :username, :password
 
   def initialize uri, username=nil, password=nil, start_offset=nil, usual=true, archived=false, id=nil
     raise ArgumentError, "not an mbox+ssh uri: #{uri.inspect}" unless uri =~ %r!^mbox\+ssh://!
@@ -32,22 +37,49 @@ class SSHLoader < Source
   end
 
   def host; @parsed_uri.host; end
-  def filename; @parsed_uri.path[1..-1] end ##XXXX TODO handle nil
+  def filename; @parsed_uri.path[1..-1] end
 
   def next
-    offset, labels = @loader.next
-    self.cur_offset = @loader.cur_offset  # only necessary because YAML is a PITA
-    [offset, (labels + @labels).uniq]
+    return if broken?
+    begin
+      offset, labels = @loader.next
+      self.cur_offset = @loader.cur_offset # superclass keeps @cur_offset which is used by yaml
+      [offset, (labels + @labels).uniq] # add our labels
+    rescue Net::SSH::Exception, SocketError, SSHFileError, Errno::ENOENT => e
+      recover_from e
+    end
+  end
+
+  def end_offset
+    begin
+      @f.size
+    rescue Net::SSH::Exception, SocketError, SSHFileError, Errno::ENOENT => e
+      recover_from e
+    end
   end
 
-  def end_offset; @f.size; end
   def cur_offset= o; @cur_offset = @loader.cur_offset = o; @dirty = true; end
   def id; @loader.id; end
   def id= o; @id = @loader.id = o; end
-  def cur_offset; @loader.cur_offset; end
+  # def cur_offset; @loader.cur_offset; end # think we'll be ok without this
   def to_s; @parsed_uri.to_s; end
 
-  defer_all_other_method_calls_to :loader
+  def recover_from e
+    m = "error communicating with SSH server #{host} (#{e.class.name}): #{e.message}"
+    Redwood::log m
+    self.broken_msg = @loader.broken_msg = m
+    raise SourceError, m
+  end
+
+  [:start_offset, :load_header, :load_message, :raw_header, :raw_full_message].each do |meth|
+    define_method meth do |*a|
+      begin
+        @loader.send meth, *a
+      rescue Net::SSH::Exception, SocketError, SSHFileError, Errno::ENOENT => e
+        recover_from e
+      end
+    end
+  end
 end
 
 Redwood::register_yaml(SSHLoader, %w(uri username password cur_offset usual archived id))
index 666da1e9f06bb994f3e840faa37881a65aadb4ee..efa2e73b4464e4910e5b31129eb1991801ee0b17 100644 (file)
@@ -189,12 +189,10 @@ class Message
     <<EOS
 #@snippet...
 
-***********
-** ERROR **
-***********
-
-An error occurred while loading this message. It is possible that the source
-has changed, or (in the case of remote sources) is down.
+***********************************************************************
+* An error occurred while loading this message. It is possible that   *
+* the source has changed, or (in the case of remote sources) is down. *
+***********************************************************************
 
 The error message was:
   #{msg}
index 3477575998413249ff94c007fd18dbebc1c123f1..3085607c5e31475efaa380b586eac4165e3f10a6 100644 (file)
@@ -77,6 +77,11 @@ class ScrollMode < Mode
     @botline = [@topline + buffer.content_height, lines].min
   end
 
+  def resize *a
+    super *a
+    ensure_mode_validity
+  end
+
 protected
 
   def draw_line ln, opts={}
index 052b9730ecfe0e6cac14ed8769c73fb408cc52f1..81ff75261b1f91c8f1a2d3a63c36f2a5b4c805c2 100644 (file)
@@ -48,7 +48,6 @@ class ThreadIndexMode < LineCursorMode
 
     ## TODO: don't regen text completely
     Redwood::reporting_thread do
-      Redwood::log "loading messages for thread"
       mode = ThreadViewMode.new t, @hidden_labels
       BufferManager.spawn t.subj, mode
       BufferManager.draw_screen
index 23a359131a9620ccdd6c2319c3a8f750d5fdd455..33639ebac167b690a4cc1853248d34d536b7d3fa 100644 (file)
@@ -213,8 +213,6 @@ class ThreadViewMode < LineCursorMode
         UpdateManager.relay :read, m
       end
     end
-
-    Redwood::log "releasing chunks and text from \"#{buffer.title}\""
     @messages = @chunks = @text = nil
   end
 
index 0f91cde24845a064a7493fb99415c2329188800d..0e0337ac62b4f2166122e8cd178daf24659fafcb 100644 (file)
@@ -24,7 +24,7 @@ class Source
   ## reraise them as source errors.
 
   bool_reader :usual, :archived, :dirty
-  attr_reader :cur_offset, :broken_msg
+  attr_reader :uri, :cur_offset, :broken_msg
   attr_accessor :id
 
   def initialize uri, initial_offset=nil, usual=true, archived=false, id=nil
@@ -59,6 +59,7 @@ class Source
   def is_source_for? s; to_s == s; end
 
   def each
+    return if broken?
     begin
       self.cur_offset ||= start_offset
       until done? || broken? # just like life!
@@ -66,8 +67,8 @@ class Source
         raise "no message" unless n
         yield n, labels
       end
-    rescue SourceError
-      # just die
+    rescue SourceError => e
+      self.broken_msg = e.message
     end
   end
 
@@ -80,7 +81,7 @@ protected
 
   def broken_msg= m
     @broken_msg = m
-    Redwood::log "#{to_s}: #{m}"
+#    Redwood::log "#{to_s}: #{m}"
   end
 end
 
index 0d94e14af32e4e22bf70ac22b748e1c1d6f57e32..ef2fdb1c7c7ea570939090b05793f2b1689c9841 100644 (file)
@@ -42,7 +42,7 @@ class Object
   ##
   ## i'm sure there's pithy comment i could make here about the
   ## superiority of lisp, but fuck lisp.
-  def with x; yield x; x; end
+  def returning x; yield x; x; end
 end
 
 class String