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