blue_colr 0.0.6
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.
- data/README.rdoc +77 -0
- data/bin/bcrun +23 -0
- data/bin/bluecolrd +182 -0
- data/lib/blue_colr.rb +189 -0
- metadata +85 -0
data/README.rdoc
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
= blue_colr, database-based process launcher
|
2
|
+
|
3
|
+
== Overview
|
4
|
+
|
5
|
+
blue_colr allows you to easily launch processes using database as a queue. It
|
6
|
+
consists of +bluecolrd+, a deamon that executes whatever finds in a queue,
|
7
|
+
and a DSL for enqueuing processes that enables you to easily describe the order
|
8
|
+
and dependencies of processes.
|
9
|
+
|
10
|
+
== Installation
|
11
|
+
|
12
|
+
git clone git://github.com/jablan/blue_colr.git
|
13
|
+
cd blue_colr
|
14
|
+
gem build blue_colr.gemspec
|
15
|
+
gem install blue_colr-0.0.6.gem
|
16
|
+
|
17
|
+
You may want to install +log4r+ gem as well, as it provides more powerful logging
|
18
|
+
features than builtin Ruby's +Logger+.
|
19
|
+
|
20
|
+
== Example
|
21
|
+
|
22
|
+
require 'blue_colr'
|
23
|
+
|
24
|
+
BlueColr.start do
|
25
|
+
run 'echo These processes'
|
26
|
+
run 'echo will be ran sequentially.'
|
27
|
+
parallel do
|
28
|
+
run 'echo And these'
|
29
|
+
sequential do
|
30
|
+
run 'echo (but not'
|
31
|
+
run 'echo these two)'
|
32
|
+
end
|
33
|
+
run 'echo in parallel.'
|
34
|
+
end
|
35
|
+
run 'echo These will execute'
|
36
|
+
run 'echo after all above are finished.'
|
37
|
+
end
|
38
|
+
|
39
|
+
Previous code will queue processes within the database, keeping them in
|
40
|
+
dependency order. Those within +sequential+ block (and in root block, by
|
41
|
+
default) will run each after the one before finishes. Those within +parallel+
|
42
|
+
block will run in parallel. The commands after +parallel+ block will be executed
|
43
|
+
after _all_ the commands in +parallel+ block are sucessfully finished.
|
44
|
+
|
45
|
+
Note: the code above will not _start_ the processes by itself, but enqueue them
|
46
|
+
to the database, by default. A separate process called +bluecolrd+ is
|
47
|
+
used for that.
|
48
|
+
|
49
|
+
== <tt>bluecolrd</tt>
|
50
|
+
|
51
|
+
Blue_colr daemon is constantly running, checking the database for newly enqueued
|
52
|
+
processes, and executing them in a subshell, observing the order.
|
53
|
+
|
54
|
+
== <tt>bcrun</tt>
|
55
|
+
|
56
|
+
This script is used to launch arbitrary command through blue_colr. You might want
|
57
|
+
to do that if you want to keep track of the stuff you launch (as everything goes
|
58
|
+
through a database table).
|
59
|
+
|
60
|
+
bcrun -c path_to_config.yaml -x "command to execute"
|
61
|
+
|
62
|
+
== Enviroments
|
63
|
+
|
64
|
+
An environment is something like _category_ which you assign to a set of processes
|
65
|
+
when enqueuing them. Then you can have multiple daemons running, each one of them
|
66
|
+
targeting specific environment. That allows easy distribution of your tasks across
|
67
|
+
multiple machines, while keeping them synchronized, like the following scenario:
|
68
|
+
|
69
|
+
* Start tasks a and b on machine X and c on machine Y
|
70
|
+
* When all above are sucessfully done, start task d on machine Z
|
71
|
+
|
72
|
+
== ToDo
|
73
|
+
|
74
|
+
* Scripts to create necessarry tables
|
75
|
+
* Proper test code
|
76
|
+
* Examples
|
77
|
+
|
data/bin/bcrun
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# utility to launch bluecolr processes
|
4
|
+
|
5
|
+
require "rubygems"
|
6
|
+
require 'blue_colr'
|
7
|
+
|
8
|
+
BlueColr.log = Logger.new(STDOUT)
|
9
|
+
cmd = ''
|
10
|
+
|
11
|
+
BlueColr.custom_args do |opts|
|
12
|
+
opts.banner = "Usage: bcrun [options]"
|
13
|
+
|
14
|
+
opts.on("-x cmd", "--execute command", "Command to execute (enclose in quotes if contains parameters)") do |command|
|
15
|
+
cmd = command
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# queuing processes sequentially
|
20
|
+
BlueColr.launch do
|
21
|
+
run cmd
|
22
|
+
end
|
23
|
+
|
data/bin/bluecolrd
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# daemon to run blue_colr processes
|
4
|
+
|
5
|
+
require "rubygems"
|
6
|
+
require 'date'
|
7
|
+
require 'yaml'
|
8
|
+
begin
|
9
|
+
require 'log4r' # try using log4r if available
|
10
|
+
require 'log4r/yamlconfigurator'
|
11
|
+
# include Log4r
|
12
|
+
rescue LoadError
|
13
|
+
require 'logger' # otherwise, use plain ruby's one
|
14
|
+
end
|
15
|
+
require "optparse"
|
16
|
+
require 'sequel'
|
17
|
+
require 'blue_colr'
|
18
|
+
require 'fileutils'
|
19
|
+
|
20
|
+
def logger(name = nil)
|
21
|
+
@logger[name || @environment] || @logger['default']
|
22
|
+
end
|
23
|
+
|
24
|
+
def init_logger
|
25
|
+
if @conf['log4r_config']
|
26
|
+
log_cfg = Log4r::YamlConfigurator # shorthand
|
27
|
+
log_cfg['ENVIRONMENT'] = @environment if @environment
|
28
|
+
log_cfg['LOGFILENAME'] = @log_file
|
29
|
+
|
30
|
+
# load the YAML file with this
|
31
|
+
log_cfg.decode_yaml(@conf['log4r_config'])
|
32
|
+
|
33
|
+
@logger = Log4r::Logger
|
34
|
+
else
|
35
|
+
@logger = {'default' => Logger.new(@log_file)}
|
36
|
+
end
|
37
|
+
logger.level = @args['debuglevel'] || Logger::WARN
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_command_line(args)
|
41
|
+
data = {}
|
42
|
+
|
43
|
+
OptionParser.new do |opts|
|
44
|
+
opts.banner = "Usage: bluecolrd [options]"
|
45
|
+
|
46
|
+
opts.on("-c CONFIG", "--conf CONFIG", "YAML config file") do |config|
|
47
|
+
data["config"] = config
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on("-e NAME", "--environment NAME", "Environment name (e.g. test, production etc.) to work on (default none)") do |env|
|
51
|
+
data["environment"] = env
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on("-m COUNT", "--max-count COUNT", "Max number of simultaneous processes to start.") do |count|
|
55
|
+
data["max"] = count.to_i
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.on("-l LOGFILE", "--logfile LOGFILE", "File to log to.") do |logfile|
|
59
|
+
data["logfile"] = logfile
|
60
|
+
end
|
61
|
+
|
62
|
+
opts.on("-d LEVEL", "--debuglevel LEVEL", "Debug level to use (0 - DEBUG, 1 - INFO etc).") do |level|
|
63
|
+
data['debuglevel'] = level.to_i
|
64
|
+
end
|
65
|
+
|
66
|
+
opts.on_tail('-h', '--help', 'display this help and exit') do
|
67
|
+
puts opts
|
68
|
+
return nil
|
69
|
+
end
|
70
|
+
|
71
|
+
opts.parse(args)
|
72
|
+
end
|
73
|
+
|
74
|
+
return data
|
75
|
+
end
|
76
|
+
|
77
|
+
# check whether it's ok to spawn another process
|
78
|
+
def ok_to_run?
|
79
|
+
# check the limit of max processes, if given TODO: @pids is not used anymore, this is not working.
|
80
|
+
@max_processes == 0 || @pids.size < @max_processes
|
81
|
+
# !@args['max'] || @pids.size < @args['max']
|
82
|
+
end
|
83
|
+
|
84
|
+
def run process
|
85
|
+
logger.debug "Running #{process[:module_name]}"
|
86
|
+
script = process[:cmd]
|
87
|
+
logger.debug script
|
88
|
+
id = process[:id]
|
89
|
+
|
90
|
+
# update process item in the db
|
91
|
+
# set status of process_item to "running"
|
92
|
+
@db[:process_items].filter(:id => id).update(:status => BlueColr::STATUS_RUNNING, :started_at => Time.now)
|
93
|
+
|
94
|
+
log_path = @conf['log_path'] || '.'
|
95
|
+
log_path = (process[:process_from] || Time.now).strftime(log_path) # interpolate date
|
96
|
+
|
97
|
+
FileUtils.mkdir_p log_path
|
98
|
+
log_file = File.join(log_path, "#{id}.out")
|
99
|
+
# run actual command
|
100
|
+
Thread.new do
|
101
|
+
begin
|
102
|
+
Dir.chdir(process[:chdir]) if process[:chdir]
|
103
|
+
Kernel.system("#{script} >> #{log_file} 2>&1")
|
104
|
+
ok = $?.success?
|
105
|
+
exitstatus = $?.exitstatus
|
106
|
+
rescue
|
107
|
+
# do nothing, just exit with error
|
108
|
+
# this usually means that exec tried to execute a file that doesn't exist
|
109
|
+
ok = false
|
110
|
+
exitstatus = 99
|
111
|
+
end
|
112
|
+
|
113
|
+
# find corresponding process_item
|
114
|
+
# change its status in the DB and update ended_at timestamp
|
115
|
+
@db[:process_items].filter(:id => process[:id]).update(
|
116
|
+
:status => ok ? BlueColr::STATUS_OK : BlueColr::STATUS_ERROR,
|
117
|
+
:exit_code => exitstatus,
|
118
|
+
:ended_at => Time.now
|
119
|
+
)
|
120
|
+
logger(process[:logger]).error(@error_log_msg % process.to_hash) unless ok
|
121
|
+
|
122
|
+
logger.info "Process ended: id #{process[:id]} #{$?}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# MAIN PROGRAM STARTS HERE
|
127
|
+
|
128
|
+
# pid => process, hash of started processes
|
129
|
+
@pids = {}
|
130
|
+
|
131
|
+
@args = parse_command_line(ARGV)
|
132
|
+
|
133
|
+
raise "No configuration file defined (-c <config>)." unless @args && @args["config"]
|
134
|
+
raise "Couldn't read #{@args["config"]} file." unless @args['config'] && @conf = YAML::load(File.new(@args["config"]).read)
|
135
|
+
@max_processes = @args['max'] || @conf['max_processes'] || 0 # default unlimited
|
136
|
+
@environment = @args['environment'] || @conf['environment'] || nil
|
137
|
+
@log_file = @args['logfile'] || "process_daemon_#{@environment}"
|
138
|
+
@error_log_msg = @conf['error_log_msg'] || 'Process failed: id %{id}'
|
139
|
+
|
140
|
+
init_logger
|
141
|
+
|
142
|
+
begin
|
143
|
+
@db = Sequel.connect(@conf['db_url'], :logger => logger('sequel')) # try to use sequel logger, if defined
|
144
|
+
|
145
|
+
logger.info 'Starting daemon'
|
146
|
+
|
147
|
+
loop do
|
148
|
+
# get all pending items
|
149
|
+
query = "select i.id
|
150
|
+
from process_items i
|
151
|
+
left join process_item_dependencies d ON i.id = d.process_item_id
|
152
|
+
left join process_items i2 ON d.depends_on_id = i2.id and i2.status NOT IN ('#{BlueColr::STATUS_OK}', '#{BlueColr::STATUS_SKIPPED}')
|
153
|
+
where i.status = '#{BlueColr::STATUS_PENDING}' and i.environment = ?
|
154
|
+
group by i.id
|
155
|
+
having count(i2.id) = 0"
|
156
|
+
process_items = @db[query, @environment]
|
157
|
+
|
158
|
+
process_items.each do |id|
|
159
|
+
logger.debug "Pending item: #{id.inspect}"
|
160
|
+
if ok_to_run?
|
161
|
+
item = @db[:process_items].filter(:id => id[:id]).first
|
162
|
+
run(item)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
sleep(@conf['sleep_interval'] || 10)
|
166
|
+
end
|
167
|
+
|
168
|
+
rescue Interrupt
|
169
|
+
if logger
|
170
|
+
logger.fatal("Ctrl-C received, exiting")
|
171
|
+
else
|
172
|
+
puts "Ctrl-C received, exiting"
|
173
|
+
end
|
174
|
+
exit 1
|
175
|
+
rescue Exception => ex
|
176
|
+
p ex.class
|
177
|
+
logger.fatal(ex.to_s) if logger
|
178
|
+
puts "#{ex.to_s} ==>"
|
179
|
+
puts ex.backtrace.join("\n")
|
180
|
+
exit 1
|
181
|
+
end
|
182
|
+
|
data/lib/blue_colr.rb
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
# This class provides a simple DSL for enqueuing processes to the database
|
2
|
+
# in particular order.
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'date'
|
6
|
+
require 'logger'
|
7
|
+
require 'ostruct'
|
8
|
+
require 'optparse'
|
9
|
+
require 'yaml'
|
10
|
+
require 'sequel'
|
11
|
+
|
12
|
+
|
13
|
+
class BlueColr
|
14
|
+
STATUS_OK = 'ok'
|
15
|
+
STATUS_ERROR = 'error'
|
16
|
+
STATUS_PENDING = 'pending'
|
17
|
+
STATUS_RUNNING = 'running'
|
18
|
+
STATUS_PREPARING = 'preparing'
|
19
|
+
STATUS_SKIPPED = 'skipped'
|
20
|
+
|
21
|
+
class << self
|
22
|
+
attr_accessor :log, :db, :environment, :db_uri
|
23
|
+
|
24
|
+
# default options to use when launching a process - every field maps to a
|
25
|
+
# column in process_items table
|
26
|
+
def default_options
|
27
|
+
@default_options ||= OpenStruct.new
|
28
|
+
end
|
29
|
+
|
30
|
+
# local hash used to store misc runtime options
|
31
|
+
def options
|
32
|
+
@options ||= OpenStruct.new
|
33
|
+
end
|
34
|
+
|
35
|
+
def sequential &block
|
36
|
+
self.new.sequential &block
|
37
|
+
end
|
38
|
+
|
39
|
+
def parallel &block
|
40
|
+
self.new.parallel &block
|
41
|
+
end
|
42
|
+
|
43
|
+
# set custom commandline parameters from parent script, will be called upon
|
44
|
+
# command line parameter extraction
|
45
|
+
def custom_args &block
|
46
|
+
@custom_args_block = block
|
47
|
+
end
|
48
|
+
|
49
|
+
# launch a set of tasks, provided within a given block
|
50
|
+
def launch &block
|
51
|
+
@log ||= Logger.new('process_daemon')
|
52
|
+
|
53
|
+
unless @db # not connected
|
54
|
+
unless @db_uri # get the config from command line
|
55
|
+
@args = parse_command_line ARGV
|
56
|
+
|
57
|
+
raise "No configuration file defined (-c <config>)." if @args["config"].nil?
|
58
|
+
raise "Couldn't read #{@args["config"]} file." unless @args['config'] && @conf = YAML::load(File.new(@args["config"]).read)
|
59
|
+
|
60
|
+
@db_uri = @conf['db_url']
|
61
|
+
# setting default options that should be written along with all the records to process_items
|
62
|
+
if @conf['default_options']
|
63
|
+
@conf['default_options'].each do |k,v|
|
64
|
+
default_options.send("#{k}=", v)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
@db = Sequel.connect(@db_uri, :logger => @log)
|
69
|
+
end
|
70
|
+
worker = self.new
|
71
|
+
db.transaction do
|
72
|
+
worker.instance_eval &block
|
73
|
+
end
|
74
|
+
worker
|
75
|
+
end
|
76
|
+
|
77
|
+
# run a set of tasks (launch it and wait until the last one finishes). exit with returned exitcode.
|
78
|
+
def run &block
|
79
|
+
worker = launch &block
|
80
|
+
exit worker.wait
|
81
|
+
end
|
82
|
+
|
83
|
+
def parse_command_line(args)
|
84
|
+
data = Hash.new()
|
85
|
+
|
86
|
+
OptionParser.new do |opts|
|
87
|
+
opts.banner = "Usage: process_daemon.rb [options]"
|
88
|
+
|
89
|
+
opts.on("-c CONFIG", "--conf CONFIG", "YAML config file") do |config|
|
90
|
+
data["config"] = config
|
91
|
+
end
|
92
|
+
|
93
|
+
# process custom args, if given
|
94
|
+
@custom_args_block.call(opts) if @custom_args_block
|
95
|
+
|
96
|
+
opts.on_tail('-h', '--help', 'display this help and exit') do
|
97
|
+
puts opts
|
98
|
+
exit
|
99
|
+
# return nil
|
100
|
+
end
|
101
|
+
|
102
|
+
# begin
|
103
|
+
opts.parse(args)
|
104
|
+
# rescue OptionParser::InvalidOption
|
105
|
+
# # do nothing
|
106
|
+
# end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
return data
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
attr_reader :all_ids, :result
|
115
|
+
|
116
|
+
def initialize type = :sequential, waitfor = []
|
117
|
+
@type = type
|
118
|
+
@waitfor = waitfor
|
119
|
+
@result = []
|
120
|
+
@all_ids = [] # list of all ids of processes enqueued, used if waiting
|
121
|
+
end
|
122
|
+
|
123
|
+
def db
|
124
|
+
self.class.db
|
125
|
+
end
|
126
|
+
|
127
|
+
def log
|
128
|
+
self.class.log
|
129
|
+
end
|
130
|
+
|
131
|
+
def sequential &block
|
132
|
+
exec :sequential, &block
|
133
|
+
end
|
134
|
+
|
135
|
+
def parallel &block
|
136
|
+
exec :parallel, &block
|
137
|
+
end
|
138
|
+
|
139
|
+
def exec type = :sequential, &block
|
140
|
+
g = self.class.new type, @waitfor
|
141
|
+
g.instance_eval &block
|
142
|
+
ids = g.result
|
143
|
+
if @type == :sequential
|
144
|
+
@waitfor = ids
|
145
|
+
@result = ids
|
146
|
+
else
|
147
|
+
@result += ids
|
148
|
+
end
|
149
|
+
@result
|
150
|
+
end
|
151
|
+
|
152
|
+
def enqueue cmd, waitfor = [], opts = {}
|
153
|
+
id = nil
|
154
|
+
def_opts = self.class.default_options.send(:table) # convert from OpenStruct to Hash
|
155
|
+
id = db[:process_items].insert(def_opts.merge(opts).merge(:status => STATUS_PREPARING, :cmd => cmd, :queued_at => Time.now))
|
156
|
+
waitfor.each do |wid|
|
157
|
+
db[:process_item_dependencies].insert(:process_item_id => id, :depends_on_id => wid)
|
158
|
+
end
|
159
|
+
db[:process_items].filter(:id => id).update(:status => STATUS_PENDING)
|
160
|
+
# id = TaskGroup.counter
|
161
|
+
log.info "enqueueing #{id}: #{cmd}, waiting for #{waitfor.inspect}"
|
162
|
+
# remember id
|
163
|
+
@all_ids << id
|
164
|
+
id
|
165
|
+
end
|
166
|
+
|
167
|
+
def run cmd, opts = {}
|
168
|
+
id = enqueue cmd, @waitfor, opts
|
169
|
+
if @type == :sequential
|
170
|
+
@waitfor = [id]
|
171
|
+
@result = [id]
|
172
|
+
else
|
173
|
+
@result << id
|
174
|
+
end
|
175
|
+
@result
|
176
|
+
end
|
177
|
+
|
178
|
+
# wait for all enqueued processes to finish
|
179
|
+
def wait
|
180
|
+
log.info 'Waiting for all processes to finish'
|
181
|
+
loop do
|
182
|
+
failed = db[:process_items].filter(:id => @all_ids, :status => STATUS_ERROR).first
|
183
|
+
return failed[:exit_code] if failed
|
184
|
+
not_ok_count = db[:process_items].filter(:id => @all_ids).exclude(:status => STATUS_OK).count
|
185
|
+
return 0 if not_ok_count == 0 # all ok, finish
|
186
|
+
sleep 10
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: blue_colr
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 19
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 6
|
10
|
+
version: 0.0.6
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Mladen Jablanovic
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-07-07 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: sequel
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
description: Blue_colr provides simple DSL to enqueue processes in given order, using database table as a queue, and a deamon to run them
|
36
|
+
email:
|
37
|
+
- jablan@radioni.ca
|
38
|
+
executables:
|
39
|
+
- bluecolrd
|
40
|
+
- bcrun
|
41
|
+
extensions: []
|
42
|
+
|
43
|
+
extra_rdoc_files: []
|
44
|
+
|
45
|
+
files:
|
46
|
+
- bin/bcrun
|
47
|
+
- bin/bluecolrd
|
48
|
+
- lib/blue_colr.rb
|
49
|
+
- README.rdoc
|
50
|
+
has_rdoc: true
|
51
|
+
homepage: http://github.com/jablan/blue_colr
|
52
|
+
licenses: []
|
53
|
+
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
hash: 3
|
65
|
+
segments:
|
66
|
+
- 0
|
67
|
+
version: "0"
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
hash: 3
|
74
|
+
segments:
|
75
|
+
- 0
|
76
|
+
version: "0"
|
77
|
+
requirements: []
|
78
|
+
|
79
|
+
rubyforge_project:
|
80
|
+
rubygems_version: 1.3.7
|
81
|
+
signing_key:
|
82
|
+
specification_version: 3
|
83
|
+
summary: Database based process launcher
|
84
|
+
test_files: []
|
85
|
+
|