| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | require "open3" | 
					
						
							|  |  |  | require "shellwords" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							|  |  |  |     attr_reader :command | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     def self.run(executable, options = {}) | 
					
						
							|  |  |  |       new(executable, options).run! | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     def self.run!(command, options = {}) | 
					
						
							|  |  |  |       run(command, options.merge(must_succeed: true)) | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     def run! | 
					
						
							|  |  |  |       @processed_output = { stdout: "", stderr: "" } | 
					
						
							|  |  |  |       odebug "Executing: #{expanded_command.utf8_inspect}" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       each_output_line do |type, line| | 
					
						
							|  |  |  |         case type | 
					
						
							|  |  |  |         when :stdout | 
					
						
							|  |  |  |           processed_output[:stdout] << line | 
					
						
							|  |  |  |           ohai line.chomp if options[:print_stdout] | 
					
						
							|  |  |  |         when :stderr | 
					
						
							|  |  |  |           processed_output[:stderr] << line | 
					
						
							|  |  |  |           ohai line.chomp if options[:print_stderr] | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       assert_success if options[:must_succeed] | 
					
						
							|  |  |  |       result | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     def initialize(executable, options) | 
					
						
							|  |  |  |       @executable = executable | 
					
						
							|  |  |  |       @options = options | 
					
						
							|  |  |  |       process_options! | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     private | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     attr_reader :executable, :options, :processed_output, :processed_status | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def process_options! | 
					
						
							| 
									
										
										
										
											2016-10-04 15:24:58 +02:00
										 |  |  |       options.extend(HashValidator) | 
					
						
							|  |  |  |              .assert_valid_keys :input, :print_stdout, :print_stderr, :args, :must_succeed, :sudo, :bsexec | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       sudo_prefix = %w[/usr/bin/sudo -E --] | 
					
						
							| 
									
										
										
										
											2016-11-11 00:30:05 +01:00
										 |  |  |       sudo_prefix = sudo_prefix.insert(1, "-A") unless ENV["SUDO_ASKPASS"].nil? | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       bsexec_prefix = ["/bin/launchctl", "bsexec", options[:bsexec] == :startup ? "/" : options[:bsexec]] | 
					
						
							|  |  |  |       @command = [executable] | 
					
						
							|  |  |  |       options[:print_stderr] = true    unless options.key?(:print_stderr) | 
					
						
							|  |  |  |       @command.unshift(*bsexec_prefix) if  options[:bsexec] | 
					
						
							|  |  |  |       @command.unshift(*sudo_prefix)   if  options[:sudo] | 
					
						
							|  |  |  |       @command.concat(options[:args])  if  options.key?(:args) && !options[:args].empty? | 
					
						
							|  |  |  |       @command[0] = Shellwords.shellescape(@command[0]) if @command.size == 1
 | 
					
						
							|  |  |  |       nil | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     def assert_success | 
					
						
							|  |  |  |       return if processed_status && processed_status.success? | 
					
						
							|  |  |  |       raise CaskCommandFailedError.new(command.utf8_inspect, processed_output[:stdout], processed_output[:stderr], processed_status) | 
					
						
							|  |  |  |     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) | 
					
						
							|  |  |  |       raw_stdin, raw_stdout, raw_stderr, raw_wait_thr = | 
					
						
							|  |  |  |         Open3.popen3(*expanded_command) | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |       write_input_to(raw_stdin) if options[:input] | 
					
						
							|  |  |  |       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) | 
					
						
							|  |  |  |       Array(options[:input]).each { |line| raw_stdin.puts line } | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |     def each_line_from(sources) | 
					
						
							|  |  |  |       loop do | 
					
						
							|  |  |  |         readable_sources = IO.select(sources)[0] | 
					
						
							|  |  |  |         readable_sources.delete_if(&:eof?).first(1).each do |source| | 
					
						
							|  |  |  |           type = (source == sources[0] ? :stdout : :stderr) | 
					
						
							|  |  |  |           begin | 
					
						
							|  |  |  |             yield(type, source.readline_nonblock || "") | 
					
						
							|  |  |  |           rescue IO::WaitReadable, EOFError | 
					
						
							|  |  |  |             next | 
					
						
							|  |  |  |           end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  |         end | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |         break if readable_sources.empty? | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  |       end | 
					
						
							| 
									
										
										
										
											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
										 |  |  |   end | 
					
						
							|  |  |  | end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  | module Hbc | 
					
						
							|  |  |  |   class SystemCommand | 
					
						
							|  |  |  |     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') | 
					
						
							| 
									
										
										
										
											2016-09-24 13:52:43 +02:00
										 |  |  |         _warn_plist_garbage(command, Regexp.last_match[1]) if Hbc.debug | 
					
						
							|  |  |  |         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? | 
					
						
							|  |  |  |           raise CaskError, <<-EOS
 | 
					
						
							|  |  |  |     Empty result parsing plist output from command. | 
					
						
							|  |  |  |       command was: | 
					
						
							|  |  |  |       #{command.utf8_inspect} | 
					
						
							|  |  |  |       output we attempted to parse: | 
					
						
							|  |  |  |       #{output} | 
					
						
							|  |  |  |           EOS | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |         xml | 
					
						
							|  |  |  |       rescue Plist::ParseError => e | 
					
						
							|  |  |  |         raise CaskError, <<-EOS
 | 
					
						
							|  |  |  |     Error parsing plist output from command. | 
					
						
							|  |  |  |       command was: | 
					
						
							|  |  |  |       #{command.utf8_inspect} | 
					
						
							|  |  |  |       error was: | 
					
						
							|  |  |  |       #{e} | 
					
						
							|  |  |  |       output we attempted to parse: | 
					
						
							|  |  |  |       #{output} | 
					
						
							|  |  |  |         EOS | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2016-08-18 22:11:42 +03:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | end |