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
data/.gitignore
ADDED
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,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
|