diff --git a/Library/Homebrew/cask/lib/hbc/artifact/base.rb b/Library/Homebrew/cask/lib/hbc/artifact/base.rb index d925ff340e..a8a17c0814 100644 --- a/Library/Homebrew/cask/lib/hbc/artifact/base.rb +++ b/Library/Homebrew/cask/lib/hbc/artifact/base.rb @@ -34,7 +34,6 @@ module Hbc # stanza may not be needed as an explicit argument description = stanza.to_s if key - arguments = arguments[key] description.concat(" #{key.inspect}") end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/uninstall_base.rb b/Library/Homebrew/cask/lib/hbc/artifact/uninstall_base.rb index 75d210931f..d438fc0264 100644 --- a/Library/Homebrew/cask/lib/hbc/artifact/uninstall_base.rb +++ b/Library/Homebrew/cask/lib/hbc/artifact/uninstall_base.rb @@ -6,11 +6,6 @@ require "hbc/artifact/base" module Hbc module Artifact class UninstallBase < Base - # TODO: 500 is also hardcoded in cask/pkg.rb, but much of - # that logic is probably in the wrong location - - PATH_ARG_SLICE_SIZE = 500 - ORDERED_DIRECTIVES = [ :early_script, :launchctl, @@ -25,47 +20,7 @@ module Hbc :rmdir, ].freeze - # TODO: these methods were consolidated here from separate - # sources and should be refactored for consistency - - def self.expand_path_strings(path_strings) - path_strings.map do |path_string| - path_string.start_with?("~") ? Pathname.new(path_string).expand_path : Pathname.new(path_string) - end - end - - def self.expand_glob(path_strings) - path_strings.flat_map(&Pathname.method(:glob)) - end - - def self.remove_relative_path_strings(action, path_strings) - relative = path_strings.map do |path_string| - path_string if %r{/\.\.(?:/|\Z)}.match(path_string) || !%r{\A/}.match(path_string) - end.compact - relative.each do |path_string| - opoo "Skipping #{action} for relative path #{path_string}" - end - path_strings - relative - end - - def self.remove_undeletable_path_strings(action, path_strings) - undeletable = path_strings.map do |path_string| - path_string if MacOS.undeletable?(Pathname.new(path_string)) - end.compact - undeletable.each do |path_string| - opoo "Skipping #{action} for undeletable path #{path_string}" - end - path_strings - undeletable - end - - def self.prepare_path_strings(action, path_strings, expand_tilde) - path_strings = expand_path_strings(path_strings) if expand_tilde - path_strings = remove_relative_path_strings(action, path_strings) - path_strings = expand_glob(path_strings) - remove_undeletable_path_strings(action, path_strings) - end - - def dispatch_uninstall_directives(expand_tilde: true) + def dispatch_uninstall_directives directives_set = @cask.artifacts[stanza] ohai "Running #{stanza} process for #{@cask}; your password may be necessary" @@ -75,9 +30,8 @@ module Hbc ORDERED_DIRECTIVES.each do |directive_sym| directives_set.select { |h| h.key?(directive_sym) }.each do |directives| - args = [directives] - args << expand_tilde if [:delete, :trash, :rmdir].include?(directive_sym) - send("uninstall_#{directive_sym}", *args) + args = directives[directive_sym] + send("uninstall_#{directive_sym}", *(args.is_a?(Hash) ? [args] : args)) end end end @@ -102,8 +56,8 @@ module Hbc end # :launchctl must come before :quit/:signal for cases where app would instantly re-launch - def uninstall_launchctl(directives) - Array(directives[:launchctl]).each do |service| + def uninstall_launchctl(*services) + services.each do |service| 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 @@ -127,45 +81,6 @@ module Hbc end end - # :quit/:signal must come before :kext so the kext will not be in use by a running process - def uninstall_quit(directives) - Array(directives[:quit]).each do |id| - ohai "Quitting application ID #{id}" - next if running_processes(id).empty? - @command.run!("/usr/bin/osascript", args: ["-e", %Q(tell application id "#{id}" to quit)], sudo: true) - - begin - Timeout.timeout(3) do - Kernel.loop do - break if running_processes(id).empty? - end - end - rescue Timeout::Error - next - end - end - end - - # :signal should come after :quit so it can be used as a backup when :quit fails - def uninstall_signal(directives) - Array(directives[:signal]).flatten.each_slice(2) do |pair| - raise CaskInvalidError.new(@cask, "Each #{stanza} :signal must have 2 elements.") unless pair.length == 2 - signal, bundle_id = pair - ohai "Signalling '#{signal}' to application ID '#{bundle_id}'" - pids = running_processes(bundle_id).map(&:first) - next unless pids.any? - # 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. - odebug "Unix ids are #{pids.inspect} for processes with bundle identifier #{bundle_id}" - Process.kill(signal, *pids) - sleep 3 - end - end - def running_processes(bundle_id) @command.run!("/bin/launchctl", args: ["list"]).stdout.lines .map { |line| line.chomp.split("\t") } @@ -176,8 +91,50 @@ module Hbc end end - def uninstall_login_item(directives) - Array(directives[:login_item]).each do |name| + # :quit/:signal must come before :kext so the kext will not be in use by a running process + def uninstall_quit(*bundle_ids) + bundle_ids.each do |bundle_id| + ohai "Quitting application ID #{bundle_id}" + next if running_processes(bundle_id).empty? + @command.run!("/usr/bin/osascript", args: ["-e", %Q(tell application id "#{bundle_id}" to quit)], sudo: true) + + begin + Timeout.timeout(3) do + Kernel.loop do + break if running_processes(bundle_id).empty? + end + end + rescue Timeout::Error + next + end + end + end + + # :signal should come after :quit so it can be used as a backup when :quit fails + def uninstall_signal(*signals) + signals.flatten.each_slice(2) do |pair| + unless pair.size == 2 + raise CaskInvalidError.new(@cask, "Each #{stanza} :signal must consist of 2 elements.") + end + + signal, bundle_id = pair + ohai "Signalling '#{signal}' to application ID '#{bundle_id}'" + pids = running_processes(bundle_id).map(&:first) + next unless pids.any? + # 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. + odebug "Unix ids are #{pids.inspect} for processes with bundle identifier #{bundle_id}" + Process.kill(signal, *pids) + sleep 3 + end + end + + def uninstall_login_item(*login_items) + login_items.each do |name| ohai "Removing login item #{name}" @command.run!("/usr/bin/osascript", args: ["-e", %Q(tell application "System Events" to delete every login item whose name is "#{name}")], @@ -187,8 +144,8 @@ module Hbc end # :kext should be unloaded before attempting to delete the relevant file - def uninstall_kext(directives) - Array(directives[:kext]).each do |kext| + def uninstall_kext(*kexts) + kexts.each do |kext| ohai "Unloading kernel extension #{kext}" is_loaded = @command.run!("/usr/sbin/kextstat", args: ["-l", "-b", kext], sudo: true).stdout if is_loaded.length > 1 @@ -209,6 +166,7 @@ module Hbc { must_succeed: true, sudo: true }, { print_stdout: true }, directive_name) + ohai "Running uninstall script #{executable}" raise CaskInvalidError.new(@cask, "#{stanza} :#{directive_name} without :executable.") if executable.nil? executable_path = @cask.staged_path.join(executable) @@ -225,43 +183,67 @@ module Hbc sleep 1 end - def uninstall_pkgutil(directives) - ohai "Removing files from pkgutil Bill-of-Materials" - Array(directives[:pkgutil]).each do |regexp| - pkgs = Hbc::Pkg.all_matching(regexp, @command) - pkgs.each(&:uninstall) + def uninstall_pkgutil(*pkgs) + ohai "Uninstalling packages:" + pkgs.each do |regex| + Hbc::Pkg.all_matching(regex, @command).each do |pkg| + puts pkg.package_id + pkg.uninstall + end end end - def uninstall_delete(directives, expand_tilde = true) - Array(directives[:delete]).concat(Array(directives[:trash])).flatten.each_slice(PATH_ARG_SLICE_SIZE) do |path_slice| - ohai "Removing files: #{path_slice.utf8_inspect}" - path_slice = self.class.prepare_path_strings(:delete, path_slice, expand_tilde) - @command.run!("/bin/rm", args: path_slice.unshift("-rf", "--"), sudo: true) + def each_resolved_path(action, paths) + paths.each do |path| + resolved_path = Pathname.new(path) + + if path.start_with?("~") + resolved_path = resolved_path.expand_path + end + + 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) end end - # :trash functionality is stubbed as a synonym for :delete - # TODO: make :trash work differently, moving files to the Trash - def uninstall_trash(directives, expand_tilde = true) - uninstall_delete(directives, expand_tilde) + def uninstall_delete(*paths) + return if paths.empty? + + ohai "Removing files:" + each_resolved_path(:delete, paths) do |path, resolved_paths| + puts path + @command.run!("/usr/bin/xargs", args: ["-0", "--", "/bin/rm", "-r", "-f", "--"], input: resolved_paths.join("\0"), sudo: true) + end end - def uninstall_rmdir(directories, expand_tilde = true) - action = :rmdir - self.class.prepare_path_strings(action, Array(directories[action]).flatten, expand_tilde).each do |directory| - next if directory.to_s.empty? - ohai "Removing directory if empty: #{directory.to_s.utf8_inspect}" - directory = Pathname.new(directory) - next unless directory.exist? - @command.run!("/bin/rm", - args: ["-f", "--", directory.join(".DS_Store")], - sudo: true, - print_stderr: false) - @command.run("/bin/rmdir", - args: ["--", directory], - sudo: true, - print_stderr: false) + def uninstall_trash(*paths) + # :trash functionality is stubbed as a synonym for :delete + # TODO: make :trash work differently, moving files to the Trash + uninstall_delete(*paths) + end + + def uninstall_rmdir(*directories) + 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 end end end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/zap.rb b/Library/Homebrew/cask/lib/hbc/artifact/zap.rb index 7793e57728..cdfe2531da 100644 --- a/Library/Homebrew/cask/lib/hbc/artifact/zap.rb +++ b/Library/Homebrew/cask/lib/hbc/artifact/zap.rb @@ -4,7 +4,7 @@ module Hbc module Artifact class Zap < UninstallBase def zap_phase - dispatch_uninstall_directives(expand_tilde: true) + dispatch_uninstall_directives end end end diff --git a/Library/Homebrew/test/cask/artifact/uninstall_spec.rb b/Library/Homebrew/test/cask/artifact/uninstall_spec.rb index b7deb4575b..49a6dce9fe 100644 --- a/Library/Homebrew/test/cask/artifact/uninstall_spec.rb +++ b/Library/Homebrew/test/cask/artifact/uninstall_spec.rb @@ -1,351 +1,7 @@ +require_relative "uninstall_zap_shared_examples" + describe Hbc::Artifact::Uninstall, :cask do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-installable.rb") } - - let(:uninstall_artifact) { - Hbc::Artifact::Uninstall.new(cask, command: Hbc::FakeSystemCommand) - } - - let(:dir) { TEST_TMPDIR } - let(:absolute_path) { Pathname.new("#{dir}/absolute_path") } - let(:path_with_tilde) { Pathname.new("#{dir}/path_with_tilde") } - let(:glob_path1) { Pathname.new("#{dir}/glob_path1") } - let(:glob_path2) { Pathname.new("#{dir}/glob_path2") } - - around(:each) do |example| - begin - ENV["HOME"] = dir - - paths = [ - absolute_path, - path_with_tilde, - glob_path1, - glob_path2, - ] - - FileUtils.touch paths - - shutup do - InstallHelper.install_without_artifacts(cask) - end - - example.run - ensure - FileUtils.rm_f paths - end - end - - describe "uninstall_phase" do - subject { - shutup do - uninstall_artifact.uninstall_phase - end - } - - context "when using launchctl" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-launchctl.rb") } - let(:launchctl_list_cmd) { %w[/bin/launchctl list my.fancy.package.service] } - let(:launchctl_remove_cmd) { %w[/bin/launchctl remove my.fancy.package.service] } - let(:unknown_response) { "launchctl list returned unknown response\n" } - let(:service_info) { - <<-EOS.undent - { - "LimitLoadToSessionType" = "Aqua"; - "Label" = "my.fancy.package.service"; - "TimeOut" = 30; - "OnDemand" = true; - "LastExitStatus" = 0; - "ProgramArguments" = ( - "argument"; - ); - }; - EOS - } - - context "when launchctl job is owned by user" do - it "can uninstall" do - Hbc::FakeSystemCommand.stubs_command( - launchctl_list_cmd, - service_info, - ) - - Hbc::FakeSystemCommand.stubs_command( - sudo(launchctl_list_cmd), - unknown_response, - ) - - Hbc::FakeSystemCommand.expects_command(launchctl_remove_cmd) - - subject - end - end - - context "when launchctl job is owned by system" do - it "can uninstall" do - Hbc::FakeSystemCommand.stubs_command( - launchctl_list_cmd, - unknown_response, - ) - - Hbc::FakeSystemCommand.stubs_command( - sudo(launchctl_list_cmd), - service_info, - ) - - Hbc::FakeSystemCommand.expects_command(sudo(launchctl_remove_cmd)) - - subject - end - end - end - - context "when using pkgutil" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-pkgutil.rb") } - let(:main_pkg_id) { "my.fancy.package.main" } - let(:agent_pkg_id) { "my.fancy.package.agent" } - let(:main_files) { - %w[ - fancy/bin/fancy.exe - fancy/var/fancy.data - ] - } - let(:main_dirs) { - %w[ - fancy - fancy/bin - fancy/var - ] - } - let(:agent_files) { - %w[ - fancy/agent/fancy-agent.exe - fancy/agent/fancy-agent.pid - fancy/agent/fancy-agent.log - ] - } - let(:agent_dirs) { - %w[ - fancy - fancy/agent - ] - } - let(:pkg_info_plist) { - <<-EOS.undent - - - - - install-location - tmp - volume - / - - - EOS - } - - it "can uninstall" do - Hbc::FakeSystemCommand.stubs_command( - %w[/usr/sbin/pkgutil --pkgs=my.fancy.package.*], - "#{main_pkg_id}\n#{agent_pkg_id}", - ) - - [ - [main_pkg_id, main_files, main_dirs], - [agent_pkg_id, agent_files, agent_dirs], - ].each do |pkg_id, pkg_files, pkg_dirs| - Hbc::FakeSystemCommand.stubs_command( - %W[/usr/sbin/pkgutil --only-files --files #{pkg_id}], - pkg_files.join("\n"), - ) - - Hbc::FakeSystemCommand.stubs_command( - %W[/usr/sbin/pkgutil --only-dirs --files #{pkg_id}], - pkg_dirs.join("\n"), - ) - - Hbc::FakeSystemCommand.stubs_command( - %W[/usr/sbin/pkgutil --files #{pkg_id}], - (pkg_files + pkg_dirs).join("\n"), - ) - - Hbc::FakeSystemCommand.stubs_command( - %W[/usr/sbin/pkgutil --pkg-info-plist #{pkg_id}], - pkg_info_plist, - ) - - Hbc::FakeSystemCommand.expects_command(sudo(%W[/usr/sbin/pkgutil --forget #{pkg_id}])) - - Hbc::FakeSystemCommand.expects_command( - sudo(%w[/bin/rm -f --] + pkg_files.map { |path| Pathname("/tmp/#{path}") }), - ) - end - - subject - end - end - - context "when using kext" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-kext.rb") } - let(:kext_id) { "my.fancy.package.kernelextension" } - - it "can uninstall" do - Hbc::FakeSystemCommand.stubs_command( - sudo(%W[/usr/sbin/kextstat -l -b #{kext_id}]), "loaded" - ) - - Hbc::FakeSystemCommand.expects_command( - sudo(%W[/sbin/kextunload -b #{kext_id}]), - ) - - Hbc::FakeSystemCommand.expects_command( - sudo(%W[/usr/sbin/kextfind -b #{kext_id}]), "/Library/Extensions/FancyPackage.kext\n" - ) - - Hbc::FakeSystemCommand.expects_command( - sudo(["/bin/rm", "-rf", "/Library/Extensions/FancyPackage.kext"]), - ) - - subject - end - end - - context "when using quit" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-quit.rb") } - let(:bundle_id) { "my.fancy.package.app" } - let(:quit_application_script) { - %Q(tell application id "#{bundle_id}" to quit) - } - - it "can uninstall" do - Hbc::FakeSystemCommand.stubs_command( - %w[/bin/launchctl list], "999\t0\t#{bundle_id}\n" - ) - - Hbc::FakeSystemCommand.stubs_command( - %w[/bin/launchctl list], - ) - - subject - end - end - - context "when using signal" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-signal.rb") } - let(:bundle_id) { "my.fancy.package.app" } - let(:signals) { %w[TERM KILL] } - let(:unix_pids) { [12_345, 67_890] } - - it "can uninstall" do - Hbc::FakeSystemCommand.stubs_command( - %w[/bin/launchctl list], unix_pids.map { |pid| [pid, 0, bundle_id].join("\t") }.join("\n") - ) - - signals.each do |signal| - expect(Process).to receive(:kill).with(signal, *unix_pids) - end - - subject - end - end - - context "when using delete" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-delete.rb") } - - it "can uninstall" do - Hbc::FakeSystemCommand.expects_command( - sudo(%w[/bin/rm -rf --], - absolute_path, - path_with_tilde, - glob_path1, - glob_path2), - ) - - subject - end - end - - context "when using trash" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-trash.rb") } - - it "can uninstall" do - Hbc::FakeSystemCommand.expects_command( - sudo(%w[/bin/rm -rf --], - absolute_path, - path_with_tilde, - glob_path1, - glob_path2), - ) - - subject - end - end - - context "when using rmdir" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-rmdir.rb") } - let(:empty_directory_path) { Pathname.new("#{TEST_TMPDIR}/empty_directory_path") } - - before(:each) do - empty_directory_path.mkdir - end - - after(:each) do - empty_directory_path.rmdir - end - - it "can uninstall" do - Hbc::FakeSystemCommand.expects_command( - sudo(%w[/bin/rm -f --], empty_directory_path/".DS_Store"), - ) - - Hbc::FakeSystemCommand.expects_command( - sudo(%w[/bin/rmdir --], empty_directory_path), - ) - - subject - end - end - - context "when using script" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-script.rb") } - let(:script_pathname) { cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool") } - - it "can uninstall" do - Hbc::FakeSystemCommand.expects_command(%w[/bin/chmod -- +x] + [script_pathname]) - - Hbc::FakeSystemCommand.expects_command( - sudo(cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool"), "--please"), - ) - - subject - end - end - - context "when using early_script" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-early-script.rb") } - let(:script_pathname) { cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool") } - - it "can uninstall" do - Hbc::FakeSystemCommand.expects_command(%w[/bin/chmod -- +x] + [script_pathname]) - - Hbc::FakeSystemCommand.expects_command( - sudo(cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool"), "--please"), - ) - - subject - end - end - - context "when using login_item" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-login-item.rb") } - - it "can uninstall" do - Hbc::FakeSystemCommand.expects_command( - ["/usr/bin/osascript", "-e", 'tell application "System Events" to delete every login ' \ - 'item whose name is "Fancy"'], - ) - - subject - end - end + describe "#uninstall_phase" do + include_examples "#uninstall_phase or #zap_phase" end end diff --git a/Library/Homebrew/test/cask/artifact/uninstall_zap_shared_examples.rb b/Library/Homebrew/test/cask/artifact/uninstall_zap_shared_examples.rb new file mode 100644 index 0000000000..2b69a06a83 --- /dev/null +++ b/Library/Homebrew/test/cask/artifact/uninstall_zap_shared_examples.rb @@ -0,0 +1,334 @@ +shared_examples "#uninstall_phase or #zap_phase" do + let(:artifact_name) { described_class.artifact_name } + let(:artifact) { described_class.new(cask, command: fake_system_command) } + let(:fake_system_command) { Hbc::FakeSystemCommand } + + subject do + shutup do + artifact.public_send(:"#{artifact_name}_phase") + end + end + + context "using :launchctl" do + let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-launchctl.rb") } + let(:launchctl_list_cmd) { %w[/bin/launchctl list my.fancy.package.service] } + let(:launchctl_remove_cmd) { %w[/bin/launchctl remove my.fancy.package.service] } + let(:unknown_response) { "launchctl list returned unknown response\n" } + let(:service_info) do + <<-EOS.undent + { + "LimitLoadToSessionType" = "Aqua"; + "Label" = "my.fancy.package.service"; + "TimeOut" = 30; + "OnDemand" = true; + "LastExitStatus" = 0; + "ProgramArguments" = ( + "argument"; + ); + }; + EOS + end + + it "works when job is owned by user" do + Hbc::FakeSystemCommand.stubs_command( + launchctl_list_cmd, + service_info, + ) + + Hbc::FakeSystemCommand.stubs_command( + sudo(launchctl_list_cmd), + unknown_response, + ) + + Hbc::FakeSystemCommand.expects_command(launchctl_remove_cmd) + + subject + end + + it "works when job is owned by system" do + Hbc::FakeSystemCommand.stubs_command( + launchctl_list_cmd, + unknown_response, + ) + + Hbc::FakeSystemCommand.stubs_command( + sudo(launchctl_list_cmd), + service_info, + ) + + Hbc::FakeSystemCommand.expects_command(sudo(launchctl_remove_cmd)) + + subject + end + end + + context "using :pkgutil" do + let(:fake_system_command) { class_double(Hbc::SystemCommand) } + + let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-pkgutil.rb") } + let(:main_pkg_id) { "my.fancy.package.main" } + let(:agent_pkg_id) { "my.fancy.package.agent" } + let(:main_files) do + %w[ + fancy/bin/fancy.exe + fancy/var/fancy.data + ] + end + let(:main_dirs) do + %w[ + fancy + fancy/bin + fancy/var + ] + end + let(:agent_files) do + %w[ + fancy/agent/fancy-agent.exe + fancy/agent/fancy-agent.pid + fancy/agent/fancy-agent.log + ] + end + let(:agent_dirs) do + %w[ + fancy + fancy/agent + ] + end + let(:pkg_info_plist) do + <<-EOS.undent + + + + + install-location + tmp + volume + / + + + EOS + end + + it "is supported" do + allow(fake_system_command).to receive(:run).with( + "/usr/sbin/pkgutil", + args: ["--pkgs=my.fancy.package.*"], + ).and_return(double(stdout: "#{main_pkg_id}\n#{agent_pkg_id}")) + + [ + [main_pkg_id, main_files, main_dirs], + [agent_pkg_id, agent_files, agent_dirs], + ].each do |pkg_id, pkg_files, pkg_dirs| + + allow(fake_system_command).to receive(:run!).with( + "/usr/sbin/pkgutil", + args: ["--only-files", "--files", pkg_id.to_s], + ).and_return(double(stdout: pkg_files.join("\n"))) + + allow(fake_system_command).to receive(:run!).with( + "/usr/sbin/pkgutil", + args: ["--only-dirs", "--files", pkg_id.to_s], + ).and_return(double(stdout: pkg_dirs.join("\n"))) + + allow(fake_system_command).to receive(:run!).with( + "/usr/sbin/pkgutil", + args: ["--files", pkg_id.to_s], + ).and_return(double(stdout: (pkg_files + pkg_dirs).join("\n"))) + + result = Hbc::SystemCommand::Result.new(nil, pkg_info_plist, nil, 0) + allow(fake_system_command).to receive(:run!).with( + "/usr/sbin/pkgutil", + args: ["--pkg-info-plist", pkg_id.to_s], + ).and_return(result) + + expect(fake_system_command).to receive(:run).with( + "/usr/bin/xargs", + args: ["-0", "--", "/bin/rm", "-f", "--"], + input: pkg_files.map { |path| "/tmp/#{path}" }.join("\0"), + sudo: true, + ) + + expect(fake_system_command).to receive(:run!).with( + "/usr/sbin/pkgutil", + args: ["--forget", pkg_id.to_s], + sudo: true, + ) + end + + subject + end + end + + context "using :kext" do + let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-kext.rb") } + let(:kext_id) { "my.fancy.package.kernelextension" } + + it "is supported" do + Hbc::FakeSystemCommand.stubs_command( + sudo(%W[/usr/sbin/kextstat -l -b #{kext_id}]), "loaded" + ) + + Hbc::FakeSystemCommand.expects_command( + sudo(%W[/sbin/kextunload -b #{kext_id}]), + ) + + Hbc::FakeSystemCommand.expects_command( + sudo(%W[/usr/sbin/kextfind -b #{kext_id}]), "/Library/Extensions/FancyPackage.kext\n" + ) + + Hbc::FakeSystemCommand.expects_command( + sudo(["/bin/rm", "-rf", "/Library/Extensions/FancyPackage.kext"]), + ) + + subject + end + end + + context "using :quit" do + let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-quit.rb") } + let(:bundle_id) { "my.fancy.package.app" } + let(:quit_application_script) do + %Q(tell application id "#{bundle_id}" to quit) + end + + it "is supported" do + Hbc::FakeSystemCommand.stubs_command( + %w[/bin/launchctl list], "999\t0\t#{bundle_id}\n" + ) + + Hbc::FakeSystemCommand.stubs_command( + %w[/bin/launchctl list], + ) + + subject + end + end + + context "using :signal" do + let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-signal.rb") } + let(:bundle_id) { "my.fancy.package.app" } + let(:signals) { %w[TERM KILL] } + let(:unix_pids) { [12_345, 67_890] } + + it "is supported" do + Hbc::FakeSystemCommand.stubs_command( + %w[/bin/launchctl list], unix_pids.map { |pid| [pid, 0, bundle_id].join("\t") }.join("\n") + ) + + signals.each do |signal| + expect(Process).to receive(:kill).with(signal, *unix_pids) + end + + subject + end + end + + [:delete, :trash].each do |directive| + context "using :#{directive}" do + let(:dir) { TEST_TMPDIR } + let(:absolute_path) { Pathname.new("#{dir}/absolute_path") } + let(:path_with_tilde) { Pathname.new("#{dir}/path_with_tilde") } + let(:glob_path1) { Pathname.new("#{dir}/glob_path1") } + let(:glob_path2) { Pathname.new("#{dir}/glob_path2") } + let(:paths) { [absolute_path, path_with_tilde, glob_path1, glob_path2] } + + around(:each) do |example| + begin + ENV["HOME"] = dir + + FileUtils.touch paths + + example.run + ensure + FileUtils.rm_f paths + end + end + + let(:fake_system_command) { Hbc::NeverSudoSystemCommand } + let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-#{directive}.rb") } + + it "is supported" do + paths.each do |path| + expect(path).to exist + end + + subject + + paths.each do |path| + expect(path).not_to exist + end + end + end + end + + context "using :rmdir" do + let(:fake_system_command) { Hbc::NeverSudoSystemCommand } + let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-rmdir.rb") } + let(:empty_directory) { Pathname.new("#{TEST_TMPDIR}/empty_directory_path") } + let(:ds_store) { empty_directory.join(".DS_Store") } + + before(:each) do + empty_directory.mkdir + FileUtils.touch ds_store + end + + after(:each) do + FileUtils.rm_rf empty_directory + end + + it "is supported" do + expect(empty_directory).to exist + expect(ds_store).to exist + + subject + + expect(ds_store).not_to exist + expect(empty_directory).not_to exist + end + end + + context "using :script" do + let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-script.rb") } + let(:script_pathname) { cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool") } + + it "is supported" do + Hbc::FakeSystemCommand.expects_command(%w[/bin/chmod -- +x] + [script_pathname]) + + Hbc::FakeSystemCommand.expects_command( + sudo(cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool"), "--please"), + ) + + InstallHelper.install_without_artifacts(cask) + subject + end + end + + context "using :early_script" do + let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-early-script.rb") } + let(:script_pathname) { cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool") } + + it "is supported" do + Hbc::FakeSystemCommand.expects_command(%w[/bin/chmod -- +x] + [script_pathname]) + + Hbc::FakeSystemCommand.expects_command( + sudo(cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool"), "--please"), + ) + + InstallHelper.install_without_artifacts(cask) + subject + end + end + + context "using :login_item" do + let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-login-item.rb") } + + it "is supported" do + Hbc::FakeSystemCommand.expects_command( + ["/usr/bin/osascript", "-e", 'tell application "System Events" to delete every login ' \ + 'item whose name is "Fancy"'], + ) + + subject + end + end +end diff --git a/Library/Homebrew/test/cask/artifact/zap_spec.rb b/Library/Homebrew/test/cask/artifact/zap_spec.rb index fdf2e4f9db..0a09d9710e 100644 --- a/Library/Homebrew/test/cask/artifact/zap_spec.rb +++ b/Library/Homebrew/test/cask/artifact/zap_spec.rb @@ -1,352 +1,7 @@ -# TODO: test that zap removes an alternate version of the same Cask +require_relative "uninstall_zap_shared_examples" + describe Hbc::Artifact::Zap, :cask do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-installable.rb") } - - let(:zap_artifact) { - Hbc::Artifact::Zap.new(cask, command: Hbc::FakeSystemCommand) - } - - let(:dir) { TEST_TMPDIR } - let(:absolute_path) { Pathname.new("#{dir}/absolute_path") } - let(:path_with_tilde) { Pathname.new("#{dir}/path_with_tilde") } - let(:glob_path1) { Pathname.new("#{dir}/glob_path1") } - let(:glob_path2) { Pathname.new("#{dir}/glob_path2") } - - around(:each) do |example| - begin - ENV["HOME"] = dir - - paths = [ - absolute_path, - path_with_tilde, - glob_path1, - glob_path2, - ] - - FileUtils.touch paths - - shutup do - InstallHelper.install_without_artifacts(cask) - end - - example.run - ensure - FileUtils.rm_f paths - end - end - describe "#zap_phase" do - subject { - shutup do - zap_artifact.zap_phase - end - } - - context "when using launchctl" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-launchctl.rb") } - let(:launchctl_list_cmd) { %w[/bin/launchctl list my.fancy.package.service] } - let(:launchctl_remove_cmd) { %w[/bin/launchctl remove my.fancy.package.service] } - let(:unknown_response) { "launchctl list returned unknown response\n" } - let(:service_info) { - <<-EOS.undent - { - "LimitLoadToSessionType" = "Aqua"; - "Label" = "my.fancy.package.service"; - "TimeOut" = 30; - "OnDemand" = true; - "LastExitStatus" = 0; - "ProgramArguments" = ( - "argument"; - ); - }; - EOS - } - - context "when launchctl job is owned by user" do - it "can zap" do - Hbc::FakeSystemCommand.stubs_command( - launchctl_list_cmd, - service_info, - ) - - Hbc::FakeSystemCommand.stubs_command( - sudo(launchctl_list_cmd), - unknown_response, - ) - - Hbc::FakeSystemCommand.expects_command(launchctl_remove_cmd) - - subject - end - end - - describe "when launchctl job is owned by system" do - it "can zap" do - Hbc::FakeSystemCommand.stubs_command( - launchctl_list_cmd, - unknown_response, - ) - - Hbc::FakeSystemCommand.stubs_command( - sudo(launchctl_list_cmd), - service_info, - ) - - Hbc::FakeSystemCommand.expects_command(sudo(launchctl_remove_cmd)) - - subject - end - end - end - - context "when using pkgutil" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-pkgutil.rb") } - let(:main_pkg_id) { "my.fancy.package.main" } - let(:agent_pkg_id) { "my.fancy.package.agent" } - let(:main_files) { - %w[ - fancy/bin/fancy.exe - fancy/var/fancy.data - ] - } - let(:main_dirs) { - %w[ - fancy - fancy/bin - fancy/var - ] - } - let(:agent_files) { - %w[ - fancy/agent/fancy-agent.exe - fancy/agent/fancy-agent.pid - fancy/agent/fancy-agent.log - ] - } - let(:agent_dirs) { - %w[ - fancy - fancy/agent - ] - } - let(:pkg_info_plist) { - <<-EOS.undent - - - - - install-location - tmp - volume - / - - - EOS - } - - it "can zap" do - Hbc::FakeSystemCommand.stubs_command( - %w[/usr/sbin/pkgutil --pkgs=my.fancy.package.*], - "#{main_pkg_id}\n#{agent_pkg_id}", - ) - - [ - [main_pkg_id, main_files, main_dirs], - [agent_pkg_id, agent_files, agent_dirs], - ].each do |pkg_id, pkg_files, pkg_dirs| - Hbc::FakeSystemCommand.stubs_command( - %W[/usr/sbin/pkgutil --only-files --files #{pkg_id}], - pkg_files.join("\n"), - ) - - Hbc::FakeSystemCommand.stubs_command( - %W[/usr/sbin/pkgutil --only-dirs --files #{pkg_id}], - pkg_dirs.join("\n"), - ) - - Hbc::FakeSystemCommand.stubs_command( - %W[/usr/sbin/pkgutil --files #{pkg_id}], - (pkg_files + pkg_dirs).join("\n"), - ) - - Hbc::FakeSystemCommand.stubs_command( - %W[/usr/sbin/pkgutil --pkg-info-plist #{pkg_id}], - pkg_info_plist, - ) - - Hbc::FakeSystemCommand.expects_command(sudo(%W[/usr/sbin/pkgutil --forget #{pkg_id}])) - - Hbc::FakeSystemCommand.expects_command( - sudo(%w[/bin/rm -f --] + pkg_files.map { |path| Pathname("/tmp/#{path}") }), - ) - end - - subject - end - end - - context "when using kext" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-kext.rb") } - let(:kext_id) { "my.fancy.package.kernelextension" } - - it "can zap" do - Hbc::FakeSystemCommand.stubs_command( - sudo(%W[/usr/sbin/kextstat -l -b #{kext_id}]), "loaded" - ) - - Hbc::FakeSystemCommand.expects_command( - sudo(%W[/sbin/kextunload -b #{kext_id}]), - ) - - Hbc::FakeSystemCommand.expects_command( - sudo(%W[/usr/sbin/kextfind -b #{kext_id}]), "/Library/Extensions/FancyPackage.kext\n" - ) - - Hbc::FakeSystemCommand.expects_command( - sudo(["/bin/rm", "-rf", "/Library/Extensions/FancyPackage.kext"]), - ) - - subject - end - end - - context "when using quit" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-quit.rb") } - let(:bundle_id) { "my.fancy.package.app" } - let(:quit_application_script) { - %Q(tell application id "#{bundle_id}" to quit) - } - - it "can zap" do - Hbc::FakeSystemCommand.stubs_command( - %w[/bin/launchctl list], "999\t0\t#{bundle_id}\n" - ) - - Hbc::FakeSystemCommand.stubs_command( - %w[/bin/launchctl list], - ) - - subject - end - end - - context "when using signal" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-signal.rb") } - let(:bundle_id) { "my.fancy.package.app" } - let(:signals) { %w[TERM KILL] } - let(:unix_pids) { [12_345, 67_890] } - - it "can zap" do - Hbc::FakeSystemCommand.stubs_command( - %w[/bin/launchctl list], unix_pids.map { |pid| [pid, 0, bundle_id].join("\t") }.join("\n") - ) - - signals.each do |signal| - expect(Process).to receive(:kill).with(signal, *unix_pids) - end - - subject - end - end - - context "when using delete" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-delete.rb") } - - it "can zap" do - Hbc::FakeSystemCommand.expects_command( - sudo(%w[/bin/rm -rf --], - absolute_path, - path_with_tilde, - glob_path1, - glob_path2), - ) - - subject - end - end - - context "when using trash" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-trash.rb") } - - it "can zap" do - Hbc::FakeSystemCommand.expects_command( - sudo(%w[/bin/rm -rf --], - absolute_path, - path_with_tilde, - glob_path1, - glob_path2), - ) - - subject - end - end - - context "when using rmdir" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-rmdir.rb") } - let(:empty_directory_path) { Pathname.new("#{TEST_TMPDIR}/empty_directory_path") } - - before(:each) do - empty_directory_path.mkdir - end - - after(:each) do - empty_directory_path.rmdir - end - - it "can zap" do - Hbc::FakeSystemCommand.expects_command( - sudo(%w[/bin/rm -f --], empty_directory_path/".DS_Store"), - ) - - Hbc::FakeSystemCommand.expects_command( - sudo(%w[/bin/rmdir --], empty_directory_path), - ) - - subject - end - end - - context "when using script" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-script.rb") } - let(:script_pathname) { cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool") } - - it "can zap" do - Hbc::FakeSystemCommand.expects_command(%w[/bin/chmod -- +x] + [script_pathname]) - - Hbc::FakeSystemCommand.expects_command( - sudo(cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool"), "--please"), - ) - - subject - end - end - - context "when using early_script" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-early-script.rb") } - let(:script_pathname) { cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool") } - - it "can zap" do - Hbc::FakeSystemCommand.expects_command(%w[/bin/chmod -- +x] + [script_pathname]) - - Hbc::FakeSystemCommand.expects_command( - sudo(cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool"), "--please"), - ) - - subject - end - end - - context "when using login_item" do - let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-login-item.rb") } - - it "can zap" do - Hbc::FakeSystemCommand.expects_command( - ["/usr/bin/osascript", "-e", 'tell application "System Events" to delete every login ' \ - 'item whose name is "Fancy"'], - ) - - subject - end - end + include_examples "#uninstall_phase or #zap_phase" end end