arachni-reactor 0.1.0.beta1

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 (58) hide show
  1. checksums.yaml +15 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.md +29 -0
  4. data/README.md +79 -0
  5. data/Rakefile +53 -0
  6. data/lib/arachni/reactor.rb +679 -0
  7. data/lib/arachni/reactor/connection.rb +302 -0
  8. data/lib/arachni/reactor/connection/callbacks.rb +73 -0
  9. data/lib/arachni/reactor/connection/error.rb +114 -0
  10. data/lib/arachni/reactor/connection/peer_info.rb +92 -0
  11. data/lib/arachni/reactor/connection/tls.rb +107 -0
  12. data/lib/arachni/reactor/global.rb +26 -0
  13. data/lib/arachni/reactor/iterator.rb +251 -0
  14. data/lib/arachni/reactor/queue.rb +91 -0
  15. data/lib/arachni/reactor/tasks.rb +107 -0
  16. data/lib/arachni/reactor/tasks/base.rb +59 -0
  17. data/lib/arachni/reactor/tasks/delayed.rb +35 -0
  18. data/lib/arachni/reactor/tasks/one_off.rb +30 -0
  19. data/lib/arachni/reactor/tasks/periodic.rb +60 -0
  20. data/lib/arachni/reactor/tasks/persistent.rb +31 -0
  21. data/lib/arachni/reactor/version.rb +15 -0
  22. data/spec/arachni/reactor/connection/tls_spec.rb +332 -0
  23. data/spec/arachni/reactor/connection_spec.rb +58 -0
  24. data/spec/arachni/reactor/iterator_spec.rb +203 -0
  25. data/spec/arachni/reactor/queue_spec.rb +91 -0
  26. data/spec/arachni/reactor/tasks/base.rb +8 -0
  27. data/spec/arachni/reactor/tasks/delayed_spec.rb +54 -0
  28. data/spec/arachni/reactor/tasks/one_off_spec.rb +51 -0
  29. data/spec/arachni/reactor/tasks/periodic_spec.rb +40 -0
  30. data/spec/arachni/reactor/tasks/persistent_spec.rb +39 -0
  31. data/spec/arachni/reactor/tasks_spec.rb +136 -0
  32. data/spec/arachni/reactor_spec.rb +20 -0
  33. data/spec/arachni/reactor_tls_spec.rb +20 -0
  34. data/spec/spec_helper.rb +16 -0
  35. data/spec/support/fixtures/handlers/echo_client.rb +34 -0
  36. data/spec/support/fixtures/handlers/echo_client_tls.rb +10 -0
  37. data/spec/support/fixtures/handlers/echo_server.rb +12 -0
  38. data/spec/support/fixtures/handlers/echo_server_tls.rb +8 -0
  39. data/spec/support/fixtures/pems/cacert.pem +37 -0
  40. data/spec/support/fixtures/pems/client/cert.pem +37 -0
  41. data/spec/support/fixtures/pems/client/foo-cert.pem +39 -0
  42. data/spec/support/fixtures/pems/client/foo-key.pem +51 -0
  43. data/spec/support/fixtures/pems/client/key.pem +51 -0
  44. data/spec/support/fixtures/pems/server/cert.pem +37 -0
  45. data/spec/support/fixtures/pems/server/key.pem +51 -0
  46. data/spec/support/helpers/paths.rb +23 -0
  47. data/spec/support/helpers/utilities.rb +117 -0
  48. data/spec/support/lib/server_option_parser.rb +29 -0
  49. data/spec/support/lib/servers.rb +133 -0
  50. data/spec/support/lib/servers/runner.rb +13 -0
  51. data/spec/support/servers/echo.rb +14 -0
  52. data/spec/support/servers/echo_tls.rb +22 -0
  53. data/spec/support/servers/echo_unix.rb +14 -0
  54. data/spec/support/servers/echo_unix_tls.rb +22 -0
  55. data/spec/support/shared/connection.rb +778 -0
  56. data/spec/support/shared/reactor.rb +785 -0
  57. data/spec/support/shared/task.rb +21 -0
  58. metadata +141 -0
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ N2QwMGQ1ZmU1MGEwYTViZDgzOWZiYmFlMzZhZjA4OTZkNjY4YmIyNA==
5
+ data.tar.gz: !binary |-
6
+ MGMyYjhhMTc1MTU1NTAyMjNlNWY3N2VjNzlhNGZjZjM5OWU3M2Y1ZA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ MGJlY2Q1Njk4OTVkODI3ODZmNTUwZGM0MTBiMDBiZjY0ZTVkNDQ4ZTgyOTEy
10
+ YWFiOWNjYzgwZTA5Y2FiMzljOTc5NmY5MWM3M2JkM2U5Nzg5ZDJhMjJlMzRm
11
+ NDMwYzI2NzkwOGI0ZTQ1YjhlMjA0NDYzZjAxMTlkZTUyNDUwOWY=
12
+ data.tar.gz: !binary |-
13
+ ZGQ2ZmE0Y2UwNWRhY2ZmMGY0ZTFjNTgzYTM5M2IyMDdiNzE5YmRiOTllOTQ5
14
+ Nzk4MzkwNjJkY2VjMDY2OWQ4NWM5MTFhNDE3MmVjNWRhYjljMzI2YTk0ZmFl
15
+ OWM0ZDAzY2Q1YzhmNWU3NWZiZmVhODZiYzc0YWQyMjNiZjFkOGY=
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # ChangeLog
2
+
3
+ ## Version 0.1.0 (_Under development_)
4
+
5
+ - Initial release.
data/LICENSE.md ADDED
@@ -0,0 +1,29 @@
1
+ # License
2
+
3
+ Copyright (C) 2014, Tasos Laskos <tasos.laskos@gmail.com>
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without modification,
7
+ are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice,
10
+ this list of conditions and the following disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ * Neither the name of the copyright holder nor the names of its contributors
17
+ may be used to endorse or promote products derived from this software
18
+ without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
21
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
24
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
27
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # Arachni::Reactor
2
+
3
+ <table>
4
+ <tr>
5
+ <th>Version</th>
6
+ <td>0.1.0.beta1</td>
7
+ </tr>
8
+ <tr>
9
+ <th>Github page</th>
10
+ <td><a href="http://github.com/Arachni/arachni-reactor">http://github.com/Arachni/arachni-reactor</a></td>
11
+ <tr/>
12
+ <tr>
13
+ <th>Code Documentation</th>
14
+ <td><a href="http://rubydoc.info/github/Arachni/arachni-reactor/">http://rubydoc.info/github/Arachni/arachni-reactor/</a></td>
15
+ </tr>
16
+ <tr>
17
+ <th>Author</th>
18
+ <td><a href="http://twitter.com/Zap0tek">Tasos Laskos</a></td>
19
+ </tr>
20
+ <tr>
21
+ <th>Twitter</th>
22
+ <td><a href="http://twitter.com/ArachniScanner">@ArachniScanner</a></td>
23
+ </tr>
24
+ <tr>
25
+ <th>Copyright</th>
26
+ <td>2014</td>
27
+ </tr>
28
+ <tr>
29
+ <th>License</th>
30
+ <td><a href="file.LICENSE.html">3-clause BSD</a></td>
31
+ </tr>
32
+ </table>
33
+
34
+ ## Synopsis
35
+
36
+ `Arachni::Reactor` is a simple, lightweight, pure-Ruby implementation of the
37
+ [Reactor](http://en.wikipedia.org/wiki/Reactor_pattern) pattern, mainly focused
38
+ on network connections -- and less so on generic tasks.
39
+
40
+ ## Features
41
+
42
+ - Extremely lightweight.
43
+ - Very simple design.
44
+ - Support for TCP/IP and UNIX-domain sockets.
45
+ - TLS encryption.
46
+ - Pure-Ruby.
47
+ - Multi-platform.
48
+
49
+ ## Supported platforms
50
+
51
+ - Rubies:
52
+ - MRI >= 1.9
53
+ - Rubinius
54
+ - JRuby (Without OpenSSL support)
55
+ - Operating Systems:
56
+ - Linux
57
+ - OSX
58
+ - Windows
59
+
60
+ ## Examples
61
+
62
+ For examples please see the `examples/` directory.
63
+
64
+ ## Installation
65
+
66
+ ## Running the Specs
67
+
68
+ rake spec
69
+
70
+ ## Bug reports/Feature requests
71
+
72
+ Please send your feedback using GitHub's issue system at
73
+ [http://github.com/arachni/arachni-reactor/issues](http://github.com/arachni/arachni-reactor/issues).
74
+
75
+
76
+ ## License
77
+
78
+ Arachni::Reactor is provided under the 3-clause BSD license.
79
+ See the [LICENSE](https://github.com/Arachni/arachni-reactor/blob/master/LICENSE.md) file for more information.
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ =begin
2
+
3
+ This file is part of the Arachni::Reactor project and may be subject to
4
+ redistribution and commercial restrictions. Please see the Arachni::Reactor
5
+ web site for more information on licensing and terms of use.
6
+
7
+ =end
8
+
9
+ require 'rubygems'
10
+ require File.expand_path( File.dirname( __FILE__ ) ) + '/lib/arachni/reactor/version'
11
+
12
+ begin
13
+ require 'rspec'
14
+ require 'rspec/core/rake_task'
15
+
16
+ RSpec::Core::RakeTask.new
17
+ rescue
18
+ end
19
+
20
+ task default: [ :build, :spec ]
21
+
22
+ desc 'Generate docs'
23
+ task :docs do
24
+ outdir = '../arachni-reactor-docs'
25
+ sh "rm -rf #{outdir}"
26
+ sh "mkdir -p #{outdir}"
27
+
28
+ sh "yardoc -o #{outdir}"
29
+
30
+ sh 'rm -rf .yardoc'
31
+ end
32
+
33
+ desc 'Clean up'
34
+ task :clean do
35
+ sh 'rm *.gem || true'
36
+ end
37
+
38
+ desc 'Build the gem.'
39
+ task build: [ :clean ] do
40
+ sh "gem build arachni-reactor.gemspec"
41
+ end
42
+
43
+ desc 'Build and install the gem.'
44
+ task install: [ :build ] do
45
+ sh "gem install arachni-reactor-#{Arachni::Reactor::VERSION}.gem"
46
+ end
47
+
48
+ desc 'Push a new version to Rubygems'
49
+ task publish: [ :build ] do
50
+ sh "git tag -a v#{Arachni::Reactor::VERSION} -m 'Version #{Arachni::Reactor::VERSION}'"
51
+ sh "gem push arachni-reactor-#{Arachni::Reactor::VERSION}.gem"
52
+ end
53
+ task release: [ :publish ]
@@ -0,0 +1,679 @@
1
+ =begin
2
+
3
+ This file is part of the Arachni::Reactor project and may be subject to
4
+ redistribution and commercial restrictions. Please see the Arachni::Reactor
5
+ web site for more information on licensing and terms of use.
6
+
7
+ =end
8
+
9
+ require 'socket'
10
+ require 'openssl'
11
+
12
+ module Arachni
13
+
14
+ # Reactor scheduler and and resource factory.
15
+ #
16
+ # You're probably interested in:
17
+ #
18
+ # * Getting access to a shared and {.global globally accessible Reactor} --
19
+ # that's probably what you want.
20
+ # * Rest of the class methods can be used to manage it.
21
+ # * Creating resources like:
22
+ # * Cross-thread, non-blocking {#create_queue Queues}.
23
+ # * Asynchronous, concurrent {#create_iterator Iterators}.
24
+ # * Network connections to:
25
+ # * {#connect Connect} to a server.
26
+ # * {#listen Listen} for clients.
27
+ # * Tasks to be scheduled:
28
+ # * {#schedule As soon as possible}.
29
+ # * {#on_tick On every loop iteration}.
30
+ # * {#delay After a configured delay}.
31
+ # * {#at_interval Every few seconds}.
32
+ # * {#on_shutdown During shutdown}.
33
+ #
34
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
35
+ class Reactor
36
+
37
+ # {Reactor} error namespace.
38
+ #
39
+ # All {Reactor} errors inherit from and live under it.
40
+ #
41
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
42
+ class Error < StandardError
43
+
44
+ # Raised when trying to perform an operation that requires the Reactor
45
+ # to be running when it is not.
46
+ #
47
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
48
+ class NotRunning < Error
49
+ end
50
+
51
+ # Raised when trying to run an already running loop.
52
+ #
53
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
54
+ class AlreadyRunning < Error
55
+ end
56
+
57
+ # Raised when trying to use UNIX-domain sockets on a host OS that
58
+ # does not support them.
59
+ #
60
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
61
+ class UNIXSocketsNotSupported < Error
62
+ end
63
+
64
+ end
65
+
66
+ %w(connection tasks queue iterator global).each do |f|
67
+ require_relative "reactor/#{f}"
68
+ end
69
+
70
+ # @return [Integer,nil]
71
+ # Amount of time to wait for a connection.
72
+ attr_accessor :max_tick_interval
73
+
74
+ # @return [Array<Connection>]
75
+ # {#attach Attached} connections.
76
+ attr_reader :connections
77
+
78
+ # @return [Integer]
79
+ # Amount of ticks.
80
+ attr_reader :ticks
81
+
82
+ DEFAULT_OPTIONS = {
83
+ select_timeout: 0.02,
84
+ max_tick_interval: 0.02
85
+ }
86
+
87
+ class <<self
88
+
89
+ # @return [Reactor]
90
+ # Lazy-loaded, globally accessible Reactor.
91
+ def global
92
+ @reactor ||= Global.instance
93
+ end
94
+
95
+ # Stops the {.global global Reactor} instance and destroys it. The next
96
+ # call to {.global} will return a new instance.
97
+ def stop
98
+ return if !@reactor
99
+
100
+ global.stop rescue Error::NotRunning
101
+
102
+ # Admittedly not the cleanest solution, but that's the only way to
103
+ # force a Singleton to re-initialize -- and we want the Singleton to
104
+ # cleanly implement the pattern in a Thread-safe way.
105
+ global.class.instance_variable_set(:@singleton__instance__, nil)
106
+
107
+ @reactor = nil
108
+ end
109
+
110
+ def supports_unix_sockets?
111
+ return false if jruby?
112
+
113
+ !!UNIXSocket
114
+ rescue NameError
115
+ false
116
+ end
117
+
118
+ def jruby?
119
+ RUBY_PLATFORM == 'java'
120
+ end
121
+ end
122
+
123
+ # @param [Hash] options
124
+ # @option options [Integer,nil] :max_tick_interval (0.02)
125
+ # How long to wait for each tick when no connections are available for
126
+ # processing.
127
+ # @option options [Integer] :select_timeout (0.02)
128
+ # How long to wait for connection activity before continuing to the next
129
+ # tick.
130
+ def initialize( options = {} )
131
+ options = DEFAULT_OPTIONS.merge( options )
132
+
133
+ @max_tick_interval = options[:max_tick_interval]
134
+ @select_timeout = options[:select_timeout]
135
+
136
+ # Socket => Connection
137
+ @connections = {}
138
+ @stop = false
139
+ @ticks = 0
140
+ @thread = nil
141
+ @tasks = Tasks.new
142
+
143
+ @shutdown_tasks = Tasks.new
144
+ @done_signal = ::Queue.new
145
+ end
146
+
147
+ # @return [Reactor::Iterator]
148
+ # New {Reactor::Iterator} with `self` as the scheduler.
149
+ # @param [#to_a] list
150
+ # List to iterate.
151
+ # @param [Integer] concurrency
152
+ # Parallel workers to spawn.
153
+ def create_iterator( list, concurrency = 1 )
154
+ Reactor::Iterator.new( self, list, concurrency )
155
+ end
156
+
157
+ # @return [Reactor::Queue]
158
+ # New {Reactor::Queue} with `self` as the scheduler.
159
+ def create_queue
160
+ Reactor::Queue.new self
161
+ end
162
+
163
+ # @note {Connection::Error Connection errors} will be passed to the `handler`'s
164
+ # {Connection::Callbacks#on_close} method as a `reason` argument.
165
+ #
166
+ # Connects to a peer.
167
+ #
168
+ # @overload connect( host, port, handler = Connection, *handler_options )
169
+ # @param [String] host
170
+ # @param [Integer] port
171
+ # @param [Connection] handler
172
+ # Connection handler, should be a subclass of {Connection}.
173
+ # @param [Hash] handler_options
174
+ # Options to pass to the `#initialize` method of the `handler`.
175
+ #
176
+ # @overload connect( unix_socket, handler = Connection, *handler_options )
177
+ # @param [String] unix_socket
178
+ # Path to the UNIX socket to connect.
179
+ # @param [Connection] handler
180
+ # Connection handler, should be a subclass of {Connection}.
181
+ # @param [Hash] handler_options
182
+ # Options to pass to the `#initialize` method of the `handler`.
183
+ #
184
+ # @return [Connection]
185
+ # Connected instance of `handler`.
186
+ #
187
+ # @raise (see #fail_if_not_running)
188
+ # @raise (see #fail_if_non_unix)
189
+ def connect( *args, &block )
190
+ fail_if_not_running
191
+
192
+ options = determine_connection_options( *args )
193
+
194
+ connection = options[:handler].new( *options[:handler_options] )
195
+ connection.reactor = self
196
+ block.call connection if block_given?
197
+
198
+ begin
199
+ Connection::Error.translate do
200
+ socket = options[:unix_socket] ?
201
+ connect_unix( options[:unix_socket] ) :
202
+ connect_tcp( options[:host], options[:port] )
203
+
204
+ connection.configure socket, :client
205
+ attach connection
206
+ end
207
+ rescue Connection::Error => e
208
+ connection.close e
209
+ end
210
+
211
+ connection
212
+ end
213
+
214
+ # @note {Connection::Error Connection errors} will be passed to the `handler`'s
215
+ # {Connection::Callbacks#on_close} method as a `reason` argument.
216
+ #
217
+ # Listens for incoming connections.
218
+ #
219
+ # @overload listen( host, port, handler = Connection, *handler_options )
220
+ # @param [String] host
221
+ # @param [Integer] port
222
+ # @param [Connection] handler
223
+ # Connection handler, should be a subclass of {Connection}.
224
+ # @param [Hash] handler_options
225
+ # Options to pass to the `#initialize` method of the `handler`.
226
+ #
227
+ # @raise [Connection::Error::HostNotFound]
228
+ # If the `host` is invalid.
229
+ # @raise [Connection::Error::Permission]
230
+ # If the `port` could not be opened due to a permission error.
231
+ #
232
+ # @overload listen( unix_socket, handler = Connection, *handler_options )
233
+ # @param [String] unix_socket
234
+ # Path to the UNIX socket to create.
235
+ # @param [Connection] handler
236
+ # Connection handler, should be a subclass of {Connection}.
237
+ # @param [Hash] handler_options
238
+ # Options to pass to the `#initialize` method of the `handler`.
239
+ #
240
+ # @raise [Connection::Error::Permission]
241
+ # If the `unix_socket` file could not be created due to a permission error.
242
+ #
243
+ # @return [Connection]
244
+ # Listening instance of `handler`.
245
+ #
246
+ # @raise (see #fail_if_not_running)
247
+ # @raise (see #fail_if_non_unix)
248
+ def listen( *args, &block )
249
+ fail_if_not_running
250
+
251
+ options = determine_connection_options( *args )
252
+
253
+ server_handler = proc do
254
+ c = options[:handler].new( *options[:handler_options] )
255
+ c.reactor = self
256
+ block.call c if block_given?
257
+ c
258
+ end
259
+
260
+ server = server_handler.call
261
+
262
+ begin
263
+ Connection::Error.translate do
264
+ socket = options[:unix_socket] ?
265
+ listen_unix( options[:unix_socket] ) :
266
+ listen_tcp( options[:host], options[:port] )
267
+
268
+ server.configure socket, :server, server_handler
269
+ attach server
270
+ end
271
+ rescue Connection::Error => e
272
+ server.close e
273
+ end
274
+
275
+ server
276
+ end
277
+
278
+ # @return [Bool]
279
+ # `true` if the {Reactor} is {#run running}, `false` otherwise.
280
+ def running?
281
+ !!thread
282
+ end
283
+
284
+ # Stops the {Reactor} {#run loop} {#schedule as soon as possible}.
285
+ #
286
+ # @raise (see #fail_if_not_running)
287
+ def stop
288
+ schedule { @stop = true }
289
+ end
290
+
291
+ # Starts the {Reactor} loop and blocks the current {#thread} until {#stop}
292
+ # is called.
293
+ #
294
+ # @param [Block] block
295
+ # Block to call right before initializing the loop.
296
+ #
297
+ # @raise (see #fail_if_running)
298
+ def run( &block )
299
+ fail_if_running
300
+
301
+ @done_signal.clear
302
+
303
+ @thread = Thread.current
304
+
305
+ block.call if block_given?
306
+
307
+ loop do
308
+ @tasks.call
309
+ break if @stop
310
+
311
+ process_connections
312
+ break if @stop
313
+
314
+ @ticks += 1
315
+ end
316
+
317
+ @tasks.clear
318
+ close_connections
319
+
320
+ @shutdown_tasks.call
321
+
322
+ @ticks = 0
323
+ @thread = nil
324
+
325
+ @done_signal << nil
326
+ end
327
+
328
+ # {#run Runs} the Reactor in a thread and blocks until it is {#running?}.
329
+ #
330
+ # @param (see #run)
331
+ #
332
+ # @return [Thread]
333
+ # {Reactor#thread}
334
+ #
335
+ # @raise (see #fail_if_running)
336
+ def run_in_thread( &block )
337
+ fail_if_running
338
+
339
+ Thread.new do
340
+ run(&block)
341
+ end
342
+
343
+ sleep 0.1 while !running?
344
+
345
+ thread
346
+ end
347
+
348
+ # Waits for the Reactor to stop {#running?}.
349
+ #
350
+ # @raise (see #fail_if_not_running)
351
+ def wait
352
+ fail_if_not_running
353
+
354
+ @done_signal.pop
355
+ true
356
+ end
357
+
358
+ # Starts the {#run Reactor loop}, blocks the current {#thread} while the
359
+ # given `block` executes and then {#stop}s it.
360
+ #
361
+ # @param [Block] block
362
+ # Block to call.
363
+ #
364
+ # @raise (see #fail_if_running)
365
+ def run_block( &block )
366
+ fail ArgumentError, 'Missing block.' if !block_given?
367
+ fail_if_running
368
+
369
+ run do
370
+ block.call
371
+ next_tick { stop }
372
+ end
373
+ end
374
+
375
+ # @param [Block] block
376
+ # Schedules a {Tasks::Persistent task} to be run at each tick.
377
+ #
378
+ # @raise (see #fail_if_not_running)
379
+ def on_tick( &block )
380
+ fail_if_not_running
381
+ @tasks << Tasks::Persistent.new( &block )
382
+ nil
383
+ end
384
+
385
+ # @param [Block] block
386
+ # Schedules a task to be run as soon as possible, either immediately if
387
+ # the caller is {#in_same_thread? in the same thread}, or at the
388
+ # {#next_tick} otherwise.
389
+ #
390
+ # @raise (see #fail_if_not_running)
391
+ def schedule( &block )
392
+ fail_if_not_running
393
+
394
+ if running? && in_same_thread?
395
+ block.call
396
+ else
397
+ next_tick(&block)
398
+ end
399
+
400
+ nil
401
+ end
402
+
403
+ # @param [Block] block
404
+ # Schedules a {Tasks::OneOff task} to be run at {#stop shutdown}.
405
+ #
406
+ # @raise (see #fail_if_not_running)
407
+ def on_shutdown( &block )
408
+ fail_if_not_running
409
+ @shutdown_tasks << Tasks::OneOff.new( &block )
410
+ nil
411
+ end
412
+
413
+ # @param [Block] block
414
+ # Schedules a {Tasks::OneOff task} to be run at the next tick.
415
+ #
416
+ # @raise (see #fail_if_not_running)
417
+ def next_tick( &block )
418
+ fail_if_not_running
419
+ @tasks << Tasks::OneOff.new( &block )
420
+ nil
421
+ end
422
+
423
+ # @note Time accuracy cannot be guaranteed.
424
+ #
425
+ # @param [Float] interval
426
+ # Time in seconds.
427
+ # @param [Block] block
428
+ # Schedules a {Tasks::Periodic task} to be run at every `interval` seconds.
429
+ #
430
+ # @raise (see #fail_if_not_running)
431
+ def at_interval( interval, &block )
432
+ fail_if_not_running
433
+ @tasks << Tasks::Periodic.new( interval, &block )
434
+ nil
435
+ end
436
+
437
+ # @note Time accuracy cannot be guaranteed.
438
+ #
439
+ # @param [Float] time
440
+ # Time in seconds.
441
+ # @param [Block] block
442
+ # Schedules a {Tasks::Delayed task} to be run in `time` seconds.
443
+ #
444
+ # @raise (see #fail_if_not_running)
445
+ def delay( time, &block )
446
+ fail_if_not_running
447
+ @tasks << Tasks::Delayed.new( time, &block )
448
+ nil
449
+ end
450
+
451
+ # @return [Thread, nil]
452
+ # Thread of the {#run loop}, `nil` if not running.
453
+ def thread
454
+ @thread
455
+ end
456
+
457
+ # @return [Bool]
458
+ # `true` if the caller is in the same {#thread} as the {#run reactor loop},
459
+ # `false` otherwise.
460
+ #
461
+ # @raise (see #fail_if_not_running)
462
+ def in_same_thread?
463
+ fail_if_not_running
464
+ Thread.current == thread
465
+ end
466
+
467
+ # @note Will call {Connection::Callbacks#on_attach}.
468
+ #
469
+ # {Connection#attach Attaches} a connection to the {Reactor} loop.
470
+ #
471
+ # @param [Connection] connection
472
+ #
473
+ # @raise (see #fail_if_not_running)
474
+ def attach( connection )
475
+ return if attached? connection
476
+
477
+ schedule do
478
+ connection.reactor = self
479
+ @connections[connection.socket] = connection
480
+ connection.on_attach
481
+ end
482
+ end
483
+
484
+ # @note Will call {Connection::Callbacks#on_detach}.
485
+ #
486
+ # {Connection#detach Detaches} a connection from the {Reactor} loop.
487
+ #
488
+ # @param [Connection] connection
489
+ #
490
+ # @raise (see #fail_if_not_running)
491
+ def detach( connection )
492
+ return if !attached?( connection )
493
+
494
+ schedule do
495
+ connection.on_detach
496
+ @connections.delete connection.socket
497
+ connection.reactor = nil
498
+ end
499
+ end
500
+
501
+ # @return [Bool]
502
+ # `true` if the connection is attached, `false` otherwise.
503
+ def attached?( connection )
504
+ @connections.include? connection.socket
505
+ end
506
+
507
+ private
508
+
509
+ # @raise [Error::NotRunning]
510
+ # If the Reactor is not {#running?}.
511
+ def fail_if_not_running
512
+ fail Error::NotRunning, 'Reactor is not running.' if !running?
513
+ end
514
+
515
+ # @raise [Error::NotRunning]
516
+ # If the Reactor is already {#running?}.
517
+ def fail_if_running
518
+ fail Error::AlreadyRunning, 'Reactor is already running.' if running?
519
+ end
520
+
521
+ # @raise [Error::UNIXSocketsNotSupported]
522
+ # If trying to use UNIX-domain sockets on a host OS that does not
523
+ # support them.
524
+ def fail_if_non_unix
525
+ return if self.class.supports_unix_sockets?
526
+
527
+ fail Error::UNIXSocketsNotSupported,
528
+ 'The host OS does not support UNIX-domain sockets.'
529
+ end
530
+
531
+ def process_connections
532
+ if @connections.empty?
533
+ sleep @max_tick_interval
534
+ return
535
+ end
536
+
537
+ # Get connections with available events - :read, :write, :error.
538
+ selected = select_connections
539
+
540
+ # Close connections that have errors.
541
+ [selected.delete(:error)].flatten.compact.each(&:close)
542
+
543
+ # Call the corresponding event on the connections.
544
+ selected.each { |event, connections| connections.each(&"_#{event}".to_sym) }
545
+ end
546
+
547
+ def determine_connection_options( *args )
548
+ options = {}
549
+ host = port = unix_socket = nil
550
+
551
+ if args[1].is_a? Integer
552
+ options[:host], options[:port], options[:handler], *handler_options = *args
553
+ else
554
+ options[:unix_socket], options[:handler], *handler_options = *args
555
+ end
556
+
557
+ if !options[:unix_socket].is_a?( String ) &&
558
+ (!options[:host].is_a?( String ) || !options[:port].is_a?( Integer ))
559
+ fail ArgumentError,
560
+ 'Either a UNIX socket path or a host and port combination are required.'
561
+ end
562
+
563
+ options[:handler] ||= Connection
564
+ options[:handler_options] = handler_options
565
+ options
566
+ end
567
+
568
+ # @return [UNIXSocket]
569
+ # Connected socket.
570
+ def connect_unix( unix_socket )
571
+ fail_if_non_unix
572
+
573
+ UNIXSocket.new( unix_socket )
574
+ end
575
+
576
+ # @return [Socket]
577
+ # Connected socket.
578
+ def connect_tcp( host, port )
579
+ socket = Socket.new(
580
+ Socket::Constants::AF_INET,
581
+ Socket::Constants::SOCK_STREAM,
582
+ Socket::Constants::IPPROTO_IP
583
+ )
584
+ socket.do_not_reverse_lookup = true
585
+
586
+ # JRuby throws java.nio.channels.NotYetConnectedException even after
587
+ # it returns the socket from Kernel.select, so wait for it to connect
588
+ # before moving on.
589
+ if self.class.jruby?
590
+ socket.connect( Socket.sockaddr_in( port, host ) )
591
+ else
592
+ begin
593
+ socket.connect_nonblock( Socket.sockaddr_in( port, host ) )
594
+ rescue IO::WaitReadable, IO::WaitWritable, Errno::EINPROGRESS
595
+ end
596
+ end
597
+
598
+ socket
599
+ end
600
+
601
+ # @return [TCPServer]
602
+ # Listening server socket.
603
+ def listen_tcp( host, port )
604
+ server = TCPServer.new( host, port )
605
+ server.do_not_reverse_lookup = true
606
+ server
607
+ end
608
+
609
+ # @return [UNIXServer]
610
+ # Listening server socket.
611
+ def listen_unix( unix_socket )
612
+ UNIXServer.new( unix_socket )
613
+ end
614
+
615
+ # Closes all client connections, both ingress and egress.
616
+ def close_connections
617
+ @connections.values.each(&:close)
618
+ end
619
+
620
+ # @return [Hash]
621
+ #
622
+ # Connections grouped by their available events:
623
+ #
624
+ # * `:read` -- Ready for reading (i.e. with data in their incoming buffer).
625
+ # * `:write` -- Ready for writing (i.e. with data in their
626
+ # {Connection#has_outgoing_data? outgoing buffer).
627
+ # * `:error`
628
+ def select_connections
629
+ grouped_sockets =
630
+ begin
631
+ Connection::Error.translate do
632
+ select(
633
+ read_sockets,
634
+ write_sockets,
635
+ read_sockets, # Read sockets are actually all sockets.
636
+ @select_timeout
637
+ )
638
+ end
639
+ rescue Connection::Error
640
+ end
641
+
642
+ return {} if !grouped_sockets
643
+
644
+ {
645
+ # Since these will be processed in order, it's better have the write
646
+ # ones first to flush the buffers ASAP.
647
+ write: connections_from_sockets( grouped_sockets[1] ),
648
+ read: connections_from_sockets( grouped_sockets[0] ),
649
+ error: connections_from_sockets( grouped_sockets[2] )
650
+ }
651
+ end
652
+
653
+ # @return [Array<Socket>]
654
+ # Sockets of all connections, we want to be ready to read at any time.
655
+ def read_sockets
656
+ @connections.keys
657
+ end
658
+
659
+ # @return [Array<Socket>]
660
+ # Sockets of connections with
661
+ # {Connection#has_outgoing_data? outgoing data}.
662
+ def write_sockets
663
+ @connections.map do |socket, connection|
664
+ next if !connection.has_outgoing_data?
665
+ socket
666
+ end.compact
667
+ end
668
+
669
+ def connections_from_sockets( sockets )
670
+ sockets.map { |s| connection_from_socket( s ) }
671
+ end
672
+
673
+ def connection_from_socket( socket )
674
+ @connections[socket]
675
+ end
676
+
677
+ end
678
+
679
+ end