| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | require "open3" | 
					
						
							| 
									
										
										
										
											2017-05-07 17:28:39 +01:00
										 |  |  | require "vendor/plist/plist" | 
					
						
							| 
									
										
										
										
											2017-11-22 23:12:13 +01:00
										 |  |  | require "shellwords" | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-10-04 15:24:58 +02:00
										 |  |  | require "extend/io" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | require "hbc/utils/hash_validator" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  | module Hbc | 
					
						
							|  |  |  |   class SystemCommand | 
					
						
							| 
									
										
										
										
											2017-10-12 19:57:23 +02:00
										 |  |  |     extend Predicable | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-10-11 15:32:39 +02:00
										 |  |  |     def self.run(executable, **options) | 
					
						
							|  |  |  |       new(executable, **options).run! | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-10-11 15:32:39 +02:00
										 |  |  |     def self.run!(command, **options) | 
					
						
							| 
									
										
										
										
											2017-10-12 19:57:23 +02:00
										 |  |  |       run(command, **options, must_succeed: true) | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     def run! | 
					
						
							|  |  |  |       @processed_output = { stdout: "", stderr: "" } | 
					
						
							| 
									
										
										
										
											2017-06-27 11:55:23 +02:00
										 |  |  |       odebug "Executing: #{expanded_command}" | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |       each_output_line do |type, line| | 
					
						
							|  |  |  |         case type | 
					
						
							|  |  |  |         when :stdout | 
					
						
							| 
									
										
										
										
											2018-06-14 22:12:19 +02:00
										 |  |  |           puts line.chomp if print_stdout? | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |           processed_output[:stdout] << line | 
					
						
							|  |  |  |         when :stderr | 
					
						
							| 
									
										
										
										
											2018-06-14 22:12:19 +02:00
										 |  |  |           $stderr.puts Formatter.error(line.chomp) if print_stderr? | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |           processed_output[:stderr] << line | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-10-12 19:57:23 +02:00
										 |  |  |       assert_success if must_succeed? | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       result | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-13 18:49:01 +01:00
										 |  |  |     def initialize(executable, args: [], sudo: false, input: [], print_stdout: false, print_stderr: true, must_succeed: false, path: ENV["PATH"], **options) | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       @executable = executable | 
					
						
							| 
									
										
										
										
											2017-10-12 19:57:23 +02:00
										 |  |  |       @args = args | 
					
						
							|  |  |  |       @sudo = sudo | 
					
						
							|  |  |  |       @input = input | 
					
						
							|  |  |  |       @print_stdout = print_stdout | 
					
						
							|  |  |  |       @print_stderr = print_stderr | 
					
						
							|  |  |  |       @must_succeed = must_succeed | 
					
						
							|  |  |  |       options.extend(HashValidator).assert_valid_keys(:chdir) | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       @options = options | 
					
						
							| 
									
										
										
										
											2018-02-13 18:49:01 +01:00
										 |  |  |       @path = path | 
					
						
							| 
									
										
										
										
											2017-10-12 19:57:23 +02:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def command | 
					
						
							| 
									
										
										
										
											2017-12-01 22:07:21 +01:00
										 |  |  |       [*sudo_prefix, executable, *args] | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     private | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-13 18:49:01 +01:00
										 |  |  |     attr_reader :executable, :args, :input, :options, :processed_output, :processed_status, :path | 
					
						
							| 
									
										
										
										
											2017-10-12 19:57:23 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     attr_predicate :sudo?, :print_stdout?, :print_stderr?, :must_succeed? | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def sudo_prefix | 
					
						
							|  |  |  |       return [] unless sudo? | 
					
						
							|  |  |  |       askpass_flags = ENV.key?("SUDO_ASKPASS") ? ["-A"] : [] | 
					
						
							|  |  |  |       ["/usr/bin/sudo", *askpass_flags, "-E", "--"] | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     def assert_success | 
					
						
							| 
									
										
										
										
											2017-09-24 19:24:46 +01:00
										 |  |  |       return if processed_status&.success? | 
					
						
							| 
									
										
										
										
											2017-06-27 11:55:23 +02:00
										 |  |  |       raise CaskCommandFailedError.new(command, processed_output[:stdout], processed_output[:stderr], processed_status) | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     def expanded_command | 
					
						
							| 
									
										
										
										
											2016-10-23 14:44:14 +02:00
										 |  |  |       @expanded_command ||= command.map do |arg| | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |         if arg.respond_to?(:to_path) | 
					
						
							|  |  |  |           File.absolute_path(arg) | 
					
						
							|  |  |  |         else | 
					
						
							|  |  |  |           String(arg) | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2016-10-23 14:44:14 +02:00
										 |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     def each_output_line(&b) | 
					
						
							| 
									
										
										
										
											2017-12-01 22:07:21 +01:00
										 |  |  |       executable, *args = expanded_command | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       raw_stdin, raw_stdout, raw_stderr, raw_wait_thr = | 
					
						
							| 
									
										
										
										
											2018-03-01 17:19:00 +10:00
										 |  |  |         Open3.popen3({ "PATH" => path }, [executable, executable], *args, **options) | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-03-08 03:03:49 +01:00
										 |  |  |       write_input_to(raw_stdin) | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       raw_stdin.close_write | 
					
						
							|  |  |  |       each_line_from [raw_stdout, raw_stderr], &b | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       @processed_status = raw_wait_thr.value | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     def write_input_to(raw_stdin) | 
					
						
							| 
									
										
										
										
											2017-10-12 19:57:23 +02:00
										 |  |  |       [*input].each(&raw_stdin.method(:print)) | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     def each_line_from(sources) | 
					
						
							|  |  |  |       loop do | 
					
						
							| 
									
										
										
										
											2018-06-14 22:12:19 +02:00
										 |  |  |         readable_sources, = IO.select(sources) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         readable_sources = readable_sources.reject(&:eof?) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         break if readable_sources.empty? | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         readable_sources.each do |source| | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |           begin | 
					
						
							| 
									
										
										
										
											2018-06-14 22:12:19 +02:00
										 |  |  |             line = source.readline_nonblock || "" | 
					
						
							|  |  |  |             type = (source == sources[0]) ? :stdout : :stderr | 
					
						
							|  |  |  |             yield(type, line) | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |           rescue IO::WaitReadable, EOFError | 
					
						
							|  |  |  |             next | 
					
						
							|  |  |  |           end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  |         end | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2018-06-14 22:12:19 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       sources.each(&:close_read) | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     def result | 
					
						
							|  |  |  |       Result.new(command, | 
					
						
							|  |  |  |                  processed_output[:stdout], | 
					
						
							|  |  |  |                  processed_output[:stderr], | 
					
						
							|  |  |  |                  processed_status.exitstatus) | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     class Result | 
					
						
							|  |  |  |       attr_accessor :command, :stdout, :stderr, :exit_status | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       def initialize(command, stdout, stderr, exit_status) | 
					
						
							|  |  |  |         @command     = command | 
					
						
							|  |  |  |         @stdout      = stdout | 
					
						
							|  |  |  |         @stderr      = stderr | 
					
						
							|  |  |  |         @exit_status = exit_status | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       def plist | 
					
						
							|  |  |  |         @plist ||= self.class._parse_plist(@command, @stdout.dup) | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       def success? | 
					
						
							|  |  |  |         @exit_status.zero? | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       def merged_output | 
					
						
							|  |  |  |         @merged_output ||= @stdout + @stderr | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       def to_s | 
					
						
							|  |  |  |         @stdout | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       def self._warn_plist_garbage(command, garbage) | 
					
						
							| 
									
										
										
										
											2016-10-14 20:03:34 +02:00
										 |  |  |         return true unless garbage =~ /\S/ | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |         external = File.basename(command.first) | 
					
						
							|  |  |  |         lines = garbage.strip.split("\n") | 
					
						
							|  |  |  |         opoo "Non-XML stdout from #{external}:" | 
					
						
							|  |  |  |         $stderr.puts lines.map { |l| "    #{l}" } | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       def self._parse_plist(command, output) | 
					
						
							| 
									
										
										
										
											2016-10-14 20:03:34 +02:00
										 |  |  |         raise CaskError, "Empty plist input" unless output =~ /\S/ | 
					
						
							|  |  |  |         output.sub!(/\A(.*?)(<\?\s*xml)/m, '\2') | 
					
						
							| 
									
										
										
										
											2017-05-19 19:59:26 +02:00
										 |  |  |         _warn_plist_garbage(command, Regexp.last_match[1]) if ARGV.debug? | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |         output.sub!(%r{(<\s*/\s*plist\s*>)(.*?)\Z}m, '\1') | 
					
						
							|  |  |  |         _warn_plist_garbage(command, Regexp.last_match[2]) | 
					
						
							|  |  |  |         xml = Plist.parse_xml(output) | 
					
						
							|  |  |  |         unless xml.respond_to?(:keys) && !xml.keys.empty? | 
					
						
							| 
									
										
										
										
											2017-10-15 02:28:32 +02:00
										 |  |  |           raise CaskError, <<~EOS | 
					
						
							|  |  |  |             Empty result parsing plist output from command. | 
					
						
							|  |  |  |               command was: | 
					
						
							|  |  |  |               #{command} | 
					
						
							|  |  |  |               output we attempted to parse: | 
					
						
							|  |  |  |               #{output} | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |           EOS | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |         xml | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | end |