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