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,104 @@
1
+ module Marvin
2
+ class AbstractClient
3
+
4
+ ## General IRC Functions / Actions
5
+
6
+ # Sends a specified command to the server.
7
+ # Takes name (e.g. :privmsg) and all of the args.
8
+ # Very simply formats them as a string correctly
9
+ # and calls send_data with the results.
10
+ def command(name, *args)
11
+ # First, get the appropriate command
12
+ name = name.to_s.upcase
13
+ args = args.flatten
14
+ args << util.last_param(args.pop)
15
+ send_line "#{name} #{args.compact.join(" ").strip}\r\n"
16
+ end
17
+
18
+ # Join one or more channels on the current server
19
+ # e.g.
20
+ # client.join "#marvin-testing"
21
+ # client.join ["#marvin-testing", "#rubyonrails"]
22
+ # client.join "#marvin-testing", "#rubyonrails"
23
+ def join(*channels_to_join)
24
+ channels_to_join = channels_to_join.flatten.map { |c| util.channel_name(c) }
25
+ # If you're joining multiple channels at once, we join them together
26
+ command :JOIN, channels_to_join.join(",")
27
+ channels_to_join.each { |channel| dispatch :outgoing_join, :target => channel }
28
+ logger.info "Sent JOIN for channels #{channels_to_join.join(", ")}"
29
+ end
30
+
31
+ # Parts a channel, with an optional reason
32
+ # e.g.
33
+ # part "#marvin-testing"
34
+ # part "#marvin-testing", "Ninjas stole by felafel"
35
+ def part(channel, reason = nil)
36
+ channel = util.channel_name(channel)
37
+ # Send the command anyway, even if we're not a
38
+ # a recorded member something might of happened.
39
+ command :part, channel, reason
40
+ if channels.include?(channel)
41
+ dispatch :outgoing_part, :target => channel, :reason => reason
42
+ logger.info "Parted channel #{channel} - #{reason.present? ? reason : "Non given"}"
43
+ else
44
+ logger.warn "Parted channel #{channel} but wasn't recorded as member of channel"
45
+ end
46
+ end
47
+
48
+ # Quites from a server, first parting all channels if a second
49
+ # argument is passed as true
50
+ # e.g.
51
+ # quit
52
+ # quit "Going to grab some z's"
53
+ def quit(reason = nil, part_before_quit = false)
54
+ @disconnect_expected = true
55
+ # If the user wants to part before quitting, they should
56
+ # pass a second, true, parameter
57
+ if part_before_quit
58
+ logger.info "Preparing to part from channels before quitting"
59
+ channels.to_a.each { |chan| part(chan, reason) }
60
+ logger.info "Parted from all channels, quitting"
61
+ end
62
+ command :quit, reason
63
+ dispatch :outgoing_quit
64
+ # Remove the connections from the pool
65
+ connections.delete(self)
66
+ logger.info "Quit from #{host_with_port}"
67
+ end
68
+
69
+ # Sends a message to a target (either a channel or a user)
70
+ # e.g.
71
+ # msg "#marvin-testing", "Hello there!"
72
+ # msg "SuttoL", "Hey, I'm playing with marvin!"
73
+ def msg(target, message)
74
+ command :privmsg, target, message
75
+ dispatch :outgoing_message, :target => target, :message => message
76
+ logger.info "Message #{target} - #{message}"
77
+ end
78
+
79
+ # Does a CTCP action in a channel (equiv. to doing /me in most IRC clients)
80
+ # e.g.
81
+ # action "#marvin-testing", "is about to sleep"
82
+ # action "SuttoL", "is about to sleep"
83
+ def action(target, message)
84
+ command :privmsg, target, "\01ACTION #{message.strip}\01"
85
+ dispatch :outgoing_action, :target => target, :message => message
86
+ logger.info "Action sent to #{target} - #{message}"
87
+ end
88
+
89
+ def pong(data)
90
+ command :pong, data
91
+ dispatch :outgoing_pong
92
+ logger.info "PONG sent to #{host_with_port} w/ data - #{data}"
93
+ end
94
+
95
+ def nick(new_nick)
96
+ logger.info "Changing nick to #{new_nick}"
97
+ command :nick, new_nick
98
+ @nickname = new_nick
99
+ dispatch :outgoing_nick, :new_nick => new_nick
100
+ logger.info "Nick changed to #{new_nick}"
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,97 @@
1
+ module Marvin
2
+ class AbstractClient
3
+
4
+ # Default handlers
5
+
6
+ # The default handler for all things initialization-related
7
+ # on the client. Usually, this will send the user command,
8
+ # set out nick, join all of the channels / rooms we wish
9
+ # to be in and if a password is specified in the configuration,
10
+ # it will also attempt to identify us.
11
+ def handle_client_connected(opts = {})
12
+ logger.info "About to handle client connected"
13
+ # If the pass is set
14
+ unless pass.blank?
15
+ logger.info "Sending pass for connection"
16
+ command :pass, pass
17
+ end
18
+ # IRC Connection is establish so we send all the required commands to the server.
19
+ logger.info "Setting default nickname"
20
+ nick nicks.shift
21
+ logger.info "Sending user command"
22
+ command :user, configuration.user, "0", "*", configuration.name
23
+ rescue Exception => e
24
+ Marvin::ExceptionTracker.log(e)
25
+ end
26
+
27
+ # handle a bunch of default events that happen at a connection
28
+ # level instead of a per-app level.
29
+
30
+ # The default response for PING's - it simply replies
31
+ # with a PONG.
32
+ def handle_incoming_ping(opts = {})
33
+ logger.info "Received Incoming Ping - Handling with a PONG"
34
+ pong(opts[:data])
35
+ end
36
+
37
+ # TODO: Get the correct mapping for a given
38
+ # Code.
39
+ def handle_incoming_numeric(opts = {})
40
+ case opts[:code]
41
+ when Marvin::IRC::Replies[:RPL_WELCOME]
42
+ handle_welcome
43
+ when Marvin::IRC::Replies[:ERR_NICKNAMEINUSE]
44
+ handle_nick_taken
45
+ when Marvin::IRC::Replies[:RPL_TOPIC]
46
+ handle_channel_topic
47
+ end
48
+ code = opts[:code].to_i
49
+ args = Marvin::Util.arguments(opts[:data])
50
+ dispatch :incoming_numeric_processed, :code => code, :data => args
51
+ end
52
+
53
+ def handle_welcome
54
+ logger.info "Welcome received from server"
55
+ # If a password is specified, we will attempt to message
56
+ # NickServ to identify ourselves.
57
+ say ":IDENTIFY #{self.configuration.password}", "NickServ" if configuration.password.present?
58
+ # Join the default channels IF they're already set
59
+ # Note that Marvin::IRC::Client.connect will set them AFTER this stuff is run.
60
+ join default_channels
61
+ end
62
+
63
+ # The default handler for when a users nickname is taken on
64
+ # on the server. It will attempt to get the nicknickname from
65
+ # the nicknames part of the configuration (if available) and
66
+ # will then call #nick to change the nickname.
67
+ def handle_nick_taken
68
+ logger.info "Nickname '#{nickname}' on #{server} taken, trying next."
69
+ logger.info "Available Nicknames: #{nicks.empty? ? "None" : nicks.join(", ")}"
70
+ if !nicks.empty?
71
+ logger.info "Getting next nickname to switch"
72
+ next_nick = nicks.shift # Get the next nickname
73
+ logger.info "Attemping to set nickname to '#{next_nick}'"
74
+ nick next_nick
75
+ else
76
+ logger.fatal "No Nicknames available - QUITTING"
77
+ quit
78
+ end
79
+ end
80
+
81
+ # Only record joins when you've successfully joined the channel.
82
+ def handle_incoming_join(opts = {})
83
+ if opts[:nick] == @nickname
84
+ channels << opts[:target]
85
+ logger.info "Successfully joined channel #{opts[:target]}"
86
+ end
87
+ end
88
+
89
+ # Make sure we show user server errors
90
+ def handle_incoming_error(opts = {})
91
+ if opts[:message].present?
92
+ logger.info "Got ERROR Message: #{opts[:message]}"
93
+ end
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,91 @@
1
+ require 'set'
2
+
3
+ module Marvin
4
+ class CommandHandler < Base
5
+
6
+ class_inheritable_accessor :command_prefix
7
+ self.command_prefix = ""
8
+
9
+ @@exposed_method_mapping = Hash.new { |h,k| h[k] = [] }
10
+ @@method_descriptions = Hash.new { |h,k| h[k] = {} }
11
+ @@registered_classes = Set.new
12
+
13
+ class << self
14
+
15
+ def command(name, method_desc = nil, &blk)
16
+ exposes name
17
+ desc method_desc unless method_desc.blank?
18
+ define_method(name, &blk)
19
+ end
20
+
21
+ def prefix_is(p)
22
+ self.command_prefix = p
23
+ end
24
+
25
+ def exposes(*args)
26
+ args.each { |name| @@exposed_method_mapping[self] << name.to_sym }
27
+ end
28
+
29
+ def exposed_methods
30
+ methods = []
31
+ klass = self
32
+ while klass != Object
33
+ methods += @@exposed_method_mapping[klass]
34
+ klass = klass.superclass
35
+ end
36
+ return methods.uniq.compact
37
+ end
38
+
39
+ def prefix_regexp
40
+ /^#{command_prefix}/
41
+ end
42
+
43
+ def desc(description)
44
+ @last_description = description
45
+ end
46
+
47
+ def exposed_name(method)
48
+ "#{command_prefix}#{method}"
49
+ end
50
+
51
+ end
52
+
53
+ on_event :incoming_message, :check_for_commands
54
+
55
+ def check_for_commands
56
+ message = options.message.to_s.strip
57
+ data, command = nil, nil
58
+ if from_channel?
59
+ name, command, data = message.split(/\s+/, 2)
60
+ return if name !~ /^#{client.nickname}:/i
61
+ else
62
+ command, data = message.splt(/\s+/, 2)
63
+ end
64
+ data ||= ""
65
+ if (command_name = extract_command_name(command)).present?
66
+ logger.info "Processing command '#{command_name}' for #{from}"
67
+ send(command_name, data.to_s) if respond_to?(command_name)
68
+ end
69
+ end
70
+
71
+ def extract_command_name(command)
72
+ re = self.class.prefix_regexp
73
+ if command =~ re
74
+ method_name = command.gsub(re, "").underscore.to_sym
75
+ return method_name if self.class.exposed_methods.include?(method_name)
76
+ end
77
+ end
78
+
79
+ def exposed_name(name)
80
+ self.class.exposed_name(name)
81
+ end
82
+
83
+ def self.method_added(name)
84
+ if @last_description.present?
85
+ @@method_descriptions[self][name.to_sym] = @last_description
86
+ @last_description = nil
87
+ end
88
+ end
89
+
90
+ end
91
+ end
@@ -0,0 +1,50 @@
1
+ require 'irb'
2
+
3
+ module Marvin
4
+ class Console
5
+
6
+ module BaseExtensions
7
+ def parse(line)
8
+ Marvin::Settings.parser.parse(line)
9
+ end
10
+
11
+ def logger
12
+ Marvin::Logger.logger
13
+ end
14
+
15
+ def client
16
+ $client ||= Marvin::Settings.client.new(:port => 6667, :server => "irc.freenode.net")
17
+ end
18
+
19
+ def user(reset = false)
20
+ unless @user_created || reset
21
+ server.receive_line "NICK SuttoL"
22
+ server.receive_line "USER SuttoL 0 * :SuttoL"
23
+ @user_created = true
24
+ end
25
+ return server.connection_implementation
26
+ end
27
+ end
28
+
29
+ def initialize(file = $0)
30
+ @file = file
31
+ setup_irb
32
+ end
33
+
34
+ def setup_irb
35
+ # This is a bit hacky, surely there is a better way?
36
+ # e.g. some way to specify which scope irb runs in.
37
+ eval("include Marvin::Console::BaseExtensions", TOPLEVEL_BINDING)
38
+ end
39
+
40
+ def run
41
+ ARGV.replace []
42
+ IRB.start
43
+ end
44
+
45
+ def self.run
46
+ self.new.run
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,49 @@
1
+ module Marvin
2
+ class CoreCommands < CommandHandler
3
+
4
+ # Returns a hash of doccumented method names
5
+ def self.method_documentation
6
+ documented = Hash.new { |h,k| h[k] = [] }
7
+ @@method_descriptions.each_key do |klass|
8
+ next unless klass.registered?
9
+ @@exposed_method_mapping[klass].each do |m|
10
+ desc = @@method_descriptions[klass][m]
11
+ documented[m.to_s] << desc if desc.present?
12
+ end
13
+ end
14
+ return documented
15
+ end
16
+
17
+ def registered_and_exposed_handlers
18
+ end
19
+
20
+ exposes :help
21
+ desc "Generates this usage statement"
22
+ def help(method)
23
+ method = method.strip
24
+ documentation = self.class.method_documentation
25
+ names = documentation.keys.sort
26
+ if method.blank?
27
+ display_names = names.map { |n| exposed_name(n) }
28
+ width = display_names.map { |d| d.length }.max
29
+ say "Hello there, I know the following documented commands:"
30
+ names.each_with_index do |name, index|
31
+ say "#{display_names[index].ljust(width)} - #{documentation[name].join("; ")}"
32
+ end
33
+ else
34
+ if names.include? method
35
+ reply "#{exposed_name(method)} - #{documentation[method].join("; ")}"
36
+ else
37
+ reply "I'm sorry, I can't help with #{m} - it seems to be undocumented."
38
+ end
39
+ end
40
+ end
41
+
42
+ exposes :about
43
+ desc "Displays the current marvin and ruby versions."
44
+ def about(*args)
45
+ reply "Marvin v#{Marvin::VERSION} running on Ruby #{RUBY_VERSION} (#{RUBY_PLATFORM})"
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,8 @@
1
+ module Marvin
2
+ module Distributed
3
+ autoload :Protocol, 'marvin/distributed/protocol'
4
+ autoload :Server, 'marvin/distributed/server'
5
+ autoload :Handler, 'marvin/distributed/handler'
6
+ autoload :Client, 'marvin/distributed/client'
7
+ end
8
+ end
@@ -0,0 +1,225 @@
1
+ require 'json'
2
+ require 'digest/sha2'
3
+ require 'eventmachine'
4
+ require 'socket'
5
+
6
+ module Marvin
7
+ module Distributed
8
+ class Client < Marvin::AbstractClient
9
+
10
+ attr_accessor :em_connection, :remote_client_host, :remote_client_nick
11
+
12
+ class RemoteClientProxy
13
+ is :loggable
14
+
15
+ def initialize(conn, host_with_port, nickname)
16
+ @connection = conn
17
+ @host_with_port = host_with_port
18
+ @nickname = nickname
19
+ end
20
+
21
+ def nickname
22
+ @nickname
23
+ end
24
+
25
+ def host_with_port
26
+ @host_with_port
27
+ end
28
+
29
+ def method_missing(name, *args)
30
+ logger.debug "Proxying #{name}(#{args.inspect[1..-2]}) to #{@host_with_port}"
31
+ @connection.send_message(:action, {
32
+ "action" => name.to_s,
33
+ "arguments" => args,
34
+ "client-host" => @host_with_port
35
+ })
36
+ end
37
+
38
+ end
39
+
40
+ class EMConnection < Marvin::Distributed::Protocol
41
+
42
+ register_handler_method :event
43
+ register_handler_method :authentication_failed
44
+ register_handler_method :authenticated
45
+ register_handler_method :unauthorized
46
+
47
+ cattr_accessor :stopping
48
+ self.stopping = false
49
+
50
+ attr_accessor :client, :port, :connection_host, :connection_port, :configuration
51
+
52
+ def initialize(*args)
53
+ @configuration = args.last.is_a?(Marvin::Nash) ? args.pop : Marvin::Nash.new
54
+ super(*args)
55
+ @callbacks = {}
56
+ @client = Marvin::Distributed::Client.new(self)
57
+ @authenticated = false
58
+ end
59
+
60
+ def post_init
61
+ super
62
+ logger.info "Connected to distributed server"
63
+ if should_use_tls?
64
+ logger.info "Attempting to initialize tls"
65
+ start_tls
66
+ else
67
+ process_authentication
68
+ end
69
+ end
70
+
71
+ def ssl_handshake_completed
72
+ logger.info "tls handshake completed"
73
+ process_authentication if should_use_tls?
74
+ end
75
+
76
+
77
+ def unbind
78
+ if self.stopping
79
+ logger.info "Stopping distributed client"
80
+ else
81
+ logger.info "Lost connection to distributed client - Scheduling reconnect"
82
+ EventMachine.add_timer(15) { EMConnection.connect(connection_host, connection_port, @configuration) }
83
+ end
84
+ super
85
+ end
86
+
87
+ def process_authentication
88
+ if configuration.token?
89
+ logger.info "Attempting to authenticate..."
90
+ send_message(:authenticate, {:token => configuration.token})
91
+ end
92
+ end
93
+
94
+ def handle_event(options = {})
95
+ event = options["event-name"]
96
+ client_host = options["client-host"]
97
+ client_nick = options["client-nick"]
98
+ options = options["event-options"]
99
+ options = {} unless options.is_a?(Hash)
100
+ return if event.blank?
101
+ begin
102
+ logger.debug "Handling #{event}"
103
+ @client.remote_client_host = client_host
104
+ @client.remote_client_nick = client_nick
105
+ @client.setup_handlers
106
+ @client.dispatch(event.to_sym, options)
107
+ rescue Exception => e
108
+ logger.warn "Got Exception - Forwarding to Remote"
109
+ Marvin::ExceptionTracker.log(e)
110
+ send_message(:exception, {
111
+ "name" => e.class.name,
112
+ "message" => e.message,
113
+ "backtrace" => e.backtrace
114
+ })
115
+ ensure
116
+ logger.debug "Sending completed message"
117
+ send_message(:completed)
118
+ @client.reset!
119
+ end
120
+ end
121
+
122
+ def handle_unauthorized(options = {})
123
+ logger.warn "Attempted action when unauthorized. Stopping client."
124
+ Marvin::Distributed::Client.stop
125
+ end
126
+
127
+ def handle_authenticated(options = {})
128
+ @authenticated = true
129
+ logger.info "Successfully authenticated with #{host_with_port}"
130
+ end
131
+
132
+ def handle_authentication_failed(options = {})
133
+ logger.info "Authentication with #{host_with_port} failed. Stopping."
134
+ Marvin::Distributed::Client.stop
135
+ end
136
+
137
+ def self.connect(host, port, config = Marvin::Nash.new)
138
+ logger.info "Attempting to connect to #{host}:#{port}"
139
+ EventMachine.connect(host, port, self, config) do |c|
140
+ c.connection_host = host
141
+ c.connection_port = port
142
+ end
143
+ end
144
+
145
+ protected
146
+
147
+ def options_for_callback(blk)
148
+ return {} if blk.blank?
149
+ cb_id = "callback-#{seld.object_id}-#{Time.now.to_f}"
150
+ count = 0
151
+ count += 1 while @callbacks.has_key?(Digest::SHA256.hexdigest("#{cb_id}-#{count}"))
152
+ final_id = Digest::SHA256.hexdigest("#{cb_id}-#{count}")
153
+ @callbacks[final_id] = blk
154
+ {"callback-id" => final_id}
155
+ end
156
+
157
+ def process_callback(hash)
158
+ if hash.is_a?(Hash) && hash.has_key?("callback-id")
159
+ callback = @callbacks.delete(hash["callback-id"])
160
+ callback.call(self, hash)
161
+ end
162
+ end
163
+
164
+ def host_with_port
165
+ @host_with_port ||= begin
166
+ port, ip = Socket.unpack_sockaddr_in(get_peername)
167
+ "#{ip}:#{port}"
168
+ end
169
+ end
170
+
171
+ def should_use_tls?
172
+ @using_tls ||= configuration.encrypted?
173
+ end
174
+
175
+ end
176
+
177
+ def initialize(em_connection)
178
+ @em_connection = em_connection
179
+ end
180
+
181
+ def remote_client
182
+ @remote_client ||= RemoteClientProxy.new(@em_connection, @remote_client_host, @remote_client_nick)
183
+ end
184
+
185
+ def reset!
186
+ @remote_client = nil
187
+ @remote_client_nick = nil
188
+ @remote_client_host = nil
189
+ reset_handlers
190
+ end
191
+
192
+ def setup_handlers
193
+ self.class.handlers.each { |h| h.client = remote_client if h.respond_to?(:client=) }
194
+ end
195
+
196
+ def reset_handlers
197
+ self.class.handlers.each { |h| h.client = nil if h.respond_to?(:client=) }
198
+ end
199
+
200
+ class << self
201
+
202
+ def run
203
+ logger.info "Preparing to start distributed client"
204
+ EventMachine.kqueue
205
+ EventMachine.epoll
206
+ EventMachine.run do
207
+ opts = Marvin::Settings.distributed || Marvin::Nash.new
208
+ opts = opts.client || Marvin::Nash.new
209
+ host = opts.host || "0.0.0.0"
210
+ port = (opts.port || 8943).to_i
211
+ EMConnection.connect(host, port, opts)
212
+ end
213
+ end
214
+
215
+ def stop
216
+ logger.info "Stopping distributed client..."
217
+ EMConnection.stopping = true
218
+ EventMachine.stop_event_loop
219
+ end
220
+
221
+ end
222
+
223
+ end
224
+ end
225
+ end