brew/Library/Homebrew/cask/artifact/abstract_uninstall.rb

364 lines
12 KiB
Ruby
Raw Normal View History

require "timeout"
2016-08-18 22:11:42 +03:00
require "utils/user"
require "cask/artifact/abstract_artifact"
require "extend/hash_validator"
using HashValidator
2016-08-18 22:11:42 +03:00
2018-09-06 08:29:14 +02:00
module Cask
2016-09-24 13:52:43 +02:00
module Artifact
class AbstractUninstall < AbstractArtifact
2016-09-24 13:52:43 +02:00
ORDERED_DIRECTIVES = [
2016-10-14 20:33:16 +02:00
:early_script,
:launchctl,
:quit,
:signal,
:login_item,
:kext,
:script,
:pkgutil,
:delete,
:trash,
:rmdir,
].freeze
2016-09-24 13:52:43 +02:00
def self.from_args(cask, **directives)
new(cask, directives)
end
attr_reader :directives
def initialize(cask, directives)
directives.assert_valid_keys!(*ORDERED_DIRECTIVES)
super(cask)
2017-09-11 23:29:38 +02:00
directives[:signal] = [*directives[:signal]].flatten.each_slice(2).to_a
@directives = directives
return unless directives.key?(:kext)
cask.caveats do
kext
end
end
2017-08-04 14:59:18 +02:00
def to_h
directives.to_h
end
2017-09-11 23:29:38 +02:00
def summarize
2017-10-01 23:33:52 +02:00
to_h.flat_map { |key, val| [*val].map { |v| "#{key.inspect} => #{v.inspect}" } }.join(", ")
2017-09-11 23:29:38 +02:00
end
private
def dispatch_uninstall_directives(**options)
ORDERED_DIRECTIVES.each do |directive_sym|
dispatch_uninstall_directive(directive_sym, **options)
end
end
2016-08-18 22:11:42 +03:00
def dispatch_uninstall_directive(directive_sym, **options)
return unless directives.key?(directive_sym)
2016-08-18 22:11:42 +03:00
args = directives[directive_sym]
2018-09-17 02:45:00 +02:00
send("uninstall_#{directive_sym}", *(args.is_a?(Hash) ? [args] : args), **options)
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
def stanza
self.class.dsl_key
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
# 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.
def uninstall_early_script(directives, **options)
uninstall_script(directives, directive_name: :early_script, **options)
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
# :launchctl must come before :quit/:signal for cases where app would instantly re-launch
def uninstall_launchctl(*services, command: nil, **_)
services.each do |service|
2016-09-24 13:52:43 +02:00
ohai "Removing launchctl service #{service}"
[false, true].each do |with_sudo|
plist_status = command.run(
"/bin/launchctl",
args: ["list", service],
sudo: with_sudo, print_stderr: false
).stdout
if plist_status =~ /^\{/
command.run!("/bin/launchctl", args: ["remove", service], sudo: with_sudo)
2016-09-24 13:52:43 +02:00
sleep 1
end
2018-11-05 02:00:40 +01:00
paths = [
"/Library/LaunchAgents/#{service}.plist",
"/Library/LaunchDaemons/#{service}.plist",
]
2016-09-24 13:52:43 +02:00
paths.each { |elt| elt.prepend(ENV["HOME"]) } unless with_sudo
paths = paths.map { |elt| Pathname(elt) }.select(&:exist?)
paths.each do |path|
command.run!("/bin/rm", args: ["-f", "--", path], sudo: with_sudo)
2016-09-24 13:52:43 +02:00
end
# undocumented and untested: pass a path to uninstall :launchctl
next unless Pathname(service).exist?
2018-09-17 02:45:00 +02:00
command.run!("/bin/launchctl", args: ["unload", "-w", "--", service], sudo: with_sudo)
2018-11-05 22:40:07 +01:00
command.run!("/bin/rm", args: ["-f", "--", service], sudo: with_sudo)
2016-09-24 13:52:43 +02:00
sleep 1
end
2016-08-18 22:11:42 +03:00
end
2016-09-24 13:52:43 +02:00
end
2018-11-05 22:40:07 +01:00
def running_processes(bundle_id)
system_command!("/bin/launchctl", args: ["list"])
.stdout.lines
.map { |line| line.chomp.split("\t") }
.map { |pid, state, id| [pid.to_i, state.to_i, id] }
.select do |(pid, _, id)|
pid.nonzero? && id.match?(/^#{Regexp.escape(bundle_id)}($|\.\d+)/)
end
end
2016-09-24 13:52:43 +02:00
# :quit/:signal must come before :kext so the kext will not be in use by a running process
def uninstall_quit(*bundle_ids, command: nil, **_)
bundle_ids.each do |bundle_id|
2018-11-05 22:40:07 +01:00
next if running_processes(bundle_id).empty?
2018-09-17 02:45:00 +02:00
unless User.current.gui?
ohai "Not logged into a GUI; skipping quitting application ID '#{bundle_id}'."
next
end
2018-11-05 02:00:40 +01:00
ohai "Quitting application '#{bundle_id}'..."
begin
2018-11-05 02:00:40 +01:00
Timeout.timeout(10) do
Kernel.loop do
2018-11-05 02:00:40 +01:00
next unless quit(bundle_id).success?
2018-11-05 22:40:07 +01:00
if running_processes(bundle_id).empty?
2018-11-05 02:00:40 +01:00
puts "Application '#{bundle_id}' quit successfully."
break
end
end
end
rescue Timeout::Error
2018-11-05 02:00:40 +01:00
opoo "Application '#{bundle_id}' did not quit."
next
end
2016-08-18 22:11:42 +03:00
end
end
2018-11-05 02:00:40 +01:00
def quit(bundle_id)
script = <<~JAVASCRIPT
'use strict';
ObjC.import('stdlib')
function run(argv) {
var app = Application(argv[0])
try {
app.quit()
} catch (err) {
if (app.running()) {
$.exit(1)
}
}
$.exit(0)
}
JAVASCRIPT
system_command "osascript", args: ["-l", "JavaScript", "-e", script, bundle_id],
print_stderr: false,
sudo: true
end
private :quit
2016-09-24 13:52:43 +02:00
# :signal should come after :quit so it can be used as a backup when :quit fails
2017-09-11 23:29:38 +02:00
def uninstall_signal(*signals, command: nil, **_)
signals.each do |pair|
raise CaskInvalidError.new(cask, "Each #{stanza} :signal must consist of 2 elements.") unless pair.size == 2
signal, bundle_id = pair
ohai "Signalling '#{signal}' to application ID '#{bundle_id}'"
2018-11-05 22:40:07 +01:00
pids = running_processes(bundle_id).map(&:first)
2016-09-24 13:52:43 +02:00
next unless pids.any?
2018-09-17 02:45:00 +02:00
2016-09-24 13:52:43 +02:00
# 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
2016-09-24 13:52:43 +02:00
# 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.
odebug "Unix ids are #{pids.inspect} for processes with bundle identifier #{bundle_id}"
2016-09-24 13:52:43 +02:00
Process.kill(signal, *pids)
sleep 3
end
end
2016-08-18 22:11:42 +03:00
2018-10-25 01:05:53 +02:00
def uninstall_login_item(*login_items, command: nil, upgrade: false, **_)
return if upgrade
login_items.each do |name|
2016-09-24 13:52:43 +02:00
ohai "Removing login item #{name}"
2018-11-05 22:40:07 +01:00
system_command!(
2018-11-05 02:00:40 +01:00
"osascript",
args: [
"-e",
%Q(tell application "System Events" to delete every login item whose name is "#{name}"),
],
)
2016-09-24 13:52:43 +02:00
sleep 1
end
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
# :kext should be unloaded before attempting to delete the relevant file
def uninstall_kext(*kexts, command: nil, **_)
kexts.each do |kext|
2016-09-24 13:52:43 +02:00
ohai "Unloading kernel extension #{kext}"
2018-11-05 22:40:07 +01:00
is_loaded = system_command!("/usr/sbin/kextstat", args: ["-l", "-b", kext], sudo: true).stdout
2016-09-24 13:52:43 +02:00
if is_loaded.length > 1
2018-11-05 22:40:07 +01:00
system_command!("/sbin/kextunload", args: ["-b", kext], sudo: true)
2016-09-24 13:52:43 +02:00
sleep 1
end
2018-11-05 22:40:07 +01:00
system_command!("/usr/sbin/kextfind", args: ["-b", kext], sudo: true).stdout.chomp.lines.each do |kext_path|
ohai "Removing kernel extension #{kext_path}"
2018-11-05 22:40:07 +01:00
system_command!("/bin/rm", args: ["-rf", kext_path], sudo: true)
end
2016-09-24 13:52:43 +02:00
end
end
2016-08-18 22:11:42 +03:00
2016-09-24 13:52:43 +02:00
# :script must come before :pkgutil, :delete, or :trash so that the script file is not already deleted
def uninstall_script(directives, directive_name: :script, force: false, command: nil, **_)
# TODO: Create a common `Script` class to run this and Artifact::Installer.
2016-09-24 13:52:43 +02:00
executable, script_arguments = self.class.read_script_arguments(directives,
"uninstall",
{ must_succeed: true, sudo: false },
2016-09-24 13:52:43 +02:00
{ print_stdout: true },
directive_name)
2016-09-24 13:52:43 +02:00
ohai "Running uninstall script #{executable}"
raise CaskInvalidError.new(cask, "#{stanza} :#{directive_name} without :executable.") if executable.nil?
2018-09-17 02:45:00 +02:00
executable_path = staged_path_join_executable(executable)
if (executable_path.absolute? && !executable_path.exist?) ||
(!executable_path.absolute? && (which executable_path).nil?)
message = "uninstall script #{executable} does not exist"
raise CaskError, "#{message}." unless force
2018-09-17 02:45:00 +02:00
opoo "#{message}, skipping."
return
end
command.run(executable_path, script_arguments)
2016-08-18 22:11:42 +03:00
sleep 1
end
def uninstall_pkgutil(*pkgs, command: nil, **_)
ohai "Uninstalling packages:"
pkgs.each do |regex|
2018-09-06 08:29:14 +02:00
::Cask::Pkg.all_matching(regex, command).each do |pkg|
puts pkg.package_id
pkg.uninstall
end
end
end
def each_resolved_path(action, paths)
return enum_for(:each_resolved_path, action, paths) unless block_given?
paths.each do |path|
resolved_path = Pathname.new(path)
2017-12-06 11:46:30 +01:00
resolved_path = resolved_path.expand_path if path.to_s.start_with?("~")
if resolved_path.relative? || resolved_path.split.any? { |part| part.to_s == ".." }
opoo "Skipping #{Formatter.identifier(action)} for relative path '#{path}'."
next
end
if MacOS.undeletable?(resolved_path)
opoo "Skipping #{Formatter.identifier(action)} for undeletable path '#{path}'."
next
end
yield path, Pathname.glob(resolved_path)
2016-09-24 13:52:43 +02:00
end
end
2016-08-18 22:11:42 +03:00
def uninstall_delete(*paths, command: nil, **_)
return if paths.empty?
ohai "Removing files:"
each_resolved_path(:delete, paths) do |path, resolved_paths|
puts path
command.run!(
"/usr/bin/xargs",
2018-11-02 17:18:07 +00:00
args: ["-0", "--", "/bin/rm", "-r", "-f", "--"],
input: resolved_paths.join("\0"),
2018-11-02 17:18:07 +00:00
sudo: true,
)
2016-09-24 13:52:43 +02:00
end
end
2016-08-18 22:11:42 +03:00
def uninstall_trash(*paths, **options)
return if paths.empty?
resolved_paths = each_resolved_path(:trash, paths).to_a
ohai "Trashing files:"
puts resolved_paths.map(&:first)
trash_paths(*resolved_paths.flat_map(&:last), **options)
2017-06-24 08:34:01 +02:00
end
def trash_paths(*paths, command: nil, **_)
2018-11-05 02:00:40 +01:00
result = command.run!("osascript", args: ["-e", <<~APPLESCRIPT, *paths])
on run argv
repeat with i from 1 to (count argv)
set item i of argv to (item i of argv as POSIX file)
end repeat
2017-06-24 08:34:01 +02:00
tell application "Finder"
set trashedItems to (move argv to trash)
set output to ""
repeat with i from 1 to (count trashedItems)
2017-07-30 18:44:53 +02:00
set trashedItem to POSIX path of (item i of trashedItems as string)
set output to output & trashedItem
if i < count trashedItems then
set output to output & character id 0
2017-07-30 18:44:53 +02:00
end if
2017-06-24 08:34:01 +02:00
end repeat
return output
end tell
end run
2018-07-11 15:17:40 +02:00
APPLESCRIPT
# Remove AppleScript's automatic newline.
result.tap { |r| r.stdout.sub!(/\n$/, "") }
2016-09-24 13:52:43 +02:00
end
2016-08-18 22:11:42 +03:00
def uninstall_rmdir(*directories, command: nil, **_)
return if directories.empty?
ohai "Removing directories if empty:"
each_resolved_path(:rmdir, directories) do |path, resolved_paths|
puts path
resolved_paths.select(&:directory?).each do |resolved_path|
if (ds_store = resolved_path.join(".DS_Store")).exist?
command.run!("/bin/rm", args: ["-f", "--", ds_store], sudo: true, print_stderr: false)
end
command.run("/bin/rmdir", args: ["--", resolved_path], sudo: true, print_stderr: false)
end
2016-09-24 13:52:43 +02:00
end
end
2016-08-18 22:11:42 +03:00
end
end
end