commit 9689d77e242351c6a8a4448172c192e33c238352
parent 05a863b71c4ea43f3af8156f0fe1045c7229e438
Author: Rich Lane <rlane@club.cc.cmu.edu>
Date: Wed, 9 Jun 2010 18:13:23 -0700
Merge branch 'sup-server-work'
Diffstat:
11 files changed, 260 insertions(+), 102 deletions(-)
diff --git a/bin/sup b/bin/sup
@@ -58,6 +58,7 @@ No return value.
EOS
if $opts[:list_hooks]
+ Redwood.start
Redwood::HookManager.print_hooks
exit
end
@@ -95,8 +96,6 @@ global_keymap = Keymap.new do |k|
kk.add :run_keybindings_hook, "Rerun keybindings hook", 'k'
end
end
-
-Redwood::Keymap.run_hook global_keymap
## the following magic enables wide characters when used with a ruby
## ncurses.so that's been compiled against libncursesw. (note the w.) why
@@ -173,6 +172,7 @@ begin
end
HookManager.run "startup"
+ Redwood::Keymap.run_hook global_keymap
debug "starting curses"
Redwood::Logger.remove_sink $stderr
diff --git a/bin/sup-cmd b/bin/sup-cmd
@@ -23,8 +23,12 @@ EOS
opt :host, "server address", :type => :string, :default => 'localhost', :short => 'o'
opt :port, "server port", :type => :int, :default => 4300
+ opt :socket, "unix domain socket path", :type => :string, :default => nil
opt :verbose
+ conflicts :host, :socket
+ conflicts :port, :socket
+
stop_on SUB_COMMANDS
end
@@ -123,8 +127,11 @@ end
EM.run do
- EM.connect global_opts[:host], global_opts[:port],
- SupCmd, cmd, ARGV, cmd_opts.merge(global_opts)
+ if global_opts[:socket]
+ EM.connect global_opts[:socket], SupCmd, cmd, ARGV, cmd_opts.merge(global_opts)
+ else
+ EM.connect global_opts[:host], global_opts[:port], SupCmd, cmd, ARGV, cmd_opts.merge(global_opts)
+ end
end
exit 0
diff --git a/bin/sup-server b/bin/sup-server
@@ -34,7 +34,8 @@ begin
Index.load
EM.run do
- EM.start_server global_opts[:host], global_opts[:port], Redwood::Server
+ EM.start_server global_opts[:host], global_opts[:port],
+ Redwood::Server, Index.instance
EM.next_tick { puts "ready" }
end
diff --git a/lib/sup.rb b/lib/sup.rb
@@ -120,26 +120,41 @@ module Redwood
o
end
+ def managers
+ %w(HookManager SentManager ContactManager LabelManager AccountManager
+ DraftManager UpdateManager PollManager CryptoManager UndoManager
+ SourceManager SearchManager IdleManager).map { |x| Redwood.const_get x.to_sym }
+ end
+
def start
+ managers.each { |x| fail "#{x} already instantiated" if x.instantiated? }
+
+ FileUtils.mkdir_p Redwood::BASE_DIR
+ $config = load_config Redwood::CONFIG_FN
+ @log_io = File.open(Redwood::LOG_FN, 'a')
+ Redwood::Logger.add_sink @log_io
+ Redwood::HookManager.init Redwood::HOOK_DIR
Redwood::SentManager.init $config[:sent_source] || 'sup://sent'
Redwood::ContactManager.init Redwood::CONTACT_FN
Redwood::LabelManager.init Redwood::LABEL_FN
Redwood::AccountManager.init $config[:accounts]
Redwood::DraftManager.init Redwood::DRAFT_DIR
- Redwood::UpdateManager.init
- Redwood::PollManager.init
- Redwood::CryptoManager.init
- Redwood::UndoManager.init
- Redwood::SourceManager.init
Redwood::SearchManager.init Redwood::SEARCH_FN
- Redwood::IdleManager.init
+
+ managers.each { |x| x.init unless x.instantiated? }
end
def finish
Redwood::LabelManager.save if Redwood::LabelManager.instantiated?
Redwood::ContactManager.save if Redwood::ContactManager.instantiated?
- Redwood::BufferManager.deinstantiate! if Redwood::BufferManager.instantiated?
Redwood::SearchManager.save if Redwood::SearchManager.instantiated?
+ Redwood::Logger.remove_sink @log_io
+
+ managers.each { |x| x.deinstantiate! if x.instantiated? }
+
+ @log_io.close
+ @log_io = nil
+ $config = nil
end
## not really a good place for this, so I'll just dump it here.
@@ -221,85 +236,83 @@ EOS
end
end
- module_function :save_yaml_obj, :load_yaml_obj, :start, :finish,
- :report_broken_sources, :check_library_version_against
-end
-
-## set up default configuration file
-if File.exists? Redwood::CONFIG_FN
- $config = Redwood::load_yaml_obj Redwood::CONFIG_FN
- abort "#{Redwood::CONFIG_FN} is not a valid configuration file (it's a #{$config.class}, not a hash)" unless $config.is_a?(Hash)
-else
- require 'etc'
- require 'socket'
- name = Etc.getpwnam(ENV["USER"]).gecos.split(/,/).first rescue nil
- name ||= ENV["USER"]
- email = ENV["USER"] + "@" +
- begin
- Socket.gethostbyname(Socket.gethostname).first
- rescue SocketError
- Socket.gethostname
- end
+ ## set up default configuration file
+ def load_config filename
+ if File.exists? filename
+ config = Redwood::load_yaml_obj filename
+ abort "#{filename} is not a valid configuration file (it's a #{config.class}, not a hash)" unless config.is_a?(Hash)
+ config
+ else
+ require 'etc'
+ require 'socket'
+ name = Etc.getpwnam(ENV["USER"]).gecos.split(/,/).first rescue nil
+ name ||= ENV["USER"]
+ email = ENV["USER"] + "@" +
+ begin
+ Socket.gethostbyname(Socket.gethostname).first
+ rescue SocketError
+ Socket.gethostname
+ end
- $config = {
- :accounts => {
- :default => {
- :name => name,
- :email => email,
- :alternates => [],
- :sendmail => "/usr/sbin/sendmail -oem -ti",
- :signature => File.join(ENV["HOME"], ".signature")
+ config = {
+ :accounts => {
+ :default => {
+ :name => name,
+ :email => email,
+ :alternates => [],
+ :sendmail => "/usr/sbin/sendmail -oem -ti",
+ :signature => File.join(ENV["HOME"], ".signature")
+ }
+ },
+ :editor => ENV["EDITOR"] || "/usr/bin/vim -f -c 'setlocal spell spelllang=en_us' -c 'set filetype=mail'",
+ :thread_by_subject => false,
+ :edit_signature => false,
+ :ask_for_from => false,
+ :ask_for_to => true,
+ :ask_for_cc => true,
+ :ask_for_bcc => false,
+ :ask_for_subject => true,
+ :confirm_no_attachments => true,
+ :confirm_top_posting => true,
+ :jump_to_open_message => true,
+ :discard_snippets_from_encrypted_messages => false,
+ :default_attachment_save_dir => "",
+ :sent_source => "sup://sent",
+ :poll_interval => 300,
+ :wrap_width => 0,
+ :slip_rows => 0
}
- },
- :editor => ENV["EDITOR"] || "/usr/bin/vim -f -c 'setlocal spell spelllang=en_us' -c 'set filetype=mail'",
- :thread_by_subject => false,
- :edit_signature => false,
- :ask_for_from => false,
- :ask_for_to => true,
- :ask_for_cc => true,
- :ask_for_bcc => false,
- :ask_for_subject => true,
- :confirm_no_attachments => true,
- :confirm_top_posting => true,
- :jump_to_open_message => true,
- :discard_snippets_from_encrypted_messages => false,
- :default_attachment_save_dir => "",
- :sent_source => "sup://sent",
- :poll_interval => 300,
- :wrap_width => 0,
- :slip_rows => 0
- }
- begin
- FileUtils.mkdir_p Redwood::BASE_DIR
- Redwood::save_yaml_obj $config, Redwood::CONFIG_FN
- rescue StandardError => e
- $stderr.puts "warning: #{e.message}"
+ begin
+ Redwood::save_yaml_obj config, filename
+ rescue StandardError => e
+ $stderr.puts "warning: #{e.message}"
+ end
+ config
+ end
end
+
+ module_function :save_yaml_obj, :load_yaml_obj, :start, :finish,
+ :report_broken_sources, :check_library_version_against,
+ :load_config, :managers
end
require "sup/util"
require "sup/hook"
-## we have to initialize this guy first, because other classes must
-## reference it in order to register hooks, and they do that at parse
-## time.
-Redwood::HookManager.init Redwood::HOOK_DIR
-
## everything we need to get logging working
require "sup/logger"
Redwood::Logger.init.add_sink $stderr
-Redwood::Logger.add_sink File.open(Redwood::LOG_FN, 'a')
include Redwood::LogsStuff
## determine encoding and character set
- $encoding = Locale.current.charset
- $encoding = "UTF-8" if $encoding == "utf8"
- if $encoding
- debug "using character set encoding #{$encoding.inspect}"
- else
- warn "can't find character set by using locale, defaulting to utf-8"
- $encoding = "UTF-8"
- end
+$encoding = Locale.current.charset
+$encoding = "UTF-8" if $encoding == "utf8"
+if $encoding
+ debug "using character set encoding #{$encoding.inspect}"
+else
+ warn "can't find character set by using locale, defaulting to utf-8"
+ $encoding = "UTF-8"
+end
require "sup/buffer"
require "sup/keymap"
diff --git a/lib/sup/client.rb b/lib/sup/client.rb
@@ -1,6 +1,8 @@
require 'sup/protocol'
-class Redwood::Client < EM::P::RedwoodClient
+module Redwood
+
+class Client < EM::P::RedwoodClient
def initialize *a
@next_tag = 1
@cbs = {}
@@ -86,3 +88,5 @@ class Redwood::Client < EM::P::RedwoodClient
cb[type, tag, args]
end
end
+
+end
diff --git a/lib/sup/hook.rb b/lib/sup/hook.rb
@@ -61,10 +61,15 @@ class HookManager
include Singleton
+ @descs = {}
+
+ class << self
+ attr_reader :descs
+ end
+
def initialize dir
@dir = dir
@hooks = {}
- @descs = {}
@contexts = {}
@tags = {}
@@ -90,17 +95,17 @@ class HookManager
result
end
- def register name, desc
+ def self.register name, desc
@descs[name] = desc
end
def print_hooks f=$stdout
puts <<EOS
-Have #{@descs.size} registered hooks:
+Have #{HookManager.descs.size} registered hooks:
EOS
- @descs.sort.each do |name, desc|
+ HookManager.descs.sort.each do |name, desc|
f.puts <<EOS
#{name}
#{"-" * name.length}
diff --git a/lib/sup/poll.rb b/lib/sup/poll.rb
@@ -28,9 +28,8 @@ num_inbox_total_unread: the total number of unread messages in the inbox
only those messages appearing in the inbox
EOS
- DELAY = $config[:poll_interval] || 300
-
def initialize
+ @delay = $config[:poll_interval] || 300
@mutex = Mutex.new
@thread = nil
@last_poll = nil
@@ -83,8 +82,8 @@ EOS
def start
@thread = Redwood::reporting_thread("periodic poll") do
while true
- sleep DELAY / 2
- poll if @last_poll.nil? || (Time.now - @last_poll) >= DELAY
+ sleep @delay / 2
+ poll if @last_poll.nil? || (Time.now - @last_poll) >= @delay
end
end
end
diff --git a/lib/sup/protocol.rb b/lib/sup/protocol.rb
@@ -7,9 +7,12 @@ class EM::P::Redwood < EM::Connection
VERSION = 1
ENCODINGS = %w(marshal json)
+ attr_reader :debug
+
def initialize *args
@state = :negotiating
@version_buf = ""
+ @debug = false
super
end
@@ -26,12 +29,15 @@ class EM::P::Redwood < EM::Connection
receive_data x
end
else
- @filter.decode(data).each { |msg| receive_message *msg }
+ @filter.decode(data).each do |msg|
+ puts "#{self.class.name} received: #{msg.inspect}" if @debug
+ validate_message *msg
+ receive_message *msg
+ end
end
end
def connection_established
- puts "client connection established"
end
def send_version encodings, extensions
@@ -41,6 +47,8 @@ class EM::P::Redwood < EM::Connection
def send_message type, tag, params={}
fail "attempted to send message during negotiation" unless @state == :established
+ puts "#{self.class.name} sent: #{[type, tag, params].inspect}" if @debug
+ validate_message type, tag, params
send_data @filter.encode([type,tag,params])
end
@@ -48,12 +56,18 @@ class EM::P::Redwood < EM::Connection
fail "unimplemented"
end
- def receive_message type, params
+ def receive_message type, tag, params
fail "unimplemented"
end
private
+ def validate_message type, tag, params
+ fail unless type.is_a? String or type.is_a? Symbol
+ fail unless tag.is_a? String or tag.is_a? Integer
+ fail unless params.is_a? Hash
+ end
+
def parse_version l
l =~ /^Redwood\s+(\d+)\s+([\w,]+)\s+([\w,]+)$/ or fail "unexpected banner #{l.inspect}"
version, encodings, extensions = $1.to_i, $2, $3
diff --git a/lib/sup/server.rb b/lib/sup/server.rb
@@ -1,16 +1,23 @@
require 'sup/protocol'
-class Redwood::Server < EM::P::RedwoodServer
+module Redwood
+
+class Server < EM::P::RedwoodServer
+ def initialize index
+ super
+ @index = index
+ end
+
def receive_message type, tag, params
if respond_to? :"request_#{type}"
send :"request_#{type}", tag, params
else
- fail "bad request type #{type}"
+ send_message 'error', tag, 'description' => "invalid request type #{type.inspect}"
end
end
def request_query tag, a
- q = Redwood::Index.parse_query a['query']
+ q = @index.parse_query a['query']
query q, a['offset'], a['limit'], a['raw'] do |r|
send_message 'message', tag, r
end
@@ -18,13 +25,13 @@ class Redwood::Server < EM::P::RedwoodServer
end
def request_count tag, a
- q = Redwood::Index.parse_query a['query']
+ q = @index.parse_query a['query']
c = count q
send_message 'count', tag, 'count' => c
end
def request_label tag, a
- q = Redwood::Index.parse_query a['query']
+ q = @index.parse_query a['query']
label q, a['add'], a['remove']
send_message 'done', tag
end
@@ -64,7 +71,7 @@ private
def query query, offset, limit, raw
c = 0
- Index.each_message query do |m|
+ @index.each_message query do |m|
next if c < offset
break if c >= offset + limit if limit
yield result_from_message(m, raw)
@@ -74,14 +81,14 @@ private
end
def count query
- Index.num_results_for query
+ @index.num_results_for query
end
def label query, remove_labels, add_labels
- Index.each_message query do |m|
+ @index.each_message query do |m|
remove_labels.each { |l| m.remove_label l }
add_labels.each { |l| m.add_label l }
- Index.update_message_state m
+ @index.update_message_state m
end
nil
end
@@ -96,15 +103,17 @@ private
m2 = m
end
m2.labels = Set.new(labels.map(&:to_sym))
- Index.update_message_state m2
+ @index.update_message_state m2
nil
end
def thread msg_id, raw
- msg = Index.build_message msg_id
- Index.each_message_in_thread_for msg do |id, builder|
+ msg = @index.build_message msg_id
+ @index.each_message_in_thread_for msg do |id, builder|
m = builder.call
yield result_from_message(m, raw)
end
end
end
+
+end
diff --git a/lib/sup/util.rb b/lib/sup/util.rb
@@ -580,7 +580,7 @@ module Singleton
@instance.send meth, *a, &b
end
def init *args
- raise "there can be only one! (instance)" if defined? @instance
+ raise "there can be only one! (instance)" if instantiated?
@instance = new(*args)
end
end
diff --git a/test/test_server.rb b/test/test_server.rb
@@ -0,0 +1,106 @@
+#!/usr/bin/ruby
+# encoding: utf-8
+
+require 'test/unit'
+require 'iconv'
+require 'stringio'
+require 'tmpdir'
+require 'fileutils'
+require 'thread'
+require 'eventmachine'
+require 'sup'
+require 'sup/server'
+
+Thread.abort_on_exception = true
+
+module EM
+ # Run the reactor in a new thread. This is useful for using EventMachine
+ # alongside synchronous code. It is recommended to use EM.error_handler to
+ # detect when an exception terminates the reactor thread.
+ def self.spawn_reactor_thread
+ fail "reactor already started" if EM.reactor_running?
+ q = ::Queue.new
+ Thread.new { EM.run { q << nil } }
+ q.pop
+ end
+
+ # Stop the reactor and wait for it to finish. This is the counterpart to #spawn_reactor_thread.
+ def self.kill_reactor_thread
+ fail "reactor is not running" unless EM.reactor_running?
+ fail "current thread is running the reactor" if EM.reactor_thread?
+ EM.stop
+ EM.reactor_thread.join
+ end
+end
+
+class QueueingClient < EM::P::RedwoodClient
+ def initialize
+ super
+ @q = Queue.new
+ @readyq = Queue.new
+ end
+
+ def receive_message type, tag, params
+ @q << [type, tag, params]
+ end
+
+ def connection_established
+ @readyq << nil
+ end
+
+ def wait_until_ready
+ @readyq.pop
+ end
+
+ def read
+ @q.pop
+ end
+
+ alias write send_message
+end
+
+class TestServer < Test::Unit::TestCase
+ def setup
+ port = rand(1000) + 30000
+ EM.spawn_reactor_thread
+ @path = Dir.mktmpdir
+ socket_path = File.join(@path, 'socket')
+ Redwood::HookManager.init File.join(@path, 'hooks')
+ Redwood::SourceManager.init
+ Redwood::SourceManager.load_sources File.join(@path, 'sources.yaml')
+ Redwood::Index.init @path
+ Redwood::SearchManager.init File.join(@path, 'searches')
+ Redwood::Index.load
+ @server = EM.start_server socket_path,
+ Redwood::Server, Redwood::Index.instance
+ @client = EM.connect socket_path, QueueingClient
+ @client.wait_until_ready
+ end
+
+ def teardown
+ FileUtils.rm_r @path if passed?
+ puts "not cleaning up #{@path}" unless passed?
+ %w(Index SearchManager SourceManager HookManager).each do |x|
+ Redwood.const_get(x.to_sym).deinstantiate!
+ end
+ EM.kill_reactor_thread
+ end
+
+ def test_invalid_request
+ @client.write 'foo', '1'
+ check @client.read, 'error', '1'
+ end
+
+ def test_query
+ @client.write 'query', '1', 'query' => 'type:mail'
+ check @client.read, 'done', '1'
+ end
+
+ def check resp, type, tag, args={}
+ assert_equal type.to_s, resp[0]
+ assert_equal tag.to_s, resp[1]
+ args.each do |k,v|
+ assert_equal v, resp[2][k.to_s]
+ end
+ end
+end