chain-reactor 0.2.0

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