gabrielg-vlad 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,43 @@
1
+ == Converting from Capistrano
2
+
3
+ * 'set scm' is removed. Vlad.load :scm => :something if you don't use subversion.
4
+ * 'task' blocks are renamed to 'remote_task'.
5
+ * Most variables are the same. See variables.txt for details.
6
+ * No +with_command+ / +sudo+ / +via+ wonkiness
7
+ * Uses real ssh so env vars and the like are not a problem
8
+ - no +with_env+ as a result.
9
+ * Vlad doesn't use ':no_release' or ':primary'.
10
+ - If you have a task that needs to run on only one host from a role,
11
+ you should declare a new role for that host:
12
+
13
+ role :master_db, "master.example.com"
14
+
15
+ ..and then override the role for the task you want to limit:
16
+
17
+ Rake::Task["mytask"].options[:roles] = :master_db
18
+
19
+ * The 'host' method can be used to consolidate multiple 'role' calls.
20
+ - host "www.example.com", :app, :web, :db
21
+ specifies a host with three roles.
22
+ * migrate_env is now migrate_args.
23
+ * Vlad doesn't have before/after magic add-on tasks.
24
+
25
+ == BEFORE:
26
+
27
+ set :application, "rubyholic"
28
+ set :domain, "zenspider.textdriven.com"
29
+ set :repository, "svn://svn.example.com/rubyholic/branches/stable"
30
+ set :deploy_to, "/users/home/zenspider/domains/new.rubyholic.com"
31
+
32
+ set :user, "zenspider"
33
+ set :use_sudo, false
34
+
35
+ role :web, domain
36
+ role :app, domain
37
+ role :db, domain, :primary => true
38
+
39
+ == AFTER:
40
+
41
+ set :domain, "zenspider.textdriven.com"
42
+ set :repository, "svn://svn.example.com/rubyholic/branches/stable"
43
+ set :deploy_to, "/users/home/zenspider/domains/new.rubyholic.com"
data/doco/perforce.txt ADDED
@@ -0,0 +1,5 @@
1
+ # Using Perforce
2
+
3
+ TODO: write real doco.
4
+
5
+ Set up a .p4config file and put it in the client's scm directories.
@@ -0,0 +1,78 @@
1
+
2
+ == Core Variables
3
+
4
+ repository:: REQUIRED: Repository path: e.g. http://repo.example.com/svn
5
+ deploy_to:: REQUIRED: Deploy path on target machines. e.g. /var/www/app
6
+ domain:: REQUIRED: Used for the common case of a single target
7
+ server. e.g. example.com
8
+ current_path:: The full path on the remote host that will be symlinked
9
+ as 'current'. Defaults to "#{deploy_to}/current".
10
+ current_release:: The full path to the current release's actual location.
11
+ Defaults to "#{releases_path}/#{releases.last}".
12
+ deploy_timestamped:: Create timestamped release directories instead of using
13
+ revision numbers. Defaults to true.
14
+ deploy_via:: Which SCM command should be used when deploying the app.
15
+ Defaults to "export".
16
+ latest_release:: The most recent release, which may not yet have been
17
+ symlinked. Defaults to release_path.
18
+ migrate_args:: Set this to change the RAILS_ENV that 'rake db:migrate'
19
+ will run under. Defaults to "".
20
+ migrate_target:: Set this if you need to specify a particular migration
21
+ 'VERSION' number. Defaults to "latest".
22
+ rails_env:: Specifies the RAILS_ENV environment variable that will
23
+ be used. Defaults to "production".
24
+ rake:: Set this if you need to specify an alternate path to
25
+ 'rake'. Defaults to "rake".
26
+ release_name:: Name of the release directory, if deploy_timestamped is
27
+ true. Defaults to timestamp: "YYYYMMDDHHMMSS".
28
+ release_path:: Path to this release, which may not have been created
29
+ yet. Defaults to "#{releases_path}/#{release_name}".
30
+ releases:: An array of all existing releases, oldest first.
31
+ Defaults to latest release directory name.
32
+ releases_path:: Full path to the 'releases' directory on the remote host.
33
+ Defaults to "#{deploy_to}/releases".
34
+ revision:: Revision to use for release. Defaults to 'head'.
35
+ rsync_cmd:: Path to rsync command. Defaults to "rsync".
36
+ rsync_flags:: Flags for rsync. Defaults to ['-azP', '--delete'].
37
+ scm_path:: Path on the remote host that will be used as 'working
38
+ space' for SCM tasks. Defaults to "#{deploy_to}/scm".
39
+ shared_path:: Full path to remote 'shared' directory, symlinked into
40
+ your app by default. Defaults to "#{deploy_to}/shared".
41
+ ssh_cmd:: Path to ssh. Defaults to "ssh".
42
+ ssh_flags:: Flags for ssh. Defaults to [].
43
+ sudo_cmd:: Path to sudo command. Defaults to "sudo".
44
+ sudo_flags:: Flogs for sudo. Defaults to nil.
45
+ sudo_password:: Asks for password when referenced.
46
+ umask:: Sets your umask value. Defaults to "02".
47
+
48
+ == Apache Web Variables:
49
+
50
+ web_command:: Command to execute when controlling the web server.
51
+ Defaults to "apachectl".
52
+
53
+ == Mongrel App Variables:
54
+
55
+ mongrel_address:: Defaults to "127.0.0.1"
56
+ mongrel_clean:: Defaults to false
57
+ mongrel_command:: Defaults to 'mongrel_rails'
58
+ mongrel_conf:: Defaults to "#{shared_path}/mongrel_cluster.conf"
59
+ mongrel_config_script:: Defaults to nil
60
+ mongrel_environment:: Defaults to "production"
61
+ mongrel_group:: Defaults to nil
62
+ mongrel_log_file:: Defaults to nil
63
+ mongrel_pid_file:: Defaults to nil
64
+ mongrel_port:: Defaults to 8000
65
+ mongrel_prefix:: Defaults to nil
66
+ mongrel_servers:: Defaults to 2
67
+ mongrel_user:: Defaults to nil
68
+
69
+ == Perforce SCM Variables:
70
+
71
+ p4_cmd:: The perforce command to use. Defaults to "p4"
72
+ source:: A perforce SCM worker instance.
73
+
74
+ == Subversion SCM Variables:
75
+
76
+ source:: A subversion SCM worker instance.
77
+ svn_cmd:: The subversion command to use. Defaults to "svn"
78
+
@@ -0,0 +1,595 @@
1
+ require 'rubygems'
2
+ require 'open4'
3
+ require 'rake'
4
+ require 'vlad'
5
+
6
+ $TESTING ||= false
7
+ $TRACE = Rake.application.options.trace
8
+
9
+ ##
10
+ # Declare a remote host and its roles. Equivalent to <tt>role</tt>,
11
+ # but shorter for multiple roles.
12
+ def host host_name, *roles
13
+ Rake::RemoteTask.host host_name, *roles
14
+ end
15
+
16
+ ##
17
+ # Copy a (usually generated) file to +remote_path+. Contents of block
18
+ # are copied to +remote_path+ and you may specify an optional
19
+ # base_name for the tempfile (aids in debugging).
20
+
21
+ def put remote_path, base_name = 'vlad.unknown'
22
+ Tempfile.open base_name do |fp|
23
+ fp.puts yield
24
+ fp.flush
25
+ rsync fp.path, remote_path
26
+ end
27
+ end
28
+
29
+ ##
30
+ # Declare a Vlad task that will execute on all hosts by default. To
31
+ # limit that task to specific roles, use:
32
+ #
33
+ # remote_task :example, :arg1, :roles => [:app, :web] do
34
+ def remote_task name, *args_options, &b
35
+ Rake::RemoteTask.remote_task name, *args_options, &b
36
+ end
37
+
38
+ ##
39
+ # Declare a role and assign a remote host to it. Equivalent to the
40
+ # <tt>host</tt> method; provided for capistrano compatibility.
41
+ def role role_name, host = nil, args = {}
42
+ if block_given? then
43
+ raise ArgumentError, 'host not allowed with block' unless host.nil?
44
+
45
+ begin
46
+ Rake::RemoteTask.current_roles << role_name
47
+ yield
48
+ ensure
49
+ Rake::RemoteTask.current_roles.delete role_name
50
+ end
51
+ else
52
+ raise ArgumentError, 'host required' if host.nil?
53
+ Rake::RemoteTask.role role_name, host, args
54
+ end
55
+ end
56
+
57
+ ##
58
+ # Execute the given command on the <tt>target_host</tt> for the
59
+ # current task.
60
+ def run *args, &b
61
+ Thread.current[:task].run(*args, &b)
62
+ end
63
+
64
+ # rsync the given files to <tt>target_host</tt>.
65
+ def rsync local, remote
66
+ Thread.current[:task].rsync local, remote
67
+ end
68
+
69
+ # Declare a variable called +name+ and assign it a value. A
70
+ # globally-visible method with the name of the variable is defined.
71
+ # If a block is given, it will be called when the variable is first
72
+ # accessed. Subsequent references to the variable will always return
73
+ # the same value. Raises <tt>ArgumentError</tt> if the +name+ would
74
+ # conflict with an existing method.
75
+ def set name, val = nil, &b
76
+ Rake::RemoteTask.set name, val, &b
77
+ end
78
+
79
+ # Returns the name of the host that the current task is executing on.
80
+ # <tt>target_host</tt> can uniquely identify a particular task/host
81
+ # combination.
82
+ def target_host
83
+ Thread.current[:task].target_host
84
+ end
85
+
86
+ if Gem::Version.new(RAKEVERSION) < Gem::Version.new('0.8') then
87
+ class Rake::Task
88
+ alias vlad_original_execute execute
89
+
90
+ def execute(args = nil)
91
+ vlad_original_execute
92
+ end
93
+ end
94
+ end
95
+
96
+ ##
97
+ # Rake::RemoteTask is a subclass of Rake::Task that adds
98
+ # remote_actions that execute in parallel on multiple hosts via ssh.
99
+
100
+ class Rake::RemoteTask < Rake::Task
101
+
102
+ @@current_roles = []
103
+
104
+ include Open4
105
+
106
+ ##
107
+ # Options for execution of this task.
108
+
109
+ attr_accessor :options
110
+
111
+ ##
112
+ # The host this task is running on during execution.
113
+
114
+ attr_accessor :target_host
115
+
116
+ ##
117
+ # An Array of Actions this host will perform during execution. Use
118
+ # enhance to add new actions to a task.
119
+
120
+ attr_reader :remote_actions
121
+
122
+ def self.current_roles
123
+ @@current_roles
124
+ end
125
+
126
+ ##
127
+ # Create a new task named +task_name+ attached to Rake::Application +app+.
128
+
129
+ def initialize(task_name, app)
130
+ super
131
+ @remote_actions = []
132
+ end
133
+
134
+ ##
135
+ # Add a local action to this task. This calls Rake::Task#enhance.
136
+
137
+ alias_method :original_enhance, :enhance
138
+
139
+ ##
140
+ # Add remote action +block+ to this task with dependencies +deps+. See
141
+ # Rake::Task#enhance.
142
+
143
+ def enhance(deps=nil, &block)
144
+ original_enhance(deps) # can't use super because block passed regardless.
145
+ @remote_actions << Action.new(self, block) if block_given?
146
+ self
147
+ end
148
+
149
+ ##
150
+ # Execute this action. Local actions will be performed first, then remote
151
+ # actions will be performed in parallel on each host configured for this
152
+ # RemoteTask.
153
+
154
+ def execute(args = nil)
155
+ raise(Vlad::ConfigurationError,
156
+ "No target hosts specified on task #{self.name} for roles #{options[:roles].inspect}") if
157
+ ! defined_target_hosts?
158
+
159
+ super args
160
+
161
+ @remote_actions.each { |act| act.execute(target_hosts, self, args) }
162
+ end
163
+
164
+ ##
165
+ # Use rsync to send +local+ to +remote+ on target_host.
166
+
167
+ def rsync local, remote
168
+ cmd = [rsync_cmd, rsync_flags, local, "#{@target_host}:#{remote}"]
169
+ cmd = cmd.flatten.compact
170
+
171
+ warn cmd.join(' ') if $TRACE
172
+
173
+ success = system(*cmd)
174
+
175
+ unless success then
176
+ raise Vlad::CommandFailedError, "execution failed: #{cmd.join ' '}"
177
+ end
178
+ end
179
+
180
+ ##
181
+ # Use ssh to execute +command+ on target_host. If +command+ uses sudo, the
182
+ # sudo password will be prompted for then saved for subsequent sudo commands.
183
+
184
+ def run command
185
+ cmd = [ssh_cmd, ssh_flags, target_host, command].flatten
186
+ result = []
187
+
188
+ warn cmd.join(' ') if $TRACE
189
+
190
+ pid, inn, out, err = popen4(*cmd)
191
+
192
+ inn.sync = true
193
+ streams = [out, err]
194
+ out_stream = {
195
+ out => $stdout,
196
+ err => $stderr,
197
+ }
198
+
199
+ # Handle process termination ourselves
200
+ status = nil
201
+ Thread.start do
202
+ status = Process.waitpid2(pid).last
203
+ end
204
+
205
+ until streams.empty? do
206
+ # don't busy loop
207
+ selected, = select streams, nil, nil, 0.1
208
+
209
+ next if selected.nil? or selected.empty?
210
+
211
+ selected.each do |stream|
212
+ if stream.eof? then
213
+ streams.delete stream if status # we've quit, so no more writing
214
+ next
215
+ end
216
+
217
+ data = stream.readpartial(1024)
218
+ out_stream[stream].write data
219
+
220
+ if stream == err and data =~ /^Password:/ then
221
+ inn.puts sudo_password
222
+ data << "\n"
223
+ $stderr.write "\n"
224
+ end
225
+
226
+ result << data
227
+ end
228
+ end
229
+
230
+ unless status.success? then
231
+ raise(Vlad::CommandFailedError,
232
+ "execution failed with status #{status.exitstatus}: #{cmd.join ' '}")
233
+ end
234
+
235
+ result.join
236
+ end
237
+
238
+ ##
239
+ # Returns an Array with every host configured.
240
+
241
+ def self.all_hosts
242
+ hosts_for(roles.keys)
243
+ end
244
+
245
+ ##
246
+ # The default environment values. Used for resetting (mostly for
247
+ # tests).
248
+
249
+ def self.default_env
250
+ @@default_env
251
+ end
252
+
253
+ ##
254
+ # The vlad environment.
255
+
256
+ def self.env
257
+ @@env
258
+ end
259
+
260
+ ##
261
+ # Fetches environment variable +name+ from the environment using
262
+ # default +default+.
263
+
264
+ def self.fetch name, default = nil
265
+ name = name.to_s if Symbol === name
266
+ if @@env.has_key? name then
267
+ protect_env(name) do
268
+ v = @@env[name]
269
+ v = @@env[name] = v.call if Proc === v
270
+ v
271
+ end
272
+ elsif default
273
+ v = @@env[name] = default
274
+ else
275
+ raise Vlad::FetchError
276
+ end
277
+ end
278
+
279
+ ##
280
+ # Add host +host_name+ that belongs to +roles+. Extra arguments may
281
+ # be specified for the host as a hash as the last argument.
282
+ #
283
+ # host is the inversion of role:
284
+ #
285
+ # host 'db1.example.com', :db, :master_db
286
+ #
287
+ # Is equivalent to:
288
+ #
289
+ # role :db, 'db1.example.com'
290
+ # role :master_db, 'db1.example.com'
291
+
292
+ def self.host host_name, *roles
293
+ opts = Hash === roles.last ? roles.pop : {}
294
+
295
+ roles.each do |role_name|
296
+ role role_name, host_name, opts.dup
297
+ end
298
+ end
299
+
300
+ ##
301
+ # Returns an Array of all hosts in +roles+.
302
+
303
+ def self.hosts_for *roles
304
+ roles.flatten.map { |r|
305
+ self.roles[r].keys
306
+ }.flatten.uniq.sort
307
+ end
308
+
309
+ def self.mandatory name, desc # :nodoc:
310
+ self.set(name) do
311
+ raise(Vlad::ConfigurationError,
312
+ "Please specify the #{desc} via the #{name.inspect} variable")
313
+ end
314
+ end
315
+
316
+ ##
317
+ # Ensures exclusive access to +name+.
318
+
319
+ def self.protect_env name # :nodoc:
320
+ @@env_locks[name].synchronize do
321
+ yield
322
+ end
323
+ end
324
+
325
+ ##
326
+ # Adds a remote task named +name+ with options +options+ that will
327
+ # execute +block+.
328
+
329
+ def self.remote_task name, *args, &block
330
+ options = (Hash === args.last) ? args.pop : {}
331
+ t = Rake::RemoteTask.define_task(name, *args, &block)
332
+ options[:roles] = Array options[:roles]
333
+ options[:roles] |= @@current_roles
334
+ t.options = options
335
+ t
336
+ end
337
+
338
+ ##
339
+ # Ensures +name+ does not conflict with an existing method.
340
+
341
+ def self.reserved_name? name # :nodoc:
342
+ !@@env.has_key?(name.to_s) && self.respond_to?(name)
343
+ end
344
+
345
+ ##
346
+ # Resets vlad, restoring all roles, tasks and environment variables
347
+ # to the defaults.
348
+
349
+ def self.reset
350
+ @@def_role_hash = {} # official default role value
351
+ @@roles = Hash.new { |h,k| h[k] = @@def_role_hash }
352
+ @@env = {}
353
+ @@tasks = {}
354
+ @@env_locks = Hash.new { |h,k| h[k] = Mutex.new }
355
+
356
+ @@default_env.each do |k,v|
357
+ case v
358
+ when Symbol, Fixnum, nil, true, false, 42 then # ummmm... yeah. bite me.
359
+ @@env[k] = v
360
+ else
361
+ @@env[k] = v.dup
362
+ end
363
+ end
364
+ end
365
+
366
+ ##
367
+ # Adds role +role_name+ with +host+ and +args+ for that host.
368
+
369
+ def self.role role_name, host, args = {}
370
+ [*host].each do |hst|
371
+ raise ArgumentError, "invalid host: #{hst}" if hst.nil? or hst.empty?
372
+ end
373
+ @@roles[role_name] = {} if @@def_role_hash.eql? @@roles[role_name]
374
+ @@roles[role_name][host] = args
375
+ end
376
+
377
+ ##
378
+ # The configured roles.
379
+
380
+ def self.roles
381
+ host domain, :app, :web, :db if @@roles.empty?
382
+
383
+ @@roles
384
+ end
385
+
386
+ ##
387
+ # Set environment variable +name+ to +value+ or +default_block+.
388
+ #
389
+ # If +default_block+ is defined, the block will be executed the
390
+ # first time the variable is fetched, and the value will be used for
391
+ # every subsequent fetch.
392
+
393
+ def self.set name, value = nil, &default_block
394
+ raise ArgumentError, "cannot provide both a value and a block" if
395
+ value and default_block
396
+ raise ArgumentError, "cannot set reserved name: '#{name}'" if
397
+ Rake::RemoteTask.reserved_name?(name) unless $TESTING
398
+
399
+ Rake::RemoteTask.default_env[name.to_s] = Rake::RemoteTask.env[name.to_s] =
400
+ value || default_block
401
+
402
+ Object.send :define_method, name do
403
+ Rake::RemoteTask.fetch name
404
+ end
405
+ end
406
+
407
+ ##
408
+ # Sets all the default values. Should only be called once. Use reset
409
+ # if you need to restore values.
410
+
411
+ def self.set_defaults
412
+ @@default_env ||= {}
413
+ self.reset
414
+
415
+ mandatory :repository, "repository path"
416
+ mandatory :deploy_to, "deploy path"
417
+ mandatory :domain, "server domain"
418
+
419
+ simple_set(:deploy_timestamped, true,
420
+ :deploy_via, :export,
421
+ :keep_releases, 5,
422
+ :migrate_args, "",
423
+ :migrate_target, :latest,
424
+ :rails_env, "production",
425
+ :rake_cmd, "rake",
426
+ :revision, "head",
427
+ :rsync_cmd, "rsync",
428
+ :rsync_flags, ['-azP', '--delete'],
429
+ :ssh_cmd, "ssh",
430
+ :ssh_flags, [],
431
+ :sudo_cmd, "sudo",
432
+ :sudo_flags, nil,
433
+ :umask, '02')
434
+
435
+ set(:current_release) { File.join(releases_path, releases[-1]) }
436
+ set(:latest_release) { deploy_timestamped ?release_path: current_release }
437
+ set(:previous_release) { File.join(releases_path, releases[-2]) }
438
+ set(:release_name) { Time.now.utc.strftime("%Y%m%d%H%M%S") }
439
+ set(:release_path) { File.join(releases_path, release_name) }
440
+ set(:releases) { task.run("ls -x #{releases_path}").split.sort }
441
+
442
+ set_path :current_path, "current"
443
+ set_path :releases_path, "releases"
444
+ set_path :scm_path, "scm"
445
+ set_path :shared_path, "shared"
446
+
447
+ set(:sudo_password) do
448
+ state = `stty -g`
449
+
450
+ raise Vlad::Error, "stty(1) not found" unless $?.success?
451
+
452
+ begin
453
+ system "stty -echo"
454
+ $stdout.print "sudo password: "
455
+ $stdout.flush
456
+ sudo_password = $stdin.gets
457
+ $stdout.puts
458
+ ensure
459
+ system "stty #{state}"
460
+ end
461
+ sudo_password
462
+ end
463
+ end
464
+
465
+ def self.set_path(name, subdir) # :nodoc:
466
+ set(name) { File.join(deploy_to, subdir) }
467
+ end
468
+
469
+ def self.simple_set(*args) # :nodoc:
470
+ args = Hash[*args]
471
+ args.each do |k, v|
472
+ set k, v
473
+ end
474
+ end
475
+
476
+ ##
477
+ # The Rake::RemoteTask executing in this Thread.
478
+
479
+ def self.task
480
+ Thread.current[:task]
481
+ end
482
+
483
+ ##
484
+ # The configured Rake::RemoteTasks.
485
+
486
+ def self.tasks
487
+ @@tasks
488
+ end
489
+
490
+ ##
491
+ # Execute +command+ under sudo using run.
492
+
493
+ def sudo command
494
+ run [sudo_cmd, sudo_flags, command].compact.join(" ")
495
+ end
496
+
497
+ ##
498
+ # The hosts this task will execute on. The hosts are determined from
499
+ # the role this task belongs to.
500
+ #
501
+ # The target hosts may be overridden by providing a comma-separated
502
+ # list of commands to the HOSTS environment variable:
503
+ #
504
+ # rake my_task HOSTS=app1.example.com,app2.example.com
505
+
506
+ def target_hosts
507
+ if hosts = ENV["HOSTS"] then
508
+ hosts.strip.gsub(/\s+/, '').split(",")
509
+ else
510
+ roles = Array options[:roles]
511
+
512
+ if roles.empty? then
513
+ Rake::RemoteTask.all_hosts
514
+ else
515
+ Rake::RemoteTask.hosts_for roles
516
+ end
517
+ end
518
+ end
519
+
520
+ ##
521
+ # Similar to target_hosts, but returns true if user defined any hosts, even
522
+ # an empty list.
523
+
524
+ def defined_target_hosts?
525
+ return true if ENV["HOSTS"]
526
+ roles = Array options[:roles]
527
+ return true if roles.empty?
528
+ # borrowed from hosts_for:
529
+ roles.flatten.each { |r|
530
+ return true unless @@def_role_hash.eql? Rake::RemoteTask.roles[r]
531
+ }
532
+ return false
533
+ end
534
+
535
+ ##
536
+ # Action is used to run a task's remote_actions in parallel on each
537
+ # of its hosts. Actions are created automatically in
538
+ # Rake::RemoteTask#enhance.
539
+
540
+ class Action
541
+
542
+ ##
543
+ # The task this action is attached to.
544
+
545
+ attr_reader :task
546
+
547
+ ##
548
+ # The block this action will execute.
549
+
550
+ attr_reader :block
551
+
552
+ ##
553
+ # An Array of threads, one for each host this action executes on.
554
+
555
+ attr_reader :workers
556
+
557
+ ##
558
+ # Creates a new Action that will run +block+ for +task+.
559
+
560
+ def initialize task, block
561
+ @task = task
562
+ @block = block
563
+ @workers = []
564
+ end
565
+
566
+ def == other # :nodoc:
567
+ return false unless Action === other
568
+ block == other.block && task == other.task
569
+ end
570
+
571
+ ##
572
+ # Execute this action on +hosts+ in parallel. Returns when block
573
+ # has completed for each host.
574
+
575
+ def execute hosts, task, args
576
+ hosts.each do |host|
577
+ t = task.clone
578
+ t.target_host = host
579
+ thread = Thread.new(t) do |task|
580
+ Thread.current[:task] = task
581
+ case block.arity
582
+ when 1
583
+ block.call task
584
+ else
585
+ block.call task, args
586
+ end
587
+ end
588
+ @workers << thread
589
+ end
590
+ @workers.each { |w| w.join }
591
+ end
592
+ end
593
+ end
594
+
595
+ Rake::RemoteTask.set_defaults