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

cask/artifact/abstract_uninstall: allow wildcard entries for launchctl #14123

Merged
merged 10 commits into from
Dec 29, 2022
24 changes: 24 additions & 0 deletions Library/Homebrew/cask/artifact/abstract_uninstall.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,21 @@ def uninstall_early_script(directives, **options)
# :launchctl must come before :quit/:signal for cases where app would instantly re-launch
def uninstall_launchctl(*services, command: nil, **_)
booleans = [false, true]

all_services = []

# if launchctl item contains a wildcard, find matching process(es)
services.each do |service|
all_services << service unless service.include?("*")
next unless service.include?("*")

found_services = find_launchctl_with_wildcard(service)
next if found_services.blank?

found_services.each { |found_service| all_services << found_service }
end

all_services.each do |service|
ohai "Removing launchctl service #{service}"
booleans.each do |with_sudo|
plist_status = command.run(
Expand Down Expand Up @@ -268,6 +282,16 @@ def uninstall_login_item(*login_items, command: nil, upgrade: false, **_)
end
end

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
.map do |line|
pid, _state, id = line.chomp.split("\t")
id if pid.to_i.nonzero? && id.match?(regex)
end.compact
end

# :kext should be unloaded before attempting to delete the relevant file
def uninstall_kext(*kexts, command: nil, **_)
kexts.each do |kext|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,45 @@
end
end

context "using :launchctl with regex wildcard" do
let(:cask) { Cask::CaskLoader.load(cask_path("with-#{artifact_dsl_key}-launchctl-wildcard")) }
let(:launchctl_regex) { "my.fancy.package.service.*" }
let(:unknown_response) { "launchctl list returned unknown response\n" }
let(:service_info) do
<<~EOS
{
"LimitLoadToSessionType" = "Aqua";
"Label" = "my.fancy.package.service.12345";
"TimeOut" = 30;
"OnDemand" = true;
"LastExitStatus" = 0;
"ProgramArguments" = (
"argument";
);
};
EOS
end

it "searches installed launchctl items" do
expect(subject).to receive(:find_launchctl_with_wildcard)
.with(launchctl_regex)
.and_return(["my.fancy.package.service.12345"])

allow(fake_system_command).to receive(:run)
.with("/bin/launchctl", args: ["list", "my.fancy.package.service.12345"], print_stderr: false, sudo: false)
.and_return(instance_double(SystemCommand::Result, stdout: unknown_response))
allow(fake_system_command).to receive(:run)
.with("/bin/launchctl", args: ["list", "my.fancy.package.service.12345"], print_stderr: false, sudo: true)
.and_return(instance_double(SystemCommand::Result, stdout: service_info))

expect(fake_system_command).to receive(:run!)
.with("/bin/launchctl", args: ["remove", "my.fancy.package.service.12345"], sudo: true)
.and_return(instance_double(SystemCommand::Result))

subject.public_send(:"#{artifact_dsl_key}_phase", command: fake_system_command)
end
end

context "using :pkgutil" do
let(:cask) { Cask::CaskLoader.load(cask_path("with-#{artifact_dsl_key}-pkgutil")) }

Expand Down Expand Up @@ -117,9 +156,9 @@
allow(User.current).to receive(:gui?).and_return false
allow(subject).to receive(:running?).with(bundle_id).and_return(true)

expect {
expect do
subject.public_send(:"#{artifact_dsl_key}_phase", command: fake_system_command)
}.to output(/Not logged into a GUI; skipping quitting application ID 'my.fancy.package.app'\./).to_stderr
end.to output(/Not logged into a GUI; skipping quitting application ID 'my.fancy.package.app'\./).to_stderr
end

it "quits a running application" do
Expand All @@ -130,9 +169,9 @@
.and_return(instance_double("SystemCommand::Result", success?: true))
expect(subject).to receive(:running?).with(bundle_id).ordered.and_return(false)

expect {
expect do
subject.public_send(:"#{artifact_dsl_key}_phase", command: fake_system_command)
}.to output(/Application 'my.fancy.package.app' quit successfully\./).to_stdout
end.to output(/Application 'my.fancy.package.app' quit successfully\./).to_stdout
end

it "tries to quit the application for 10 seconds" do
Expand All @@ -143,9 +182,9 @@
.and_return(instance_double("SystemCommand::Result", success?: false))

time = Benchmark.measure do
expect {
expect do
subject.public_send(:"#{artifact_dsl_key}_phase", command: fake_system_command)
}.to output(/Application 'my.fancy.package.app' did not quit\./).to_stderr
end.to output(/Application 'my.fancy.package.app' did not quit\./).to_stderr
end

expect(time.real).to be_within(3).of(10)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
cask "with-uninstall-launchctl-wildcard" do
version "1.2.3"
sha256 "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"

url "file://#{TEST_FIXTURE_DIR}/cask/MyFancyApp.zip"
homepage "https://brew.sh/fancy"

app "Fancy.app"

uninstall launchctl: "my.fancy.package.service.*"
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
cask "with-zap-launchctl-wildcard" do
version "1.2.3"
sha256 "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"

url "file://#{TEST_FIXTURE_DIR}/cask/MyFancyApp.zip"
homepage "https://brew.sh/fancy"

app "Fancy.app"

zap launchctl: "my.fancy.package.service.*"
end