chain-reactor 0.2.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.
- data/.gitignore +17 -0
- data/Gemfile +15 -0
- data/LICENSE.txt +1 -0
- data/README.rdoc +54 -0
- data/Rakefile +24 -0
- data/bin/chain-reactor +3 -0
- data/bin/chain-reactor-client +3 -0
- data/chain-reactor.gemspec +20 -0
- data/lib/chain-reactor/chain_reactor.rb +208 -0
- data/lib/chain-reactor/chain_reactor_client.rb +83 -0
- data/lib/chain-reactor/chainfile_parser.rb +81 -0
- data/lib/chain-reactor/client.rb +64 -0
- data/lib/chain-reactor/client_connection.rb +48 -0
- data/lib/chain-reactor/conf.rb +137 -0
- data/lib/chain-reactor/controller.rb +57 -0
- data/lib/chain-reactor/create_log.rb +22 -0
- data/lib/chain-reactor/parser_factory.rb +23 -0
- data/lib/chain-reactor/parsers/json_parser.rb +20 -0
- data/lib/chain-reactor/parsers/parser.rb +50 -0
- data/lib/chain-reactor/parsers/xml_simple_parser.rb +20 -0
- data/lib/chain-reactor/reaction.rb +47 -0
- data/lib/chain-reactor/reactor.rb +69 -0
- data/lib/chain-reactor/server.rb +94 -0
- data/lib/chain-reactor/version.rb +5 -0
- data/test/chainfile.test +7 -0
- data/test/dummy_parser.rb +9 -0
- data/test/helpers.rb +71 -0
- data/test/test_chain_reactor_options.rb +69 -0
- data/test/test_chain_reactor_start.rb +88 -0
- data/test/test_chainfile_parser.rb +67 -0
- data/test/test_client_connection.rb +57 -0
- data/test/test_conf.rb +90 -0
- data/test/test_json_parser.rb +55 -0
- data/test/test_parser_factory.rb +30 -0
- data/test/test_reaction.rb +39 -0
- data/test/test_reactor.rb +63 -0
- data/test/test_xml_simple_parser.rb +65 -0
- metadata +85 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
module ChainReactor
|
2
|
+
|
3
|
+
# Raised if there's an error running a reaction.
|
4
|
+
#
|
5
|
+
# This is a simple wrapper around the originally raised exception.
|
6
|
+
class ReactionError < StandardError
|
7
|
+
attr_accessor :original_exception
|
8
|
+
def initialize(original_exception)
|
9
|
+
@original_exception = original_exception
|
10
|
+
super(original_exception.message)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Represents a single reaction block, defined in the chain file with the 'react_to' method.
|
15
|
+
class Reaction
|
16
|
+
|
17
|
+
require 'parser_factory'
|
18
|
+
|
19
|
+
# The previous return value from this reaction, if executed before.
|
20
|
+
attr_accessor :previous_result
|
21
|
+
# The previous data set sent to this reaction, if executed before.
|
22
|
+
attr_accessor :previous_data
|
23
|
+
# Options used by this reaction.
|
24
|
+
attr_accessor :options
|
25
|
+
|
26
|
+
# Create a new reaction, with the options and code block to run.
|
27
|
+
def initialize(options = {},block,logger)
|
28
|
+
@options = { :parser => :json, :required_keys => [], :keys_to_sym => true }.merge(options)
|
29
|
+
@block = block
|
30
|
+
@log = logger
|
31
|
+
@log.debug { "Created reaction with options: #{options}" }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Executes the block of code after parsing the data string.
|
35
|
+
def execute(data_string)
|
36
|
+
parser = ParserFactory.get_parser(@options[:parser],@log)
|
37
|
+
data = parser.parse(data_string,@options[:required_keys],@options[:keys_to_sym])
|
38
|
+
begin
|
39
|
+
@previous_result = @block.call(data)
|
40
|
+
@previous_data = data
|
41
|
+
rescue StandardError => e
|
42
|
+
raise ReactionError.new(e)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module ChainReactor
|
2
|
+
require 'reaction'
|
3
|
+
|
4
|
+
# Contains the map of reactions and allowed client addresses.
|
5
|
+
#
|
6
|
+
# This is used to determine which clients are allowed to connect, and to
|
7
|
+
# dispatch reactions.
|
8
|
+
class Reactor
|
9
|
+
|
10
|
+
# Pass a logger object and define an empty address map.
|
11
|
+
def initialize(logger)
|
12
|
+
@address_map = {}
|
13
|
+
@log = logger
|
14
|
+
end
|
15
|
+
|
16
|
+
# Add a react_to block from the chain file.
|
17
|
+
#
|
18
|
+
# Creates a Reaction object and calls add_address().
|
19
|
+
def add(addresses,options,block)
|
20
|
+
reaction = Reaction.new(options,block,@log)
|
21
|
+
addresses.each { |addr| add_address(addr,reaction) }
|
22
|
+
end
|
23
|
+
|
24
|
+
# Add an address with a reaction.
|
25
|
+
def add_address(address,reaction)
|
26
|
+
@log.debug { "Linking reaction to address #{address}" }
|
27
|
+
|
28
|
+
if @address_map.has_key? address
|
29
|
+
@address_map[address] << reaction
|
30
|
+
else
|
31
|
+
@address_map[address] = [reaction]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# React to a client by running the associated reactions.
|
36
|
+
#
|
37
|
+
# Raises an error if the address is not allowed - use
|
38
|
+
# address_allowed? first.
|
39
|
+
def react(address,data_string)
|
40
|
+
address_allowed?(address) or raise 'Address is not allowed'
|
41
|
+
@address_map[address].each do |reaction|
|
42
|
+
@log.info { "Executing reaction for address #{address}" }
|
43
|
+
begin
|
44
|
+
reaction.execute(data_string)
|
45
|
+
rescue Parsers::ParseError => e
|
46
|
+
@log.error { "Parser error: #{e.message}" }
|
47
|
+
rescue Parsers::RequiredKeyError => e
|
48
|
+
@log.error { "Client data invalid: #{e.message}" }
|
49
|
+
rescue ReactionError => e
|
50
|
+
@log.error { 'Exception raised in reaction: '+e.message }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Get an array of reactions for a given address
|
56
|
+
def reactions_for(address)
|
57
|
+
@address_map[address] if address_allowed?(address)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Check whether the IP address is allowed.
|
61
|
+
#
|
62
|
+
# An IP address is allowed if the chain file specifies a "react_to" block
|
63
|
+
# with that address.
|
64
|
+
def address_allowed?(address)
|
65
|
+
@address_map.has_key? address
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module ChainReactor
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'parsers/parser'
|
5
|
+
require 'client_connection'
|
6
|
+
|
7
|
+
# Creates a server socket and listens for client connections on an infinite
|
8
|
+
# loop. Each connecting client creates a new thread, allowing multiple
|
9
|
+
# connections at the same time.
|
10
|
+
class Server
|
11
|
+
|
12
|
+
# Create a new <tt>TCPServer</tt> listening on the given port.
|
13
|
+
def initialize(address,port,reactor,logger)
|
14
|
+
@server = TCPServer.open(address,port)
|
15
|
+
@reactor = reactor
|
16
|
+
@log = logger
|
17
|
+
@log.info { "Started ChainReactor v#{VERSION} server on #{address}:#{port.to_s}" }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Start the server listening on an infinite loop.
|
21
|
+
#
|
22
|
+
# The multithreaded option allows each client connection to be opened
|
23
|
+
# in a new thread.
|
24
|
+
#
|
25
|
+
# Keyboard interrupts are caught and handled gracefully.
|
26
|
+
def start(multithreaded)
|
27
|
+
if multithreaded
|
28
|
+
Thread.abort_on_exception = true
|
29
|
+
@log.info "Accepting concurrent connections with new threads"
|
30
|
+
end
|
31
|
+
begin
|
32
|
+
loop do
|
33
|
+
if multithreaded
|
34
|
+
Thread.new(@server.accept) do |client_sock|
|
35
|
+
handle_sock(client_sock)
|
36
|
+
end
|
37
|
+
else
|
38
|
+
handle_sock(@server.accept)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
rescue Interrupt, SystemExit => e
|
42
|
+
@server.close
|
43
|
+
@log.info { "Shutting down the ChainReactor server" }
|
44
|
+
raise e
|
45
|
+
rescue Exception => e
|
46
|
+
@server.close
|
47
|
+
raise e
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Create a new ClientConnection when a socket is opened.
|
54
|
+
def handle_sock(client_sock)
|
55
|
+
begin
|
56
|
+
client = ClientConnection.new(client_sock,@log)
|
57
|
+
handle_client(client)
|
58
|
+
rescue ClientConnectionError => e
|
59
|
+
@log.error { "Client error: #{e.message}" }
|
60
|
+
client.close
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Handle a single client connection.
|
65
|
+
#
|
66
|
+
# First, the IP address is checked to determine whether it's
|
67
|
+
# allowed to connect. Then the server sends a welcome
|
68
|
+
# message so the client knows what it's connected to. Finally,
|
69
|
+
# data is read from the client and a reaction is executed.
|
70
|
+
def handle_client(client)
|
71
|
+
unless @reactor.address_allowed? client.ip
|
72
|
+
client.close
|
73
|
+
@log.warn { "Terminated connection from unauthorized client #{client.ip}" }
|
74
|
+
return
|
75
|
+
end
|
76
|
+
|
77
|
+
client.say("ChainReactor v#{VERSION}")
|
78
|
+
|
79
|
+
client_string = ''
|
80
|
+
while l = client.read
|
81
|
+
client_string += l
|
82
|
+
end
|
83
|
+
|
84
|
+
@log.debug { "Read from client #{client.ip}: #{client_string}" }
|
85
|
+
@log.info { "Finished reading from client #{client.ip}, closing connection" }
|
86
|
+
|
87
|
+
client.close
|
88
|
+
|
89
|
+
@reactor.react(client.ip,client_string)
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
data/test/chainfile.test
ADDED
data/test/helpers.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
|
2
|
+
module ChainReactor
|
3
|
+
module TestHelpers
|
4
|
+
class Logger
|
5
|
+
def debug
|
6
|
+
end
|
7
|
+
def info
|
8
|
+
end
|
9
|
+
def warning
|
10
|
+
end
|
11
|
+
def error
|
12
|
+
end
|
13
|
+
def fatal
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Params
|
18
|
+
def initialize(hash_data)
|
19
|
+
@hash = hash_data
|
20
|
+
end
|
21
|
+
|
22
|
+
def method_missing(name, *args, &block)
|
23
|
+
@hash.send(name, *args, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def fetch(key,&block)
|
27
|
+
begin
|
28
|
+
@hash.fetch(key,&block)
|
29
|
+
rescue KeyError => e
|
30
|
+
raise ::Main::Parameter::NoneSuch, key
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
class CliParam
|
37
|
+
def initialize(value)
|
38
|
+
@value = value
|
39
|
+
@given = true
|
40
|
+
end
|
41
|
+
|
42
|
+
def value
|
43
|
+
@value
|
44
|
+
end
|
45
|
+
|
46
|
+
def given=(given)
|
47
|
+
@given = given
|
48
|
+
end
|
49
|
+
|
50
|
+
def given?
|
51
|
+
@given
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class File
|
56
|
+
def initialize(string)
|
57
|
+
@string = string
|
58
|
+
end
|
59
|
+
|
60
|
+
def read
|
61
|
+
@string
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_logger
|
66
|
+
Logger.new
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'open3'
|
4
|
+
|
5
|
+
# Test case for the options of the chain reactor executable.
|
6
|
+
class TestChainReactorOptions < Test::Unit::TestCase
|
7
|
+
|
8
|
+
def exec_with(arg_string)
|
9
|
+
bin_path = File.dirname(__FILE__)+'/../bin/chain-reactor'
|
10
|
+
Open3.popen3("#{bin_path} #{arg_string}")
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_help_returns_help_in_stdout
|
14
|
+
out = exec_with('--help')
|
15
|
+
help = out[1].read
|
16
|
+
status = out[3].value
|
17
|
+
|
18
|
+
assert_equal 1, status.exitstatus
|
19
|
+
assert_match(/NAME\s*chain\-reactor/, help)
|
20
|
+
assert_match(/SYNOPSIS\s*chain\-reactor \(start|stop|template\) chainfile\.rb/, help)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_no_args_returns_help_in_stdout_and_fails
|
24
|
+
out = exec_with('')
|
25
|
+
help = out[1].read
|
26
|
+
status = out[3].value
|
27
|
+
|
28
|
+
assert_equal 1, status.exitstatus
|
29
|
+
assert_match(/NAME\s*chain\-reactor/, help)
|
30
|
+
assert_match(/SYNOPSIS\s*chain\-reactor \(start|stop|template\) chainfile\.rb/, help)
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_invalid_mode_returns_help_and_fails
|
34
|
+
out = exec_with('ssaduhdui')
|
35
|
+
help = out[1].read
|
36
|
+
status = out[3].value
|
37
|
+
|
38
|
+
assert_equal 1, status.exitstatus
|
39
|
+
assert_match(/NAME\s*chain\-reactor/, help)
|
40
|
+
assert_match(/SYNOPSIS\s*chain\-reactor \(start|stop|template\) chainfile\.rb/, help)
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_template_returns_chainfile_in_stdout
|
44
|
+
out = exec_with('template')
|
45
|
+
template = out[1].read
|
46
|
+
status = out[3].value
|
47
|
+
|
48
|
+
assert_equal 0, status.exitstatus
|
49
|
+
assert_match(/react_to\(/, template)
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_start_without_chainfile_fails
|
53
|
+
out = exec_with('start')
|
54
|
+
error = out[2].read
|
55
|
+
status = out[3].value
|
56
|
+
|
57
|
+
assert_equal 1, status.exitstatus
|
58
|
+
assert_match(/chain-reactor: A valid chainfile must be supplied/, error)
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_stop_without_chainfile_fails
|
62
|
+
out = exec_with('stop')
|
63
|
+
error = out[2].read
|
64
|
+
status = out[3].value
|
65
|
+
|
66
|
+
assert_equal 1, status.exitstatus
|
67
|
+
assert_match(/chain-reactor: A valid chainfile must be supplied/, error)
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'open3'
|
4
|
+
require 'sys/proctable'
|
5
|
+
require 'client'
|
6
|
+
|
7
|
+
# Test case for the options of the chain reactor executable.
|
8
|
+
class TestChainReactorStart < Test::Unit::TestCase
|
9
|
+
include Sys
|
10
|
+
|
11
|
+
def setup
|
12
|
+
@test_path = File.dirname(__FILE__)
|
13
|
+
@bin_path = @test_path + '/../bin/chain-reactor'
|
14
|
+
@chainfile_path = @test_path + '/chainfile.test'
|
15
|
+
@pid_path = @test_path + '/chain-reactor.pid'
|
16
|
+
end
|
17
|
+
|
18
|
+
def teardown
|
19
|
+
_,_,_,t = stop_chain_reactor
|
20
|
+
t.value
|
21
|
+
|
22
|
+
$stdout = STDOUT
|
23
|
+
end
|
24
|
+
|
25
|
+
def start_chain_reactor(arg_string)
|
26
|
+
Open3.popen3("#{@bin_path} start #{@chainfile_path} #{arg_string} --pidfile #{@pid_path}")
|
27
|
+
end
|
28
|
+
|
29
|
+
def stop_chain_reactor
|
30
|
+
Open3.popen3("#{@bin_path} stop #{@chainfile_path} --pidfile #{@pid_path}")
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_start_daemon
|
34
|
+
_, stdout, _, thread = start_chain_reactor('')
|
35
|
+
output = stdout.read
|
36
|
+
|
37
|
+
assert_match(/Registered 1 reactions/,output)
|
38
|
+
assert_match(/Starting daemon, PID file => #{@pid_path}/,output)
|
39
|
+
assert_match(/Daemon has started successfully/,output)
|
40
|
+
|
41
|
+
# Thread stopped as it's daemonized
|
42
|
+
assert_equal true, thread.stop?
|
43
|
+
|
44
|
+
assert File.file? @pid_path
|
45
|
+
pid = File.new(@pid_path).read.to_i
|
46
|
+
prc = ProcTable.ps(pid)
|
47
|
+
|
48
|
+
assert_not_nil prc
|
49
|
+
assert_match(/chain\-reactor start/,prc.cmdline)
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_start_daemon_and_communicate_with_client
|
53
|
+
_, stdout, _, _ = start_chain_reactor('')
|
54
|
+
output = stdout.read
|
55
|
+
assert_match(/Daemon has started successfully/,output)
|
56
|
+
|
57
|
+
assert_nothing_raised ChainReactor::ClientError do
|
58
|
+
$stdout = File.new('/dev/null','w')
|
59
|
+
client = ChainReactor::Client.new('127.0.0.1',20000)
|
60
|
+
client.send_as_json({:hello => :world})
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_start_daemon_with_port_override_and_communicate_with_client
|
65
|
+
_, stdout, _, _ = start_chain_reactor('--port 20100')
|
66
|
+
output = stdout.read
|
67
|
+
assert_match(/Daemon has started successfully/,output)
|
68
|
+
|
69
|
+
assert_nothing_raised ChainReactor::ClientError do
|
70
|
+
$stdout = File.new('/dev/null','w')
|
71
|
+
client = ChainReactor::Client.new('127.0.0.1',20100)
|
72
|
+
client.send({:hello => :world})
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_start_daemon_with_address_override_and_communicate_with_client
|
77
|
+
_, stdout, _, _ = start_chain_reactor('--address 0.0.0.0')
|
78
|
+
output = stdout.read
|
79
|
+
assert_match(/Daemon has started successfully/,output)
|
80
|
+
|
81
|
+
assert_nothing_raised ChainReactor::ClientError do
|
82
|
+
$stdout = File.new('/dev/null','w')
|
83
|
+
client = ChainReactor::Client.new('0.0.0.0',20000)
|
84
|
+
client.send({:hello => :world})
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|