2023-02-22 09:11:29 -08:00
|
|
|
# typed: true
|
2019-04-19 15:38:03 +09:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2016-11-09 09:32:54 +01:00
|
|
|
require "timeout"
|
2016-08-18 22:11:42 +03:00
|
|
|
|
2018-10-01 12:29:21 +02:00
|
|
|
require "utils/user"
|
2018-09-03 19:39:07 +01:00
|
|
|
require "cask/artifact/abstract_artifact"
|
2020-07-27 18:04:29 +02:00
|
|
|
require "cask/pkg"
|
2024-01-12 09:38:49 -08:00
|
|
|
require "extend/hash/keys"
|
2024-01-26 17:33:55 -08:00
|
|
|
require "system_command"
|
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
|
2020-08-19 10:23:41 +02:00
|
|
|
# Abstract superclass for uninstall artifacts.
|
2017-04-06 00:33:31 +02:00
|
|
|
class AbstractUninstall < AbstractArtifact
|
2024-01-26 17:33:55 -08:00
|
|
|
include SystemCommand::Mixin
|
|
|
|
|
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
|
|
|
|
2017-04-06 00:33:31 +02:00
|
|
|
def self.from_args(cask, **directives)
|
2023-09-29 04:47:44 +01:00
|
|
|
new(cask, **directives)
|
2017-04-06 00:33:31 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
attr_reader :directives
|
|
|
|
|
2023-09-29 04:47:44 +01:00
|
|
|
def initialize(cask, **directives)
|
2023-03-19 17:31:51 -07:00
|
|
|
directives.assert_valid_keys(*ORDERED_DIRECTIVES)
|
2019-02-06 19:04:29 +01:00
|
|
|
|
2024-05-23 17:08:41 +01:00
|
|
|
super
|
2020-07-13 22:48:53 +10:00
|
|
|
directives[:signal] = Array(directives[:signal]).flatten.each_slice(2).to_a
|
2017-04-06 00:33:31 +02:00
|
|
|
@directives = directives
|
2017-11-24 17:44:01 +01:00
|
|
|
|
2023-02-18 12:08:54 -08:00
|
|
|
# This is already included when loading from the API.
|
|
|
|
return if cask.loaded_from_api?
|
2017-11-24 17:44:01 +01:00
|
|
|
return unless directives.key?(:kext)
|
|
|
|
|
|
|
|
cask.caveats do
|
2023-02-22 09:11:29 -08:00
|
|
|
T.bind(self, ::Cask::DSL::Caveats)
|
2017-11-24 17:44:01 +01:00
|
|
|
kext
|
|
|
|
end
|
2017-04-06 00:33:31 +02:00
|
|
|
end
|
|
|
|
|
2017-08-04 14:59:18 +02:00
|
|
|
def to_h
|
|
|
|
directives.to_h
|
|
|
|
end
|
|
|
|
|
2023-02-22 09:11:29 -08:00
|
|
|
sig { override.returns(String) }
|
2017-09-11 23:29:38 +02:00
|
|
|
def summarize
|
2020-07-13 22:48:53 +10:00
|
|
|
to_h.flat_map { |key, val| Array(val).map { |v| "#{key.inspect} => #{v.inspect}" } }.join(", ")
|
2017-09-11 23:29:38 +02:00
|
|
|
end
|
|
|
|
|
2017-04-06 00:33:31 +02:00
|
|
|
private
|
|
|
|
|
|
|
|
def dispatch_uninstall_directives(**options)
|
2019-02-06 19:04:29 +01:00
|
|
|
ORDERED_DIRECTIVES.each do |directive_sym|
|
|
|
|
dispatch_uninstall_directive(directive_sym, **options)
|
|
|
|
end
|
|
|
|
end
|
2016-08-18 22:11:42 +03:00
|
|
|
|
2019-02-06 19:04:29 +01:00
|
|
|
def dispatch_uninstall_directive(directive_sym, **options)
|
|
|
|
return unless directives.key?(directive_sym)
|
2016-08-18 22:11:42 +03:00
|
|
|
|
2019-02-06 19:04:29 +01:00
|
|
|
args = directives[directive_sym]
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2023-12-18 09:34:01 -08: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
|
2017-04-06 00:33:31 +02:00
|
|
|
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.
|
2020-11-05 17:17:03 -05:00
|
|
|
# If cask writers never need :early_script it may be removed in the future.
|
2017-04-06 00:33:31 +02:00
|
|
|
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
|
2023-03-07 06:46:59 -08:00
|
|
|
def uninstall_launchctl(*services, command: nil, **_)
|
2020-09-11 10:29:21 +01:00
|
|
|
booleans = [false, true]
|
2022-11-09 13:21:49 +11:00
|
|
|
|
|
|
|
all_services = []
|
|
|
|
|
|
|
|
# if launchctl item contains a wildcard, find matching process(es)
|
2017-03-08 03:03:36 +01:00
|
|
|
services.each do |service|
|
2022-12-13 00:00:43 +11:00
|
|
|
all_services << service unless service.include?("*")
|
2022-12-05 15:08:23 +11:00
|
|
|
next unless service.include?("*")
|
2022-11-09 13:21:49 +11:00
|
|
|
|
2022-12-19 14:35:20 +11:00
|
|
|
found_services = find_launchctl_with_wildcard(service)
|
|
|
|
next if found_services.blank?
|
|
|
|
|
2022-12-28 12:50:16 +11:00
|
|
|
found_services.each { |found_service| all_services << found_service }
|
2022-11-09 13:21:49 +11:00
|
|
|
end
|
|
|
|
|
|
|
|
all_services.each do |service|
|
2016-09-24 13:52:43 +02:00
|
|
|
ohai "Removing launchctl service #{service}"
|
2023-03-27 14:06:07 -07:00
|
|
|
booleans.each do |sudo|
|
2018-09-02 16:15:09 +01:00
|
|
|
plist_status = command.run(
|
|
|
|
"/bin/launchctl",
|
2023-02-17 14:48:13 -08:00
|
|
|
args: ["list", service],
|
2024-03-07 16:20:20 +00:00
|
|
|
sudo:,
|
2023-03-27 14:06:07 -07:00
|
|
|
sudo_as_root: sudo,
|
2023-02-17 14:48:13 -08:00
|
|
|
print_stderr: false,
|
2018-09-02 16:15:09 +01:00
|
|
|
).stdout
|
2020-05-22 08:52:26 +01:00
|
|
|
if plist_status.start_with?("{")
|
2023-03-27 14:06:07 -07:00
|
|
|
command.run!(
|
|
|
|
"/bin/launchctl",
|
|
|
|
args: ["remove", service],
|
2024-03-07 16:20:20 +00:00
|
|
|
sudo:,
|
2023-03-27 14:06:07 -07:00
|
|
|
sudo_as_root: sudo,
|
|
|
|
)
|
2016-09-24 13:52:43 +02:00
|
|
|
sleep 1
|
|
|
|
end
|
2018-11-05 02:00:40 +01:00
|
|
|
paths = [
|
2019-04-20 14:07:29 +09:00
|
|
|
+"/Library/LaunchAgents/#{service}.plist",
|
|
|
|
+"/Library/LaunchDaemons/#{service}.plist",
|
2018-11-05 02:00:40 +01:00
|
|
|
]
|
2023-03-27 14:06:07 -07:00
|
|
|
paths.each { |elt| elt.prepend(Dir.home).freeze } unless sudo
|
2016-09-24 13:52:43 +02:00
|
|
|
paths = paths.map { |elt| Pathname(elt) }.select(&:exist?)
|
|
|
|
paths.each do |path|
|
2024-03-07 16:20:20 +00:00
|
|
|
command.run!("/bin/rm", args: ["-f", "--", path], sudo:, sudo_as_root: 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
|
|
|
|
2023-02-17 14:48:13 -08:00
|
|
|
command.run!(
|
|
|
|
"/bin/launchctl",
|
2023-03-27 14:06:07 -07:00
|
|
|
args: ["unload", "-w", "--", service],
|
2024-03-07 16:20:20 +00:00
|
|
|
sudo:,
|
2023-03-27 14:06:07 -07:00
|
|
|
sudo_as_root: sudo,
|
2023-02-17 14:48:13 -08:00
|
|
|
)
|
|
|
|
command.run!(
|
|
|
|
"/bin/rm",
|
2023-03-27 14:06:07 -07:00
|
|
|
args: ["-f", "--", service],
|
2024-03-07 16:20:20 +00:00
|
|
|
sudo:,
|
2023-03-27 14:06:07 -07:00
|
|
|
sudo_as_root: sudo,
|
2023-02-17 14:48:13 -08:00
|
|
|
)
|
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"])
|
2020-12-26 14:01:22 +01:00
|
|
|
.stdout.lines.drop(1)
|
2018-11-05 22:40:07 +01:00
|
|
|
.map { |line| line.chomp.split("\t") }
|
|
|
|
.map { |pid, state, id| [pid.to_i, state.to_i, id] }
|
|
|
|
.select do |(pid, _, id)|
|
2020-12-26 14:01:22 +01:00
|
|
|
pid.nonzero? && /\A(?:application\.)?#{Regexp.escape(bundle_id)}(?:\.\d+){0,2}\Z/.match?(id)
|
2018-11-05 22:40:07 +01:00
|
|
|
end
|
2017-03-08 03:03:36 +01:00
|
|
|
end
|
|
|
|
|
2022-12-29 12:54:59 +11:00
|
|
|
def find_launchctl_with_wildcard(search)
|
|
|
|
regex = Regexp.escape(search).gsub("\\*", ".*")
|
|
|
|
system_command!("/bin/launchctl", args: ["list"])
|
|
|
|
.stdout.lines.drop(1) # skip stdout column headers
|
2024-02-22 23:29:55 +00:00
|
|
|
.filter_map do |line|
|
2022-12-29 12:54:59 +11:00
|
|
|
pid, _state, id = line.chomp.split(/\s+/)
|
|
|
|
id if pid.to_i.nonzero? && id.match?(regex)
|
2024-02-22 23:29:55 +00:00
|
|
|
end
|
2022-12-29 12:54:59 +11:00
|
|
|
end
|
|
|
|
|
2020-10-20 12:03:48 +02:00
|
|
|
sig { returns(String) }
|
2019-10-22 05:34:39 +02:00
|
|
|
def automation_access_instructions
|
2023-03-17 19:13:08 +08:00
|
|
|
navigation_path = if MacOS.version >= :ventura
|
|
|
|
"System Settings → Privacy & Security"
|
|
|
|
else
|
|
|
|
"System Preferences → Security & Privacy → Privacy"
|
|
|
|
end
|
|
|
|
|
2020-07-06 15:31:47 -04:00
|
|
|
<<~EOS
|
|
|
|
Enable Automation access for "Terminal → System Events" in:
|
2023-03-17 19:13:08 +08:00
|
|
|
#{navigation_path} → Automation
|
2020-07-06 15:31:47 -04:00
|
|
|
if you haven't already.
|
|
|
|
EOS
|
2019-10-22 05:34:39 +02:00
|
|
|
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
|
2023-03-07 06:46:59 -08:00
|
|
|
def uninstall_quit(*bundle_ids, command: nil, **_)
|
2017-03-08 03:03:36 +01:00
|
|
|
bundle_ids.each do |bundle_id|
|
2019-09-15 01:08:04 +02:00
|
|
|
next unless running?(bundle_id)
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2023-02-22 09:11:29 -08:00
|
|
|
unless T.must(User.current).gui?
|
2019-09-13 05:50:26 +02:00
|
|
|
opoo "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}'..."
|
2016-11-09 09:32:54 +01:00
|
|
|
|
|
|
|
begin
|
2018-11-05 02:00:40 +01:00
|
|
|
Timeout.timeout(10) do
|
2016-11-09 09:32:54 +01:00
|
|
|
Kernel.loop do
|
2018-11-05 02:00:40 +01:00
|
|
|
next unless quit(bundle_id).success?
|
2019-02-19 13:12:52 +00:00
|
|
|
|
2019-09-15 01:08:04 +02:00
|
|
|
next if running?(bundle_id)
|
|
|
|
|
|
|
|
puts "Application '#{bundle_id}' quit successfully."
|
|
|
|
break
|
2016-11-09 09:32:54 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
rescue Timeout::Error
|
2019-10-22 05:34:39 +02:00
|
|
|
opoo "Application '#{bundle_id}' did not quit. #{automation_access_instructions}"
|
2016-11-09 09:32:54 +01:00
|
|
|
end
|
2016-08-18 22:11:42 +03:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-09-15 01:08:04 +02:00
|
|
|
def running?(bundle_id)
|
|
|
|
script = <<~JAVASCRIPT
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
ObjC.import('stdlib')
|
|
|
|
|
|
|
|
function run(argv) {
|
|
|
|
try {
|
|
|
|
var app = Application(argv[0])
|
|
|
|
if (app.running()) {
|
|
|
|
$.exit(0)
|
|
|
|
}
|
|
|
|
} catch (err) { }
|
|
|
|
|
|
|
|
$.exit(1)
|
|
|
|
}
|
|
|
|
JAVASCRIPT
|
|
|
|
|
|
|
|
system_command("osascript", args: ["-l", "JavaScript", "-e", script, bundle_id],
|
|
|
|
print_stderr: true).status.success?
|
|
|
|
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],
|
2019-09-15 01:08:04 +02:00
|
|
|
print_stderr: false
|
2018-11-05 02:00:40 +01:00
|
|
|
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
|
2023-03-07 06:46:59 -08:00
|
|
|
def uninstall_signal(*signals, command: nil, **_)
|
2017-09-11 23:29:38 +02:00
|
|
|
signals.each do |pair|
|
2023-04-18 15:06:50 -07:00
|
|
|
raise CaskInvalidError.new(cask, "Each #{stanza} :signal must consist of 2 elements.") if pair.size != 2
|
2017-03-08 03:03:36 +01:00
|
|
|
|
2016-11-09 09:32:54 +01:00
|
|
|
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)
|
2023-04-18 15:06:50 -07:00
|
|
|
next if pids.none?
|
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
|
2017-03-08 03:03:36 +01:00
|
|
|
# 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.
|
2023-12-20 19:43:46 +00:00
|
|
|
# TODO: check the user that owns the PID and don't try to kill those from other users.
|
2016-11-09 09:32:54 +01:00
|
|
|
odebug "Unix ids are #{pids.inspect} for processes with bundle identifier #{bundle_id}"
|
2023-12-20 19:43:46 +00:00
|
|
|
begin
|
|
|
|
Process.kill(signal, *pids)
|
|
|
|
rescue Errno::EPERM => e
|
|
|
|
opoo "Failed to kill #{bundle_id} PIDs #{pids.join(", ")} with signal #{signal}: #{e}"
|
|
|
|
end
|
2016-09-24 13:52:43 +02:00
|
|
|
sleep 3
|
|
|
|
end
|
|
|
|
end
|
2016-08-18 22:11:42 +03:00
|
|
|
|
2023-04-08 16:13:12 -05:00
|
|
|
def uninstall_login_item(*login_items, command: nil, successor: nil, **_)
|
2023-04-24 10:50:01 -05:00
|
|
|
return if successor
|
2018-10-25 01:05:53 +02:00
|
|
|
|
2019-08-12 23:45:31 +02:00
|
|
|
apps = cask.artifacts.select { |a| a.class.dsl_key == :app }
|
|
|
|
derived_login_items = apps.map { |a| { path: a.target } }
|
|
|
|
|
|
|
|
[*derived_login_items, *login_items].each do |item|
|
|
|
|
type, id = if item.respond_to?(:key) && item.key?(:path)
|
|
|
|
["path", item[:path]]
|
|
|
|
else
|
|
|
|
["name", item]
|
|
|
|
end
|
|
|
|
|
|
|
|
ohai "Removing login item #{id}"
|
2019-10-22 05:34:39 +02:00
|
|
|
|
|
|
|
result = system_command(
|
2018-11-05 02:00:40 +01:00
|
|
|
"osascript",
|
2018-09-02 16:15:09 +01:00
|
|
|
args: [
|
|
|
|
"-e",
|
2019-08-12 23:45:31 +02:00
|
|
|
%Q(tell application "System Events" to delete every login item whose #{type} is #{id.to_s.inspect}),
|
2018-09-02 16:15:09 +01:00
|
|
|
],
|
|
|
|
)
|
2019-10-22 05:34:39 +02:00
|
|
|
|
|
|
|
opoo "Removal of login item #{id} failed. #{automation_access_instructions}" unless result.success?
|
|
|
|
|
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
|
2023-03-07 06:46:59 -08:00
|
|
|
def uninstall_kext(*kexts, command: nil, **_)
|
2017-03-08 03:03:36 +01:00
|
|
|
kexts.each do |kext|
|
2016-09-24 13:52:43 +02:00
|
|
|
ohai "Unloading kernel extension #{kext}"
|
2023-02-17 14:48:13 -08:00
|
|
|
is_loaded = system_command!(
|
|
|
|
"/usr/sbin/kextstat",
|
2023-03-27 14:06:07 -07:00
|
|
|
args: ["-l", "-b", kext],
|
|
|
|
sudo: true,
|
|
|
|
sudo_as_root: true,
|
2023-02-17 14:48:13 -08:00
|
|
|
).stdout
|
2016-09-24 13:52:43 +02:00
|
|
|
if is_loaded.length > 1
|
2023-02-17 14:48:13 -08:00
|
|
|
system_command!(
|
|
|
|
"/sbin/kextunload",
|
2023-03-27 14:06:07 -07:00
|
|
|
args: ["-b", kext],
|
|
|
|
sudo: true,
|
|
|
|
sudo_as_root: true,
|
2023-02-17 14:48:13 -08:00
|
|
|
)
|
2016-09-24 13:52:43 +02:00
|
|
|
sleep 1
|
|
|
|
end
|
2023-03-27 14:06:07 -07:00
|
|
|
found_kexts = system_command!(
|
2023-02-17 14:48:13 -08:00
|
|
|
"/usr/sbin/kextfind",
|
2023-03-27 14:06:07 -07:00
|
|
|
args: ["-b", kext],
|
|
|
|
sudo: true,
|
|
|
|
sudo_as_root: true,
|
|
|
|
).stdout.chomp.lines
|
|
|
|
found_kexts.each do |kext_path|
|
2016-11-18 22:53:25 +01:00
|
|
|
ohai "Removing kernel extension #{kext_path}"
|
2023-02-17 14:48:13 -08:00
|
|
|
system_command!(
|
|
|
|
"/bin/rm",
|
2023-03-27 14:06:07 -07:00
|
|
|
args: ["-rf", kext_path],
|
|
|
|
sudo: true,
|
|
|
|
sudo_as_root: true,
|
2023-02-17 14:48:13 -08:00
|
|
|
)
|
2016-11-18 22:53:25 +01:00
|
|
|
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
|
2017-04-06 00:33:31 +02:00
|
|
|
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",
|
2017-03-12 22:09:13 +01:00
|
|
|
{ must_succeed: true, sudo: false },
|
2016-09-24 13:52:43 +02:00
|
|
|
{ print_stdout: true },
|
|
|
|
directive_name)
|
2017-03-08 03:03:36 +01:00
|
|
|
|
2016-09-24 13:52:43 +02:00
|
|
|
ohai "Running uninstall script #{executable}"
|
2017-04-06 00:33:31 +02:00
|
|
|
raise CaskInvalidError.new(cask, "#{stanza} :#{directive_name} without :executable.") if executable.nil?
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2019-02-25 21:05:35 +01:00
|
|
|
executable_path = staged_path_join_executable(executable)
|
2017-02-16 21:53:22 +01:00
|
|
|
|
2019-02-25 21:05:35 +01:00
|
|
|
if (executable_path.absolute? && !executable_path.exist?) ||
|
|
|
|
(!executable_path.absolute? && (which executable_path).nil?)
|
2017-02-16 21:53:22 +01:00
|
|
|
message = "uninstall script #{executable} does not exist"
|
2017-04-06 00:33:31 +02:00
|
|
|
raise CaskError, "#{message}." unless force
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2021-01-26 15:21:24 -05:00
|
|
|
opoo "#{message}; skipping."
|
2017-02-16 21:53:22 +01:00
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2022-10-18 01:32:55 +01:00
|
|
|
command.run(executable_path, **script_arguments)
|
2016-08-18 22:11:42 +03:00
|
|
|
sleep 1
|
|
|
|
end
|
|
|
|
|
2023-03-07 06:46:59 -08:00
|
|
|
def uninstall_pkgutil(*pkgs, command: nil, **_)
|
2023-09-29 18:49:44 +01:00
|
|
|
ohai "Uninstalling packages with sudo; the password may be necessary:"
|
2017-03-08 03:03:36 +01:00
|
|
|
pkgs.each do |regex|
|
2018-09-06 08:29:14 +02:00
|
|
|
::Cask::Pkg.all_matching(regex, command).each do |pkg|
|
2017-03-08 03:03:36 +01:00
|
|
|
puts pkg.package_id
|
|
|
|
pkg.uninstall
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def each_resolved_path(action, paths)
|
2017-06-24 07:01:35 +02:00
|
|
|
return enum_for(:each_resolved_path, action, paths) unless block_given?
|
|
|
|
|
2017-03-08 03:03:36 +01:00
|
|
|
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?("~")
|
2017-03-08 03:03:36 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2020-05-17 14:07:14 -04:00
|
|
|
begin
|
|
|
|
yield path, Pathname.glob(resolved_path)
|
|
|
|
rescue Errno::EPERM
|
|
|
|
raise if File.readable?(File.expand_path("~/Library/Application Support/com.apple.TCC"))
|
|
|
|
|
2023-03-17 19:13:08 +08:00
|
|
|
navigation_path = if MacOS.version >= :ventura
|
|
|
|
"System Settings → Privacy & Security"
|
|
|
|
else
|
|
|
|
"System Preferences → Security & Privacy → Privacy"
|
|
|
|
end
|
|
|
|
|
2020-05-17 14:07:14 -04:00
|
|
|
odie "Unable to remove some files. Please enable Full Disk Access for your terminal under " \
|
2023-03-17 19:13:08 +08:00
|
|
|
"#{navigation_path} → Full Disk Access."
|
2020-05-17 14:07:14 -04:00
|
|
|
end
|
2016-09-24 13:52:43 +02:00
|
|
|
end
|
|
|
|
end
|
2016-08-18 22:11:42 +03:00
|
|
|
|
2023-03-07 06:46:59 -08:00
|
|
|
def uninstall_delete(*paths, command: nil, **_)
|
2017-03-08 03:03:36 +01:00
|
|
|
return if paths.empty?
|
|
|
|
|
|
|
|
ohai "Removing files:"
|
|
|
|
each_resolved_path(:delete, paths) do |path, resolved_paths|
|
|
|
|
puts path
|
2018-09-02 16:15:09 +01:00
|
|
|
command.run!(
|
|
|
|
"/usr/bin/xargs",
|
2018-11-02 17:18:07 +00:00
|
|
|
args: ["-0", "--", "/bin/rm", "-r", "-f", "--"],
|
2018-09-02 16:15:09 +01:00
|
|
|
input: resolved_paths.join("\0"),
|
2018-11-02 17:18:07 +00:00
|
|
|
sudo: true,
|
2018-09-02 16:15:09 +01:00
|
|
|
)
|
2016-09-24 13:52:43 +02:00
|
|
|
end
|
|
|
|
end
|
2016-08-18 22:11:42 +03:00
|
|
|
|
2023-03-07 06:46:59 -08:00
|
|
|
def uninstall_trash(*paths, **options)
|
2017-06-16 17:01:30 +02:00
|
|
|
return if paths.empty?
|
|
|
|
|
2017-06-24 07:01:35 +02:00
|
|
|
resolved_paths = each_resolved_path(:trash, paths).to_a
|
|
|
|
|
2020-07-06 15:30:57 -04:00
|
|
|
ohai "Trashing files:", resolved_paths.map(&:first)
|
2023-03-07 06:46:59 -08:00
|
|
|
trash_paths(*resolved_paths.flat_map(&:last), **options)
|
2017-06-24 08:34:01 +02:00
|
|
|
end
|
|
|
|
|
2023-03-07 06:46:59 -08:00
|
|
|
def trash_paths(*paths, command: nil, **_)
|
2019-05-21 12:56:23 +00:00
|
|
|
return if paths.empty?
|
|
|
|
|
2019-10-23 05:21:21 +02:00
|
|
|
stdout, stderr, = system_command HOMEBREW_LIBRARY_PATH/"cask/utils/trash.swift",
|
|
|
|
args: paths,
|
|
|
|
print_stderr: false
|
|
|
|
|
|
|
|
trashed = stdout.split(":").sort
|
|
|
|
untrashable = stderr.split(":").sort
|
|
|
|
|
|
|
|
return trashed, untrashable if untrashable.empty?
|
|
|
|
|
|
|
|
untrashable.delete_if do |path|
|
|
|
|
Utils.gain_permissions(path, ["-R"], SystemCommand) do
|
|
|
|
system_command! HOMEBREW_LIBRARY_PATH/"cask/utils/trash.swift",
|
|
|
|
args: [path],
|
|
|
|
print_stderr: false
|
|
|
|
end
|
|
|
|
|
|
|
|
true
|
|
|
|
rescue
|
|
|
|
false
|
2019-10-02 13:36:05 +00:00
|
|
|
end
|
2019-10-01 11:12:23 +00:00
|
|
|
|
2021-01-26 15:21:24 -05:00
|
|
|
opoo "The following files could not be trashed, please do so manually:"
|
2019-10-23 05:21:21 +02:00
|
|
|
$stderr.puts untrashable
|
2017-10-01 01:47:08 +02:00
|
|
|
|
2019-10-23 05:21:21 +02:00
|
|
|
[trashed, untrashable]
|
2016-09-24 13:52:43 +02:00
|
|
|
end
|
2016-08-18 22:11:42 +03:00
|
|
|
|
2020-01-10 18:14:53 +00:00
|
|
|
def all_dirs?(*directories)
|
|
|
|
directories.all?(&:directory?)
|
|
|
|
end
|
|
|
|
|
|
|
|
def recursive_rmdir(*directories, command: nil, **_)
|
2023-05-13 06:44:28 +02:00
|
|
|
directories.all? do |resolved_path|
|
|
|
|
puts resolved_path.sub(Dir.home, "~")
|
2017-03-08 03:03:36 +01:00
|
|
|
|
2023-05-13 06:44:28 +02:00
|
|
|
if resolved_path.readable?
|
|
|
|
children = resolved_path.children
|
2017-03-08 03:03:36 +01:00
|
|
|
|
2023-05-13 06:44:28 +02:00
|
|
|
next false unless children.all? { |child| child.directory? || child.basename.to_s == ".DS_Store" }
|
|
|
|
else
|
|
|
|
lines = command.run!("/bin/ls", args: ["-A", "-F", "--", resolved_path], sudo: true, print_stderr: false)
|
|
|
|
.stdout.lines.map(&:chomp)
|
|
|
|
.flat_map(&:chomp)
|
2020-01-10 18:14:53 +00:00
|
|
|
|
2023-05-13 06:44:28 +02:00
|
|
|
# Using `-F` above outputs directories ending with `/`.
|
|
|
|
next false unless lines.all? { |l| l.end_with?("/") || l == ".DS_Store" }
|
|
|
|
|
|
|
|
children = lines.map { |l| resolved_path/l.delete_suffix("/") }
|
|
|
|
end
|
|
|
|
|
|
|
|
# Directory counts as empty if it only contains a `.DS_Store`.
|
2024-04-08 19:23:33 +01:00
|
|
|
if children.include?((ds_store = resolved_path/".DS_Store"))
|
2024-03-07 16:20:20 +00:00
|
|
|
Utils.gain_permissions_remove(ds_store, command:)
|
2023-05-13 06:44:28 +02:00
|
|
|
children.delete(ds_store)
|
2017-03-08 03:03:36 +01:00
|
|
|
end
|
2023-05-13 06:44:28 +02:00
|
|
|
|
2024-03-07 16:20:20 +00:00
|
|
|
next false unless recursive_rmdir(*children, command:)
|
2023-05-13 06:44:28 +02:00
|
|
|
|
2024-03-07 16:20:20 +00:00
|
|
|
Utils.gain_permissions_rmdir(resolved_path, command:)
|
2023-05-13 06:44:28 +02:00
|
|
|
|
|
|
|
true
|
2016-09-24 13:52:43 +02:00
|
|
|
end
|
2020-01-10 18:14:53 +00:00
|
|
|
end
|
|
|
|
|
2023-05-13 06:44:28 +02:00
|
|
|
def uninstall_rmdir(*directories, **kwargs)
|
|
|
|
return if directories.empty?
|
2020-01-10 18:14:53 +00:00
|
|
|
|
|
|
|
ohai "Removing directories if empty:"
|
2023-05-13 06:44:28 +02:00
|
|
|
|
|
|
|
each_resolved_path(:rmdir, directories) do |_path, resolved_paths|
|
|
|
|
next unless resolved_paths.all?(&:directory?)
|
|
|
|
|
|
|
|
recursive_rmdir(*resolved_paths, **kwargs)
|
|
|
|
end
|
2016-09-24 13:52:43 +02:00
|
|
|
end
|
2016-08-18 22:11:42 +03:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|