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
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
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
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
Redwood::log "oh crap, an exception"
end
- Redwood::unlock
+ Index.unlock
end
if $exception
$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
Redwood::start
index = Redwood::Index.new
-index.load
restored_state =
if opts[:restore]
{}
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] }
$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
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
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
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 }
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
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?
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
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
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
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
@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
-require 'fileutils'
module Redwood
-class SuicideException < StandardError; end
-
class SuicideManager
include Singleton
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
+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