Sutto-marvin 0.1.20081120 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/README.textile +8 -0
  2. data/VERSION.yml +2 -2
  3. data/bin/marvin +88 -55
  4. data/config/settings.yml.sample +6 -1
  5. data/config/setup.rb +19 -4
  6. data/handlers/debug_handler.rb +2 -2
  7. data/handlers/hello_world.rb +1 -1
  8. data/lib/marvin.rb +14 -9
  9. data/lib/marvin/abstract_client.rb +10 -2
  10. data/lib/marvin/console.rb +54 -0
  11. data/lib/marvin/daemon.rb +71 -0
  12. data/lib/marvin/distributed.rb +14 -0
  13. data/lib/marvin/distributed/dispatch_handler.rb +82 -0
  14. data/lib/marvin/distributed/drb_client.rb +78 -0
  15. data/lib/marvin/distributed/ring_server.rb +41 -0
  16. data/lib/marvin/exception_tracker.rb +1 -1
  17. data/lib/marvin/irc.rb +4 -5
  18. data/lib/marvin/irc/client.rb +13 -2
  19. data/lib/marvin/irc/server.rb +57 -0
  20. data/lib/marvin/irc/server/abstract_connection.rb +79 -0
  21. data/lib/marvin/irc/server/base_connection.rb +66 -0
  22. data/lib/marvin/irc/server/channel.rb +111 -0
  23. data/lib/marvin/irc/server/named_store.rb +14 -0
  24. data/lib/marvin/irc/server/user.rb +5 -0
  25. data/lib/marvin/irc/server/user/handle_mixin.rb +138 -0
  26. data/lib/marvin/irc/server/user_connection.rb +127 -0
  27. data/lib/marvin/loader.rb +111 -36
  28. data/lib/marvin/logger.rb +2 -2
  29. data/lib/marvin/options.rb +11 -2
  30. data/lib/marvin/parsers/command.rb +43 -11
  31. data/lib/marvin/settings.rb +7 -6
  32. data/lib/marvin/status.rb +72 -0
  33. data/lib/marvin/test_client.rb +5 -1
  34. data/lib/marvin/util.rb +18 -1
  35. data/script/client +2 -18
  36. data/script/console +9 -0
  37. data/script/distributed_client +9 -0
  38. data/script/ring_server +12 -0
  39. data/script/server +12 -0
  40. data/script/status +11 -0
  41. data/test/parser_comparison.rb +27 -1
  42. data/test/parser_test.rb +254 -10
  43. data/test/test_helper.rb +1 -1
  44. metadata +62 -8
  45. data/lib/marvin/drb_handler.rb +0 -12
  46. data/lib/marvin/irc/abstract_server.rb +0 -4
  47. data/lib/marvin/irc/base_server.rb +0 -11
  48. data/script/daemon-runner +0 -12
@@ -0,0 +1,14 @@
1
+ require 'drb'
2
+ require 'rinda/ring'
3
+
4
+
5
+ module Marvin
6
+ # Distributed tools for Marvin instances.
7
+ # Uses a tuple space etc + DRb to provide
8
+ # IRC Processing across the network.
9
+ module Distributed
10
+ autoload :RingServer, 'marvin/distributed/ring_server'
11
+ autoload :DispatchHandler, 'marvin/distributed/dispatch_handler'
12
+ autoload :DRbClient, 'marvin/distributed/drb_client'
13
+ end
14
+ end
@@ -0,0 +1,82 @@
1
+ require 'rinda/ring'
2
+ require 'rinda/tuplespace'
3
+
4
+ DRb.start_service
5
+
6
+ module Marvin
7
+ module Distributed
8
+
9
+ # Handler to provide
10
+ class DispatchHandler < Marvin::Base
11
+
12
+ LOOKUP_TIMEOUT = 0.5
13
+
14
+ # Tell the client that we shouldn't be dumped.
15
+ Marvin::AbstractClient.class_eval { include(DRbUndumped) }
16
+
17
+ # Get the ring server - if it exists, we will return the current
18
+ # instance other wise it follows a few steps to try and find a new
19
+ # one. Since there can be a delay in getting a response, we'll only
20
+ # check every 5 messages.
21
+ def ring_server
22
+ if @@rs.nil? && (@lookup_attempts ||= 6) > 5
23
+ @lookup_attempts = 0
24
+ @@rs = Rinda::RingFinger.finger.lookup_ring(LOOKUP_TIMEOUT)
25
+ logger.info "Found new ring server => #{@@rs.__drburi}"
26
+ elsif @@rs.nil?
27
+ @lookup_attempts += 1
28
+ end
29
+ return @@rs
30
+ rescue RingNotFound
31
+ @@rs = nil
32
+ end
33
+
34
+ # Takes an incoming messsage and does all the fancy
35
+ # Stuff with it.
36
+ def handle(message, options)
37
+ # Don't forward lines for the moment to halve the traffic.
38
+ return if message == :incoming_line
39
+ super(message, options)
40
+ dispatch(message, options)
41
+ end
42
+
43
+ # Attempts to add a message to the current tuple space,
44
+ # adding it to a message queue if it can't be added.
45
+ # If there are many items, it will log a warning.
46
+ # TODO: Improve it to dump messages to disk at a predefined limit.
47
+ def dispatch(name, options)
48
+ options[:dispatched_at] ||= Time.now
49
+ tuple = [:marvin_event, name, options, self.client]
50
+ begin
51
+ (@queued_messages ||= []) << tuple
52
+ if self.ring_server.nil?
53
+ size = @queued_messages.size
54
+ if size > 0 && size % 25 == 0
55
+ logger.warn "Dispatch handler queue is currently holding #{size} items"
56
+ end
57
+ else
58
+ @queued_messages.dup.each do |t|
59
+ ring_server.write(t)
60
+ @queued_messages.delete(t)
61
+ end
62
+ end
63
+ rescue
64
+ # Reset the ring finger to the next choice.
65
+ logger.debug "Ring server unavailable, resetting..."
66
+ @@rs = nil
67
+ end
68
+ end
69
+
70
+ # Register this as a handler, but only if we're
71
+ # running in "client mode" - in other words, we
72
+ # want to make sure it won't start up an infinite
73
+ # loop.
74
+ def self.register!(*args)
75
+ # DO NOT register if this is not a normal client.
76
+ return unless Marvin::Loader.type == :client
77
+ super
78
+ end
79
+
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,78 @@
1
+ module Marvin
2
+ module Distributed
3
+ # A method for operating on marvin objects.
4
+ class DRbClient
5
+
6
+ # Wait 1 second on a lookup.
7
+ LOOKUP_TIMEOUT = 1
8
+
9
+ class << self
10
+
11
+ @@handlers = []
12
+ attr_accessor :stopped
13
+
14
+ def register_handler(handler)
15
+ @@handlers << handler unless handler.nil? || !handler.respond_to?(:handle)
16
+ end
17
+
18
+ def dispatch(name, opts, client)
19
+ Marvin::Logger.debug "Processing Event: #{name}"
20
+ full_handler_name = :"handle_#{name.to_s.underscore}"
21
+ @@handlers.each do |handler|
22
+ has_client = handler.respond_to?(:client=)
23
+ handler.client = client if has_client
24
+ if handler.respond_to?(full_handler_name)
25
+ handler.send(full_handler_name, opts)
26
+ else
27
+ handler.handle name, opts
28
+ end
29
+ handler.client = nil if has_client
30
+ end
31
+ rescue HaltHandlerProcessing => e
32
+ Marvin::Logger.info "Halting processing chain"
33
+ rescue Exception => e
34
+ Marvin::ExceptionTracker.log(e)
35
+ end
36
+
37
+ # Starts up a drb processor / client, and walks through the process of dealing
38
+ # with it / processing events.
39
+ def run
40
+ self.stopped = false
41
+ Marvin::Logger.info "Starting up DRb Client"
42
+ DRb.start_service
43
+ # Loop through, making sure we have a valid
44
+ # RingFinger and then process events as they
45
+ # appear.
46
+ enter_loop!
47
+ end
48
+
49
+ def stop
50
+ self.stopped = true
51
+ end
52
+
53
+ def ring_server
54
+ @ring_server = Rinda::RingFinger.finger.lookup_ring(LOOKUP_TIMEOUT) if @ring_server.nil?
55
+ return @ring_server
56
+ rescue RingNotFound
57
+ @ring_server = nil
58
+ end
59
+
60
+ def enter_loop!
61
+ Marvin::Logger.info "Entering processing loop"
62
+ while !self.stopped
63
+ begin
64
+ unless self.ring_server.blank?
65
+ event = self.ring_server.take([:marvin_event, nil, nil, nil], 5)
66
+ dispatch(*event[1..-1]) unless event.blank?
67
+ end
68
+ rescue
69
+ # Reset the ring server on event of connection refused etc.
70
+ @ring_server = nil
71
+ end
72
+ end
73
+ end
74
+
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,41 @@
1
+ require 'rinda/ring'
2
+ require 'rinda/tuplespace'
3
+
4
+ module Marvin
5
+ module Distributed
6
+ class RingServer
7
+
8
+ attr_accessor :tuple_space, :ring_server
9
+ cattr_accessor :logger
10
+ self.logger = Marvin::Logger
11
+
12
+ def initialize
13
+ self.tuple_space = Rinda::TupleSpace.new
14
+ if Marvin::Settings.log_level == :debug
15
+ observer = self.tuple_space.notify('write', [:marvin_event, nil, nil, nil])
16
+ Thread.start do
17
+ observer.each do |i|
18
+ event_name, args = i[1][1..2]
19
+ Marvin::Logger.logger.debug "Marvin event added - #{event_name.inspect} w/ #{args.inspect}"
20
+ end
21
+ end
22
+ end
23
+ self.ring_server = Rinda::RingServer.new(self.tuple_space)
24
+ end
25
+
26
+ def self.run
27
+ begin
28
+ logger.info "Starting up DRb"
29
+ drb_server = DRb.start_service
30
+ logger.info "Creating TupleSpace & Ring Server Instances - Running on #{DRb.uri}"
31
+ self.new
32
+ logger.info "Started - Joining thread."
33
+ DRb.thread.join
34
+ rescue
35
+ logger.fatal "Error starting ring server - please ensure another instance isn't already running."
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -2,7 +2,7 @@ module Marvin
2
2
  class ExceptionTracker
3
3
 
4
4
  cattr_accessor :logger
5
- self.logger = Marvin::Logger.logger
5
+ self.logger = Marvin::Logger
6
6
 
7
7
  def self.log(e)
8
8
  logger.fatal "Exception raised inside Marvin Instance."
data/lib/marvin/irc.rb CHANGED
@@ -1,9 +1,8 @@
1
1
  module Marvin
2
2
  module IRC
3
- autoload :Client, 'marvin/irc/client'
4
- autoload :Event, 'marvin/irc/event'
5
- autoload :AbstractServer, 'marvin/irc/abstract_server'
6
- autoload :BaseServer, 'marvin/irc/base_server'
7
- autoload :Replies, 'marvin/irc/replies'
3
+ autoload :Client, 'marvin/irc/client'
4
+ autoload :Event, 'marvin/irc/event'
5
+ autoload :Server, 'marvin/irc/server'
6
+ autoload :Replies, 'marvin/irc/replies'
8
7
  end
9
8
  end
@@ -46,6 +46,8 @@ module Marvin::IRC
46
46
  # handler as the only argument.
47
47
  class Client < Marvin::AbstractClient
48
48
  cattr_accessor :stopped
49
+ self.stopped = false
50
+
49
51
  attr_accessor :em_connection
50
52
 
51
53
  class EMConnection < EventMachine::Protocols::LineAndTextProtocol
@@ -84,10 +86,12 @@ module Marvin::IRC
84
86
 
85
87
  # Starts the EventMachine loop and hence starts up the actual
86
88
  # networking portion of the IRC Client.
87
- def self.run
89
+ def self.run(force = false)
90
+ return if self.stopped && !force
88
91
  self.setup # So we have options etc
89
92
  settings = YAML.load_file(Marvin::Settings.root / "config/connections.yml")
90
93
  if settings.is_a?(Hash)
94
+ # Use epoll if available
91
95
  EventMachine.epoll
92
96
  EventMachine::run do
93
97
  settings.each do |name, options|
@@ -105,7 +109,6 @@ module Marvin::IRC
105
109
  def self.connect(opts = {})
106
110
  logger.info "Connecting to #{opts[:server]}:#{opts[:port]} - Channels: #{opts[:channels].join(", ")}"
107
111
  EventMachine::connect(opts[:server], opts[:port], EMConnection, opts)
108
- logger.info "Connection created for #{opts[:server]}:#{opts[:port]}"
109
112
  end
110
113
 
111
114
  def self.stop
@@ -118,6 +121,14 @@ module Marvin::IRC
118
121
  self.stopped = true
119
122
  end
120
123
 
124
+ def self.add_reconnect(opts = {})
125
+ Marvin::Logger.warn "Adding entry to reconnect to #{opts[:server]}:#{opts[:port]} in 15 seconds"
126
+ EventMachine::add_timer(15) do
127
+ Marvin::Logger.warn "Attempting to reconnect to #{opts[:server]}:#{opts[:port]}"
128
+ Marvin::IRC::Client.connect(opts)
129
+ end
130
+ end
131
+
121
132
  # Registers a callback handle that will be periodically run.
122
133
  def periodically(timing, event_callback)
123
134
  callback = proc { self.dispatch event_callback.to_sym }
@@ -0,0 +1,57 @@
1
+ module Marvin
2
+ module IRC
3
+ module Server
4
+
5
+ # Server utilities
6
+ autoload :NamedStore, 'marvin/irc/server/named_store'
7
+
8
+ # Store each user
9
+ UserStore = NamedStore.new(:nicks, :user) do
10
+ def nick_taken?(nick)
11
+ has_key?(nick)
12
+ end
13
+
14
+ def each_user_except(user)
15
+ self.each_user do |u|
16
+ yield u unless user == u
17
+ end
18
+ end
19
+ end
20
+
21
+ # Store each channel
22
+ ChannelStore = NamedStore.new(:names, :channel)
23
+
24
+ autoload :Channel, 'marvin/irc/server/channel'
25
+ # The actual network connection
26
+ autoload :BaseConnection, 'marvin/irc/server/base_connection'
27
+ # An our implementations of protocol-specific stuff.
28
+ autoload :AbstractConnection, 'marvin/irc/server/abstract_connection'
29
+ autoload :UserConnection, 'marvin/irc/server/user_connection'
30
+ autoload :ServerConnection, 'marvin/irc/server/server_connection'
31
+ # Extensions for each part
32
+ autoload :User, 'marvin/irc/server/user'
33
+
34
+ # call start_server w/ the default options
35
+ # and inside an EM::run block.
36
+ def self.run
37
+ EventMachine::run do
38
+ Marvin::Logger.info "Starting server..."
39
+ start_server :bind_addr => "0.0.0.0"
40
+ end
41
+ end
42
+
43
+ # Starts the server with a set of given options
44
+ def self.start_server(opts = {})
45
+ opts[:started_at] ||= Time.now
46
+ opts[:host] ||= self.host_name
47
+ opts[:port] ||= 6667
48
+ EventMachine::start_server(opts[:bind_addr] || opts[:host], opts[:port], BaseConnection, opts)
49
+ end
50
+
51
+ def self.host_name
52
+ @@host_name ||= Socket.gethostname
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,79 @@
1
+ module Marvin::IRC::Server
2
+ class AbstractConnection
3
+ include Marvin::Dispatchable
4
+
5
+ cattr_accessor :connections, :logger, :alive
6
+ self.connections = []
7
+ self.logger = Marvin::Logger
8
+
9
+ attr_accessor :connection
10
+
11
+ # Create a new connection with a given parent
12
+ # and an incoming buffer of messages
13
+ def initialize(parent, buffer = [])
14
+ @connection = parent
15
+ buffer.each { |line| receive_line(line) }
16
+ end
17
+
18
+ def receive_line(line)
19
+ dispatch :incoming_line, :line => line
20
+ event = Marvin::Settings.default_parser.parse(line)
21
+ dispatch(event.to_incoming_event_name, event.to_hash) unless event.nil?
22
+ end
23
+
24
+ def send_line(line)
25
+ @connection.send_line(line)
26
+ end
27
+
28
+ def process_connect
29
+ @alive = true
30
+ # Send the welcome notice / auth command.
31
+ command :NOTICE, "AUTH", ":Marvin v#{Marvin.version} initialized, welcome."
32
+ dispatch :client_connected, :client => self
33
+ end
34
+
35
+ def process_disconnect
36
+ @@connections.delete(self)
37
+ @alive = false
38
+ dispatch :client_disconnected, :client => self
39
+ end
40
+
41
+ class << self
42
+
43
+ # Return an array of all registered handlers, stored in the
44
+ # class variable @@handlers. Used inside the #handlers instance
45
+ # method as well as inside things such as register_handler.
46
+ def handlers
47
+ (@@handlers ||= {})[self] ||= []
48
+ end
49
+
50
+ # Assigns a new array of handlers and assigns each.
51
+ def handlers=(new_value)
52
+ (@@handlers ||= {})[self] = []
53
+ new_value.to_a.each { |h| register_handler h }
54
+ end
55
+
56
+ end
57
+
58
+ private
59
+
60
+ def command(name, *args)
61
+ opts = args.extract_options!
62
+ formatted = [name.to_s.upcase, *args].join(" ")
63
+ formatted = ":#{opts[:prefix]} #{formatted}" if opts[:prefix]
64
+ send_line formatted
65
+ end
66
+
67
+ def peer_name
68
+ return @peer_name unless @peer_name.blank?
69
+ sock_addr = @connection.get_peername
70
+ begin
71
+ @peer_name = Socket.getnameinfo(sock_addr, Socket::NI_NAMEREQD).first
72
+ rescue
73
+ @peer_name = Socket.getnameinfo(sock_addr).first
74
+ end
75
+ return @peer_name
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,66 @@
1
+ require 'eventmachine'
2
+
3
+ module Marvin
4
+ module IRC
5
+ module Server
6
+ class BaseConnection < EventMachine::Protocols::LineAndTextProtocol
7
+
8
+ attr_accessor :port, :host, :started_at
9
+
10
+ # Our initialize method
11
+ def initialize(opts = {})
12
+ super
13
+ @buffer = []
14
+ @port = opts[:port]
15
+ @host = opts[:host]
16
+ @started_at = opts[:started_at] || Time.now
17
+ end
18
+
19
+ attr_accessor :connection_implementation, :buffer
20
+
21
+ # Receive the line, processing as it needs to be.
22
+ # Not that we have a conditional check to setup
23
+ # the correct connection
24
+ def receive_line(line)
25
+ Marvin::Logger.debug "<< #{line}"
26
+ if !@connection_implementation.nil?
27
+ @connection_implementation.receive_line(line)
28
+ elsif line[0..3] == "USER"
29
+ @buffer << line
30
+ self.connection_implementation = UserConnection.new(self, @buffer)
31
+ @buffer = nil
32
+ elsif line[0..5] == "SERVER"
33
+ @buffer << line
34
+ self.connection_implementation = ServerConnection.new(self, @buffer)
35
+ @buffer = nil
36
+ else
37
+ @buffer << line
38
+ end
39
+ end
40
+
41
+ def send_line(line)
42
+ Marvin::Logger.debug ">> #{line.strip}"
43
+ line += "\r\n" unless line[-2..-1] == "\r\n"
44
+ send_data line
45
+ end
46
+
47
+ def kill_connection!
48
+ close_connection_after_writing
49
+ end
50
+
51
+ # Do things on the unbind
52
+ def unbind
53
+ super # Call the old version
54
+ @connection_implementation.process_disconnect
55
+ end
56
+
57
+ # Do things on the connection implementation
58
+ def post_init
59
+ super
60
+ send_line "NOTICE AUTH :Marvin Server v#{Marvin.version} initialized, welcome."
61
+ end
62
+
63
+ end
64
+ end
65
+ end
66
+ end