From 881bac62392d86edf46bcb0d269a7195e41ff8c1 Mon Sep 17 00:00:00 2001 From: wmorgan Date: Mon, 26 Nov 2007 20:06:02 +0000 Subject: [PATCH] many many changes. this is what happens when i have 5 hours on an airplane in the middle of the day and no book. - multi-thread dump upon crash - hook manager caches values of any proc "variables" - bugfix: broken source handling improved - speed up querying - bugfix: sources sometimes aren't added by sup-add - more widgets: terminal title, statusbar git-svn-id: svn://rubyforge.org/var/svn/sup/trunk@722 5c8cc53c-5e98-4d25-b20a-d8db53a31250 --- bin/sup | 48 ++++----- doc/TODO | 13 ++- lib/sup.rb | 14 +-- lib/sup/buffer.rb | 109 ++++++++++++++++----- lib/sup/hook.rb | 8 +- lib/sup/index.rb | 23 +++-- lib/sup/message.rb | 8 ++ lib/sup/modes/contact-list-mode.rb | 2 +- lib/sup/modes/edit-message-mode.rb | 2 +- lib/sup/modes/label-search-results-mode.rb | 4 +- lib/sup/modes/scroll-mode.rb | 1 + lib/sup/modes/thread-index-mode.rb | 6 +- lib/sup/modes/thread-view-mode.rb | 8 +- lib/sup/poll.rb | 5 +- lib/sup/source.rb | 7 +- lib/sup/suicide.rb | 2 +- lib/sup/util.rb | 17 ++-- 17 files changed, 188 insertions(+), 89 deletions(-) diff --git a/bin/sup b/bin/sup index d779ad0..d3f8c78 100644 --- a/bin/sup +++ b/bin/sup @@ -7,6 +7,7 @@ require 'fileutils' require 'trollop' require "sup" +$exceptions = [] $opts = Trollop::options do version "sup v#{Redwood::VERSION}" banner < e @@ -181,7 +182,7 @@ begin end end - imode.load_threads :num => ibuf.content_height, :when_done => lambda { reporting_thread { sleep 1; PollManager.poll } unless $opts[:no_threads] } + imode.load_threads :num => ibuf.content_height, :when_done => lambda { reporting_thread("poll after loading inbox") { sleep 1; PollManager.poll } unless $opts[:no_threads] } unless $opts[:no_threads] PollManager.start @@ -193,7 +194,7 @@ begin SearchResultsMode.spawn_from_query $opts[:search] end - until $exception || SuicideManager.die? + until $exceptions.nonempty? || SuicideManager.die? c = Ncurses.nonblocking_getch next unless c bm.erase_flash @@ -215,8 +216,8 @@ begin when :list_buffers bm.spawn_unless_exists("Buffer List") { BufferListMode.new } when :list_contacts - b = bm.spawn_unless_exists("Contact List") { ContactListMode.new } - b.mode.load_in_background if b + b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new } + b.mode.load_in_background if new when :search query = BufferManager.ask :search, "search all messages: " next unless query && query !~ /^\s*$/ @@ -234,7 +235,7 @@ begin when :compose ComposeMode.spawn_nicely when :poll - reporting_thread { PollManager.poll } + reporting_thread("user-invoked poll") { PollManager.poll } when :recall_draft case Index.num_results_for :label => :draft when 0 @@ -246,8 +247,8 @@ begin BufferManager.spawn "Edit message", r r.edit_message else - b = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] } - b.mode.load_threads :num => b.content_height if b + b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] } + b.mode.load_threads :num => b.content_height if new end when :nothing when :redraw @@ -260,7 +261,7 @@ begin bm.draw_screen end rescue Exception => e - $exception ||= e + $exceptions << [e, "main"] ensure unless $opts[:no_threads] PollManager.stop if PollManager.instantiated? @@ -276,8 +277,7 @@ ensure Redwood::log "I've been ordered to commit sepuku. I obey!" end - case $exception - when nil + if $exceptions.empty? Redwood::log "no fatal errors. good job, william." Index.save else @@ -287,27 +287,29 @@ ensure Index.unlock end -if $exception +unless $exceptions.empty? File.open("sup-exception-log.txt", "w") do |f| - f.puts "--- #{e.class.name} at #{Time.now}" - f.puts e.message, e.backtrace + $exceptions.each do |e, name| + f.puts "--- #{e.class.name} from thread: #{name}" + f.puts e.message, e.backtrace + end end $stderr.puts < e - $exception ||= e + $exceptions ||= [] + $exceptions << [e, name] raise end end @@ -120,11 +120,13 @@ module Redwood end ## not really a good place for this, so I'll just dump it here. + ## + ## a source error is either a FatalSourceError or an OutOfSyncSourceError. + ## the superclass SourceError is just a generic. def report_broken_sources opts={} return unless BufferManager.instantiated? - broken_sources = Index.usual_sources.select { |s| s.error.is_a? FatalSourceError } - File.open("goat", "w") { |f| f.puts Kernel.caller } + broken_sources = Index.sources.select { |s| s.error.is_a? FatalSourceError } unless broken_sources.empty? BufferManager.spawn_unless_exists("Broken source notification for #{broken_sources.join(',')}", opts) do TextMode.new(< :status_color + def draw_status status + write @height - 1, 0, status, :color => :status_color end def focus @@ -138,6 +141,30 @@ class BufferManager ## are canceled by any keypress except this one. CONTINUE_IN_BUFFER_SEARCH_KEY = "n" + HookManager.register "status-bar-text", < false + draw_screen :sync => false, :status => status, :title => title end end def draw_screen opts={} return if @shelled + status, title = + if opts.member? :status + [opts[:status], opts[:title]] + else + get_status_and_title(@focus_buf) # must be called outside of the ncurses lock + end + + print "\033]2;#{title}\07" if title + Ncurses.mutex.lock unless opts[:sync] == false ## disabling this for the time being, to help with debugging @@ -234,7 +272,7 @@ class BufferManager false && @buffers.inject(@dirty) do |dirty, buf| buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols #dirty ? buf.draw : buf.redraw - buf.draw + buf.draw status dirty end @@ -242,7 +280,7 @@ class BufferManager if true buf = @buffers.last buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols - @dirty ? buf.draw : buf.redraw + @dirty ? buf.draw(status) : buf.redraw(status) end draw_minibuf :sync => false unless opts[:skip_minibuf] @@ -253,18 +291,21 @@ class BufferManager Ncurses.mutex.unlock unless opts[:sync] == false end - ## gets the mode from the block, which is only called if the buffer - ## doesn't already exist. this is useful in the case that generating - ## the mode is expensive, as it often is. + ## if the named buffer already exists, pops it to the front without + ## calling the block. otherwise, gets the mode from the block and + ## creates a new buffer. returns two things: the buffer, and a boolean + ## indicating whether it's a new buffer or not. def spawn_unless_exists title, opts={} - if @name_map.member? title - raise_to_front @name_map[title] unless opts[:hidden] - nil - else - mode = yield - spawn title, mode, opts - @name_map[title] - end + new = + if @name_map.member? title + raise_to_front @name_map[title] unless opts[:hidden] + false + else + mode = yield + spawn title, mode, opts + true + end + [@name_map[title], new] end def spawn title, mode, opts={} @@ -647,6 +688,30 @@ class BufferManager end private + def default_status_bar buf + " [#{buf.mode.name}] #{buf.title} #{buf.mode.status}" + end + + def default_terminal_title buf + "Sup #{Redwood::VERSION} :: #{buf.title}" + end + + def get_status_and_title buf + opts = { + :num_inbox => lambda { Index.num_results_for :label => :inbox }, + :num_inbox_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] }, + :num_total => lambda { Index.size }, + :num_spam => lambda { Index.num_results_for :label => :spam }, + :title => buf.title, + :mode => buf.mode.name, + :status => buf.mode.status + } + + statusbar_text = HookManager.run("status-bar-text", opts) || default_status_bar(buf) + term_title_text = HookManager.run("terminal-title-text", opts) || default_terminal_title(buf) + + [statusbar_text, term_title_text] + end def users unless @users diff --git a/lib/sup/hook.rb b/lib/sup/hook.rb index 327ce2d..98fcf57 100644 --- a/lib/sup/hook.rb +++ b/lib/sup/hook.rb @@ -9,7 +9,7 @@ class HookManager ## ## i don't bother providing setters, since i'm pretty sure the ## charade will fall apart pretty quickly with respect to scoping. - ## this is basically fail-fast. + ## "fail-fast", we'll call it. class HookContext def initialize name @__say_id = nil @@ -22,7 +22,7 @@ class HookManager def method_missing m, *a case @__locals[m] when Proc - @__locals[m].call(*a) + @__locals[m] = @__locals[m].call(*a) # only call the proc once when nil super else @@ -85,8 +85,8 @@ class HookManager rescue Exception => e log "error running hook: #{e.message}" log e.backtrace.join("\n") - BufferManager.flash "Error running hook: #{e.message}" @hooks[name] = nil # disable it + BufferManager.flash "Error running hook: #{e.message}" end context.__cleanup result @@ -112,6 +112,8 @@ EOS end end + def enabled? name; !hook_for(name).nil? end + private def hook_for name diff --git a/lib/sup/index.rb b/lib/sup/index.rb index 3678295..c2224bc 100644 --- a/lib/sup/index.rb +++ b/lib/sup/index.rb @@ -6,7 +6,7 @@ begin require 'chronic' $have_chronic = true rescue LoadError => e - Redwood::log "'chronic' library not found. run 'gem install chronic' to install." + Redwood::log "optional 'chronic' library not found (run 'gem install chronic' to install)" $have_chronic = false end @@ -53,7 +53,7 @@ class Index end def start_lock_update_thread - @lock_update_thread = Redwood::reporting_thread do + @lock_update_thread = Redwood::reporting_thread("lock update") do while true sleep 30 @lock.touch_yourself @@ -119,8 +119,8 @@ EOS def add_source source raise "duplicate source!" if @sources.include? source @sources_dirty = true - source.id ||= @sources.size - ##TODO: why was this necessary? + max = @sources.max_of { |id, s| s.is_a?(DraftLoader) || s.is_a?(SentLoader) ? 0 : id } + source.id ||= (max || 0) + 1 ##source.id += 1 while @sources.member? source.id @sources[source.id] = source end @@ -263,12 +263,14 @@ EOS end until pending.empty? || (opts[:limit] && messages.size >= opts[:limit]) - id = pending.pop - next if searched.member? id - searched[id] = true q = Ferret::Search::BooleanQuery.new true - q.add_query Ferret::Search::TermQuery.new(:message_id, id), :should - q.add_query Ferret::Search::TermQuery.new(:refs, id), :should + + pending.each do |id| + searched[id] = true + q.add_query Ferret::Search::TermQuery.new(:message_id, id), :should + q.add_query Ferret::Search::TermQuery.new(:refs, id), :should + end + pending = [] q = build_query :qobj => q @@ -285,10 +287,11 @@ EOS #Redwood::log "got #{mid} as a child of #{id}" messages[mid] ||= lambda { build_message docid } refs = @index[docid][:refs].split(" ") - pending += refs + pending += refs.select { |id| !searched[id] } end end end + if killed Redwood::log "thread for #{m.id} is killed, ignoring" false diff --git a/lib/sup/message.rb b/lib/sup/message.rb index 0ef1a61..8ec8a67 100644 --- a/lib/sup/message.rb +++ b/lib/sup/message.rb @@ -13,6 +13,10 @@ class MessageFormatError < StandardError; end ## 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 cathces all source exceptions. if the underlying source throws +## an error, it is caught and handled. + class Message SNIPPET_LEN = 80 RE_PATTERN = /^((re|re[\[\(]\d[\]\)]):\s*)+/i @@ -169,6 +173,7 @@ class Message Redwood::log "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))] end @@ -194,11 +199,14 @@ The error message was: EOS end + ## wrap any source methods that might throw sourceerrors def with_source_errors_handled begin yield rescue SourceError => e Redwood::log "problem getting messages from #{@source}: #{e.message}" + @source.error ||= e + Redwood::report_broken_sources :force_to_top => true error_message e.message end end diff --git a/lib/sup/modes/contact-list-mode.rb b/lib/sup/modes/contact-list-mode.rb index 036369e..399d48e 100644 --- a/lib/sup/modes/contact-list-mode.rb +++ b/lib/sup/modes/contact-list-mode.rb @@ -94,7 +94,7 @@ class ContactListMode < LineCursorMode end def load_in_background - Redwood::reporting_thread do + Redwood::reporting_thread("contact manager load in bg") do load update BufferManager.draw_screen diff --git a/lib/sup/modes/edit-message-mode.rb b/lib/sup/modes/edit-message-mode.rb index 64ca73d..e04fc30 100644 --- a/lib/sup/modes/edit-message-mode.rb +++ b/lib/sup/modes/edit-message-mode.rb @@ -13,7 +13,7 @@ class EditMessageMode < LineCursorMode NON_EDITABLE_HEADERS = %w(Message-Id Date) HookManager.register "signature", < b.content_height if b + b, new = BufferManager.spawn_unless_exists("All threads with label '#{label}'") { LabelSearchResultsMode.new [label] } + b.mode.load_threads :num => b.content_height if new end end end diff --git a/lib/sup/modes/scroll-mode.rb b/lib/sup/modes/scroll-mode.rb index 89684bf..328a07d 100644 --- a/lib/sup/modes/scroll-mode.rb +++ b/lib/sup/modes/scroll-mode.rb @@ -35,6 +35,7 @@ class ScrollMode < Mode @twiddles = opts.member?(:twiddles) ? opts[:twiddles] : true @search_query = nil @search_line = nil + @status = "" super() end diff --git a/lib/sup/modes/thread-index-mode.rb b/lib/sup/modes/thread-index-mode.rb index 1f61f20..8dec42d 100644 --- a/lib/sup/modes/thread-index-mode.rb +++ b/lib/sup/modes/thread-index-mode.rb @@ -65,7 +65,6 @@ EOS def lines; @text.length; end def [] i; @text[i]; end - #def contains_thread? t; !@lines[t].nil?; end def contains_thread? t; @threads.include?(t) end def reload @@ -78,8 +77,7 @@ EOS def select t=nil t ||= cursor_thread or return - ## TODO: don't regen text completely - Redwood::reporting_thread do + Redwood::reporting_thread("load messages for thread-view-mode") do num = t.size message = "Loading #{num.pluralize 'message body'}..." BufferManager.say(message) do |sid| @@ -402,7 +400,7 @@ EOS def load_n_threads_background n=LOAD_MORE_THREAD_NUM, opts={} return if @load_thread # todo: wrap in mutex - @load_thread = Redwood::reporting_thread do + @load_thread = Redwood::reporting_thread("load threads for thread-index-mode") do num = load_n_threads n, opts opts[:when_done].call(num) if opts[:when_done] @load_thread = nil diff --git a/lib/sup/modes/thread-view-mode.rb b/lib/sup/modes/thread-view-mode.rb index ae30423..8755709 100644 --- a/lib/sup/modes/thread-view-mode.rb +++ b/lib/sup/modes/thread-view-mode.rb @@ -119,7 +119,7 @@ class ThreadViewMode < LineCursorMode def unsubscribe_from_list m = @message_lines[curpos] or return if m.list_unsubscribe && m.list_unsubscribe =~ // - spawn_compose_mode :from => AccountManager.account_for(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3 + ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3 else BufferManager.flash "Can't find List-Unsubscribe header for this message." end @@ -127,7 +127,7 @@ class ThreadViewMode < LineCursorMode def forward m = @message_lines[curpos] or return - spawn_forward_mode m + ForwardMode.spawn_nicely m end include CanAliasContacts @@ -147,9 +147,9 @@ class ThreadViewMode < LineCursorMode def compose p = @person_lines[curpos] if p - spawn_compose_mode :to => [p] + ComposeMode.spawn_nicely :to => [p] else - spawn_compose_mode + ComposeMode.spawn_nicely end end diff --git a/lib/sup/poll.rb b/lib/sup/poll.rb index 2faadfe..387f06e 100644 --- a/lib/sup/poll.rb +++ b/lib/sup/poll.rb @@ -39,7 +39,8 @@ EOS end def buffer - @buffer ||= BufferManager.spawn_unless_exists("", :hidden => true) { PollMode.new } + b, new = BufferManager.spawn_unless_exists("", :hidden => true) { PollMode.new } + b end def poll @@ -62,7 +63,7 @@ EOS end def start - @thread = Redwood::reporting_thread do + @thread = Redwood::reporting_thread("periodic poll") do while true sleep DELAY / 2 poll if @last_poll.nil? || (Time.now - @last_poll) >= DELAY diff --git a/lib/sup/source.rb b/lib/sup/source.rb index 50fc163..6510aae 100644 --- a/lib/sup/source.rb +++ b/lib/sup/source.rb @@ -1,6 +1,11 @@ module Redwood -class SourceError < StandardError; end +class SourceError < StandardError + def initialize *a + raise "don't instantiate me!" if SourceError.is_a?(self.class) + super + end +end class OutOfSyncSourceError < SourceError; end class FatalSourceError < SourceError; end diff --git a/lib/sup/suicide.rb b/lib/sup/suicide.rb index 7d2738c..98b4346 100644 --- a/lib/sup/suicide.rb +++ b/lib/sup/suicide.rb @@ -16,7 +16,7 @@ class SuicideManager bool_reader :die def start - @thread = Redwood::reporting_thread do + @thread = Redwood::reporting_thread("suicide watch") do while true sleep DELAY if File.exists? @fn diff --git a/lib/sup/util.rb b/lib/sup/util.rb index b2a0a4d..3ffb92b 100644 --- a/lib/sup/util.rb +++ b/lib/sup/util.rb @@ -399,6 +399,7 @@ class Array def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end def last= e; self[-1] = e end + def nonempty?; !empty? end end class Time @@ -505,18 +506,18 @@ module Singleton end end -## wraps an object. if it throws an exception, keeps a copy, and -## rethrows it for any further method calls. +## wraps an object. if it throws an exception, keeps a copy. class Recoverable def initialize o @o = o - @e = nil + @error = nil @mutex = Mutex.new end - def clear_error!; @e = nil; end - def has_errors?; !@e.nil?; end - def error; @e; 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 @@ -531,8 +532,8 @@ class Recoverable begin @o.send(m, *a, &b) rescue Exception => e - @e ||= e - raise e + @error ||= e + raise end end end -- 2.45.2