brew-launchd 1.1.1

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