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,64 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module ChainReactor
|
5
|
+
|
6
|
+
# An error raised if there are communication problems with the server.
|
7
|
+
class ClientError < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
# A client for connecting to a chain reactor server.
|
11
|
+
class Client
|
12
|
+
# The version of the chain reactor server.
|
13
|
+
attr_reader :version
|
14
|
+
|
15
|
+
# Create a connection to a chain reactor server at the given address and port.
|
16
|
+
def initialize(server_addr,server_port)
|
17
|
+
begin
|
18
|
+
@socket = TCPSocket.new(server_addr, server_port)
|
19
|
+
rescue Errno::ECONNREFUSED
|
20
|
+
raise ClientError, "Failed to connect to Chain Reactor server on #{server_addr}:#{server_port}"
|
21
|
+
end
|
22
|
+
connect
|
23
|
+
end
|
24
|
+
|
25
|
+
# Send hash data to the server as a JSON
|
26
|
+
def send_as_json(data_hash)
|
27
|
+
if data_hash.length > 0
|
28
|
+
json_string = JSON.generate(data_hash)
|
29
|
+
send(json_string)
|
30
|
+
else
|
31
|
+
raise ClientError, 'Cannot send empty data to chain reactor server'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Send a data string to the server
|
36
|
+
def send(data_string)
|
37
|
+
if data_string.length > 0
|
38
|
+
puts "Sending data: #{data_string}"
|
39
|
+
@socket.puts data_string
|
40
|
+
else
|
41
|
+
raise ClientError, 'Cannot send empty data to chain reactor server'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Close the client socket.
|
46
|
+
def close
|
47
|
+
@socket.close
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
# Connect and check the server response.
|
52
|
+
def connect
|
53
|
+
intro = @socket.gets
|
54
|
+
matches = /^ChainReactor v([\d.]+)/.match(intro)
|
55
|
+
if matches
|
56
|
+
@version = matches[1]
|
57
|
+
else
|
58
|
+
@socket.close
|
59
|
+
raise ClientError, "Invalid server response: #{intro}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module ChainReactor
|
2
|
+
|
3
|
+
# Exception used by the ClientConnection class, signifying incorrect connection
|
4
|
+
# details for a client.
|
5
|
+
class ClientConnectionError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
# Holds information about the connected client, and provides access to the
|
9
|
+
# socket.
|
10
|
+
#
|
11
|
+
# The host, ip and port of the client are provided as attributes.
|
12
|
+
class ClientConnection
|
13
|
+
|
14
|
+
# Client's IP address.
|
15
|
+
attr_reader :ip
|
16
|
+
# Client's port.
|
17
|
+
attr_reader :port
|
18
|
+
|
19
|
+
# Create the ClientConnection with a TCPSocket. This socket holds connection
|
20
|
+
# parameters and allows data transfer both ways.
|
21
|
+
def initialize(socket,logger)
|
22
|
+
@socket = socket
|
23
|
+
fam, @port, *addr = @socket.getpeername.unpack('nnC4')
|
24
|
+
|
25
|
+
@ip = addr.join('.')
|
26
|
+
@log = logger
|
27
|
+
@log.info { "New connection from #{@ip}:#{@port}" }
|
28
|
+
end
|
29
|
+
|
30
|
+
# Write a string to the client socket, using <tt>TCPSocket.puts</tt>.
|
31
|
+
def say(string)
|
32
|
+
@socket.puts string
|
33
|
+
end
|
34
|
+
|
35
|
+
# Read from the client socket, using <tt>TCPSocket.gets</tt>.
|
36
|
+
def read
|
37
|
+
@socket.gets
|
38
|
+
end
|
39
|
+
|
40
|
+
# Close the socket connection, using <tt>TCPSocket.close</tt>.
|
41
|
+
def close
|
42
|
+
@log.info { "Closing connection to client #{@ip}" }
|
43
|
+
@socket.close
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module ChainReactor
|
2
|
+
# Raised when trying to access a non-existent configuration option.
|
3
|
+
class ConfError < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
require 'main'
|
7
|
+
|
8
|
+
# Configuration object that combines default values and command line options.
|
9
|
+
#
|
10
|
+
# The command line parameters are provided through the gem 'main', and these
|
11
|
+
# take precedent over options set through the chain file.
|
12
|
+
class Conf
|
13
|
+
|
14
|
+
# Create a new Conf object with the parameters from Main.
|
15
|
+
def initialize(cli_params)
|
16
|
+
@params = cli_params
|
17
|
+
@defaults = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Get the chainfile, as a File object.
|
21
|
+
#
|
22
|
+
# This is the exception - it has to come as a CLI parameter.
|
23
|
+
def chainfile
|
24
|
+
@params[:chainfile].value
|
25
|
+
end
|
26
|
+
|
27
|
+
# Set the default bind IP address.
|
28
|
+
def address=(address)
|
29
|
+
set_default :address, address
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get the IP address to bind to.
|
33
|
+
def address
|
34
|
+
get_value :address
|
35
|
+
end
|
36
|
+
|
37
|
+
# Set the log file location.
|
38
|
+
def log_file=(log_file)
|
39
|
+
set_default :logfile, log_file
|
40
|
+
end
|
41
|
+
|
42
|
+
# Get the log file location.
|
43
|
+
def log_file
|
44
|
+
get_value :logfile
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set the pid file location.
|
48
|
+
def pid_file=(pid_file)
|
49
|
+
set_default :pidfile, pid_file
|
50
|
+
end
|
51
|
+
|
52
|
+
# Get the pid file location.
|
53
|
+
def pid_file
|
54
|
+
get_value :pidfile
|
55
|
+
end
|
56
|
+
|
57
|
+
# Set the default port number.
|
58
|
+
def port=(port)
|
59
|
+
set_default :port, port
|
60
|
+
end
|
61
|
+
|
62
|
+
# Get the port number.
|
63
|
+
def port
|
64
|
+
get_value :port
|
65
|
+
end
|
66
|
+
|
67
|
+
# Set the whether to run multithreaded by default.
|
68
|
+
def multithreaded=(multithreaded)
|
69
|
+
set_default :multithreaded, multithreaded
|
70
|
+
end
|
71
|
+
|
72
|
+
# Whether the server should open each client connection in a new thread.
|
73
|
+
def multithreaded
|
74
|
+
get_value :multithreaded
|
75
|
+
end
|
76
|
+
|
77
|
+
# Set the default verbosity.
|
78
|
+
def verbosity=(verbosity)
|
79
|
+
set_default :verbosity, verbosity
|
80
|
+
end
|
81
|
+
|
82
|
+
# How verbose the program should be.
|
83
|
+
def verbosity
|
84
|
+
get_value :verbosity
|
85
|
+
end
|
86
|
+
|
87
|
+
# Set whether to run on top (instead of daemonizing).
|
88
|
+
def on_top=(on_top)
|
89
|
+
set_default :ontop, on_top
|
90
|
+
end
|
91
|
+
|
92
|
+
# Whether to run on top (instead of daemonizing).
|
93
|
+
def on_top
|
94
|
+
get_value :ontop
|
95
|
+
end
|
96
|
+
|
97
|
+
# Alias for on_top().
|
98
|
+
alias :on_top? :on_top
|
99
|
+
# Alias for multithreaded().
|
100
|
+
alias :multithreaded? :multithreaded
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
# Set the default value for a key.
|
105
|
+
#
|
106
|
+
# This value is used only if the equivalent command line
|
107
|
+
# parameter is not given.
|
108
|
+
def set_default(key,value)
|
109
|
+
if @defaults.nil?
|
110
|
+
@defaults = {}
|
111
|
+
end
|
112
|
+
@defaults[key] = value
|
113
|
+
end
|
114
|
+
|
115
|
+
# Get the value for a key.
|
116
|
+
#
|
117
|
+
# The command line parameters take precedence if given explicitly,
|
118
|
+
# followed by values set on the Conf object. Finally, default command
|
119
|
+
# line values are used.
|
120
|
+
#
|
121
|
+
# A ConfError exception is thrown if the key is unknown.
|
122
|
+
def get_value(key)
|
123
|
+
begin
|
124
|
+
param = @params.fetch(key)
|
125
|
+
if param.given?
|
126
|
+
param.value
|
127
|
+
elsif @defaults.has_key? key
|
128
|
+
@defaults[key]
|
129
|
+
else
|
130
|
+
param.value
|
131
|
+
end
|
132
|
+
rescue ::Main::Parameter::NoneSuch
|
133
|
+
raise ConfError, "Missing configuration parameter '#{key}'"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module ChainReactor
|
2
|
+
|
3
|
+
# The Controller manages starting and stopping of the server, and daemonizing.
|
4
|
+
class Controller
|
5
|
+
|
6
|
+
require 'chainfile_parser'
|
7
|
+
require 'server'
|
8
|
+
require 'dante'
|
9
|
+
|
10
|
+
# Parse the chain file with a ChainfileParser object.
|
11
|
+
def initialize(config,log)
|
12
|
+
@config = config
|
13
|
+
@log = log
|
14
|
+
# log to STDOUT/ERR while parsing the chain file, only daemonize when complete.
|
15
|
+
@reactor = ChainfileParser.new(@config.chainfile,@config,@log).parse
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
# Start the server, as a daemon or on top if --ontop is supplied as a CLI arg.
|
20
|
+
#
|
21
|
+
# Uses dante as the daemonizer.
|
22
|
+
def start
|
23
|
+
# Change output format for logging to file if daemonizing
|
24
|
+
unless @config.on_top?
|
25
|
+
@log.outputters.first.formatter = ChainReactor::PatternFormatter.new(:pattern => "[%l] %d :: %m")
|
26
|
+
@log.info { "Starting daemon, PID file => #{@config.pid_file}" }
|
27
|
+
end
|
28
|
+
|
29
|
+
# Dante picks up ARGV, so remove it
|
30
|
+
ARGV.replace []
|
31
|
+
server = Server.new(@config.address,@config.port,@reactor,@log)
|
32
|
+
|
33
|
+
Dante::Runner.new('chain-reactor').execute(daemon_opts) do
|
34
|
+
server.start(@config.multithreaded?)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Stop a daemonized process via the PID file specified in the options.
|
39
|
+
def stop
|
40
|
+
ARGV.replace []
|
41
|
+
Dante::Runner.new('chain-reactor').execute(:kill => true,
|
42
|
+
:pid_path => @config.pid_file)
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
# Get a hash of the options passed to the daemonizer, dante.
|
48
|
+
def daemon_opts
|
49
|
+
{
|
50
|
+
:daemonize => !@config.on_top,
|
51
|
+
:pid_path => @config.pid_file,
|
52
|
+
:log_path => @config.log_file,
|
53
|
+
:debug => true
|
54
|
+
}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ChainReactor
|
2
|
+
|
3
|
+
require 'log4r'
|
4
|
+
include Log4r
|
5
|
+
|
6
|
+
# Creates a logger object that prints to STDOUT.
|
7
|
+
def self.create_logger(level)
|
8
|
+
log = self.create_empty_logger(level)
|
9
|
+
|
10
|
+
outputter = Outputter.stdout
|
11
|
+
outputter.formatter = PatternFormatter.new(:pattern => "%l\t%m")
|
12
|
+
log.outputters << outputter
|
13
|
+
log
|
14
|
+
end
|
15
|
+
|
16
|
+
# Creates a logger object with no outputter.
|
17
|
+
def self.create_empty_logger(level)
|
18
|
+
log = Logger.new 'chain-reactor'
|
19
|
+
log.level = ChainReactor.const_get(level.upcase)
|
20
|
+
log
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ChainReactor
|
2
|
+
|
3
|
+
require 'parsers/parser'
|
4
|
+
require 'parsers/json_parser'
|
5
|
+
|
6
|
+
# Used to parse strings using a method defined by child classes.
|
7
|
+
class ParserFactory
|
8
|
+
|
9
|
+
# Class method for retrieving a new Parser object depending on the type
|
10
|
+
# variable.
|
11
|
+
def self.get_parser(type,logger)
|
12
|
+
class_name = type.to_s.capitalize
|
13
|
+
if class_name.include? "_"
|
14
|
+
class_name = class_namesplit('_').map{|e| e.capitalize}.join
|
15
|
+
end
|
16
|
+
parser_class_name = class_name + 'Parser'
|
17
|
+
logger.debug { "Creating parser: #{parser_class_name}" }
|
18
|
+
parser_class = ChainReactor::Parsers.const_get parser_class_name
|
19
|
+
parser_class.new(logger)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ChainReactor::Parsers
|
2
|
+
|
3
|
+
# Parse the string as a JSON object.
|
4
|
+
class JsonParser < Parser
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
# Parse a JSON string, returning the result as a hash.
|
8
|
+
#
|
9
|
+
# Raises a ParseError on failure.
|
10
|
+
def do_parse(string)
|
11
|
+
begin
|
12
|
+
@log.debug { "Parsing JSON string #{string.inspect}" }
|
13
|
+
JSON.parse(string)
|
14
|
+
rescue JSON::ParserError => e
|
15
|
+
raise ParseError, "Data from client is not a valid JSON: #{string}, error: #{e.message}, data: #{string}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# Contains all parsers used to convert clients' data strings to ruby hashes.
|
2
|
+
module ChainReactor::Parsers
|
3
|
+
|
4
|
+
# Error raised if there's an error parsing a string.
|
5
|
+
class ParseError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
# Error raised if a required key is missing
|
9
|
+
class RequiredKeyError < StandardError
|
10
|
+
end
|
11
|
+
|
12
|
+
# Base class for all concrete implementations of parsers.
|
13
|
+
class Parser
|
14
|
+
|
15
|
+
# Set up the parser with a logger object.
|
16
|
+
def initialize(logger)
|
17
|
+
@log = logger
|
18
|
+
end
|
19
|
+
|
20
|
+
# Parse the string sent by the client.
|
21
|
+
#
|
22
|
+
# Calls the do_parse() method which is defined by a subclass.
|
23
|
+
#
|
24
|
+
# Required keys can be passed as an array, and a RequiredKeyError
|
25
|
+
# is raised if any of those keys don't exist in the returned value
|
26
|
+
# from do_parse().
|
27
|
+
#
|
28
|
+
# keys_to_sym converts all string keys to symbol keys, and is true
|
29
|
+
# by default.
|
30
|
+
def parse(string,required_keys,keys_to_sym)
|
31
|
+
data = do_parse(string.strip)
|
32
|
+
if keys_to_sym
|
33
|
+
@log.debug { "Converting data keys #{data.keys} to symbols" }
|
34
|
+
data = Hash[data.map { |k,v| [k.to_sym, v] }]
|
35
|
+
end
|
36
|
+
required_keys.each do |key|
|
37
|
+
data.has_key? key or raise RequiredKeyError, "Required key '#{key}' is missing from data #{data}"
|
38
|
+
end
|
39
|
+
data
|
40
|
+
end
|
41
|
+
|
42
|
+
# Parse the string, using an implmentation defined by child classes.
|
43
|
+
#
|
44
|
+
# Should return a hash.
|
45
|
+
def do_parse(string)
|
46
|
+
raise 'This method should implement a string to hash parser'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ChainReactor::Parsers
|
2
|
+
|
3
|
+
# Parse the string using the xml simple library.
|
4
|
+
class XmlSimpleParser < Parser
|
5
|
+
|
6
|
+
require 'xmlsimple'
|
7
|
+
|
8
|
+
# Parse an XML string, returning the result as a hash.
|
9
|
+
#
|
10
|
+
# Raises a ParseError on failure.
|
11
|
+
def do_parse(string)
|
12
|
+
begin
|
13
|
+
@log.debug { "Parsing XML string #{string.inspect}" }
|
14
|
+
XmlSimple.xml_in(string)
|
15
|
+
rescue StandardError => e
|
16
|
+
raise ParseError, "Data from client is not a valid XML string: #{string}, #{e.class.name} error: #{e.message}, data: #{string}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|