- 
      
- 
        Save bryanveloso/1446575 to your computer and use it in GitHub Desktop. 
| #!/usr/bin/env ruby -w | |
| # brew-services(1) - Easily start and stop formulas via launchctl | |
| # =============================================================== | |
| # | |
| # ## SYNOPSIS | |
| # | |
| # [<sudo>] `brew services` `list`<br> | |
| # [<sudo>] `brew services` `restart` <formula><br> | |
| # [<sudo>] `brew services` `start` <formula> [<plist>]<br> | |
| # [<sudo>] `brew services` `stop` <formula><br> | |
| # [<sudo>] `brew services` `cleanup`<br> | |
| # | |
| # ## DESCRIPTION | |
| # | |
| # Integrates homebrew formulas with MacOS X' `launchctl` manager. Services | |
| # can either be added to `/Library/LaunchDaemons` or `~/Library/LaunchAgents`. | |
| # Basically items added to `/Library/LaunchDaemons` are started at boot, | |
| # those in `~/Library/LaunchAgents` at login. | |
| # | |
| # When started with `sudo` it operates on `/Library/LaunchDaemons`, else | |
| # in the user space. | |
| # | |
| # Basically on `start` the plist file is generated and written to a `Tempfile`, | |
| # then copied to the launch path (existing plists are overwritten). | |
| # | |
| # ## OPTIONS | |
| # | |
| # To access everything quickly, some aliases have been added: | |
| # | |
| # * `rm`: | |
| # Shortcut for `cleanup`, because that's basically whats being done. | |
| # | |
| # * `ls`: | |
| # Because `list` is too much to type :) | |
| # | |
| # * `reload', 'r': | |
| # Alias for `restart`, which gracefully restarts selected service. | |
| # | |
| # * `load`, `s`: | |
| # Alias for `start`, guess what it does... | |
| # | |
| # * `unload`, `term`, `t`: | |
| # Alias for `stop`, stops and unloads selected service. | |
| # | |
| # ## SYNTAX | |
| # | |
| # Several existing formulas (like mysql, nginx) already write custom plist | |
| # files to the formulas prefix. Most of these implement `#startup_plist` | |
| # which then in turn returns a neat-o plist file as string. | |
| # | |
| # `brew services` operates on `#startup_plist` as well and requires | |
| # supporting formulas to implement it. This method should either string | |
| # containing the generated XML file, or return a `Pathname` instance which | |
| # points to a plist template, or a hash like: | |
| # | |
| # { :url => "https://gist.github.com/raw/534777/63c4698872aaef11fe6e6c0c5514f35fd1b1687b/nginx.plist.xml" } | |
| # | |
| # Some simple template parsing is performed, all variables like `{{name}}` are | |
| # replaced by basically doing: | |
| # `formula.send('name').to_s if formula.respond_to?('name')`, a bit like | |
| # mustache. So any variable in the `Formula` is available as template | |
| # variable, like `{{var}}`, `{{bin}}` usw. | |
| # | |
| # ## EXAMPLES | |
| # | |
| # Install and start service mysql at boot: | |
| # | |
| # $ brew install mysql | |
| # $ sudo brew services start mysql | |
| # | |
| # Stop service mysql (when launched at boot): | |
| # | |
| # $ sudo brew services stop mysql | |
| # | |
| # Start memcached at login: | |
| # | |
| # $ brew install memcached | |
| # $ brew services start memcached | |
| # | |
| # List all running services for current user, and root: | |
| # | |
| # $ brew services list | |
| # $ sudo brew services list | |
| # | |
| # ## BUGS | |
| # | |
| # `brew-services.rb` might not handle all edge cases, though it tries | |
| # to fix problems by running `brew services cleanup`. | |
| # | |
| # ## COPYRIGHT | |
| # | |
| # Copyright (c) 2010 Lukas Westermann <[email protected]> | |
| # | |
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |
| # of this software and associated documentation files (the "Software"), to deal | |
| # in the Software without restriction, including without limitation the rights | |
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| # copies of the Software, and to permit persons to whom the Software is | |
| # furnished to do so, subject to the following conditions: | |
| # | |
| # The above copyright notice and this permission notice shall be included in | |
| # all copies or substantial portions of the Software. | |
| # | |
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| # THE SOFTWARE. | |
| # | |
| module ServicesCli | |
| class << self | |
| # Binary name. | |
| def bin; "brew services" end | |
| # Path to launchctl binary. | |
| def launchctl; "/bin/launchctl" end | |
| # Wohoo, we are root dude! | |
| def root?; Process.uid == 0 end | |
| # Current user, i.e. owner of `HOMEBREW_CELLAR`. | |
| def user; @user ||= %x{/usr/bin/stat -f '%Su' #{HOMEBREW_CELLAR} 2>/dev/null}.chomp || %x{/usr/bin/whoami}.chomp end | |
| # Run at boot. | |
| def boot_path; Pathname.new("/Library/LaunchDaemons") end | |
| # Run at login. | |
| def user_path; Pathname.new(ENV['HOME'] + '/Library/LaunchAgents') end | |
| # If root returns `boot_path` else `user_path`. | |
| def path; root? ? boot_path : user_path end | |
| # Print warning message, which looks similar to homebrew's. | |
| def eww(msg); STDERR.puts("#{Tty.red}Warning#{Tty.reset}: #{msg}"); true end | |
| # Display error message and fail using `abort` if `fail == true`. | |
| def argh(msg, fail = true); "#{Tty.red}Error#{Tty.reset}: #{msg}".tap { |s| fail ? abort(s) : STDERR.puts(s) } end | |
| # Find all currently running services via launchctl list | |
| def running; %x{#{launchctl} list | grep com.github.homebrew}.chomp.split("\n").map { |svc| $1 if svc =~ /(com\.github\.homebrew\..+)\z/ }.compact end | |
| # Check if running as homebre and load required libraries et al. | |
| def homebrew! | |
| abort("Runtime error: homebrew is required, please start via `#{bin} ...`") unless defined?(HOMEBREW_LIBRARY_PATH) | |
| %w{fileutils pathname tempfile formula}.each { |req| require(req) } | |
| self.send(:extend, ::FileUtils) | |
| ::Formula.send(:include, Service::PlistSupport) | |
| end | |
| # Access current service | |
| def service; @service ||= Service.new(Formula.factory(@formula)) if @formula end | |
| # Print usage and `exit(...)` with supplied exit code, if code | |
| # is set to `false`, then exit is ignored. | |
| def usage(code = 0) | |
| puts "usage: [sudo] #{bin} [--help] <command> [<formula>]" | |
| puts | |
| puts "Small wrapper around `launchctl` for supported formulas, commands available:" | |
| puts " cleanup Get rid of stale services and unused plists" | |
| puts " list List all services managed by `#{bin}`" | |
| puts " restart Gracefully restart selected service" | |
| puts " start Start selected service" | |
| puts " stop Stop selected service" | |
| puts | |
| puts "Options, sudo and paths:" | |
| puts | |
| puts " sudo When run as root, operates on #{boot_path} (run at boot!)" | |
| puts " Run at boot: #{boot_path}" | |
| puts " Run at login: #{user_path}" | |
| puts | |
| exit(code) unless exit == false | |
| true | |
| end | |
| # Run and start the command loop. | |
| def run! | |
| homebrew! | |
| usage if ARGV.empty? || ARGV.include?('help') || ARGV.include?('--help') || ARGV.include?('-h') | |
| # parse arguments | |
| @args = ARGV.reject { |arg| arg[0] == 45 }.map { |arg| arg.include?("/") ? arg : arg.downcase } # 45.chr == '-' | |
| @cmd = @args.shift | |
| @formula = @args.shift | |
| # dispatch commands and aliases | |
| case @cmd | |
| when 'cleanup', 'clean', 'cl', 'rm' then cleanup | |
| when 'list', 'ls' then list | |
| when 'restart', 'relaunch', 'reload', 'r' then check and restart | |
| when 'start', 'launch', 'load', 's', 'l' then check and start | |
| when 'stop', 'unload', 'terminate', 'term', 't', 'u' then check and stop | |
| else | |
| argh "Unknown command `#{@cmd}`", false | |
| usage(1) | |
| end | |
| end | |
| # Check if formula has been found | |
| def check | |
| argh("Formula missing, please provide a formula name") unless service | |
| true | |
| end | |
| # List all running services with PID and status and path to plist file, if available | |
| def list | |
| eww("No %s services controlled by `#{bin}` running..." % [root? ? 'root' : 'user-space']) and return if running.empty? | |
| running.each do |label| | |
| if svc = Service.from(label) | |
| status = !svc.dest.file? ? "#{Tty.red}stale " : "#{Tty.white}started" | |
| puts "%-10.10s %s#{Tty.reset} %7s %s" % [svc.name, status, svc.pid ? svc.pid.to_s : '-', svc.dest.file? ? svc.dest : label] | |
| else | |
| puts "%-10.10s #{Tty.red}unknown#{Tty.reset} %7s #{label}" % ["?", "-"] | |
| end | |
| end | |
| end | |
| # Kill services without plist file and remove unused plists | |
| def cleanup | |
| cleaned = [] | |
| # 1. kill services which have no plist file | |
| running.each do |label| | |
| if svc = Service.from(label) | |
| if !svc.dest.file? | |
| puts "%-15.15s #{Tty.white}stale#{Tty.reset} => killing service..." % svc.name | |
| kill(svc) | |
| cleaned << label | |
| end | |
| else | |
| eww "Service #{label} not managed by `#{bin}` => skipping" | |
| end | |
| end | |
| # 2. remove unused plist files | |
| Dir[path + 'com.github.homebrew.*.plist'].each do |file| | |
| unless running.include?(File.basename(file).sub(/\.plist$/i, '')) | |
| puts "Removing unused plist #{file}" | |
| rm file | |
| cleaned << file | |
| end | |
| end | |
| puts "All #{root? ? 'root' : 'user-space'} services OK, nothing cleaned..." if cleaned.empty? | |
| end | |
| # Stop if loaded, then start again | |
| def restart | |
| stop if service.loaded? | |
| start | |
| end | |
| # Start a service | |
| def start | |
| argh "Service `#{service.name}` already started, use `#{bin} restart #{service.name}`" if service.loaded? | |
| custom_plist = @args.first | |
| if custom_plist | |
| if custom_plist =~ %r{\Ahttps?://.+} | |
| custom_plist = { :url => custom_plist } | |
| elsif File.exist?(custom_plist) | |
| custom_plist = Pathname.new(custom_plist) | |
| else | |
| argh "#{custom_plist} is not a url or exising file" | |
| end | |
| end | |
| argh "Formula `#{service.name}` not installed, #startup_plist not implemented or no plist file found" if !custom_plist && !service.plist? | |
| temp = Tempfile.new(service.label) | |
| temp << service.generate_plist(custom_plist) | |
| temp.flush | |
| rm service.dest if service.dest.exist? | |
| cp temp.path, service.dest | |
| # clear tempfile | |
| temp.close | |
| safe_system launchctl, "load", "-w", service.dest.to_s | |
| $?.to_i != 0 ? argh("Failed to start `#{service.name}`") : ohai("Successfully started `#{service.name}` as #{service.label}") | |
| end | |
| # Stop a service or kill if no plist file available... | |
| def stop | |
| unless service.loaded? | |
| rm service.dest if service.dest.exist? # get rid of installed plist anyway, dude | |
| argh "Service `#{service.name}` not running, wanna start it? Try `#{bin} start #{service.name}`" | |
| end | |
| if service.dest.exist? | |
| puts "Stopping `#{service.name}`... (might take a while)" | |
| safe_system launchctl, "unload", "-w", service.dest.to_s | |
| $?.to_i != 0 ? argh("Failed to stop `#{service.name}`") : ohai("Successfully stopped `#{service.name}` via #{service.label}") | |
| else | |
| puts "Stopping stale service `#{service.name}`... (might take a while)" | |
| kill(service) | |
| end | |
| rm service.dest if service.dest.exist? | |
| end | |
| # Kill service without plist file by issuing a `launchctl remove` command | |
| def kill(svc) | |
| safe_system launchctl, "remove", svc.label | |
| argh("Failed to remove `#{svc.name}`, try again?") unless $?.to_i == 0 | |
| while svc.loaded? | |
| puts " ...checking status" | |
| sleep(5) | |
| end | |
| ohai "Successfully stopped `#{svc.name}` via #{svc.label}" | |
| end | |
| end | |
| end | |
| # Wrapper for a formula to handle service related stuff like parsing | |
| # and generating the plist file. | |
| class Service | |
| # Support module which will be used to extend Formula with a method :) | |
| module PlistSupport | |
| # As a replacement value for `<key>UserName</key>`. | |
| def startup_user; ServicesCli.user end | |
| end | |
| # Access the `Formula` instance | |
| attr_reader :formula | |
| # Create a new `Service` instance from either a path or label. | |
| def self.from(path_or_label) | |
| return nil unless path_or_label =~ /com\.github\.homebrew\.([^\.]+)(\.plist)?\z/ | |
| new(Formula.factory($1)) rescue nil | |
| end | |
| # Initialize new `Service` instance with supplied formula. | |
| def initialize(formula); @formula = formula end | |
| # Delegate access to `formula.name`. | |
| def name; @name ||= formula.name end | |
| # Label, static, always looks like `com.github.homebrew.<formula>`. | |
| def label; @label ||= "com.github.homebrew.#{name}" end | |
| # Path to a static plist file, this is always `com.github.homebrew.<formula>.plist`. | |
| def plist; @plist ||= formula.prefix + "#{label}.plist" end | |
| # Path to destination plist, if run as root it's in `boot_path`, else `user_path`. | |
| def dest; (ServicesCli.root? ? ServicesCli.boot_path : ServicesCli.user_path) + "#{label}.plist" end | |
| # Returns `true` if formula implements #startup_plist or file exists. | |
| def plist?; formula.installed? && (plist.file? || formula.respond_to?(:startup_plist)) end | |
| # Returns `true` if service is loaded, else false. | |
| def loaded?; %x{#{ServicesCli.launchctl} list | grep #{label} 2>/dev/null}.chomp =~ /#{label}\z/ end | |
| # Get current PID of daemon process from launchctl. | |
| def pid | |
| status = %x{#{ServicesCli.launchctl} list | grep #{label} 2>/dev/null}.chomp | |
| return $1.to_i if status =~ /\A([\d]+)\s+.+#{label}\z/ | |
| end | |
| # Generate that plist file, dude. | |
| def generate_plist(data = nil) | |
| data ||= plist.file? ? plist : formula.startup_plist | |
| if data.respond_to?(:file?) && data.file? | |
| data = data.read | |
| elsif data.respond_to?(:keys) && data.keys.include?(:url) | |
| require 'open-uri' | |
| data = open(data).read | |
| end | |
| # replace "template" variables and ensure label is always, always com.github.homebrew.<formula> | |
| data = data.to_s.gsub(/\{\{([a-z][a-z0-9_]*)\}\}/i) { |m| formula.send($1).to_s if formula.respond_to?($1) }. | |
| gsub(%r{(<key>Label</key>\s*<string>)[^<]*(</string>)}, '\1' + label + '\2') | |
| # and force fix UserName, if necessary | |
| if formula.startup_user != "root" && data =~ %r{<key>UserName</key>\s*<string>root</string>} | |
| data = data.gsub(%r{(<key>UserName</key>\s*<string>)[^<]*(</string>)}, '\1' + formula.startup_user + '\2') | |
| elsif ServicesCli.root? && formula.startup_user != "root" && data !~ %r{<key>UserName</key>} | |
| data = data.gsub(%r{(</dict>\s*</plist>)}, " <key>UserName</key><string>#{formula.startup_user}</string>\n\\1") | |
| end | |
| if ARGV.verbose? | |
| ohai "Generated plist for #{formula.name}:" | |
| puts " " + data.gsub("\n", "\n ") | |
| puts | |
| end | |
| data | |
| end | |
| end | |
| # Start the cli dispatch stuff. | |
| # | |
| ServicesCli.run! | 
@dbrock: I'll have to learn how to make a Gem first, but I'd be willing to do that!
Cool! :-) It’s easy:
set -e
mkdir brew-services
pushd !$
mkdir bin
curl https://raw.github.com/gist/1446575/brew-services.rb -o bin/brew-services
chmod +x !$
echo 'Gem::Specification.new do |gem|
  gem.name = "brew-services"
  gem.version = "0.0.1"
  gem.author = "Bryan Veloso"
  gem.email = "[email protected]"
  gem.summary = "Easily start and stop formulas via launchctl"
  gem.homepage = "http://github.com/bryanveloso/brew-services"
  gem.executable = "brew-services"
  gem.files = %w[bin/brew-services]
end
' > brew-services.gemspec
git init .
echo '*.gem' > .gitignore
git add .
git commit -m "Import from https://gist.github.com/1446575."
github create-from-local
gem build brew-services.gemspec
gem push brew-services-0.0.1.gem
echo Thanks for making the thing\! :-)
In case it is not obvious, copy the script to /usr/local/bin and drop the .rb file extension. This will allow you to run brew services command locally in any folder.
So I'm not sure what I'm doing wrong, but I always get
"Runtime error: homebrew is required, please start via brew services ..."
when I try to run brew services.
I have homebrew installed, but I'm not sure where the HOMEBREW_LIBRARY_PATH is being set. I removed that line from the script and re-ran it and the error it gives me makes me think that if homebrew was loaded correctly that global variable would be set.
Nevermind; apparently this made it into homebrew proper. All I had to do to get your script to work was to delete it. :)
Hey, how about making this into a gem? :D