chain-reactor 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in chain-reactor.gemspec
4
+ gemspec
5
+
6
+ gem 'json'
7
+ gem 'mocha'
8
+ gem 'dante'
9
+ gem 'log4r'
10
+
11
+ gem 'xml-simple', :require => false
12
+
13
+ group :test do
14
+ gem 'sys-proctable'
15
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1 @@
1
+ See Ruby's license
data/README.rdoc ADDED
@@ -0,0 +1,54 @@
1
+ = Chain Reactor
2
+
3
+ Chain Reactor is a simple server that waits for incoming connections, then kicks off some code when one is made. Clients are locked down by IP address, so you will only recieve connections from known clients. Also, data can be passed via JSON, XML, or anything you like.
4
+
5
+ The "reactions" are written in ruby code, like this:
6
+
7
+ react_to '192.168.0.1' do |data|
8
+ puts "Hello, 192.168.0.1, I'm going to do something now!"
9
+ puts data.inspect #=> Prints a hash
10
+ end
11
+
12
+ It really is that easy. Also, you can respond to multiple clients with the same code block:
13
+
14
+ react_to ['192.168.0.1','192.168.0.2'] do |data|
15
+ puts "Hello, I'm going to do something now!"
16
+ end
17
+
18
+ It can be run as a daemon, multithreaded (each client connection in a new thread), and bind to different addresses and ports.
19
+
20
+ == Why?
21
+
22
+ This project started as a way of notifying other machines about builds happening on Jenkins, our continuous integration server. A chain reactor server would be sitting, listening for connections. When a build fails or passes on Jenkins, it lets the chain reactor server(s) know, pushing the build data. The chain reactor server then does what it likes, and the Jenkins server doesn't need to know.
23
+
24
+ I'm sure this has plenty of other uses, but it works great with continuous integration set-ups.
25
+
26
+ == Installation
27
+
28
+ This can be installed through ruby gems:
29
+
30
+ $ gem install chain-reactor
31
+
32
+ == Usage
33
+
34
+ A chain file is required to run, as this specifies which clients to accept and what to do. Fortunately, it's easy to create a template:
35
+
36
+ $ chain-reactor template > Chainfile.rb
37
+
38
+ Open it up and edit it to your liking. You might want to test it locally first, i.e. only react to 127.0.0.1 addresses. In one terminal, run:
39
+
40
+ $ chain-reactor start Chainfile.rb --ontop
41
+
42
+ The --ontop option stops it from running as a daemon, making it easier to see what's happening. By default, the chain reactor server runs on port 1987, but that's configurable if you want to change it. In another terminal, run the client with:
43
+
44
+ $ chain-reactor-client --address 127.0.0.1
45
+
46
+ Follow the instructions by adding key/value pair data to send to the server, then watch it react!
47
+
48
+ == Contributing
49
+
50
+ 1. Fork it
51
+ 2. Create your feature branch (<tt>git checkout -b my-new-feature</tt>)
52
+ 3. Commit your changes (<tt>git commit -am 'Add some feature'</tt>)
53
+ 4. Push to the branch (<tt>git push origin my-new-feature</tt>)
54
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+ require 'rdoc/task'
4
+
5
+ # Test task ----------------------------
6
+
7
+ Rake::TestTask.new do |t|
8
+ t.libs << 'lib/chain-reactor'
9
+ t.libs << 'test'
10
+ t.libs << 'bin'
11
+ end
12
+
13
+ desc "Run tests"
14
+ task :default => :test
15
+
16
+ # RDoc task ----------------------------
17
+
18
+ rd = RDoc::Task.new("rdoc") { |rdoc|
19
+ rdoc.rdoc_dir = 'doc'
20
+ rdoc.title = "Chain Reactor"
21
+ rdoc.options << '--line-numbers' << '--main' << 'README.rdoc'
22
+ rdoc.rdoc_files.include('lib/**/*.rb', '[A-Z]*\.[a-z]*')
23
+ }
24
+
data/bin/chain-reactor ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+ require 'chain-reactor/chain_reactor.rb'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+ require 'chain-reactor/chain_reactor_client.rb'
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'chain-reactor/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "chain-reactor"
8
+ gem.version = ChainReactor::VERSION
9
+ gem.license = 'MIT'
10
+ gem.authors = ["Jon Cairns"]
11
+ gem.email = ["jon@joncairns.com"]
12
+ gem.description = %q{Trigger events across networks using TCP/IP sockets}
13
+ gem.summary = %q{A TCP/IP server that triggers code on connection}
14
+ gem.homepage = "http://github.com/joonty/chain-reactor"
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = %w(chain-reactor chain-reactor-client)
18
+ gem.test_files = `git ls-files -- test/test_`.split($/)
19
+ gem.require_paths = ["lib"]
20
+ end
@@ -0,0 +1,208 @@
1
+ module ChainReactor
2
+
3
+ lib_dir = File.dirname(__FILE__)
4
+ $:.unshift lib_dir
5
+
6
+ require 'version'
7
+ require 'rubygems'
8
+ require 'main'
9
+ require 'create_log'
10
+ require 'conf'
11
+
12
+ # Set up command line options and usage with the main gem.
13
+ Main do
14
+ examples 'chain-reactor start chainfile.rb ',
15
+ 'chain-reactor stop chainfile.rb',
16
+ 'chain-reactor template'
17
+
18
+ description 'Chain reactor is a server that responds to network events and runs ruby code. Run with the `start\' mode and \'--help\' to see options, or use the `template\' mode to create an example chainfile.'
19
+
20
+ # Show the command line options if run without a mode.
21
+ def run
22
+ help!
23
+ end
24
+
25
+ # Start the server, as a daemon or on top.
26
+ mode :start do
27
+
28
+ description 'Start the chain reactor server, as a daemon or (optionally) on top. Daemonizing starts a background process, creates a pid file and a log file.'
29
+
30
+ examples 'chain-reactor start chainfile.rb ',
31
+ 'chain-reactor start chainfile.rb --ontop',
32
+ 'chain-reactor start chainfile.rb --pidfile /path/to/pidfile.pid'
33
+
34
+ input :chainfile do
35
+ description 'A valid chainfile - run with the template mode to create an example'
36
+ error do |e|
37
+ STDERR.puts "chain-reactor: A valid chainfile must be supplied - run with the 'template' mode to generate an example"
38
+ end
39
+ end
40
+
41
+ option :debug do
42
+ log_levels = %w(debug info warn error fatal)
43
+ argument :required
44
+ validate { |str| log_levels.include? str }
45
+ synopsis '--debug=('+log_levels.join('|')+') (0 ~> debug=info)'
46
+ defaults 'info'
47
+ description 'Type of messages to send to output'
48
+ end
49
+
50
+ option :pidfile do
51
+ argument :required
52
+ description 'Pid file for the daemonized process'
53
+ defaults Dir.pwd+'/chain-reactor.pid'
54
+ end
55
+
56
+ option :multithreaded do
57
+ cast :bool
58
+ defaults false
59
+ description 'Start each new connection in a separate thread'
60
+ end
61
+
62
+ option :ontop do
63
+ cast :bool
64
+ defaults false
65
+ description 'Keep the process on top instead of daemonizing'
66
+ end
67
+
68
+ option :logfile do
69
+ argument :required
70
+ description 'Log file to write messages to'
71
+ defaults Dir.pwd+'/chain-reactor.log'
72
+ end
73
+
74
+ option :address do
75
+ argument :required
76
+ defaults '127.0.0.1'
77
+ description 'IP address to bind to'
78
+ end
79
+
80
+ option :port do
81
+ argument :required
82
+ defaults 1987
83
+ description 'Port number to bind to'
84
+ end
85
+
86
+ # Run when using the 'start' mode from the command line.
87
+ def run
88
+ require 'controller'
89
+
90
+ conf = Conf.new(params)
91
+ log = ChainReactor.create_logger(params[:debug].value)
92
+
93
+ begin
94
+ Controller.new(conf,log).start
95
+ rescue Interrupt, SystemExit
96
+ exit_status exit_success
97
+ rescue ChainfileParserError => e
98
+ log.fatal { "Failed to parse chainfile {#{e.original.class.name}}: #{e.message}" }
99
+ exit_status exit_failure
100
+ rescue Exception => e
101
+ log.fatal { "Unexpected exception {#{e.class.name}}: #{e.message}" }
102
+ log.debug { $!.backtrace.join("\n\t") }
103
+ exit_status exit_failure
104
+ end
105
+ end
106
+ end
107
+
108
+ mode :stop do
109
+ description 'Stop a running chain reactor server daemon. Specify the path to the pid file to stop a specific chain reactor instance.'
110
+
111
+ input :chainfile do
112
+ description 'A valid chainfile - run with the template argument to create a template'
113
+ error do |e|
114
+ STDERR.puts "chain-reactor: A valid chainfile must be supplied - run with the 'template' mode to generate an example"
115
+ end
116
+ end
117
+
118
+ option :debug do
119
+ log_levels = %w(debug info warn error fatal)
120
+ argument :required
121
+ validate { |str| log_levels.include? str }
122
+ synopsis '--debug=('+log_levels.join('|')+') (0 ~> debug=info)'
123
+ defaults 'info'
124
+ description 'Type of messages to send to output'
125
+ end
126
+
127
+ option :pidfile do
128
+ argument :required
129
+ description 'Pid file for the daemonized process'
130
+ defaults Dir.pwd+'/chain-reactor.pid'
131
+ end
132
+
133
+ # Run when using the 'stop' mode from the command line.
134
+ def run
135
+ require 'controller'
136
+
137
+ log = ChainReactor.create_empty_logger(params[:debug].value)
138
+ conf = Conf.new(params)
139
+
140
+ puts "Attempting to stop chain reactor server with pid file: #{conf.pid_file}"
141
+ c = Controller.new(conf,log)
142
+ failed = c.stop
143
+
144
+ exit_status(exit_failure) if failed
145
+ end
146
+
147
+ end
148
+
149
+ mode 'template' do
150
+ description 'Create a template chainfile, to see examples of configuration options and reaction syntax.'
151
+
152
+ examples 'chain-reactor template'
153
+
154
+ # Run when using the 'template' mode from the command line.
155
+ def run
156
+ puts <<-eos
157
+ #####################
158
+ # Chainfile #
159
+ #####################
160
+
161
+ # Do something when 127.0.0.1 sends a JSON
162
+ react_to('127.0.0.1') do |data|
163
+
164
+ # The JSON string sent by the client is now a ruby hash
165
+ puts data.inspect
166
+
167
+ end
168
+
169
+ # Use the same reaction for multiple clients, and require keys to exist
170
+ react_to( ['127.0.0.1','192.168.0.2'], :requires => [:mykey] ) do |data|
171
+
172
+ # You can be sure this exists
173
+ puts data[:mykey]
174
+
175
+ end
176
+
177
+ # Change the parser, if the client sends something other than a JSON
178
+ react_to('192.168.0.2', :parser => :xml_simple) do |data|
179
+
180
+ # The XML string sent by the client is now a ruby hash
181
+ puts data.inspect
182
+
183
+ end
184
+
185
+ ## Everything from here on is optional, and can be overridden
186
+ ## by equivalent command line parameters
187
+
188
+ ## Address to bind to
189
+ address '127.0.0.1'
190
+
191
+ ## Port to listen on
192
+ port 1987
193
+
194
+ ## Location of pid file, for daemon
195
+ # pidfile '/var/run/chain-reactor.pid'
196
+
197
+ ## Location of log file, for daemon
198
+ # logfile '/var/log/chain-reactor.log'
199
+
200
+ ## Whether to accept each client in a separate thread.
201
+ # multithreaded true
202
+
203
+ eos
204
+ end
205
+ end
206
+ end
207
+
208
+ end
@@ -0,0 +1,83 @@
1
+
2
+ module ChainReactor
3
+ lib_dir = File.dirname(__FILE__)
4
+ $:.unshift lib_dir
5
+
6
+ require 'version'
7
+ require 'rubygems'
8
+ require 'main'
9
+
10
+ Main do
11
+ input :data do
12
+ optional
13
+ end
14
+
15
+ option :address do
16
+ argument_required
17
+ required
18
+ cast :string
19
+ description 'Chain reactor server address'
20
+ end
21
+
22
+ option :port do
23
+ argument_required
24
+ required
25
+ cast :int
26
+ description 'Chain reactor server port number'
27
+ defaults 1987
28
+ end
29
+
30
+ # Connect to a running chain reactor server and send data.
31
+ def run
32
+ require 'client'
33
+
34
+ begin
35
+ client = Client.new params[:address].value, params[:port].value
36
+ puts "Connected to Chain Reactor server, version #{client.version}"
37
+ data_input = params[:data].value
38
+
39
+ if data_input
40
+ client.send(data_input.gets.strip)
41
+ else
42
+ client.send_as_json(get_hash_from_stdin)
43
+ client.close()
44
+ end
45
+ exit_status exit_success
46
+ rescue ClientError => e
47
+ STDERR.puts e.message
48
+ exit_status exit_failure
49
+ rescue SystemExit, Interrupt
50
+ STDERR.puts "Caught exit signal"
51
+ exit_status exit_failure
52
+ rescue Exception => e
53
+ STDERR.puts "An error occured {#{e.class.name}}: #{e.message}"
54
+ exit_status exit_failure
55
+ end
56
+ end
57
+
58
+ # Ask the user for key value pairs, and return as a hash.
59
+ def get_hash_from_stdin
60
+ STDOUT.sync = true
61
+ hash_to_send = {}
62
+ puts "No data supplied, what do you want to send? (Leave key blank to end)"
63
+ incr = 1
64
+
65
+ loop do
66
+ print "Key ##{incr}: "
67
+ key = STDIN.gets.chomp
68
+
69
+ if key == ""
70
+ break
71
+ end
72
+
73
+ print "Value ##{incr}: "
74
+ value = STDIN.gets.chomp
75
+
76
+ hash_to_send[key] = value
77
+ incr += 1
78
+ end
79
+ hash_to_send
80
+ end
81
+ end
82
+
83
+ end
@@ -0,0 +1,81 @@
1
+ module ChainReactor
2
+
3
+ require 'reactor'
4
+
5
+ # Error raised when there's a error parsing the chain file.
6
+ #
7
+ # This contains the original exception raised by eval().
8
+ class ChainfileParserError < StandardError
9
+ attr_reader :original
10
+ def initialize(original)
11
+ @original = original
12
+ super(original.message.gsub(/\sfor #<ChainReactor.*>/,''))
13
+ end
14
+ end
15
+
16
+ # Parse the chain file and register reaction code blocks and network addresses.
17
+ class ChainfileParser
18
+
19
+ # Set up a parser with a File object representing the chain file.
20
+ def initialize(chainfile,config,logger)
21
+ @file = chainfile
22
+ @config = config
23
+ @reactions = 0
24
+ @reactor = Reactor.new(logger)
25
+ @log = logger
26
+ end
27
+
28
+ # Parse the chain file and return a reactor object.
29
+ #
30
+ # A ChainfileParserError is raised if the chain file has invalid syntax.
31
+ def parse
32
+ begin
33
+ eval @file.read
34
+ rescue Exception => e
35
+ raise ChainfileParserError, e
36
+ end
37
+
38
+ if @reactions.zero?
39
+ @log.error { "No reactions registered, no reason to continue" }
40
+ raise 'No reactions registered, no reason to continue'
41
+ else
42
+ @log.info { "Registered #{@reactions} reactions in the chain file" }
43
+ end
44
+
45
+ @reactor
46
+ end
47
+
48
+ # Register a reaction block, with IP addresses, arguments and a code block
49
+ def react_to(addresses,args = {},&block)
50
+ addresses = [*addresses]
51
+ @reactions += 1
52
+ @reactor.add(addresses,args,block)
53
+ end
54
+
55
+ # Set the address to bind to.
56
+ def address(addr)
57
+ @config.address = addr
58
+ end
59
+
60
+ # Set the port to listen on.
61
+ def port(port)
62
+ @config.port = port
63
+ end
64
+
65
+ # Set the pid file path.
66
+ def pidfile(pidfile)
67
+ @config.pidfile = pidfile
68
+ end
69
+ #
70
+ # Set the log file path.
71
+ def logfile(logfile)
72
+ @config.logfile = logfile
73
+ end
74
+
75
+ # Set whether to run multithreaded.
76
+ def multithreaded(multithreaded = true)
77
+ @config.multithreaded = !!multithreaded
78
+ end
79
+
80
+ end
81
+ end