Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DSL: allow all Casks to use uninstall stanzas #4865

Merged
merged 7 commits into from
Jun 28, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/cask/artifact.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module Cask::Artifact; end

require 'cask/artifact/base'
require 'cask/artifact/uninstall_base'
require 'cask/artifact/symlinked'
require 'cask/artifact/hardlinked'

Expand All @@ -19,6 +20,7 @@ module Cask::Artifact; end
require 'cask/artifact/caskroom_only'
require 'cask/artifact/input_method'
require 'cask/artifact/screen_saver'
require 'cask/artifact/uninstall'


module Cask::Artifact
Expand All @@ -42,6 +44,7 @@ def self.artifacts
Cask::Artifact::Binary,
Cask::Artifact::InputMethod,
Cask::Artifact::ScreenSaver,
Cask::Artifact::Uninstall,
Cask::Artifact::AfterBlock,
]
end
Expand Down
4 changes: 2 additions & 2 deletions lib/cask/artifact/after_block.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ def self.me?(cask)
cask.artifacts[:after_uninstall].any?
end

def install
def install_phase
@cask.artifacts[:after_install].each { |block| @cask.instance_eval &block }
end

def uninstall
def uninstall_phase
@cask.artifacts[:after_uninstall].each { |block| @cask.instance_eval &block }
end
end
4 changes: 2 additions & 2 deletions lib/cask/artifact/before_block.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ def self.me?(cask)
cask.artifacts[:before_uninstall].any?
end

def install
def install_phase
@cask.artifacts[:before_install].each { |block| @cask.instance_eval &block }
end

def uninstall
def uninstall_phase
@cask.artifacts[:before_uninstall].each { |block| @cask.instance_eval &block }
end
end
2 changes: 1 addition & 1 deletion lib/cask/artifact/binary.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class Cask::Artifact::Binary < Cask::Artifact::Symlinked
def install
def install_phase
super unless Cask.no_binaries
end
end
4 changes: 2 additions & 2 deletions lib/cask/artifact/nested_container.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
class Cask::Artifact::NestedContainer < Cask::Artifact::Base
def install
def install_phase
@cask.artifacts[:nested_container].each { |container| extract(container) }
end

def uninstall
def uninstall_phase
# no need to take action; we will get removed by rmtree of parent
end

Expand Down
147 changes: 3 additions & 144 deletions lib/cask/artifact/pkg.rb
Original file line number Diff line number Diff line change
@@ -1,40 +1,8 @@
class Cask::Artifact::Pkg < Cask::Artifact::Base
# this class actually covers two keys, :install and :uninstall
def self.artifact_dsl_key
:install
end

def self.read_script_arguments(uninstall_options, key)
script_arguments = uninstall_options[key]

# backwards-compatible string value
if script_arguments.kind_of?(String)
script_arguments = { :executable => script_arguments }
end

# key sanity
permitted_keys = [:args, :input, :executable, :must_succeed]
unknown_keys = script_arguments.keys - permitted_keys
unless unknown_keys.empty?
opoo %Q{Unknown arguments to uninstall :#{key} -- :#{unknown_keys.join(", :")} (ignored). Running "brew update && brew upgrade brew-cask && brew cleanup && brew cask cleanup" will likely fix it.}
end
script_arguments.reject! {|k,v| ! permitted_keys.include?(k)}

# extract executable
if script_arguments.key?(:executable)
executable = script_arguments.delete(:executable)
else
executable = nil
end

unless script_arguments.key?(:must_succeed)
script_arguments[:must_succeed] = true
end

script_arguments.merge!(:sudo => true, :print => true)
return executable, script_arguments
end

def load_pkg_description(pkg_description)
@pkg_relative_path = pkg_description.shift
@pkg_install_opts = pkg_description.shift
Expand All @@ -58,12 +26,12 @@ def pkg_relative_path
@pkg_relative_path
end

def install
def install_phase
@cask.artifacts[:install].each { |pkg_description| run_installer(pkg_description) }
end

def uninstall
manually_uninstall(@cask.artifacts[:uninstall])
def uninstall_phase
# Do nothing. Must be handled explicitly by a separate :uninstall stanza.
end

def run_installer(pkg_description)
Expand All @@ -81,113 +49,4 @@ def run_installer(pkg_description)
args << '-allowUntrusted' if pkg_install_opts :allow_untrusted
@command.run!('/usr/sbin/installer', {:sudo => true, :args => args, :print => true})
end

def manually_uninstall(uninstall_set)
ohai "Running uninstall process for #{@cask}; your password may be necessary"

uninstall_set.each do |uninstall_options|
unknown_keys = uninstall_options.keys - [:early_script, :launchctl, :quit, :signal, :kext, :script, :pkgutil, :files]
unless unknown_keys.empty?
opoo %Q{Unknown arguments to uninstall: #{unknown_keys.join(", ")}. Running "brew update && brew upgrade brew-cask && brew cleanup && brew cask cleanup" will likely fix it.}
end
end

# Preserve prior functionality of script which runs first. Should rarely be needed.
# :early_script should not delete files, better defer that to :script.
# If Cask writers never need :early_script it may be removed in the future.
uninstall_set.select{ |h| h.key?(:early_script) }.each do |uninstall_options|
executable, script_arguments = self.class.read_script_arguments(uninstall_options, :early_script)
ohai "Running uninstall script #{executable}"
raise CaskInvalidError.new(@cask, 'uninstall :early_script without :executable') if executable.nil?
@command.run(@cask.destination_path.join(executable), script_arguments)
sleep 1
end

# :launchctl must come before :quit/:signal for cases where app would instantly re-launch
uninstall_set.select{ |h| h.key?(:launchctl) }.each do |uninstall_options|
Array(uninstall_options[:launchctl]).each do |service|
ohai "Removing launchctl service #{service}"
[false, true].each do |with_sudo|
xml_status = @command.run('/bin/launchctl', :args => ['list', '-x', service], :sudo => with_sudo)
if %r{^<\?xml}.match(xml_status)
@command.run('/bin/launchctl', :args => ['unload', '-w', '--', service], :sudo => with_sudo)
sleep 1
@command.run!('/bin/launchctl', :args => ['remove', service], :sudo => with_sudo)
sleep 1
end
end
end
end

# :quit/:signal must come before :kext so the kext will not be in use by a running process
uninstall_set.select{ |h| h.key?(:quit) }.each do |uninstall_options|
Array(uninstall_options[:quit]).each do |id|
ohai "Quitting application ID #{id}"
num_running = @command.run!('/usr/bin/osascript', :args => ['-e', %Q{tell application "System Events" to count processes whose bundle identifier is "#{id}"}], :sudo => true).to_i
if num_running > 0
@command.run!('/usr/bin/osascript', :args => ['-e', %Q{tell application id "#{id}" to quit}], :sudo => true)
sleep 3
end
end
end

# :signal should come after :quit so it can be used as a backup when :quit fails
uninstall_set.select{ |h| h.key?(:signal) }.each do |uninstall_options|
Array(uninstall_options[:signal]).flatten.each_slice(2) do |pair|
raise CaskInvalidError.new(@cask, 'Each uninstall :signal must have 2 elements.') unless pair.length == 2
signal, id = pair
ohai "Signalling '#{signal}' to application ID '#{id}'"
pid_string = @command.run!('/usr/bin/osascript', :args => ['-e', %Q{tell application "System Events" to get the unix id of every process whose bundle identifier is "#{id}"}], :sudo => true)
if pid_string.match(%r{\A\d+(?:\s*,\s*\d+)*\Z}) # sanity check
pids = pid_string.split(%r{\s*,\s*}).map(&:strip).map(&:to_i)
if pids.length > 0
# Note that unlike :quit, signals are sent from the
# current user (not upgraded to the superuser). This is a
# todo item for the future, but there should be some
# additional thought/safety checks about that, as a
# misapplied "kill" by root could bring down the system.
# The fact that we learned the pid from AppleScript is
# already some degree of protection, though indirect.
Process.kill(signal, *pids)
sleep 3
end
end
end
end

# :kext should be unloaded before attempting to delete the relevant file
uninstall_set.select{ |h| h.key?(:kext) }.each do |uninstall_options|
Array(uninstall_options[:kext]).each do |kext|
ohai "Unloading kernel extension #{kext}"
is_loaded = @command.run!('/usr/sbin/kextstat', :args => ['-l', '-b', kext], :sudo => true)
if is_loaded.length > 1
@command.run!('/sbin/kextunload', :args => ['-b', '--', kext], :sudo => true)
sleep 1
end
end
end

# :script must come before :pkgutil or :files so that the script file is not already deleted
uninstall_set.select{ |h| h.key?(:script) }.each do |uninstall_options|
executable, script_arguments = self.class.read_script_arguments(uninstall_options, :script)
raise CaskInvalidError.new(@cask, 'uninstall :script without :executable.') if executable.nil?
@command.run(@cask.destination_path.join(executable), script_arguments)
sleep 1
end

uninstall_set.select{ |h| h.key?(:pkgutil) }.each do |uninstall_options|
ohai "Removing files from pkgutil Bill-of-Materials"
Array(uninstall_options[:pkgutil]).each do |regexp|
pkgs = Cask::Pkg.all_matching(regexp, @command)
pkgs.each(&:uninstall)
end
end

uninstall_set.select{ |h| h.key?(:files) }.each do |uninstall_options|
Array(uninstall_options[:files]).flatten.each_slice(500) do |file_slice|
ohai "Removing files: #{file_slice.utf8_inspect}"
@command.run!('/bin/rm', :args => file_slice.unshift('-rf', '--'), :sudo => true)
end
end
end
end
4 changes: 2 additions & 2 deletions lib/cask/artifact/symlinked.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,12 @@ def unlink(artifact_spec)
end
end

def install
def install_phase
# the sort is for predictability between Ruby versions
@cask.artifacts[self.class.artifact_dsl_key].sort.each { |artifact| link(artifact) }
end

def uninstall
def uninstall_phase
# the sort is for predictability between Ruby versions
@cask.artifacts[self.class.artifact_dsl_key].sort.each { |artifact| unlink(artifact) }
end
Expand Down
2 changes: 2 additions & 0 deletions lib/cask/artifact/uninstall.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class Cask::Artifact::Uninstall < Cask::Artifact::UninstallBase
end
Loading