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.
- data/README.textile +8 -0
- data/VERSION.yml +2 -2
- data/bin/marvin +88 -55
- data/config/settings.yml.sample +6 -1
- data/config/setup.rb +19 -4
- data/handlers/debug_handler.rb +2 -2
- data/handlers/hello_world.rb +1 -1
- data/lib/marvin.rb +14 -9
- data/lib/marvin/abstract_client.rb +10 -2
- data/lib/marvin/console.rb +54 -0
- data/lib/marvin/daemon.rb +71 -0
- data/lib/marvin/distributed.rb +14 -0
- data/lib/marvin/distributed/dispatch_handler.rb +82 -0
- data/lib/marvin/distributed/drb_client.rb +78 -0
- data/lib/marvin/distributed/ring_server.rb +41 -0
- data/lib/marvin/exception_tracker.rb +1 -1
- data/lib/marvin/irc.rb +4 -5
- data/lib/marvin/irc/client.rb +13 -2
- data/lib/marvin/irc/server.rb +57 -0
- data/lib/marvin/irc/server/abstract_connection.rb +79 -0
- data/lib/marvin/irc/server/base_connection.rb +66 -0
- data/lib/marvin/irc/server/channel.rb +111 -0
- data/lib/marvin/irc/server/named_store.rb +14 -0
- data/lib/marvin/irc/server/user.rb +5 -0
- data/lib/marvin/irc/server/user/handle_mixin.rb +138 -0
- data/lib/marvin/irc/server/user_connection.rb +127 -0
- data/lib/marvin/loader.rb +111 -36
- data/lib/marvin/logger.rb +2 -2
- data/lib/marvin/options.rb +11 -2
- data/lib/marvin/parsers/command.rb +43 -11
- data/lib/marvin/settings.rb +7 -6
- data/lib/marvin/status.rb +72 -0
- data/lib/marvin/test_client.rb +5 -1
- data/lib/marvin/util.rb +18 -1
- data/script/client +2 -18
- data/script/console +9 -0
- data/script/distributed_client +9 -0
- data/script/ring_server +12 -0
- data/script/server +12 -0
- data/script/status +11 -0
- data/test/parser_comparison.rb +27 -1
- data/test/parser_test.rb +254 -10
- data/test/test_helper.rb +1 -1
- metadata +62 -8
- data/lib/marvin/drb_handler.rb +0 -12
- data/lib/marvin/irc/abstract_server.rb +0 -4
- data/lib/marvin/irc/base_server.rb +0 -11
- 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
|
data/lib/marvin/irc.rb
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
module Marvin
|
2
2
|
module IRC
|
3
|
-
autoload :Client,
|
4
|
-
autoload :Event,
|
5
|
-
autoload :
|
6
|
-
autoload :
|
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
|
data/lib/marvin/irc/client.rb
CHANGED
@@ -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
|