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