diff --git a/Library/Homebrew/cask/artifact/abstract_uninstall.rb b/Library/Homebrew/cask/artifact/abstract_uninstall.rb index 19015c00d1e9b..5c2fab1884b1e 100644 --- a/Library/Homebrew/cask/artifact/abstract_uninstall.rb +++ b/Library/Homebrew/cask/artifact/abstract_uninstall.rb @@ -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( @@ -131,6 +145,16 @@ def running_processes(bundle_id) 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(/\s+/) + id if pid.to_i.nonzero? && id.match?(regex) + end.compact + end + sig { returns(String) } def automation_access_instructions <<~EOS diff --git a/Library/Homebrew/test/cask/artifact/shared_examples/uninstall_zap.rb b/Library/Homebrew/test/cask/artifact/shared_examples/uninstall_zap.rb index 3d97672cc423a..06bacb96e0e86 100644 --- a/Library/Homebrew/test/cask/artifact/shared_examples/uninstall_zap.rb +++ b/Library/Homebrew/test/cask/artifact/shared_examples/uninstall_zap.rb @@ -61,6 +61,64 @@ 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 + let(:launchctl_list) do + <<~EOS + PID Status Label + 1111 0 my.fancy.package.service.12345 + - 0 com.apple.SafariHistoryServiceAgent + - 0 com.apple.progressd + 555 0 my.fancy.package.service.test + 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 + + it "returns the matching launchctl services" do + expect(subject).to receive(:system_command!) + .with("/bin/launchctl", args: ["list"]) + .and_return(instance_double(SystemCommand::Result, stdout: launchctl_list)) + + expect(subject.send(:find_launchctl_with_wildcard, + "my.fancy.package.service.*")).to eq(["my.fancy.package.service.12345", + "my.fancy.package.service.test"]) + end + end + context "using :pkgutil" do let(:cask) { Cask::CaskLoader.load(cask_path("with-#{artifact_dsl_key}-pkgutil")) } @@ -117,9 +175,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 @@ -130,9 +188,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 @@ -143,9 +201,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) diff --git a/Library/Homebrew/test/support/fixtures/cask/Casks/with-uninstall-launchctl-wildcard.rb b/Library/Homebrew/test/support/fixtures/cask/Casks/with-uninstall-launchctl-wildcard.rb new file mode 100644 index 0000000000000..89d14231b6893 --- /dev/null +++ b/Library/Homebrew/test/support/fixtures/cask/Casks/with-uninstall-launchctl-wildcard.rb @@ -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 diff --git a/Library/Homebrew/test/support/fixtures/cask/Casks/with-zap-launchctl-wildcard.rb b/Library/Homebrew/test/support/fixtures/cask/Casks/with-zap-launchctl-wildcard.rb new file mode 100644 index 0000000000000..598dc549a7bf2 --- /dev/null +++ b/Library/Homebrew/test/support/fixtures/cask/Casks/with-zap-launchctl-wildcard.rb @@ -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