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.
@@ -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