marvin 0.8.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/bin/marvin +33 -0
  2. data/handlers/debug_handler.rb +5 -0
  3. data/handlers/hello_world.rb +9 -0
  4. data/handlers/keiki_thwopper.rb +21 -0
  5. data/handlers/simple_logger.rb +24 -0
  6. data/handlers/tweet_tweet.rb +19 -0
  7. data/lib/marvin.rb +56 -0
  8. data/lib/marvin/abstract_client.rb +146 -0
  9. data/lib/marvin/abstract_parser.rb +29 -0
  10. data/lib/marvin/base.rb +195 -0
  11. data/lib/marvin/client/actions.rb +104 -0
  12. data/lib/marvin/client/default_handlers.rb +97 -0
  13. data/lib/marvin/command_handler.rb +91 -0
  14. data/lib/marvin/console.rb +50 -0
  15. data/lib/marvin/core_commands.rb +49 -0
  16. data/lib/marvin/distributed.rb +8 -0
  17. data/lib/marvin/distributed/client.rb +225 -0
  18. data/lib/marvin/distributed/handler.rb +85 -0
  19. data/lib/marvin/distributed/protocol.rb +88 -0
  20. data/lib/marvin/distributed/server.rb +154 -0
  21. data/lib/marvin/dsl.rb +103 -0
  22. data/lib/marvin/exception_tracker.rb +19 -0
  23. data/lib/marvin/exceptions.rb +11 -0
  24. data/lib/marvin/irc.rb +7 -0
  25. data/lib/marvin/irc/client.rb +168 -0
  26. data/lib/marvin/irc/event.rb +39 -0
  27. data/lib/marvin/irc/replies.rb +154 -0
  28. data/lib/marvin/logging_handler.rb +76 -0
  29. data/lib/marvin/middle_man.rb +103 -0
  30. data/lib/marvin/parsers.rb +9 -0
  31. data/lib/marvin/parsers/command.rb +107 -0
  32. data/lib/marvin/parsers/prefixes.rb +8 -0
  33. data/lib/marvin/parsers/prefixes/host_mask.rb +35 -0
  34. data/lib/marvin/parsers/prefixes/server.rb +24 -0
  35. data/lib/marvin/parsers/ragel_parser.rb +720 -0
  36. data/lib/marvin/parsers/ragel_parser.rl +143 -0
  37. data/lib/marvin/parsers/simple_parser.rb +35 -0
  38. data/lib/marvin/settings.rb +31 -0
  39. data/lib/marvin/test_client.rb +58 -0
  40. data/lib/marvin/util.rb +54 -0
  41. data/templates/boot.erb +3 -0
  42. data/templates/connections.yml.erb +10 -0
  43. data/templates/debug_handler.erb +5 -0
  44. data/templates/hello_world.erb +10 -0
  45. data/templates/rakefile.erb +15 -0
  46. data/templates/settings.yml.erb +8 -0
  47. data/templates/setup.erb +31 -0
  48. data/templates/test_helper.erb +17 -0
  49. data/test/abstract_client_test.rb +63 -0
  50. data/test/parser_comparison.rb +62 -0
  51. data/test/parser_test.rb +266 -0
  52. data/test/test_helper.rb +62 -0
  53. data/test/util_test.rb +57 -0
  54. 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