documentcloud-cloud-crowd 0.0.1

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.
@@ -0,0 +1,44 @@
1
+ # The GraphicsMagick action, dependent on the `gm` command, is able to perform
2
+ # any number of GraphicsMagick conversions on an image passed in as an input.
3
+ # The options hash should specify the +name+ for the particular step (which is
4
+ # appended to the resulting image filename) the +command+ (eg. convert, mogrify),
5
+ # the +options+ (to the command, eg. -shadow -blur), and the +extension+ which
6
+ # will determine the resulting image type. Optionally, you may also specify
7
+ # +input+ as the name of a previous step; doing this will use the result of
8
+ # that step as the source image, otherwise each step uses the original image
9
+ # as its source.
10
+ class GraphicsMagick < CloudCrowd::Action
11
+
12
+ # Download the initial image, and run each of the specified GraphicsMagick
13
+ # commands against it, returning the aggregate output.
14
+ def run
15
+ return options['steps'].map {|step| run_step(step) }
16
+ end
17
+
18
+ # Run an individual step (single GraphicsMagick command) in a shell-injection
19
+ # safe way, uploading the result to the AssetStore, and returning the public
20
+ # URL as the result.
21
+ # TODO: +system+ wasn't working, figure out some other way to escape.
22
+ def run_step(step)
23
+ name, cmd, opts, ext = step['name'], step['command'], step['options'], step['extension']
24
+ in_path, out_path = input_path_for(step), output_path_for(step)
25
+ `gm #{cmd} #{opts} #{in_path} #{out_path}`
26
+ public_url = save(out_path)
27
+ {'name' => name, 'url' => public_url}.to_json
28
+ end
29
+
30
+ # Where should the starting image be located?
31
+ # If you pass in an optional step, returns the path to that step's output
32
+ # as input for further processing.
33
+ def input_path_for(step)
34
+ in_step = step && step['input'] && options['steps'].detect {|s| s['name'] == step['input']}
35
+ return input_path unless in_step
36
+ return output_path_for(in_step)
37
+ end
38
+
39
+ # Where should resulting images be saved locally?
40
+ def output_path_for(step)
41
+ "#{work_directory}/#{file_name}_#{step['name']}.#{step['extension']}"
42
+ end
43
+
44
+ end
data/bin/crowd ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "#{File.dirname(__FILE__)}/../lib/cloud_crowd/command_line"
4
+
5
+ CloudCrowd::CommandLine.new
@@ -0,0 +1,71 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'cloud-crowd'
3
+ s.version = '0.0.1'
4
+ s.date = '2009-08-23'
5
+
6
+ s.homepage = "http://documentcloud.org" # wiki page on github?
7
+ s.summary = "Better living through Map --> Ruby --> Reduce"
8
+ s.description = <<-EOS
9
+ The crowd, suddenly there where there was nothing before, is a mysterious and
10
+ universal phenomenon. A few people may have been standing together -- five, ten
11
+ or twelve, nor more; nothing has been announced, nothing is expected. Suddenly
12
+ everywhere is black with people and more come streaming from all sides as though
13
+ streets had only one direction.
14
+ EOS
15
+
16
+ s.authors = ['Jeremy Ashkenas']
17
+ s.email = 'jeremy@documentcloud.org'
18
+
19
+ s.require_paths = ['lib']
20
+ s.executables = ['crowd']
21
+
22
+ s.post_install_message = "Run `crowd help` for information on using CloudCrowd."
23
+ s.rubyforge_project = 'cloud-crowd'
24
+ s.has_rdoc = true
25
+
26
+ s.add_dependency 'sinatra', ['>= 0.9.4']
27
+ s.add_dependency 'activerecord', ['>= 2.3.3']
28
+ s.add_dependency 'json', ['>= 1.1.7']
29
+ s.add_dependency 'rest-client', ['>= 1.0.3']
30
+ s.add_dependency 'right_aws', ['>= 1.10.0']
31
+ s.add_dependency 'daemons', ['>= 1.0.10']
32
+
33
+ if s.respond_to?(:add_development_dependency)
34
+ s.add_development_dependency 'faker', ['>= 0.3.1']
35
+ s.add_development_dependency 'thoughtbot-shoulda', ['>= 2.10.2']
36
+ s.add_development_dependency 'notahat-machinist', ['>= 1.0.3']
37
+ s.add_development_dependency 'rack-test', ['>= 0.4.1']
38
+ s.add_development_dependency 'mocha', ['>= 0.9.7']
39
+ end
40
+
41
+ s.files = %w(
42
+ actions/graphics_magick.rb
43
+ cloud-crowd.gemspec
44
+ config/config.example.ru
45
+ config/config.example.yml
46
+ config/database.example.yml
47
+ lib/cloud-crowd.rb
48
+ lib/cloud_crowd/action.rb
49
+ lib/cloud_crowd/app.rb
50
+ lib/cloud_crowd/asset_store.rb
51
+ lib/cloud_crowd/command_line.rb
52
+ lib/cloud_crowd/core_ext.rb
53
+ lib/cloud_crowd/daemon.rb
54
+ lib/cloud_crowd/helpers/resources.rb
55
+ lib/cloud_crowd/helpers/urls.rb
56
+ lib/cloud_crowd/helpers.rb
57
+ lib/cloud_crowd/models/job.rb
58
+ lib/cloud_crowd/models/work_unit.rb
59
+ lib/cloud_crowd/models.rb
60
+ lib/cloud_crowd/runner.rb
61
+ lib/cloud_crowd/schema.rb
62
+ lib/cloud_crowd/worker.rb
63
+ test/acceptance/test_failing_work_units.rb
64
+ test/blueprints.rb
65
+ test/config/test_config.yml
66
+ test/config/test_database.yml
67
+ test/test_helper.rb
68
+ test/unit/test_job.rb
69
+ test/unit/test_work_unit.rb
70
+ )
71
+ end
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This rackup script can be used to start the central CloudCrowd server
4
+ # using any Rack-compliant server handler. For example, start up three servers
5
+ # with a specified port number, using Thin:
6
+ #
7
+ # thin start -R config.ru -p 9173 --servers 3
8
+
9
+ require 'rubygems'
10
+ require 'cloud-crowd'
11
+
12
+ CloudCrowd.configure(File.dirname(__FILE__) + '/config.yml')
13
+ CloudCrowd.configure_database(File.dirname(__FILE__) + '/database.yml')
14
+
15
+ map '/' do
16
+ run CloudCrowd::App
17
+ end
@@ -0,0 +1,11 @@
1
+ :num_workers: 4
2
+ :default_worker_wait: 1
3
+ :max_worker_wait: 20
4
+ :worker_wait_multiplier: 1.3
5
+ :worker_retry_wait: 5
6
+ :work_unit_retries: 3
7
+
8
+ :central_server: http://localhost:9173
9
+ :s3_bucket: [your CloudCrowd bucket]
10
+ :aws_access_key: [your AWS access key]
11
+ :aws_secret_key: [your AWS secret access key]
@@ -0,0 +1,6 @@
1
+ :adapter: mysql
2
+ :encoding: utf8
3
+ :username: root
4
+ :password:
5
+ :socket: /tmp/mysql.sock
6
+ :database: cloud_crowd
@@ -0,0 +1,96 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
2
+
3
+ # Standard Library:
4
+ require 'tmpdir'
5
+ require 'erb'
6
+
7
+ # Gems:
8
+ require 'sinatra'
9
+ require 'activerecord'
10
+ require 'json'
11
+ require 'daemons'
12
+ require 'rest_client'
13
+ require 'right_aws'
14
+
15
+ module CloudCrowd
16
+
17
+ class App < Sinatra::Default
18
+ set :root, File.expand_path(File.dirname(__FILE__) + '/..')
19
+ end
20
+
21
+ # Keep the version in sync with the gemspec.
22
+ VERSION = '0.0.1'
23
+
24
+ # A Job is processing if its WorkUnits in the queue to be handled by workers.
25
+ PROCESSING = 1
26
+
27
+ # A Job has succeeded if all of its WorkUnits have finished successfully.
28
+ SUCCEEDED = 2
29
+
30
+ # A Job has failed if even a single one of its WorkUnits has failed (they may
31
+ # be attempted multiple times on failure, however).
32
+ FAILED = 3
33
+
34
+ # A Job is splitting if it's in the process of dividing its inputs up into
35
+ # multiple WorkUnits.
36
+ SPLITTING = 4
37
+
38
+ # A Job is merging if it's busy collecting all of its successful WorkUnits
39
+ # back together into the final result.
40
+ MERGING = 5
41
+
42
+ # A work unit is considered to be complete if it succeeded or if it failed.
43
+ COMPLETE = [SUCCEEDED, FAILED]
44
+
45
+ # A work unit is considered incomplete if it's being processed, split up or
46
+ # merged together.
47
+ INCOMPLETE = [PROCESSING, SPLITTING, MERGING]
48
+
49
+ # Mapping of statuses to their display strings.
50
+ DISPLAY_STATUS_MAP = {
51
+ 1 => 'processing', 2 => 'succeeded', 3 => 'failed', 4 => 'splitting', 5 => 'merging'
52
+ }
53
+
54
+ class << self
55
+ attr_reader :config
56
+
57
+ # Configure CloudCrowd by passing in the path to +config.yml+.
58
+ def configure(config_path)
59
+ @config = YAML.load_file(config_path)
60
+ end
61
+
62
+ # Configure the CloudCrowd central database (and connect to it), by passing
63
+ # in a path to +database.yml+.
64
+ def configure_database(config_path)
65
+ configuration = YAML.load_file(config_path)
66
+ ActiveRecord::Base.establish_connection(configuration)
67
+ end
68
+
69
+ # Return the readable status name of an internal CloudCrowd status number.
70
+ def display_status(status)
71
+ DISPLAY_STATUS_MAP[status]
72
+ end
73
+
74
+ # Some workers might not ever need to load all the installed actions,
75
+ # so we lazy-load them. Think about a variant of this for installing and
76
+ # loading actions into a running CloudCrowd cluster on the fly.
77
+ def actions(name)
78
+ action_class = name.camelize
79
+ begin
80
+ Module.const_get(action_class)
81
+ rescue NameError => e
82
+ require "#{CloudCrowd::App.root}/actions/#{name}"
83
+ retry
84
+ end
85
+ end
86
+ end
87
+
88
+ end
89
+
90
+ # CloudCrowd:
91
+ require 'cloud_crowd/core_ext'
92
+ require 'cloud_crowd/models'
93
+ require 'cloud_crowd/asset_store'
94
+ require 'cloud_crowd/action'
95
+ require 'cloud_crowd/helpers'
96
+ require 'cloud_crowd/app'
@@ -0,0 +1,88 @@
1
+ module CloudCrowd
2
+
3
+ # Base CloudCrowd::Action class. Override this with your custom action steps.
4
+ #
5
+ # Public API to CloudCrowd::Action subclasses:
6
+ # +input+, +input_path+, +file_name+, +work_directory+, +options+, +save+
7
+ #
8
+ # CloudCrowd::Actions must implement a +process+ method, which must return a
9
+ # JSON-serializeable object that will be used as the output for the work unit.
10
+ # Optionally, actions may define +split+ and +merge+ methods to do mapping
11
+ # and reducing around the input.
12
+ # +split+ must return an array of inputs.
13
+ # +merge+ must return the output for the job.
14
+ # All actions run inside of their individual +work_directory+.
15
+ class Action
16
+
17
+ attr_reader :input, :input_path, :file_name, :options, :work_directory
18
+
19
+ # Configuring a new Action sets up all of the read-only variables that
20
+ # form the bulk of the API for action subclasses. (Paths to read from and
21
+ # write to).
22
+ def configure(status, input, options, store)
23
+ @input, @options, @store = input, options, store
24
+ @job_id, @work_unit_id = options['job_id'], options['work_unit_id']
25
+ @work_directory = File.expand_path(File.join(@store.temp_storage_path, storage_prefix))
26
+ FileUtils.mkdir_p(@work_directory) unless File.exists?(@work_directory)
27
+ Dir.chdir @work_directory
28
+ unless status == CloudCrowd::MERGING
29
+ @input_path = File.join(@work_directory, File.basename(@input))
30
+ @file_name = File.basename(@input_path, File.extname(@input_path))
31
+ download(@input, @input_path)
32
+ end
33
+ end
34
+
35
+ # Each CloudCrowd::Action must implement a +process+ method.
36
+ def process
37
+ raise NotImplementedError.new("CloudCrowd::Actions must override 'run' with their own processing code.")
38
+ end
39
+
40
+ # Download a file to the specified path using curl.
41
+ def download(url, path)
42
+ `curl -s "#{url}" > #{path}`
43
+ path
44
+ end
45
+
46
+ # Takes a local filesystem path, and returns the public url on S3 where the
47
+ # file was saved.
48
+ def save(file_path)
49
+ save_path = File.join(s3_storage_path, File.basename(file_path))
50
+ @store.save(file_path, save_path)
51
+ return @store.url(save_path)
52
+ end
53
+
54
+ # After the Action has finished, we remove the work directory.
55
+ def cleanup_work_directory
56
+ Dir.chdir '/'
57
+ FileUtils.rm_r(@work_directory)
58
+ end
59
+
60
+
61
+ private
62
+
63
+ # The directory prefix to use for both local and S3 storage.
64
+ # [action_name]/job_[job_id]/unit_[work_unit_it]
65
+ def storage_prefix
66
+ path_parts = []
67
+ path_parts << underscore(self.class.to_s)
68
+ path_parts << "job_#{@job_id}"
69
+ path_parts << "unit_#{@work_unit_id}" if @work_unit_id
70
+ @storage_prefix ||= File.join(path_parts)
71
+ end
72
+
73
+ def s3_storage_path
74
+ @s3_storage_path ||= storage_prefix
75
+ end
76
+
77
+ # Pilfered from the ActiveSupport::Inflector.
78
+ def underscore(word)
79
+ word.to_s.gsub(/::/, '/').
80
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
81
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
82
+ tr("-", "_").
83
+ downcase
84
+ end
85
+
86
+ end
87
+
88
+ end
@@ -0,0 +1,54 @@
1
+ module CloudCrowd
2
+
3
+ class App < Sinatra::Default
4
+
5
+ # static serves files from /public, methodoverride allows the _method param.
6
+ enable :static, :methodoverride
7
+
8
+ helpers CloudCrowd::Helpers
9
+
10
+ # Start a new job. Accepts a JSON representation of the job-to-be.
11
+ post '/jobs' do
12
+ Job.create_from_request(JSON.parse(params[:json])).to_json
13
+ end
14
+
15
+ # Check the status of a job, returning the output if finished, and the
16
+ # number of work units remaining otherwise.
17
+ get '/jobs/:job_id' do
18
+ current_job.to_json
19
+ end
20
+
21
+ # Cleans up a Job's saved S3 files. Delete a Job after you're done
22
+ # downloading the results.
23
+ delete '/jobs/:job_id' do
24
+ current_job.cleanup
25
+ ''
26
+ end
27
+
28
+ # Internal method for worker daemons to fetch the work unit at the front
29
+ # of the queue. Work unit is marked as taken and handed off to the worker.
30
+ get '/work' do
31
+ begin
32
+ unit = WorkUnit.first(:conditions => {:status => CloudCrowd::INCOMPLETE, :taken => false}, :order => "created_at desc")
33
+ return status(204) && '' unless unit
34
+ unit.update_attributes(:taken => true)
35
+ unit.to_json
36
+ rescue ActiveRecord::StaleObjectError => e
37
+ return status(204) && ''
38
+ end
39
+ end
40
+
41
+ # When workers are done with their unit, either successfully on in failure,
42
+ # they mark it back on the central server.
43
+ put '/work/:work_unit_id' do
44
+ case params[:status]
45
+ when 'succeeded' then current_work_unit.finish(params[:output], params[:time])
46
+ when 'failed' then current_work_unit.fail(params[:output], params[:time])
47
+ else return error(500, "Completing a work unit must specify status.")
48
+ end
49
+ return status(204) && ''
50
+ end
51
+
52
+ end
53
+
54
+ end
@@ -0,0 +1,58 @@
1
+ module CloudCrowd
2
+
3
+ # The CloudCrowd::AssetStore should provide a common API for stashing and retrieving
4
+ # assets via URLs, in production this will be S3 but in development it may
5
+ # be the filesystem or /tmp.
6
+ class AssetStore
7
+ include FileUtils
8
+
9
+ def initialize
10
+ mkdir_p temp_storage_path unless File.exists? temp_storage_path
11
+ end
12
+
13
+ # Path to CloudCrowd's temporary local storage.
14
+ def temp_storage_path
15
+ "#{Dir.tmpdir}/cloud_crowd_tmp"
16
+ end
17
+
18
+ # Copy a finished file from our local storage to S3.
19
+ def save(local_path, save_path)
20
+ ensure_s3_connection
21
+ @bucket.put(save_path, File.open(local_path), {}, 'public-read')
22
+ end
23
+
24
+ # Cleanup all S3 files for a job that's been completed and retrieved.
25
+ def cleanup_job(job)
26
+ ensure_s3_connection
27
+ @bucket.delete_folder("#{job.action}/job_#{job.id}")
28
+ end
29
+
30
+ # Return the S3 public URL for a finshed file.
31
+ def url(save_path)
32
+ @bucket.key(save_path).public_link
33
+ end
34
+
35
+ private
36
+
37
+ # Unused for the moment. Think about using the filesystem instead of S3
38
+ # in development.
39
+ def save_to_filesystem(local_path, save_path)
40
+ save_path = File.join("/tmp/cloud_crowd_storage", save_path)
41
+ save_dir = File.dirname(save_path)
42
+ mkdir_p save_dir unless File.exists? save_dir
43
+ cp(local_path, save_path)
44
+ end
45
+
46
+ # Workers, through the course of many WorkUnits, keep around an AssetStore.
47
+ # Ensure we have a persistent S3 connection after first use.
48
+ def ensure_s3_connection
49
+ unless @s3 && @bucket
50
+ params = {:port => 80, :protocol => 'http'}
51
+ @s3 = RightAws::S3.new(CloudCrowd.config[:aws_access_key], CloudCrowd.config[:aws_secret_key], params)
52
+ @bucket = @s3.bucket(CloudCrowd.config[:s3_bucket], true)
53
+ end
54
+ end
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,198 @@
1
+ require 'optparse'
2
+
3
+ module CloudCrowd
4
+ class CommandLine
5
+
6
+ # Configuration files required for the `crowd` command to function.
7
+ CONFIG_FILES = ['config.yml', 'config.ru', 'database.yml']
8
+
9
+ # Path to the Daemons gem script which launches workers.
10
+ WORKER_RUNNER = File.expand_path("#{File.dirname(__FILE__)}/runner.rb")
11
+
12
+ # Command-line banner for the usage message.
13
+ BANNER = <<-EOS
14
+ Usage: crowd COMMAND OPTIONS
15
+
16
+ COMMANDS:
17
+ install Install the CloudCrowd configuration files to the specified directory
18
+ server Start up the central server (requires a database)
19
+ workers Control worker daemons, use: (start | stop | restart | status | run)
20
+ console Launch a CloudCrowd console, connected to the central database
21
+ load_schema Load the schema into the database specified by database.yml
22
+
23
+ OPTIONS:
24
+ EOS
25
+
26
+ # Creating a CloudCrowd::CommandLine runs from the contents of ARGV.
27
+ def initialize
28
+ parse_options
29
+ command = ARGV.shift
30
+ case command
31
+ when 'console' then run_console
32
+ when 'server' then run_server
33
+ when 'workers' then run_workers_command
34
+ when 'load_schema' then run_load_schema
35
+ when 'install' then run_install
36
+ else usage
37
+ end
38
+ end
39
+
40
+ # Spin up an IRB session with the CloudCrowd code loaded in, and a database
41
+ # connection established. The equivalent of Rails' `script/console`.
42
+ def run_console
43
+ require 'irb'
44
+ require 'irb/completion'
45
+ load_code
46
+ connect_to_database
47
+ IRB.start
48
+ end
49
+
50
+ # Convenience command for quickly spinning up the central server. More
51
+ # sophisticated deployments, load-balancing across multiple app servers,
52
+ # should use the config.ru rackup file directly. This method will start
53
+ # a single Thin server, if Thin is installed, otherwise the rackup defaults
54
+ # (Mongrel, falling back to WEBrick). The equivalent of Rails' script/server.
55
+ def run_server
56
+ ensure_config
57
+ require 'rubygems'
58
+ rackup_path = File.expand_path('config.ru')
59
+ if Gem.available? 'thin'
60
+ exec "thin -e production -p #{@options[:port]} -R #{rackup_path} start"
61
+ else
62
+ exec "rackup -E production -p #{@options[:port]} #{rackup_path}"
63
+ end
64
+ end
65
+
66
+ # Load in the database schema to the database specified in 'database.yml'.
67
+ def run_load_schema
68
+ load_code
69
+ connect_to_database
70
+ require 'cloud_crowd/schema.rb'
71
+ end
72
+
73
+ # Install the required CloudCrowd configuration files into the specified
74
+ # directory, or the current one.
75
+ def run_install
76
+ require 'fileutils'
77
+ install_path = ARGV.shift || '.'
78
+ cc_root = File.dirname(__FILE__) + '/../..'
79
+ FileUtils.mkdir_p install_path unless File.exists?(install_path)
80
+ install_file "#{cc_root}/config/config.example.yml", "#{install_path}/config.yml"
81
+ install_file "#{cc_root}/config/config.example.ru", "#{install_path}/config.ru"
82
+ install_file "#{cc_root}/config/database.example.yml", "#{install_path}/database.yml"
83
+ install_file "#{cc_root}/actions", "#{install_path}/actions", true
84
+ end
85
+
86
+ # Manipulate worker daemons -- handles all commands that the Daemons gem
87
+ # provides: start, stop, restart, run, and status.
88
+ def run_workers_command
89
+ ensure_config
90
+ command = ARGV.shift
91
+ case command
92
+ when 'start' then start_workers
93
+ when 'stop' then stop_workers
94
+ when 'restart' then stop_workers && start_workers
95
+ when 'run' then run_worker
96
+ when 'status' then show_worker_status
97
+ else usage
98
+ end
99
+ end
100
+
101
+ # Start up N workers, specified by argument or the number of workers in
102
+ # config.yml.
103
+ def start_workers
104
+ load_code
105
+ num_workers = @options[:num_workers] || CloudCrowd.config[:num_workers]
106
+ num_workers.times do
107
+ `CLOUD_CROWD_CONFIG='#{File.expand_path('config.yml')}' ruby #{WORKER_RUNNER} start`
108
+ end
109
+ end
110
+
111
+ # For debugging, run a single worker in the current process, showing output.
112
+ def run_worker
113
+ exec "CLOUD_CROWD_CONFIG='#{File.expand_path('config.yml')}' ruby #{WORKER_RUNNER} run"
114
+ end
115
+
116
+ # Stop all active workers.
117
+ def stop_workers
118
+ `ruby #{WORKER_RUNNER} stop`
119
+ end
120
+
121
+ # Display the status of all active workers.
122
+ def show_worker_status
123
+ puts `ruby #{WORKER_RUNNER} status`
124
+ end
125
+
126
+ # Print `crowd` usage.
127
+ def usage
128
+ puts @option_parser
129
+ end
130
+
131
+
132
+ private
133
+
134
+ # Check for configuration files, either in the current directory, or in
135
+ # the CLOUD_CROWD_CONFIG environment variable. Exit if they're not found.
136
+ def ensure_config
137
+ return if @config_found
138
+ config_dir = ENV['CLOUD_CROWD_CONFIG'] || '.'
139
+ Dir.chdir config_dir
140
+ CONFIG_FILES.all? {|f| File.exists? f } ? @config_dir = true : config_not_found
141
+ end
142
+
143
+ # Parse all options for all actions.
144
+ # TODO: Think about parsing options per sub-command separately.
145
+ def parse_options
146
+ @options = {
147
+ :db_config => 'database.yml',
148
+ :port => 9173,
149
+ }
150
+ @option_parser = OptionParser.new do |opts|
151
+ opts.on('-n', '--num-workers NUM', OptionParser::DecimalInteger, 'number of worker processes') do |num|
152
+ @options[:num_workers] = num
153
+ end
154
+ opts.on('-d', '--database-config PATH', 'path to database.yml') do |conf_path|
155
+ @options[:db_config] = conf_path
156
+ end
157
+ opts.on('-p', '--port PORT', 'central server port number') do |port_num|
158
+ @options[:port] = port_num
159
+ end
160
+ opts.on_tail('-v', '--version', 'show version') do
161
+ load_code
162
+ puts "CloudCrowd version #{CloudCrowd::VERSION}"
163
+ exit
164
+ end
165
+ end
166
+ @option_parser.banner = BANNER
167
+ @option_parser.parse!(ARGV)
168
+ end
169
+
170
+ # Load in the CloudCrowd module code, dependencies, lib files and models.
171
+ # Not all commands require this.
172
+ def load_code
173
+ ensure_config
174
+ require 'rubygems'
175
+ require File.dirname(__FILE__) + '/../cloud-crowd'
176
+ CloudCrowd.configure('config.yml')
177
+ end
178
+
179
+ # Establish a connection to the central server's database. Not all commands
180
+ # require this.
181
+ def connect_to_database
182
+ CloudCrowd.configure_database(@options[:db_config])
183
+ end
184
+
185
+ # Exit with an explanation if the configuration files couldn't be found.
186
+ def config_not_found
187
+ puts "`crowd` can't find the CloudCrowd configuration directory. Please either run `crowd` from inside of the configuration directory, or add a CLOUD_CROWD_CONFIG variable to your environment."
188
+ exit(1)
189
+ end
190
+
191
+ # Install a file and log the installation.
192
+ def install_file(source, dest, is_dir=false)
193
+ is_dir ? FileUtils.cp_r(source, dest) : FileUtils.cp(source, dest)
194
+ puts "installed #{dest}"
195
+ end
196
+
197
+ end
198
+ end
@@ -0,0 +1,10 @@
1
+ # Extensions to core Ruby.
2
+
3
+ class String
4
+
5
+ # Stolen-ish in parts from ActiveSupport::Inflector.
6
+ def camelize
7
+ self.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
8
+ end
9
+
10
+ end