Refactor Cask uninstall and zap stanza.

This commit is contained in:
Markus Reiter 2017-03-08 03:03:36 +01:00
parent 9105acab6b
commit 2691eb6f65
6 changed files with 447 additions and 821 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>install-location</key>
<string>tmp</string>
<key>volume</key>
<string>/</string>
</dict>
</plist>
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

View File

@ -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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>install-location</key>
<string>tmp</string>
<key>volume</key>
<string>/</string>
</dict>
</plist>
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

View File

@ -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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>install-location</key>
<string>tmp</string>
<key>volume</key>
<string>/</string>
</dict>
</plist>
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