brew-launchd 1.1.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.
- data/.document +5 -0
- data/.gitignore +43 -0
- data/.yardopts +4 -0
- data/Gemfile +24 -0
- data/LICENSE +20 -0
- data/README.rdoc +63 -0
- data/Rakefile +78 -0
- data/VERSION +1 -0
- data/bin/brew-launchd.rb +24 -0
- data/bin/brew-restart.rb +22 -0
- data/bin/brew-start.rb +24 -0
- data/bin/brew-stop.rb +24 -0
- data/features/launchr.feature +9 -0
- data/features/step_definitions/launchr_steps.rb +0 -0
- data/features/support/env.rb +14 -0
- data/lib/launchr.rb +105 -0
- data/lib/launchr/application.rb +24 -0
- data/lib/launchr/cli.rb +142 -0
- data/lib/launchr/commands.rb +118 -0
- data/lib/launchr/extend/pathname.rb +27 -0
- data/lib/launchr/mixin/mixlib_cli.rb +693 -0
- data/lib/launchr/mixin/ordered_hash.rb +189 -0
- data/lib/launchr/mixin/popen4.rb +219 -0
- data/lib/launchr/path.rb +106 -0
- data/lib/launchr/service.rb +522 -0
- data/man1/brew-launchd.1 +95 -0
- data/man1/brew-launchd.1.html +140 -0
- data/man1/brew-launchd.1.ronn +71 -0
- data/spec/launchr/application_spec.rb +37 -0
- data/spec/launchr/cli_spec.rb +25 -0
- data/spec/launchr/commands_spec.rb +20 -0
- data/spec/launchr/config_spec.rb +38 -0
- data/spec/launchr_spec.rb +7 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- metadata +213 -0
@@ -0,0 +1,522 @@
|
|
1
|
+
|
2
|
+
require 'launchr/extend/pathname'
|
3
|
+
require 'launchr/path'
|
4
|
+
require 'launchr/mixin/popen4'
|
5
|
+
require 'timeout'
|
6
|
+
|
7
|
+
module Launchr
|
8
|
+
class Service
|
9
|
+
class << self
|
10
|
+
def sudo_launchctl_exec *args
|
11
|
+
popen "/usr/bin/sudo", "/bin/launchctl", *args
|
12
|
+
end
|
13
|
+
|
14
|
+
def launchctl_exec *args
|
15
|
+
popen "/bin/launchctl", *args
|
16
|
+
end
|
17
|
+
|
18
|
+
def popen cmd, *args
|
19
|
+
stdin_str = nil
|
20
|
+
|
21
|
+
pid, stdin, stdout, stderr = ::Launchr::Popen4::popen4 [cmd, *args]
|
22
|
+
|
23
|
+
stdin.puts stdin_str if stdin_str
|
24
|
+
stdin.close
|
25
|
+
|
26
|
+
ignored, status = [nil,nil]
|
27
|
+
|
28
|
+
begin
|
29
|
+
Timeout::timeout(Launchr.launchctl_timeout) do
|
30
|
+
ignored, status = Process::waitpid2 pid
|
31
|
+
end
|
32
|
+
rescue Timeout::Error => exc
|
33
|
+
puts "#{exc.message}, killing pid #{pid}"
|
34
|
+
Process::kill('TERM', pid)
|
35
|
+
# Process::kill('HUP', pid)
|
36
|
+
ignored, status = Process::waitpid2 pid
|
37
|
+
end
|
38
|
+
|
39
|
+
stdout_result = stdout.read.strip
|
40
|
+
stderr_result = stderr.read.strip
|
41
|
+
|
42
|
+
return { :cmd => cmd, :status => status, :stdout => stdout_result, :stderr => stderr_result }
|
43
|
+
end
|
44
|
+
|
45
|
+
def launchctl action, job, sudo=nil
|
46
|
+
result = nil
|
47
|
+
|
48
|
+
case job
|
49
|
+
when String
|
50
|
+
job = LaunchdJob.new :plist => Pathname.new(job)
|
51
|
+
when Pathname
|
52
|
+
job = LaunchdJob.new :plist => job
|
53
|
+
when LaunchdJob
|
54
|
+
else
|
55
|
+
raise "unrecognized argument"
|
56
|
+
end
|
57
|
+
|
58
|
+
args = []
|
59
|
+
case action
|
60
|
+
when :start, :stop, :remove
|
61
|
+
args << job.label
|
62
|
+
when :list
|
63
|
+
args << "-x" << job.label
|
64
|
+
when :load, :unload
|
65
|
+
args << "-w" << job.plist.readlink
|
66
|
+
when :list
|
67
|
+
args << job.label
|
68
|
+
else
|
69
|
+
raise "unsupported launchctl cmd"
|
70
|
+
end
|
71
|
+
|
72
|
+
if sudo || job.level == :boot
|
73
|
+
result = sudo_launchctl_exec action.to_s, *args
|
74
|
+
else
|
75
|
+
result = launchctl_exec action.to_s, *args
|
76
|
+
end
|
77
|
+
|
78
|
+
# puts result[:stdout] unless result[:stdout].empty?
|
79
|
+
# puts result[:stderr] unless result[:stderr].empty?
|
80
|
+
result
|
81
|
+
end
|
82
|
+
|
83
|
+
def sudo_launchctl action, job
|
84
|
+
if Launchr.superuser?
|
85
|
+
sudo = true
|
86
|
+
launchctl action, job, sudo
|
87
|
+
else
|
88
|
+
raise "Insufficient permissions, cant sudo"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def fix_broken_symlinks
|
93
|
+
brew_plists = Pathname.glob(Launchr::Path.homebrew_prefix+"Library/LaunchDaemons/*")
|
94
|
+
broken_plists = brew_plists.select { |p| p.readable? == false }
|
95
|
+
|
96
|
+
installed_plists_realpaths = Pathname.glob(Launchr::Path.homebrew_prefix+"Cellar/**/Library/LaunchDaemons/*.plist")
|
97
|
+
|
98
|
+
installed_plists_realpaths.each do |real|
|
99
|
+
pool_link = Launchr::Path.homebrew_prefix+"Library/LaunchDaemons"+real.basename
|
100
|
+
if !pool_link.exist?
|
101
|
+
pool_link.make_symlink(real.relative_path_from(Launchr::Path.brew_launchdaemons))
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
if !broken_plists.empty?
|
106
|
+
|
107
|
+
broken_plists.each do |broken|
|
108
|
+
match = installed_plists_realpaths.select { |r| r.include?(broken.basename) }.first
|
109
|
+
target = nil
|
110
|
+
case broken.readlink
|
111
|
+
when /Cellar/
|
112
|
+
broken.unlink
|
113
|
+
|
114
|
+
when /^\/Library\/LaunchDaemons/
|
115
|
+
if !Launchr.superuser?
|
116
|
+
puts "Launchctl database was left in an inconsistent state"
|
117
|
+
puts "This happens when a formula is uninstalled, but the"
|
118
|
+
puts "service was not stopped. To cleanup run the command"
|
119
|
+
puts "`sudo brew launchd clean`"
|
120
|
+
else
|
121
|
+
target = Launchr::Path.boot_launchdaemons + broken.basename
|
122
|
+
target.make_symlink(match)
|
123
|
+
launchctl :stop, broken
|
124
|
+
target.unlink
|
125
|
+
broken.unlink
|
126
|
+
if match
|
127
|
+
broken.make_symlink(match)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
when /Library\/LaunchAgents/
|
131
|
+
target = Launchr::Path.user_launchdaemons + broken.basename
|
132
|
+
target.make_symlink(match)
|
133
|
+
launchctl :stop, broken
|
134
|
+
target.unlink
|
135
|
+
broken.unlink
|
136
|
+
if match
|
137
|
+
broken.make_symlink(match)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def clean_plists_in apple_launchdaemons
|
145
|
+
# scan through the homebrew LaunchDaemons folder and readlink (read the links if a symlink)
|
146
|
+
# where the 1st level symlink points to determines the service status (up/down, running etc)
|
147
|
+
brew_plists = Pathname.glob(Launchr::Path.homebrew_prefix+"Library/LaunchDaemons/*")
|
148
|
+
brew_plists.map! { |p| p.readlink }
|
149
|
+
|
150
|
+
# the services associated to these plists are installed, but should be in the down state
|
151
|
+
# as they arent symlinked into the Apple LaunchDaemons folder
|
152
|
+
down_brew_plists = brew_plists.select { |p| p.include? Launchr::Path.homebrew_prefix }
|
153
|
+
|
154
|
+
# scan through the Apple LaunchDaemons folder and for all plists check their real path (final destination symlink)
|
155
|
+
# select any plist links which point to locations in the brew tree. These should be installed and running brew services
|
156
|
+
brew_launchdaemons_plists = apple_launchdaemons.children.select { |p| p.realpath.include? Launchr::Path.homebrew_prefix }
|
157
|
+
|
158
|
+
# sub select (select again) any plist links which are found to be a mismatch
|
159
|
+
# either they are not installed anymore (rm -rf'd without stopping the service)
|
160
|
+
# and/or were resinstalled again so their service state is no longer matching
|
161
|
+
# and reflecting the entries in the launchd database (service loaded/unloaded)
|
162
|
+
missing_brew_launchdaemons_plists = brew_launchdaemons_plists.select do |plist|
|
163
|
+
!plist.realpath.exist? || down_brew_plists.include?(plist.realpath)
|
164
|
+
end
|
165
|
+
|
166
|
+
if !missing_brew_launchdaemons_plists.empty?
|
167
|
+
# if there are broken services at boot level, it requires root permissions
|
168
|
+
# to fix the issue. We can detect these, and ask to re-run as root
|
169
|
+
if !Launchr.superuser? && apple_launchdaemons == Launchr::Path.boot_launchdaemons
|
170
|
+
puts "Launchctl database was left in an inconsistent state"
|
171
|
+
puts "This happens when a formula is uninstalled, but the"
|
172
|
+
puts "service was not stopped. To cleanup run the command"
|
173
|
+
puts "`sudo brew launchd clean`"
|
174
|
+
else
|
175
|
+
# repair the launchctl service status and remove the symlink
|
176
|
+
# from Apple's LaunchDaemons folder (we still keep a link in brew_launchdaemons)
|
177
|
+
missing_brew_launchdaemons_plists.each do |plist|
|
178
|
+
launchctl :stop, plist # ignore the return code
|
179
|
+
plist.unlink # delete broken symlink
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Clean any inconcistencies between homebrew and launchctl launcd database
|
186
|
+
# This can happen if formula were uninstalled with 'rm -rf'
|
187
|
+
def cleanup
|
188
|
+
fix_broken_symlinks
|
189
|
+
clean_plists_in Launchr::Path.user_launchdaemons
|
190
|
+
clean_plists_in Launchr::Path.boot_launchdaemons
|
191
|
+
end
|
192
|
+
|
193
|
+
def find_all
|
194
|
+
prefix = Launchr::Path.homebrew_prefix
|
195
|
+
installed_plists = Pathname.glob(prefix+"Library/LaunchDaemons/*")
|
196
|
+
|
197
|
+
formula_names = installed_plists.map do |plist|
|
198
|
+
plist.realpath.to_s.gsub(/^.*Cellar\/|\/.*$/,"")
|
199
|
+
end
|
200
|
+
formula_names.uniq!
|
201
|
+
|
202
|
+
services = formula_names.map do |formula|
|
203
|
+
find(formula)
|
204
|
+
end
|
205
|
+
|
206
|
+
services
|
207
|
+
end
|
208
|
+
|
209
|
+
# Resolve a service name to plist files, and create a new brew service object
|
210
|
+
# A service name can be a formula name, a formula alias, or plist filename / label
|
211
|
+
def find svc
|
212
|
+
formula_name = nil
|
213
|
+
jobs = []
|
214
|
+
|
215
|
+
prefix = Launchr::Path.homebrew_prefix
|
216
|
+
installed_plists = Pathname.glob(prefix+"Library/LaunchDaemons/*")
|
217
|
+
|
218
|
+
case svc
|
219
|
+
when /^[^\.]+$/
|
220
|
+
# should be a formula name, or formula alias name
|
221
|
+
|
222
|
+
if (prefix+"Library/Aliases/#{svc}").exist?
|
223
|
+
# its an alias
|
224
|
+
alias_realpath = (prefix+"Library/Aliases/#{svc}").realpath
|
225
|
+
formula_name = alias_realpath.basename(".rb").to_s
|
226
|
+
end
|
227
|
+
|
228
|
+
if (prefix+"Library/Formula/#{svc}.rb").exist?
|
229
|
+
# its a formula name
|
230
|
+
formula_name = svc
|
231
|
+
end
|
232
|
+
|
233
|
+
if formula_name
|
234
|
+
formula_plists = installed_plists.select { |p| p.realpath.include? "Cellar\/#{formula_name}" }
|
235
|
+
|
236
|
+
jobs = formula_plists.map do |p|
|
237
|
+
LaunchdJob.new :plist => p, :selected => true
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
else
|
242
|
+
# should be a launchd job label or plist filename
|
243
|
+
label = File.basename(svc,".plist")
|
244
|
+
plist_link = prefix+"Library/LaunchDaemons/#{label}.plist"
|
245
|
+
|
246
|
+
if plist_link.exist?
|
247
|
+
formula_name = plist_link.realpath.to_s.gsub(/^.*Cellar\/|\/.*$/,"")
|
248
|
+
formula_plists = installed_plists.select { |p| p.realpath.include? "Cellar\/#{formula_name}" }
|
249
|
+
|
250
|
+
jobs = formula_plists.map do |p|
|
251
|
+
selected = nil
|
252
|
+
(p == plist_link) ? selected = true : selected = false
|
253
|
+
LaunchdJob.new :plist => p, :selected => selected
|
254
|
+
end
|
255
|
+
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
if formula_name
|
260
|
+
Launchr::Service.new(formula_name, jobs)
|
261
|
+
else
|
262
|
+
# svc could be a valid service identifier, but just not installed yet. would
|
263
|
+
# have to grep inside all the Formula files for "launchd_plist <job_label>" definitions
|
264
|
+
# in order to figure that out. an expensive operation (but very cacheable)
|
265
|
+
puts "Couldnt find any installed service matching \"#{svc}\" (no matches)"
|
266
|
+
raise "Service #{svc} not found"
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
end
|
271
|
+
|
272
|
+
class LaunchdJob
|
273
|
+
attr_accessor :label, :plist, :selected
|
274
|
+
attr_accessor :level
|
275
|
+
|
276
|
+
def plist= plist
|
277
|
+
@plist = plist
|
278
|
+
@label = plist.basename(".plist")
|
279
|
+
end
|
280
|
+
|
281
|
+
def level
|
282
|
+
if @plist
|
283
|
+
case @plist.readlink
|
284
|
+
when /Cellar/
|
285
|
+
@level = nil # stopped service
|
286
|
+
when /^\/Library\/LaunchDaemons/
|
287
|
+
@level = :boot # started system service
|
288
|
+
when /Library\/LaunchAgents/
|
289
|
+
@level = :user # started user service
|
290
|
+
end
|
291
|
+
else
|
292
|
+
@level = nil
|
293
|
+
end
|
294
|
+
@level
|
295
|
+
end
|
296
|
+
|
297
|
+
|
298
|
+
def started?
|
299
|
+
!! level
|
300
|
+
end
|
301
|
+
|
302
|
+
def initialize *args, &blk
|
303
|
+
case args.first
|
304
|
+
when Hash
|
305
|
+
opts = args.first
|
306
|
+
opts.each do |key, value|
|
307
|
+
self.send "#{key}=".to_sym, value
|
308
|
+
end
|
309
|
+
else
|
310
|
+
raise "Unrecognized first argument: #{args.first.inspect}"
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
def hash
|
316
|
+
@name.hash
|
317
|
+
end
|
318
|
+
|
319
|
+
def eql? other
|
320
|
+
@name = other.name
|
321
|
+
end
|
322
|
+
|
323
|
+
attr_accessor :name, :jobs
|
324
|
+
attr_accessor :plists, :plist_states, :selected_plists
|
325
|
+
|
326
|
+
def initialize formula_name, jobs
|
327
|
+
@name = formula_name
|
328
|
+
@jobs = jobs
|
329
|
+
@keg = keg
|
330
|
+
end
|
331
|
+
|
332
|
+
def launchctl action, job
|
333
|
+
self.class.launchctl action, job
|
334
|
+
end
|
335
|
+
|
336
|
+
def sudo_launchctl action, job
|
337
|
+
self.class.sudo_launchctl action, job
|
338
|
+
end
|
339
|
+
|
340
|
+
def keg
|
341
|
+
unless @keg
|
342
|
+
@keg = false
|
343
|
+
@jobs.each do |job|
|
344
|
+
if job.plist && job.plist.readable?
|
345
|
+
keg_relative = Pathname.new(job.plist.realpath.to_s.gsub(/\/Library\/LaunchDaemons.*/,""))
|
346
|
+
@keg = keg_relative.expand_path(job.plist)
|
347
|
+
break
|
348
|
+
end
|
349
|
+
end
|
350
|
+
else
|
351
|
+
@keg ||= false
|
352
|
+
end
|
353
|
+
@keg
|
354
|
+
end
|
355
|
+
|
356
|
+
def selected_jobs
|
357
|
+
@jobs.select { |job| job.selected == true }
|
358
|
+
end
|
359
|
+
|
360
|
+
def selected_stopped_jobs
|
361
|
+
@jobs.select { |job| job.selected == true && ! job.started? }
|
362
|
+
end
|
363
|
+
|
364
|
+
def start
|
365
|
+
if selected_jobs.empty?
|
366
|
+
puts "#{name} - Nothing to start"
|
367
|
+
return
|
368
|
+
end
|
369
|
+
|
370
|
+
if Launchr.config[:boot] && ! Launchr.superuser?
|
371
|
+
raise "To start a boot time service requires sudo. Use sudo brew start --boot #{Launchr.config[:args][:start].join(' ')}"
|
372
|
+
end
|
373
|
+
|
374
|
+
launchdaemons = nil
|
375
|
+
unless selected_stopped_jobs.empty?
|
376
|
+
if Launchr.config[:boot]
|
377
|
+
puts "Chowning #{@keg} to root:wheel"
|
378
|
+
@keg.chown_R "root", "wheel"
|
379
|
+
launchdaemons = Launchr::Path.boot_launchdaemons
|
380
|
+
else
|
381
|
+
launchdaemons = Launchr::Path.user_launchdaemons
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
selected_jobs.each do |job|
|
386
|
+
if Launchr.superuser? && job.level == :user
|
387
|
+
raise "#{job.label} is already started at user login. Stop the service first, or sudo brew restart --boot"
|
388
|
+
elsif job.level == :boot
|
389
|
+
raise "#{job.label} is already started at boot. Stop the service first, or brew restart --user"
|
390
|
+
end
|
391
|
+
|
392
|
+
if !job.started?
|
393
|
+
target = launchdaemons + job.plist.realpath.basename
|
394
|
+
target.make_symlink(job.plist.realpath)
|
395
|
+
|
396
|
+
job.plist.unlink
|
397
|
+
job.plist.make_symlink(target)
|
398
|
+
|
399
|
+
result = nil
|
400
|
+
if Launchr.config[:boot]
|
401
|
+
result = sudo_launchctl :load, job
|
402
|
+
else
|
403
|
+
result = launchctl :load, job
|
404
|
+
end
|
405
|
+
|
406
|
+
if result[:status].exitstatus != 0
|
407
|
+
if result[:status].exitstatus
|
408
|
+
puts "Launchctl exited with code #{result[:status].exitstatus} when trying to stop \"#{job.label}\""
|
409
|
+
else
|
410
|
+
puts "Launchctl terminated unexpectedly with #{result[:status].inspect}"
|
411
|
+
end
|
412
|
+
puts result[:stdout] unless result[:stdout].empty?
|
413
|
+
puts result[:stderr] unless result[:stderr].empty?
|
414
|
+
end
|
415
|
+
|
416
|
+
end
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
def stop
|
421
|
+
if selected_jobs.empty?
|
422
|
+
puts "#{name} - Nothing to stop"
|
423
|
+
return
|
424
|
+
end
|
425
|
+
|
426
|
+
selected_jobs.each do |job|
|
427
|
+
if job.started?
|
428
|
+
if job.level == :boot && !Launchr.superuser?
|
429
|
+
raise "To stop a boot time service requires sudo. Use sudo brew stop #{Launchr.config[:args][:stop].join(' ')}"
|
430
|
+
end
|
431
|
+
|
432
|
+
result = launchctl :unload, job
|
433
|
+
|
434
|
+
if result[:status].exitstatus != 0
|
435
|
+
if result[:status].exitstatus
|
436
|
+
puts "Launchctl exited with code #{result[:status].exitstatus} when trying to stop \"#{job.label}\""
|
437
|
+
else
|
438
|
+
puts "Launchctl terminated unexpectedly with #{result[:status].inspect}"
|
439
|
+
end
|
440
|
+
puts result[:stdout] unless result[:stdout].empty?
|
441
|
+
puts result[:stderr] unless result[:stderr].empty?
|
442
|
+
end
|
443
|
+
|
444
|
+
source = job.plist.realpath
|
445
|
+
job.plist.readlink.unlink
|
446
|
+
job.plist.unlink
|
447
|
+
job.plist.make_symlink(source.relative_path_from(Launchr::Path.brew_launchdaemons))
|
448
|
+
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
if Launchr.superuser? && @jobs.select { |job| job.level == :boot }.empty?
|
453
|
+
if @keg.user == "root"
|
454
|
+
puts "Chowning #{@keg} to #{@keg.parent.user}:#{@keg.parent.group}"
|
455
|
+
@keg.chown_R @keg.parent.user, @keg.parent.group
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
def restart
|
461
|
+
if Launchr.config[:boot] && ! Launchr.superuser?
|
462
|
+
raise "To restart a service for boot time requires sudo. Use sudo brew restart --boot #{Launchr.config[:args][:restart].join(' ')}"
|
463
|
+
end
|
464
|
+
|
465
|
+
selected_jobs.select { |job| job.started? }. each do |job|
|
466
|
+
if job.level == :boot && !Launchr.superuser?
|
467
|
+
raise "To restart a running boot time service requires sudo. Use sudo brew restart #{Launchr.config[:args][:restart].join(' ')}"
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
stop
|
472
|
+
start
|
473
|
+
end
|
474
|
+
|
475
|
+
def self.header
|
476
|
+
out = []
|
477
|
+
out << sprintf("%-20.20s %-30.30s %-10.10s %-20.20s", "Service", "Launchd job label", "Status", "Level")
|
478
|
+
out << sprintf("%-20.20s %-30.30s %-10.10s %-20.20s", "-------", "-----------------", "------", "-----")
|
479
|
+
out.join("\n")
|
480
|
+
end
|
481
|
+
|
482
|
+
def printline
|
483
|
+
out = []
|
484
|
+
jobs.each do |job|
|
485
|
+
s = ""
|
486
|
+
l = ""
|
487
|
+
case job.level
|
488
|
+
when :boot
|
489
|
+
s << "Started"
|
490
|
+
l << "System Boot"
|
491
|
+
when :user
|
492
|
+
s << "Started"
|
493
|
+
l << "User login"
|
494
|
+
else
|
495
|
+
s << "Stopped"
|
496
|
+
# l << "n/a"
|
497
|
+
l << "-"
|
498
|
+
end
|
499
|
+
if job == jobs.first
|
500
|
+
out << sprintf("%-20.20s %-30.30s %-10.10s %-20.20s", name, job.label, s, l)
|
501
|
+
else
|
502
|
+
out << sprintf("%-20.20s %-30.30s %-10.10s %-20.20s", "", job.label, s, l)
|
503
|
+
end
|
504
|
+
end
|
505
|
+
out.join("\n")
|
506
|
+
end
|
507
|
+
|
508
|
+
def to_s
|
509
|
+
name
|
510
|
+
end
|
511
|
+
|
512
|
+
def inspect
|
513
|
+
self.class.header + printline
|
514
|
+
end
|
515
|
+
|
516
|
+
def info
|
517
|
+
puts printline
|
518
|
+
end
|
519
|
+
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|