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.
- checksums.yaml +15 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.md +29 -0
- data/README.md +79 -0
- data/Rakefile +53 -0
- data/lib/arachni/reactor.rb +679 -0
- data/lib/arachni/reactor/connection.rb +302 -0
- data/lib/arachni/reactor/connection/callbacks.rb +73 -0
- data/lib/arachni/reactor/connection/error.rb +114 -0
- data/lib/arachni/reactor/connection/peer_info.rb +92 -0
- data/lib/arachni/reactor/connection/tls.rb +107 -0
- data/lib/arachni/reactor/global.rb +26 -0
- data/lib/arachni/reactor/iterator.rb +251 -0
- data/lib/arachni/reactor/queue.rb +91 -0
- data/lib/arachni/reactor/tasks.rb +107 -0
- data/lib/arachni/reactor/tasks/base.rb +59 -0
- data/lib/arachni/reactor/tasks/delayed.rb +35 -0
- data/lib/arachni/reactor/tasks/one_off.rb +30 -0
- data/lib/arachni/reactor/tasks/periodic.rb +60 -0
- data/lib/arachni/reactor/tasks/persistent.rb +31 -0
- data/lib/arachni/reactor/version.rb +15 -0
- data/spec/arachni/reactor/connection/tls_spec.rb +332 -0
- data/spec/arachni/reactor/connection_spec.rb +58 -0
- data/spec/arachni/reactor/iterator_spec.rb +203 -0
- data/spec/arachni/reactor/queue_spec.rb +91 -0
- data/spec/arachni/reactor/tasks/base.rb +8 -0
- data/spec/arachni/reactor/tasks/delayed_spec.rb +54 -0
- data/spec/arachni/reactor/tasks/one_off_spec.rb +51 -0
- data/spec/arachni/reactor/tasks/periodic_spec.rb +40 -0
- data/spec/arachni/reactor/tasks/persistent_spec.rb +39 -0
- data/spec/arachni/reactor/tasks_spec.rb +136 -0
- data/spec/arachni/reactor_spec.rb +20 -0
- data/spec/arachni/reactor_tls_spec.rb +20 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/fixtures/handlers/echo_client.rb +34 -0
- data/spec/support/fixtures/handlers/echo_client_tls.rb +10 -0
- data/spec/support/fixtures/handlers/echo_server.rb +12 -0
- data/spec/support/fixtures/handlers/echo_server_tls.rb +8 -0
- data/spec/support/fixtures/pems/cacert.pem +37 -0
- data/spec/support/fixtures/pems/client/cert.pem +37 -0
- data/spec/support/fixtures/pems/client/foo-cert.pem +39 -0
- data/spec/support/fixtures/pems/client/foo-key.pem +51 -0
- data/spec/support/fixtures/pems/client/key.pem +51 -0
- data/spec/support/fixtures/pems/server/cert.pem +37 -0
- data/spec/support/fixtures/pems/server/key.pem +51 -0
- data/spec/support/helpers/paths.rb +23 -0
- data/spec/support/helpers/utilities.rb +117 -0
- data/spec/support/lib/server_option_parser.rb +29 -0
- data/spec/support/lib/servers.rb +133 -0
- data/spec/support/lib/servers/runner.rb +13 -0
- data/spec/support/servers/echo.rb +14 -0
- data/spec/support/servers/echo_tls.rb +22 -0
- data/spec/support/servers/echo_unix.rb +14 -0
- data/spec/support/servers/echo_unix_tls.rb +22 -0
- data/spec/support/shared/connection.rb +778 -0
- data/spec/support/shared/reactor.rb +785 -0
- data/spec/support/shared/task.rb +21 -0
- 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
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
|