marvin 0.8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/bin/marvin +33 -0
- data/handlers/debug_handler.rb +5 -0
- data/handlers/hello_world.rb +9 -0
- data/handlers/keiki_thwopper.rb +21 -0
- data/handlers/simple_logger.rb +24 -0
- data/handlers/tweet_tweet.rb +19 -0
- data/lib/marvin.rb +56 -0
- data/lib/marvin/abstract_client.rb +146 -0
- data/lib/marvin/abstract_parser.rb +29 -0
- data/lib/marvin/base.rb +195 -0
- data/lib/marvin/client/actions.rb +104 -0
- data/lib/marvin/client/default_handlers.rb +97 -0
- data/lib/marvin/command_handler.rb +91 -0
- data/lib/marvin/console.rb +50 -0
- data/lib/marvin/core_commands.rb +49 -0
- data/lib/marvin/distributed.rb +8 -0
- data/lib/marvin/distributed/client.rb +225 -0
- data/lib/marvin/distributed/handler.rb +85 -0
- data/lib/marvin/distributed/protocol.rb +88 -0
- data/lib/marvin/distributed/server.rb +154 -0
- data/lib/marvin/dsl.rb +103 -0
- data/lib/marvin/exception_tracker.rb +19 -0
- data/lib/marvin/exceptions.rb +11 -0
- data/lib/marvin/irc.rb +7 -0
- data/lib/marvin/irc/client.rb +168 -0
- data/lib/marvin/irc/event.rb +39 -0
- data/lib/marvin/irc/replies.rb +154 -0
- data/lib/marvin/logging_handler.rb +76 -0
- data/lib/marvin/middle_man.rb +103 -0
- data/lib/marvin/parsers.rb +9 -0
- data/lib/marvin/parsers/command.rb +107 -0
- data/lib/marvin/parsers/prefixes.rb +8 -0
- data/lib/marvin/parsers/prefixes/host_mask.rb +35 -0
- data/lib/marvin/parsers/prefixes/server.rb +24 -0
- data/lib/marvin/parsers/ragel_parser.rb +720 -0
- data/lib/marvin/parsers/ragel_parser.rl +143 -0
- data/lib/marvin/parsers/simple_parser.rb +35 -0
- data/lib/marvin/settings.rb +31 -0
- data/lib/marvin/test_client.rb +58 -0
- data/lib/marvin/util.rb +54 -0
- data/templates/boot.erb +3 -0
- data/templates/connections.yml.erb +10 -0
- data/templates/debug_handler.erb +5 -0
- data/templates/hello_world.erb +10 -0
- data/templates/rakefile.erb +15 -0
- data/templates/settings.yml.erb +8 -0
- data/templates/setup.erb +31 -0
- data/templates/test_helper.erb +17 -0
- data/test/abstract_client_test.rb +63 -0
- data/test/parser_comparison.rb +62 -0
- data/test/parser_test.rb +266 -0
- data/test/test_helper.rb +62 -0
- data/test/util_test.rb +57 -0
- metadata +136 -0
@@ -0,0 +1,85 @@
|
|
1
|
+
module Marvin
|
2
|
+
module Distributed
|
3
|
+
class Handler < Marvin::Base
|
4
|
+
|
5
|
+
|
6
|
+
EVENT_WHITELIST = [:incoming_message, :incoming_action]
|
7
|
+
QUEUE_PROCESSING_SPACING = 3
|
8
|
+
|
9
|
+
attr_accessor :message_queue
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
super
|
13
|
+
@message_queue = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def handle(message, options)
|
17
|
+
return unless EVENT_WHITELIST.include?(message)
|
18
|
+
super(message, options)
|
19
|
+
dispatch(message, options)
|
20
|
+
end
|
21
|
+
|
22
|
+
def dispatch(name, options, client = self.client)
|
23
|
+
return if client.blank?
|
24
|
+
server = Marvin::Distributed::Server.next
|
25
|
+
if server.blank?
|
26
|
+
logger.debug "Distributed handler is currently busy - adding to queue"
|
27
|
+
# TODO: Add to queued messages, wait
|
28
|
+
@message_queue << [name, options, client]
|
29
|
+
run! unless running?
|
30
|
+
else
|
31
|
+
server.dispatch(client, name, options)
|
32
|
+
end
|
33
|
+
rescue Exception => e
|
34
|
+
logger.warn "Error dispatching #{name}"
|
35
|
+
Marvin::ExceptionTracker.log(e)
|
36
|
+
end
|
37
|
+
|
38
|
+
def process_queue
|
39
|
+
count = [@message_queue.size, Server.free_connections.size].min
|
40
|
+
logger.debug "Processing #{count} item(s) from the message queue"
|
41
|
+
count.times { |item| dispatch(*@message_queue.shift) }
|
42
|
+
if @message_queue.empty?
|
43
|
+
logger.debug "The message queue is now empty"
|
44
|
+
else
|
45
|
+
logger.debug "The message queue still has #{count} item(s)"
|
46
|
+
end
|
47
|
+
check_queue_progress
|
48
|
+
end
|
49
|
+
|
50
|
+
def running?
|
51
|
+
@running_timer.present?
|
52
|
+
end
|
53
|
+
|
54
|
+
def run!
|
55
|
+
@running_timer = EventMachine::PeriodicTimer.new(QUEUE_PROCESSING_SPACING) { process_queue }
|
56
|
+
end
|
57
|
+
|
58
|
+
def check_queue_progress
|
59
|
+
if @message_queue.blank? && running?
|
60
|
+
@running_timer.cancel
|
61
|
+
@running_timer = nil
|
62
|
+
elsif @message_queue.present? && !running?
|
63
|
+
run!
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class << self
|
68
|
+
|
69
|
+
def whitelist_event(name)
|
70
|
+
EVENT_WHITELIST << name.to_sym
|
71
|
+
EVENT_WHITELIST.uniq!
|
72
|
+
end
|
73
|
+
|
74
|
+
def register!(*args)
|
75
|
+
# DO NOT register if this is not a normal client.
|
76
|
+
return unless Marvin::Loader.client?
|
77
|
+
logger.info "Registering distributed handler on #{Marvin::Settings.client}"
|
78
|
+
super
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Marvin
|
2
|
+
module Distributed
|
3
|
+
class Protocol < EventMachine::Protocols::LineAndTextProtocol
|
4
|
+
is :loggable
|
5
|
+
|
6
|
+
class_inheritable_accessor :handler_methods
|
7
|
+
self.handler_methods = {}
|
8
|
+
|
9
|
+
attr_accessor :callbacks
|
10
|
+
|
11
|
+
def receive_line(line)
|
12
|
+
line.strip!
|
13
|
+
logger.debug "<< #{line}"
|
14
|
+
response = JSON.parse(line)
|
15
|
+
handle_response(response)
|
16
|
+
rescue JSON::ParserError
|
17
|
+
logger.debug "JSON parsing error for #{line.inspect}"
|
18
|
+
rescue Exception => e
|
19
|
+
Marvin::ExceptionTracker.log(e)
|
20
|
+
end
|
21
|
+
|
22
|
+
def send_message(name, arguments = {}, &callback)
|
23
|
+
logger.debug "Sending #{name.inspect} to #{self.host_with_port}"
|
24
|
+
payload = {
|
25
|
+
"message" => name.to_s,
|
26
|
+
"options" => arguments,
|
27
|
+
"sent-at" => Time.now
|
28
|
+
}
|
29
|
+
payload.merge!(options_for_callback(callback))
|
30
|
+
payload = JSON.dump(payload)
|
31
|
+
logger.debug ">> #{payload}"
|
32
|
+
send_data "#{payload}\n"
|
33
|
+
end
|
34
|
+
|
35
|
+
def handle_response(response)
|
36
|
+
logger.debug "Handling response in distributed protocol (response => #{response.inspect})"
|
37
|
+
return unless response.is_a?(Hash) && response.has_key?("message")
|
38
|
+
options = response["options"] || {}
|
39
|
+
process_response_message(response["message"], options)
|
40
|
+
end
|
41
|
+
|
42
|
+
def host_with_port
|
43
|
+
@host_with_port ||= begin
|
44
|
+
port, ip = Socket.unpack_sockaddr_in(get_peername)
|
45
|
+
"#{ip}:#{port}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def options_for_callback(blk)
|
52
|
+
return {} if blk.blank?
|
53
|
+
cb_id = "callback-#{seld.object_id}-#{Time.now.to_f}"
|
54
|
+
count = 0
|
55
|
+
count += 1 while @callbacks.has_key?(Digest::SHA256.hexdigest("#{cb_id}-#{count}"))
|
56
|
+
final_id = Digest::SHA256.hexdigest("#{cb_id}-#{count}")
|
57
|
+
@callbacks ||= {}
|
58
|
+
@callbacks[final_id] = blk
|
59
|
+
{"callback-id" => final_id}
|
60
|
+
end
|
61
|
+
|
62
|
+
def process_callback(hash)
|
63
|
+
@callbacks ||= {}
|
64
|
+
if hash.is_a?(Hash) && hash.has_key?("callback-id")
|
65
|
+
callback = @callbacks.delete(hash["callback-id"])
|
66
|
+
callback.call(self, hash)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def process_response_message(message, options)
|
71
|
+
method = self.handler_methods[message.to_s]
|
72
|
+
if method.present? && respond_to?(method)
|
73
|
+
logger.debug "Dispatching #{message} to #{method}"
|
74
|
+
send(method, options)
|
75
|
+
else
|
76
|
+
logger.warn "Got unknown message (#{message}) with options: #{options.inspect}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.register_handler_method(name, method = nil)
|
81
|
+
name = name.to_s
|
82
|
+
method ||= "handle_#{name}".to_sym
|
83
|
+
self.handler_methods[name] = method
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'digest/sha2'
|
3
|
+
require 'eventmachine'
|
4
|
+
require 'socket'
|
5
|
+
|
6
|
+
module Marvin
|
7
|
+
module Distributed
|
8
|
+
class Server < Protocol
|
9
|
+
|
10
|
+
register_handler_method :completed
|
11
|
+
register_handler_method :exception
|
12
|
+
register_handler_method :action
|
13
|
+
register_handler_method :authenticate
|
14
|
+
|
15
|
+
cattr_accessor :free_connections, :action_whitelist
|
16
|
+
self.free_connections = []
|
17
|
+
self.action_whitelist = [:nick, :pong, :action, :msg, :quit, :part, :join, :command]
|
18
|
+
|
19
|
+
attr_accessor :processing, :configuration, :using_tls
|
20
|
+
|
21
|
+
def initialize(*args)
|
22
|
+
@configuration = args.last.is_a?(Marvin::Nash) ? args.pop : Marvin::nash.new
|
23
|
+
super(*args)
|
24
|
+
end
|
25
|
+
|
26
|
+
def post_init
|
27
|
+
super
|
28
|
+
@callbacks = {}
|
29
|
+
logger.info "Got distributed client connection with #{self.host_with_port}"
|
30
|
+
if should_use_tls?
|
31
|
+
start_tls
|
32
|
+
else
|
33
|
+
complete_processing
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def ssl_handshake_completed
|
38
|
+
complete_processing if should_use_tls?
|
39
|
+
end
|
40
|
+
|
41
|
+
def unbind
|
42
|
+
logger.info "Lost distributed client connection with #{self.host_with_port}"
|
43
|
+
@@free_connections.delete(self)
|
44
|
+
super
|
45
|
+
end
|
46
|
+
|
47
|
+
def dispatch(client, name, options)
|
48
|
+
@processing = true
|
49
|
+
send_message(:event, {
|
50
|
+
"event-name" => name.to_s,
|
51
|
+
"event-options" => options,
|
52
|
+
"client-host" => client.host_with_port,
|
53
|
+
"client-nick" => client.nickname
|
54
|
+
})
|
55
|
+
end
|
56
|
+
|
57
|
+
def handle_authenticate(options = {})
|
58
|
+
return unless requires_auth?
|
59
|
+
logger.info "Attempting authentication for distributed client"
|
60
|
+
if options["token"].present? && options["token"] == configuration.token
|
61
|
+
@authenticated = true
|
62
|
+
send_message(:authenticated)
|
63
|
+
else
|
64
|
+
send_message(:authentication_failed)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def handle_completed(options = {})
|
69
|
+
return if fails_auth!
|
70
|
+
logger.debug "Completed message from #{self.host_with_port}"
|
71
|
+
complete_processing
|
72
|
+
end
|
73
|
+
|
74
|
+
def handle_exception(options = {})
|
75
|
+
return if fails_auth!
|
76
|
+
logger.info "Handling exception on #{self.host_with_port}"
|
77
|
+
name = options["name"]
|
78
|
+
message = options["message"]
|
79
|
+
backtrace = options["backtrace"]
|
80
|
+
logger.warn "Error in remote client - #{name}: #{message}"
|
81
|
+
[*backtrace].each { |line| logger.warn "--> #{line}" } if backtrace.present?
|
82
|
+
end
|
83
|
+
|
84
|
+
def handle_action(options = {})
|
85
|
+
return if fails_auth!
|
86
|
+
logger.debug "Handling action from on #{self.host_with_port}"
|
87
|
+
server = lookup_client_for(options["client-host"])
|
88
|
+
action = options["action"]
|
89
|
+
arguments = [*options["arguments"]]
|
90
|
+
return if server.blank? || action.blank?
|
91
|
+
begin
|
92
|
+
a = action.to_sym
|
93
|
+
if self.action_whitelist.include?(a)
|
94
|
+
server.send(a, *arguments) if server.respond_to?(a)
|
95
|
+
else
|
96
|
+
logger.warn "Client attempted invalid action #{a.inspect}"
|
97
|
+
end
|
98
|
+
rescue Exception => e
|
99
|
+
Marvin::ExceptionTracker.log(e)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def complete_processing
|
104
|
+
@@free_connections << self
|
105
|
+
@processing = false
|
106
|
+
end
|
107
|
+
|
108
|
+
def start_processing
|
109
|
+
@processing = true
|
110
|
+
end
|
111
|
+
|
112
|
+
def lookup_client_for(key)
|
113
|
+
Marvin::IRC::Client.connections.detect do |c|
|
114
|
+
c.host_with_port == key
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def requires_auth?
|
119
|
+
configuration.token? && !authenticated?
|
120
|
+
end
|
121
|
+
|
122
|
+
def authenticated?
|
123
|
+
@authenticated ||= false
|
124
|
+
end
|
125
|
+
|
126
|
+
def should_use_tls?
|
127
|
+
@using_tls ||= configuration.encrypted?
|
128
|
+
end
|
129
|
+
|
130
|
+
def fails_auth!
|
131
|
+
if requires_auth?
|
132
|
+
logger.debug "Authentication missing for distributed client"
|
133
|
+
send_message(:unauthorized)
|
134
|
+
close_connection_after_writing
|
135
|
+
return true
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.start
|
140
|
+
opts = Marvin::Settings.distributed || Marvin::Nash.new
|
141
|
+
opts = opts.server || Marvin::Nash.new
|
142
|
+
host = opts.host || "0.0.0.0"
|
143
|
+
port = (opts.port || 8943).to_i
|
144
|
+
logger.info "Starting distributed server on #{host}:#{port} (requires authentication = #{opts.token?})"
|
145
|
+
EventMachine.start_server(host, port, self, opts)
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.next
|
149
|
+
@@free_connections.shift
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
data/lib/marvin/dsl.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# Handy Dandy DSL style stuff for Marvin
|
2
|
+
module Marvin
|
3
|
+
class DSL
|
4
|
+
|
5
|
+
class Proxy < Perennial::Proxy
|
6
|
+
|
7
|
+
def initialize(klass)
|
8
|
+
@prototype_klass = Class.new(klass)
|
9
|
+
@mapping = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def define_shortcut(name, method_name)
|
13
|
+
@mapping[name] = method_name
|
14
|
+
end
|
15
|
+
|
16
|
+
def shortdef(hash = {})
|
17
|
+
hash.each_pair { |k,v| define_shortcut(k, v) }
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
def initialize_class!
|
23
|
+
@klass = Class.new(@prototype_klass)
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_instance
|
27
|
+
@klass.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_class
|
31
|
+
@klass
|
32
|
+
end
|
33
|
+
|
34
|
+
def method_missing(name, *args, &blk)
|
35
|
+
name = name.to_sym
|
36
|
+
if @mapping.has_key?(name)
|
37
|
+
@klass.define_method(@mapping[name], &blk)
|
38
|
+
else
|
39
|
+
@klass.send(name, *args, &blk)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(&blk)
|
46
|
+
instance_eval(&blk)
|
47
|
+
end
|
48
|
+
|
49
|
+
def logging(&blk)
|
50
|
+
call_prototype(:logging, &blk).register!
|
51
|
+
end
|
52
|
+
|
53
|
+
def handler(&blk)
|
54
|
+
call_prototype(:handler, &blk).register!
|
55
|
+
end
|
56
|
+
|
57
|
+
def commands(&blk)
|
58
|
+
call_prototype(:commands, &blk).register!
|
59
|
+
end
|
60
|
+
|
61
|
+
def configure(&blk)
|
62
|
+
Marvin::Settings.client.configure(&blk)
|
63
|
+
end
|
64
|
+
|
65
|
+
def server(name, port = nil)
|
66
|
+
name = name.to_s.dup
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
def initialize_prototypes
|
74
|
+
prototype_for(:logging, Marvin::LoggingHandler) do
|
75
|
+
shortdef :setup => :setup_logging,
|
76
|
+
:teardown => :teardown_logging,
|
77
|
+
:incoming => :log_incoming,
|
78
|
+
:outgoing => :log_outgoing,
|
79
|
+
:message => :log_message
|
80
|
+
end
|
81
|
+
prototype_for(:handler, Marvin::Base) do
|
82
|
+
map :on => :on_event,
|
83
|
+
:numeric => :on_numeric
|
84
|
+
end
|
85
|
+
prototype_for(:handler, Marvin::CommandHandler)
|
86
|
+
end
|
87
|
+
|
88
|
+
def prototype_for(name, klass, &blk)
|
89
|
+
@prototypes ||= {}
|
90
|
+
p = Proxy.new(klass)
|
91
|
+
p.instance_eval(&blk)
|
92
|
+
@prototypes[name] = p
|
93
|
+
end
|
94
|
+
|
95
|
+
def call_prototype(name, &blk)
|
96
|
+
p = @prototypes[name]
|
97
|
+
p.initialize_class!
|
98
|
+
p.instance_eval(&blk)
|
99
|
+
return p.to_class
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Marvin
|
2
|
+
class ExceptionTracker
|
3
|
+
|
4
|
+
is :loggable
|
5
|
+
|
6
|
+
cattr_accessor :log_exception_proc
|
7
|
+
self.log_exception_proc = proc { |e| e }
|
8
|
+
|
9
|
+
def self.log(e)
|
10
|
+
logger.fatal "Oh noes cap'n - we have an exception!."
|
11
|
+
logger.fatal "#{e.class.name}: #{e.message}"
|
12
|
+
e.backtrace.each do |line|
|
13
|
+
logger.fatal line
|
14
|
+
end
|
15
|
+
@@log_exception_proc.call(e)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|