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.
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