]> git.cworth.org Git - sup/commitdiff
better locking
authorwmorgan <wmorgan@5c8cc53c-5e98-4d25-b20a-d8db53a31250>
Mon, 4 Jun 2007 04:32:29 +0000 (04:32 +0000)
committerwmorgan <wmorgan@5c8cc53c-5e98-4d25-b20a-d8db53a31250>
Mon, 4 Jun 2007 04:32:29 +0000 (04:32 +0000)
git-svn-id: svn://rubyforge.org/var/svn/sup/trunk@431 5c8cc53c-5e98-4d25-b20a-d8db53a31250

bin/sup
bin/sup-add
bin/sup-sync
lib/sup.rb
lib/sup/buffer.rb
lib/sup/index.rb
lib/sup/suicide.rb
lib/sup/util.rb

diff --git a/bin/sup b/bin/sup
index ab835e7ba4edcdec548cf43cef117ddafb6fe493..d16ae802796eeff3777d02d3325b9fca57491fb1 100644 (file)
--- a/bin/sup
+++ b/bin/sup
@@ -55,17 +55,17 @@ def stop_cursing
 end
 module_function :start_cursing, :stop_cursing
 
+Index.new
 begin
-  Redwood::lock
-rescue LockError => e
+  Index.lock
+rescue Index::LockError => e
   require 'highline'
+
   h = HighLine.new
-  h.say <<EOS
-Error: sup is already running! User #{e.user} on host #{e.host} was running sup
-with pid #{e.pid} as of #{e.time}.
-EOS
+  h.wrap_at = :auto
+  h.say Index.fancy_lock_error_message_for(e)
 
-  case h.ask("Should I try and kill that process? ")
+  case h.ask("Should I ask that the process kill itself? ")
   when /^\s*y\s*$/i
     h.say "Ok, suggesting sepuku..."
     FileUtils.touch Redwood::SUICIDE_FN
@@ -74,13 +74,16 @@ EOS
     h.say "Let's try that again."
     retry
   else
-    h.say "Ok, see you later."
+    h.say <<EOS
+Ok, giving up. If the process crashed and left a stale lockfile, you
+can fix this by manually deleting #{Index.lockfile}.
+EOS
     exit
   end
 end
 
 Redwood::start
-Index.new.load
+Index.load
 
 if(s = Index.source_for DraftManager.source_name)
   DraftManager.source = s
@@ -168,83 +171,82 @@ begin
   unless $opts[:no_threads]
     PollManager.start_thread
     SuicideManager.start_thread
+    Index.start_lock_update_thread
   end
 
-  until $exception
-    bm.draw_screen
+  until $exception || SuicideManager.die?
     c = Ncurses.nonblocking_getch
-    bm.erase_flash
-
-    if c == Ncurses::KEY_RESIZE
-      bm.handle_resize
-    elsif c
-      unless bm.handle_input(c)
-        x = global_keymap.action_for c
-        case x
-        when :quit
-          break if bm.kill_all_buffers_safely
-        when :help
-          curmode = bm.focus_buf.mode
-          bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
-        when :roll_buffers
-          bm.roll_buffers
-        when :roll_buffers_backwards
-          bm.roll_buffers_backwards
-        when :kill_buffer
-          bm.kill_buffer_safely bm.focus_buf
-        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
-        when :search
-          text = bm.ask :search, "query: "
-          next unless text && text !~ /^\s*$/
-
-          begin
-            qobj = Index.parse_user_query_string text
-            short_text = text.length < 20 ? text : text[0 ... 20] + "..."
-            log "built query from #{text.inspect}: #{qobj}"
-            mode = SearchResultsMode.new qobj
-            bm.spawn "search: \"#{short_text}\"", mode
-            mode.load_threads :num => mode.buffer.content_height
-          rescue Ferret::QueryParser::QueryParseException => e
-            bm.flash "Couldn't parse query."
-          end
-        when :list_labels
-          b = bm.spawn_unless_exists("Label list") { LabelListMode.new }
-          b.mode.load_in_background
-        when :compose
-          mode = ComposeMode.new
-          bm.spawn "New Message", mode
-          mode.edit
-        when :poll
-#          bm.raise_to_front PollManager.buffer
-          reporting_thread { PollManager.poll }
-        when :recall_draft
-          case Index.num_results_for :label => :draft
-          when 0
-            bm.flash "No draft messages."
-          when 1
-            m = nil
-            Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call }
-            r = ResumeMode.new(m)
-            BufferManager.spawn "Edit message", r
-            r.edit
-          else
-            b = BufferManager.spawn_unless_exists("All drafts") do
-              mode = LabelSearchResultsMode.new [:draft]
-            end
-            b.mode.load_threads :num => b.content_height
-          end
-        when :nothing
-        when :redraw
-          bm.completely_redraw_screen
+    next unless c
+
+    unless bm.handle_input(c)
+      x = global_keymap.action_for c
+      case x
+      when :quit
+        break if bm.kill_all_buffers_safely
+      when :help
+        curmode = bm.focus_buf.mode
+        bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
+      when :roll_buffers
+        bm.roll_buffers
+      when :roll_buffers_backwards
+        bm.roll_buffers_backwards
+      when :kill_buffer
+        bm.kill_buffer_safely bm.focus_buf
+      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
+      when :search
+        text = bm.ask :search, "query: "
+        next unless text && text !~ /^\s*$/
+
+        begin
+          qobj = Index.parse_user_query_string text
+          short_text = text.length < 20 ? text : text[0 ... 20] + "..."
+          log "built query from #{text.inspect}: #{qobj}"
+          mode = SearchResultsMode.new qobj
+          bm.spawn "search: \"#{short_text}\"", mode
+          mode.load_threads :num => mode.buffer.content_height
+        rescue Ferret::QueryParser::QueryParseException => e
+          bm.flash "Couldn't parse query."
+        end
+      when :list_labels
+        b = bm.spawn_unless_exists("Label list") { LabelListMode.new }
+        b.mode.load_in_background
+      when :compose
+        mode = ComposeMode.new
+        bm.spawn "New Message", mode
+        mode.edit
+      when :poll
+        #          bm.raise_to_front PollManager.buffer
+        reporting_thread { PollManager.poll }
+      when :recall_draft
+        case Index.num_results_for :label => :draft
+        when 0
+          bm.flash "No draft messages."
+        when 1
+          m = nil
+          Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call }
+          r = ResumeMode.new(m)
+          BufferManager.spawn "Edit message", r
+          r.edit
         else
-          bm.flash "Unknown key press '#{c.to_character}' for #{bm.focus_buf.mode.name}."
+          b = BufferManager.spawn_unless_exists("All drafts") do
+            mode = LabelSearchResultsMode.new [:draft]
+          end
+          b.mode.load_threads :num => b.content_height
         end
+      when :nothing
+      when :redraw
+        bm.completely_redraw_screen
+      else
+        bm.flash "Unknown key press '#{c.to_character}' for #{bm.focus_buf.mode.name}."
       end
     end
+
+    bm.draw_screen
+    bm.erase_flash
   end
 rescue Exception => e
   $exception ||= e
@@ -252,10 +254,11 @@ ensure
   Redwood::finish
   stop_cursing
 
-  case $exception
-  when SuicideException
+  if SuicideManager.die?
     Redwood::log "I've been asked to commit sepuku. I obey!"
-    exit
+  end
+
+  case $exception
   when nil
     Redwood::log "good night, sweet prince!"
     Index.save
@@ -263,7 +266,7 @@ ensure
     Redwood::log "oh crap, an exception"
   end
 
-  Redwood::unlock
+  Index.unlock
 end
 
 if $exception 
index f0def12a678ca5f12ddc115eddcd2063807ed057..78df02220dfa705bf59077daadc8f33197fa77c6 100644 (file)
@@ -78,39 +78,45 @@ end
 $terminal.wrap_at = :auto
 Redwood::start
 index = Redwood::Index.new
-index.load
 
-ARGV.each do |uri|
-  labels = $opts[:labels] ? $opts[:labels].split(/\s*,\s*/).uniq : []
+index.lock_or_die
 
-  if !$opts[:force_new] && index.source_for(uri) 
-    say "Already know about #{uri}; skipping."
-    next
-  end
+begin
+  index.load_sources
+
+  ARGV.each do |uri|
+    labels = $opts[:labels] ? $opts[:labels].split(/\s*,\s*/).uniq : []
 
-  parsed_uri = URI(uri)
-  Trollop::die "no path component to uri: #{parsed_uri}" unless parsed_uri.path
-
-  source = 
-    case parsed_uri.scheme
-    when "mbox+ssh"
-      say "For SSH connections, if you will use public key authentication, you may leave the username and password blank."
-      say ""
-      username, password = get_login_info uri, index.sources
-      Redwood::MBox::SSHLoader.new uri, username, password, nil, !$opts[:unusual], $opts[:archive], nil, labels
-    when "imap", "imaps"
-      username, password = get_login_info uri, index.sources
-      Redwood::IMAP.new uri, username, password, nil, !$opts[:unusual], $opts[:archive], nil, labels
-    when "maildir"
-      Redwood::Maildir.new uri, nil, !$opts[:unusual], $opts[:archive], nil, labels
-    when "mbox"
-      Redwood::MBox::Loader.new uri, nil, !$opts[:unusual], $opts[:archive], nil, labels
-    else
-      Trollop::die "Unknown source type #{parsed_uri.scheme.inspect}"      
+    if !$opts[:force_new] && index.source_for(uri) 
+      say "Already know about #{uri}; skipping."
+      next
     end
-  say "Adding #{source}..."
-  index.add_source source
-end
 
-index.save
-Redwood::finish
+    parsed_uri = URI(uri)
+    Trollop::die "no path component to uri: #{parsed_uri}" unless parsed_uri.path
+
+    source = 
+      case parsed_uri.scheme
+      when "mbox+ssh"
+        say "For SSH connections, if you will use public key authentication, you may leave the username and password blank."
+        say ""
+        username, password = get_login_info uri, index.sources
+        Redwood::MBox::SSHLoader.new uri, username, password, nil, !$opts[:unusual], $opts[:archive], nil, labels
+      when "imap", "imaps"
+        username, password = get_login_info uri, index.sources
+        Redwood::IMAP.new uri, username, password, nil, !$opts[:unusual], $opts[:archive], nil, labels
+      when "maildir"
+        Redwood::Maildir.new uri, nil, !$opts[:unusual], $opts[:archive], nil, labels
+      when "mbox"
+        Redwood::MBox::Loader.new uri, nil, !$opts[:unusual], $opts[:archive], nil, labels
+      else
+        Trollop::die "Unknown source type #{parsed_uri.scheme.inspect}"      
+      end
+    say "Adding #{source}..."
+    index.add_source source
+  end
+ensure
+  index.save
+  index.unlock
+  Redwood::finish
+end
index 6c28a761442947dda2150a284beaf5102f03b1b7..371c6957cbb4a0ae8577699a465855de0a386e38 100644 (file)
@@ -89,7 +89,6 @@ op = [:asis, :restore, :discard].find { |x| opts[x] } || :asis
 
 Redwood::start
 index = Redwood::Index.new
-index.load
 
 restored_state =
   if opts[:restore]
@@ -106,15 +105,18 @@ restored_state =
     {}
   end
 
-sources = ARGV.map do |uri|
-  index.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
-end
-
-sources = index.usual_sources if sources.empty?
-sources = index.sources if opts[:all_sources]
-
 seen = {}
+index.lock_or_die
 begin
+  index.load
+
+  sources = ARGV.map do |uri|
+    index.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
+  end
+  
+  sources = index.usual_sources if sources.empty?
+  sources = index.sources if opts[:all_sources]
+
   unless target == :new
     if opts[:start_at]
       sources.each { |s| s.seek_to! opts[:start_at] }
@@ -192,6 +194,41 @@ begin
     $stderr.puts "Scanned #{num_scanned}, added #{num_added}, updated #{num_updated} messages from #{source}."
     $stderr.puts "Restored state on #{num_restored} (#{100.0 * num_restored / num_scanned}%) messages." if num_restored > 0
   end
+
+  ## delete any messages in the index that claim they're from one of
+  ## these sources, but that we didn't see.
+  ##
+  ## kinda crappy code here, because we delve directly into the Ferret
+  ## API.
+  ##
+  ## TODO: move this to Index, i suppose.
+
+
+  if target == :all || target == :changed
+    $stderr.puts "Deleting missing messages from the index..."
+    num_del, num_scanned = 0, 0
+    sources.each do |source|
+      raise "no source id for #{source}" unless source.id
+      q = "+source_id:#{source.id}"
+      q += " +source_info: >= #{opts[:start_at]}" if opts[:start_at]
+      index.index.search_each(q, :limit => :all) do |docid, score|
+        num_scanned += 1
+        mid = index.index[docid][:message_id]
+        unless seen[mid]
+          puts "Deleting #{mid}" if opts[:verbose]
+          index.index.delete docid unless opts[:dry_run]
+          num_del += 1
+        end
+      end
+    end
+    $stderr.puts "Deleted #{num_del} / #{num_scanned} messages"
+  end
+
+  if opts[:optimize]
+    $stderr.puts "Optimizing index..."
+    optt = time { index.index.optimize unless opts[:dry_run] }
+    $stderr.puts "Optimized index of size #{index.size} in #{optt}s."
+  end
 rescue Redwood::FatalSourceError => e
   $stderr.puts "Sorry, I couldn't communicate with a source: #{e.message}"
 rescue Exception => e
@@ -200,37 +237,5 @@ rescue Exception => e
 ensure
   index.save
   Redwood::finish
-end
-
-## delete any messages in the index that claim they're from one of
-## these sources, but that we didn't see.
-##
-## kinda crappy code here, because we delve directly into the Ferret
-## API.
-##
-## TODO: move this to Index, i suppose.
-if target == :all || target == :changed
-  $stderr.puts "Deleting missing messages from the index..."
-  num_del, num_scanned = 0, 0
-  sources.each do |source|
-    raise "no source id for #{source}" unless source.id
-    q = "+source_id:#{source.id}"
-    q += " +source_info: >= #{opts[:start_at]}" if opts[:start_at]
-    index.index.search_each(q, :limit => :all) do |docid, score|
-      num_scanned += 1
-      mid = index.index[docid][:message_id]
-      unless seen[mid]
-        puts "Deleting #{mid}" if opts[:verbose]
-        index.index.delete docid unless opts[:dry_run]
-        num_del += 1
-      end
-    end
-  end
-  $stderr.puts "Deleted #{num_del} / #{num_scanned} messages"
-end
-
-if opts[:optimize]
-  $stderr.puts "Optimizing index..."
-  optt = time { index.index.optimize unless opts[:dry_run] }
-  $stderr.puts "Optimized index of size #{index.size} in #{optt}s."
+  index.unlock
 end
index ff11dcc2ac8065af37c856a6c101f299a81f4038..a6c4994f5ec38c2f3bb170c5cf76efb785677345 100644 (file)
@@ -3,29 +3,6 @@ require 'yaml'
 require 'zlib'
 require 'thread'
 require 'fileutils'
-require 'lockfile'
-
-## time for some monkeypatching!
-class Lockfile
-  def gen_lock_id
-    Hash[
-         'host' => "#{ Socket.gethostname }",
-         'pid' => "#{ Process.pid }",
-         'ppid' => "#{ Process.ppid }",
-         'time' => timestamp,
-         'user' => ENV["USER"]
-        ]
-  end
-
-  def dump_lock_id lock_id = @lock_id
-      "host: %s\npid: %s\nppid: %s\ntime: %s\nuser: %s\n" %
-        lock_id.values_at('host','pid','ppid','time','user')
-    end
-
-  def lockinfo_on_disk
-    load_lock_id IO.read(path)
-  end
-end
 
 class Object
   ## this is for debugging purposes because i keep calling #id on the
@@ -35,15 +12,6 @@ class Object
   end
 end
 
-class LockError < StandardError
-  def initialize h
-    super ""
-    @h = h
-  end
-
-  def method_missing m; @h[m.to_s] end
-end
-
 class Module
   def yaml_properties *props
     props = props.map { |p| p.to_s }
@@ -92,7 +60,7 @@ module Redwood
           File.open("sup-exception-log.txt", "w") do |f|
             f.puts "--- #{e.class.name} at #{Time.now}"
             f.puts e.message, e.backtrace
-          end unless e.is_a? SuicideException
+          end
           $exception ||= e
           raise
         end
@@ -139,23 +107,6 @@ module Redwood
     Redwood::BufferManager.deinstantiate!
   end
 
-  def lock
-    FileUtils.rm_f SUICIDE_FN
-
-    Redwood::log "locking #{LOCK_FN}..."
-    $lock = Lockfile.new LOCK_FN, :retries => 0
-      begin
-        $lock.lock
-      rescue Lockfile::MaxTriesLockError
-        raise LockError, $lock.lockinfo_on_disk
-      end
-  end
-
-  def unlock
-    Redwood::log "unlocking #{LOCK_FN}..."
-    $lock.unlock if $lock
-  end
-
   ## not really a good place for this, so I'll just dump it here.
   def report_broken_sources opts={}
     return unless BufferManager.instantiated?
@@ -199,7 +150,7 @@ EOM
   end
 
   module_function :save_yaml_obj, :load_yaml_obj, :start, :finish,
-                  :lock, :unlock, :report_broken_sources
+                  :report_broken_sources
 end
 
 ## set up default configuration file
index 80eb18e3a22191256c5531f58cbe90cd6cce6abb..1bd03321a0ded5740882630e7c3dd718fa042157 100644 (file)
@@ -16,24 +16,10 @@ module Ncurses
   def mutex; @mutex ||= Mutex.new; end
   def sync &b; mutex.synchronize(&b); end
 
-  ## aaahhh, user input. who would have though that such a simple
-  ## idea would be SO FUCKING COMPLICATED?! because apparently
-  ## Ncurses.getch (and Curses.getch), even in cbreak mode, BLOCKS
-  ## ALL THREAD ACTIVITY. as in, no threads anywhere will run while
-  ## it's waiting for input. ok, fine, so we wrap it in a select. Of
-  ## course we also rely on Ncurses.getch to tell us when an xterm
-  ## resize has occurred, which select won't catch, so we won't
-  ## resize outselves after a sigwinch until the user hits a key.
-  ## and installing our own sigwinch handler means that the screen
-  ## size returned by getmaxyx() DOESN'T UPDATE! and Kernel#trap
-  ## RETURNS NIL as the previous handler! 
-  ##
-  ## so basically, resizing with multi-threaded ruby Ncurses
-  ## applications will always be broken.
-  ##
-  ## i've coined a new word for this: lametarded.
+  ## magically, this stuff seems to work now. i could swear it didn't
+  ## before. hm.
   def nonblocking_getch
-    if IO.select([$stdin], nil, nil, nil)
+    if IO.select([$stdin], nil, nil, 1)
       Ncurses.getch
     else
       nil
@@ -210,14 +196,6 @@ class BufferManager
     end
   end
 
-  def handle_resize
-    return if @shelled
-    rows, cols = Ncurses.rows, Ncurses.cols
-    @buffers.each { |b| b.resize rows - minibuf_lines, cols }
-    completely_redraw_screen
-    flash "Resized to #{rows}x#{cols}"
-  end
-
   def draw_screen opts={}
     return if @shelled
 
index 56429d37962638cceea33c102158d91fb2f2ade8..fe7297d3d88a2696dfaaaf68de13916921e520ff 100644 (file)
@@ -7,6 +7,14 @@ require 'ferret'
 module Redwood
 
 class Index
+  class LockError < StandardError
+    def initialize h
+      @h = h
+    end
+
+    def method_missing m; @h[m.to_s] end
+  end
+
   include Singleton
 
   attr_reader :index
@@ -21,10 +29,58 @@ class Index
     @analyzer[:body] = sa
     @analyzer[:subject] = sa
     @qparser ||= Ferret::QueryParser.new :default_field => :body, :analyzer => @analyzer
+    @lock = Lockfile.new lockfile, :retries => 0
 
     self.class.i_am_the_instance self
   end
 
+  def lockfile; File.join @dir, "lock" end
+
+  def lock
+    Redwood::log "locking #{lockfile}..."
+    begin
+      @lock.lock
+    rescue Lockfile::MaxTriesLockError
+      raise LockError, @lock.lockinfo_on_disk
+    end
+  end
+
+  def start_lock_update_thread
+    Redwood::reporting_thread do
+      sleep 30
+      @lock.touch_yourself
+    end
+  end
+
+  def fancy_lock_error_message_for e
+    mins = (Time.now - e.mtime).to_i / 60
+
+    <<EOS
+Error: the sup index is locked by another process! User '#{e.user}' on
+host '#{e.host}' is running #{e.pname} with pid #{e.pid} (at least, as of #{mins}
+minutes ago).
+
+Wait for the process to finish, or, if the lockfile is stale, delete it
+manually.
+EOS
+  end
+
+  def lock_or_die
+    begin
+      lock
+    rescue LockError => e
+      $stderr.puts fancy_lock_error_message_for(e)
+      exit
+    end
+  end
+
+  def unlock
+    if @lock && @lock.locked?
+      Redwood::log "unlocking #{lockfile}..."
+      @lock.unlock
+    end
+  end
+
   def load
     load_sources
     load_index
index 6c3141e7cea1fbdaa875fdad471d9a66e4e887fc..5085c163c1f4f5a5005b6a87ae747d9afcd4626a 100644 (file)
@@ -1,8 +1,5 @@
-require 'fileutils'
 module Redwood
 
-class SuicideException < StandardError; end
-
 class SuicideManager
   include Singleton
 
@@ -10,16 +7,20 @@ class SuicideManager
 
   def initialize fn
     @fn = fn
+    @die = false
     self.class.i_am_the_instance self
+    FileUtils.rm_f @fn
   end
 
+  bool_reader :die
+
   def start_thread
     Redwood::reporting_thread do
       while true
         sleep DELAY
         if File.exists? @fn
-          FileUtils.rm_rf @fn
-          raise SuicideException
+          FileUtils.rm_f @fn
+          @die = true
         end
       end
     end
index 77c2cc2edb274af57f4afa78b4d1f87bc00a4a3a..582e26f0786f07bb8df43e708aded46037dcf178 100644 (file)
@@ -1,3 +1,34 @@
+require 'lockfile'
+
+## time for some monkeypatching!
+class Lockfile
+  def gen_lock_id
+    Hash[
+         'host' => "#{ Socket.gethostname }",
+         'pid' => "#{ Process.pid }",
+         'ppid' => "#{ Process.ppid }",
+         'time' => timestamp,
+         'pname' => $0,
+         'user' => ENV["USER"]
+        ]
+  end
+
+  def dump_lock_id lock_id = @lock_id
+      "host: %s\npid: %s\nppid: %s\ntime: %s\nuser: %s\npname: %s\n" %
+        lock_id.values_at('host','pid','ppid','time','user', 'pname')
+    end
+
+  def lockinfo_on_disk
+    h = load_lock_id IO.read(path)
+    h['mtime'] = File.stat(path).mtime
+    h
+  end
+
+  def touch_yourself
+    touch path
+  end
+end
+
 class Range
   ## only valid for integer ranges (unless I guess it's exclusive)
   def size