launchr 1.1.0

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,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
+