Skip to content

Instantly share code, notes, and snippets.

@mitsuhiko
Created March 7, 2013 11:56
Show Gist options
  • Save mitsuhiko/5107546 to your computer and use it in GitHub Desktop.
Save mitsuhiko/5107546 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
require 'optparse'
require 'fileutils'
class Lunchy
VERSION = '0.7.0'
def start(params)
raise ArgumentError, "start [-wF] [name]" if params.empty?
with_match params[0] do |name, path|
execute("launchctl load #{force}#{write}#{path.inspect}")
puts "started #{name}"
end
end
def stop(params)
raise ArgumentError, "stop [-w] [name]" if params.empty?
with_match params[0] do |name, path|
execute("launchctl unload #{write}#{path.inspect}")
puts "stopped #{name}"
end
end
def restart(params)
stop(params.dup)
start(params.dup)
end
def status(params)
pattern = params[0]
cmd = "launchctl list"
unless verbose?
agents = plists.keys.map { |k| "-e \"#{k}\"" }.join(" ")
cmd << " | grep -i #{agents}"
end
cmd.gsub!('.','\.')
cmd << " | grep -i \"#{pattern}\"" if pattern
execute(cmd)
end
def ls(params)
agents = plists.keys
agents = agents.grep(/#{params[0]}/) if !params.empty?
if long
puts agents.map { |agent| plists[agent] }.sort.join("\n")
else
puts agents.sort.join("\n")
end
end
alias_method :list, :ls
def install(params)
raise ArgumentError, "install [file]" if params.empty?
filename = params[0]
%w(~/Library/LaunchAgents /Library/LaunchAgents).each do |dir|
if File.exist?(File.expand_path(dir))
FileUtils.cp filename, File.join(File.expand_path(dir), File.basename(filename))
return puts "#{filename} installed to #{dir}"
end
end
end
def show(params)
raise ArgumentError, "show [name]" if params.empty?
with_match params[0] do |_, path|
puts IO.read(path)
end
end
def edit(params)
raise ArgumentError, "edit [name]" if params.empty?
with_match params[0] do |_, path|
editor = ENV['EDITOR']
if editor.nil?
raise 'EDITOR environment variable is not set'
else
execute("#{editor} #{path.inspect} > `tty`")
end
end
end
private
def force
CONFIG[:force] and '-F '
end
def write
CONFIG[:write] and '-w '
end
def long
CONFIG[:long]
end
def with_match name
files = plists.select {|k,_| k =~ /#{name}/i }
files = Hash[files] if files.is_a?(Array) # ruby 1.8
if files.size > 1
puts "Multiple daemons found matching '#{name}'. You need to be more specific. Matches found are:\n#{files.keys.join("\n")}"
elsif files.empty?
puts "No daemon found matching '#{name}'" unless name
else
yield(*files.to_a.first)
end
end
def execute(cmd)
puts "Executing: #{cmd}" if verbose?
emitted = `#{cmd}`
puts emitted unless emitted.empty?
end
def plists
@plists ||= begin
plists = {}
dirs.each do |dir|
Dir["#{File.expand_path(dir)}/*.plist"].inject(plists) do |memo, filename|
memo[File.basename(filename, ".plist")] = filename; memo
end
end
plists
end
end
def dirs
result = %w(/Library/LaunchAgents ~/Library/LaunchAgents)
result.push('/Library/LaunchDaemons', '/System/Library/LaunchDaemons') if root?
result
end
def root?
Process.euid == 0
end
def verbose?
CONFIG[:verbose]
end
end
CONFIG = {}
OPERATIONS = %w(start stop restart ls list status install show edit)
option_parser = OptionParser.new do |opts|
opts.banner = "Lunchy #{Lunchy::VERSION}, the friendly launchctl wrapper\n" \
"Usage: #{File.basename(__FILE__)} [#{OPERATIONS.join('|')}] [options]"
opts.on("-F", "--force", "Force start (disabled) agents") do |verbose|
CONFIG[:force] = true
end
opts.on("-v", "--verbose", "Show command executions") do |verbose|
CONFIG[:verbose] = true
end
opts.on("-w", "--write", "Persist command") do |verbose|
CONFIG[:write] = true
end
opts.on("-l", "--long", "Display absolute paths when listing agents") do
CONFIG[:long] = true
end
opts.separator <<-EOS
Supported commands:
ls [-l] [pattern] Show the list of installed agents, with optional [pattern] filter
list [-l] [pattern] Alias for 'ls'
start [-wF] [pattern] Start the first agent matching [pattern]
stop [-w] [pattern] Stop the first agent matching [pattern]
restart [pattern] Stop and start the first agent matching [pattern]
status [pattern] Show the PID and label for all agents, with optional [pattern] filter
install [file] Install [file] to ~/Library/LaunchAgents or /Library/LaunchAgents (whichever it finds first)
show [pattern] Show the contents of the launchctl daemon file
edit [pattern] Open the launchctl daemon file in the default editor (EDITOR environment variable)
-w will persist the start/stop command so the agent will load on startup or never load, respectively.
-l will display absolute paths of the launchctl daemon files when showing list of installed agents.
Example:
lunchy ls
lunchy ls -l nginx
lunchy start -w redis
lunchy stop mongo
lunchy status mysql
lunchy install /usr/local/Cellar/redis/2.2.2/io.redis.redis-server.plist
lunchy show redis
lunchy edit mongo
Note: if you run lunchy as root, you can manage daemons in /Library/LaunchDaemons also.
EOS
end
option_parser.parse!
op = ARGV.shift
if OPERATIONS.include?(op)
begin
Lunchy.new.send(op.to_sym, ARGV)
rescue ArgumentError => ex
puts ex.message
rescue Exception => e
puts "Uh oh, I didn't expect this:"
puts e.message
puts e.backtrace.join("\n")
end
else
puts option_parser.help
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment