arachni-reactor 0.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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