From 2c0bc997ecf7faa8aad3aa740624aa50473dc2c4 Mon Sep 17 00:00:00 2001 From: wmorgan Date: Mon, 4 Jun 2007 04:32:29 +0000 Subject: [PATCH] better locking git-svn-id: svn://rubyforge.org/var/svn/sup/trunk@431 5c8cc53c-5e98-4d25-b20a-d8db53a31250 --- bin/sup | 169 +++++++++++++++++++++++---------------------- bin/sup-add | 68 +++++++++--------- bin/sup-sync | 87 ++++++++++++----------- lib/sup.rb | 53 +------------- lib/sup/buffer.rb | 28 +------- lib/sup/index.rb | 56 +++++++++++++++ lib/sup/suicide.rb | 11 +-- lib/sup/util.rb | 31 +++++++++ 8 files changed, 267 insertions(+), 236 deletions(-) diff --git a/bin/sup b/bin/sup index ab835e7..d16ae80 100644 --- 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 <") { 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("") { 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 diff --git a/bin/sup-add b/bin/sup-add index f0def12..78df022 100644 --- a/bin/sup-add +++ b/bin/sup-add @@ -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 diff --git a/bin/sup-sync b/bin/sup-sync index 6c28a76..371c695 100644 --- a/bin/sup-sync +++ b/bin/sup-sync @@ -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 diff --git a/lib/sup.rb b/lib/sup.rb index ff11dcc..a6c4994 100644 --- a/lib/sup.rb +++ b/lib/sup.rb @@ -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 diff --git a/lib/sup/buffer.rb b/lib/sup/buffer.rb index 80eb18e..1bd0332 100644 --- a/lib/sup/buffer.rb +++ b/lib/sup/buffer.rb @@ -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 diff --git a/lib/sup/index.rb b/lib/sup/index.rb index 56429d3..fe7297d 100644 --- a/lib/sup/index.rb +++ b/lib/sup/index.rb @@ -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 + + < 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 diff --git a/lib/sup/suicide.rb b/lib/sup/suicide.rb index 6c3141e..5085c16 100644 --- a/lib/sup/suicide.rb +++ b/lib/sup/suicide.rb @@ -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 diff --git a/lib/sup/util.rb b/lib/sup/util.rb index 77c2cc2..582e26f 100644 --- a/lib/sup/util.rb +++ b/lib/sup/util.rb @@ -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 -- 2.45.2