| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | require "pathname" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | require "hbc/artifact/base" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  | 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, | 
					
						
							|  |  |  |                              :quit, | 
					
						
							|  |  |  |                              :signal, | 
					
						
							|  |  |  |                              :login_item, | 
					
						
							|  |  |  |                              :kext, | 
					
						
							|  |  |  |                              :script, | 
					
						
							|  |  |  |                              :pkgutil, | 
					
						
							|  |  |  |                              :delete, | 
					
						
							|  |  |  |                              :trash, | 
					
						
							|  |  |  |                              :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 { |path_string| | 
					
						
							|  |  |  |           path_string.start_with?("~") ? Pathname.new(path_string).expand_path : Pathname.new(path_string) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       def self.remove_relative_path_strings(action, path_strings) | 
					
						
							|  |  |  |         relative = path_strings.map { |path_string| | 
					
						
							|  |  |  |           path_string if %r{/\.\.(?:/|\Z)}.match(path_string) || !%r{\A/}.match(path_string) | 
					
						
							|  |  |  |         }.compact | 
					
						
							|  |  |  |         relative.each do |path_string| | 
					
						
							|  |  |  |           opoo "Skipping #{action} for relative path #{path_string}" | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |         path_strings - relative | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       def self.remove_undeletable_path_strings(action, path_strings) | 
					
						
							|  |  |  |         undeletable = path_strings.map { |path_string| | 
					
						
							|  |  |  |           path_string if MacOS.undeletable?(Pathname.new(path_string)) | 
					
						
							|  |  |  |         }.compact | 
					
						
							|  |  |  |         undeletable.each do |path_string| | 
					
						
							|  |  |  |           opoo "Skipping #{action} for undeletable path #{path_string}" | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |         path_strings - undeletable | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       def install_phase | 
					
						
							|  |  |  |         odebug "Nothing to do. The uninstall artifact has no install phase." | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       def uninstall_phase | 
					
						
							|  |  |  |         dispatch_uninstall_directives | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       def dispatch_uninstall_directives(expand_tilde = true) | 
					
						
							|  |  |  |         directives_set = @cask.artifacts[stanza] | 
					
						
							|  |  |  |         ohai "Running #{stanza} process for #{@cask}; your password may be necessary" | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |         directives_set.each do |directives| | 
					
						
							|  |  |  |           warn_for_unknown_directives(directives) | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |         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) | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       private | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       def stanza | 
					
						
							|  |  |  |         self.class.artifact_dsl_key | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       def warn_for_unknown_directives(directives) | 
					
						
							|  |  |  |         unknown_keys = directives.keys - ORDERED_DIRECTIVES | 
					
						
							|  |  |  |         return if unknown_keys.empty? | 
					
						
							|  |  |  |         opoo %Q{Unknown arguments to #{stanza} -- #{unknown_keys.inspect}. Running "brew update; brew cleanup; brew cask cleanup" will likely fix it.} | 
					
						
							|  |  |  |       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. | 
					
						
							|  |  |  |       # If Cask writers never need :early_script it may be removed in the future. | 
					
						
							|  |  |  |       def uninstall_early_script(directives) | 
					
						
							|  |  |  |         uninstall_script(directives, directive_name: :early_script) | 
					
						
							|  |  |  |       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 | 
					
						
							|  |  |  |       def uninstall_launchctl(directives) | 
					
						
							|  |  |  |         Array(directives[:launchctl]).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 | 
					
						
							|  |  |  |             if plist_status =~ %r{^\{} | 
					
						
							|  |  |  |               @command.run!("/bin/launchctl", args: ["remove", service], sudo: with_sudo) | 
					
						
							|  |  |  |               sleep 1
 | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  |             paths = ["/Library/LaunchAgents/#{service}.plist", | 
					
						
							|  |  |  |                      "/Library/LaunchDaemons/#{service}.plist"] | 
					
						
							|  |  |  |             paths.each { |elt| elt.prepend(ENV["HOME"]) } unless with_sudo | 
					
						
							|  |  |  |             paths = paths.map { |elt| Pathname(elt) }.select(&:exist?) | 
					
						
							|  |  |  |             paths.each do |path| | 
					
						
							|  |  |  |               @command.run!("/bin/rm", args: ["-f", "--", path], sudo: with_sudo) | 
					
						
							|  |  |  |             end | 
					
						
							|  |  |  |             # undocumented and untested: pass a path to uninstall :launchctl | 
					
						
							|  |  |  |             next unless Pathname(service).exist? | 
					
						
							|  |  |  |             @command.run!("/bin/launchctl", args: ["unload", "-w", "--", service], sudo: with_sudo) | 
					
						
							|  |  |  |             @command.run!("/bin/rm",        args: ["-f", "--", service], sudo: with_sudo) | 
					
						
							|  |  |  |             sleep 1
 | 
					
						
							|  |  |  |           end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  |         end | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       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}" | 
					
						
							|  |  |  |           num_running = count_running_processes(id) | 
					
						
							|  |  |  |           next unless num_running > 0
 | 
					
						
							|  |  |  |           @command.run!("/usr/bin/osascript", args: ["-e", %Q{tell application id "#{id}" to quit}], sudo: true) | 
					
						
							|  |  |  |           sleep 3
 | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       # :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, id = pair | 
					
						
							|  |  |  |           ohai "Signalling '#{signal}' to application ID '#{id}'" | 
					
						
							|  |  |  |           pids = get_unix_pids(id) | 
					
						
							|  |  |  |           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 #{id}" | 
					
						
							|  |  |  |           Process.kill(signal, *pids) | 
					
						
							|  |  |  |           sleep 3
 | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       def count_running_processes(bundle_id) | 
					
						
							|  |  |  |         @command.run!("/usr/bin/osascript", | 
					
						
							|  |  |  |                       args: ["-e", %Q{tell application "System Events" to count processes whose bundle identifier is "#{bundle_id}"}], | 
					
						
							|  |  |  |                       sudo: true).stdout.to_i | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       def get_unix_pids(bundle_id) | 
					
						
							|  |  |  |         pid_string = @command.run!("/usr/bin/osascript", | 
					
						
							|  |  |  |                                    args: ["-e", %Q{tell application "System Events" to get the unix id of every process whose bundle identifier is "#{bundle_id}"}], | 
					
						
							|  |  |  |                                    sudo: true).stdout.chomp | 
					
						
							|  |  |  |         return [] unless pid_string =~ %r{\A\d+(?:\s*,\s*\d+)*\Z} # sanity check | 
					
						
							|  |  |  |         pid_string.split(%r{\s*,\s*}).map(&:strip).map(&:to_i) | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       def uninstall_login_item(directives) | 
					
						
							|  |  |  |         Array(directives[:login_item]).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}"}], | 
					
						
							|  |  |  |                         sudo: false) | 
					
						
							|  |  |  |           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 | 
					
						
							|  |  |  |       def uninstall_kext(directives) | 
					
						
							|  |  |  |         Array(directives[:kext]).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
 | 
					
						
							|  |  |  |             @command.run!("/sbin/kextunload", args: ["-b", kext], sudo: true) | 
					
						
							|  |  |  |             sleep 1
 | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  |         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 | 
					
						
							|  |  |  |       def uninstall_script(directives, directive_name: :script) | 
					
						
							|  |  |  |         executable, script_arguments = self.class.read_script_arguments(directives, | 
					
						
							|  |  |  |                                                                         "uninstall", | 
					
						
							|  |  |  |                                                                         { 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) | 
					
						
							|  |  |  |         @command.run("/bin/chmod", args: ["--", "+x", executable_path]) if File.exist?(executable_path) | 
					
						
							|  |  |  |         @command.run(executable_path, script_arguments) | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  |         sleep 1
 | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       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) | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       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.expand_path_strings(path_slice) if expand_tilde | 
					
						
							|  |  |  |           path_slice = self.class.remove_relative_path_strings(:delete, path_slice) | 
					
						
							|  |  |  |           path_slice = self.class.remove_undeletable_path_strings(:delete, path_slice) | 
					
						
							|  |  |  |           @command.run!("/bin/rm", args: path_slice.unshift("-rf", "--"), sudo: true) | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       # :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) | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       def uninstall_rmdir(directives, expand_tilde = true) | 
					
						
							|  |  |  |         Array(directives[:rmdir]).flatten.each do |directory| | 
					
						
							|  |  |  |           directory = self.class.expand_path_strings([directory]).first if expand_tilde | 
					
						
							|  |  |  |           directory = self.class.remove_relative_path_strings(:rmdir, [directory]).first | 
					
						
							|  |  |  |           directory = self.class.remove_undeletable_path_strings(:rmdir, [directory]).first | 
					
						
							|  |  |  |           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) | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | end |