| 
									
										
										
										
											2019-04-19 15:38:03 +09:00
										 |  |  | # frozen_string_literal: true | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  | require "open3" | 
					
						
							| 
									
										
										
										
											2018-07-24 00:09:11 +02:00
										 |  |  | require "ostruct" | 
					
						
							| 
									
										
										
										
											2018-09-13 15:24:18 +01:00
										 |  |  | require "plist" | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  | require "shellwords" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | require "extend/io" | 
					
						
							|  |  |  | require "extend/hash_validator" | 
					
						
							|  |  |  | using HashValidator | 
					
						
							| 
									
										
										
										
											2018-07-23 23:04:49 +02:00
										 |  |  | require "extend/predicable" | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-10-01 12:29:21 +02:00
										 |  |  | module Kernel | 
					
						
							|  |  |  |   def system_command(*args) | 
					
						
							|  |  |  |     SystemCommand.run(*args) | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2018-07-22 23:13:32 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-10-01 12:29:21 +02:00
										 |  |  |   def system_command!(*args) | 
					
						
							|  |  |  |     SystemCommand.run!(*args) | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2018-07-22 23:13:32 +02:00
										 |  |  | end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  | class SystemCommand | 
					
						
							|  |  |  |   extend Predicable | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-20 10:57:27 +01:00
										 |  |  |   attr_reader :pid | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |   def self.run(executable, **options) | 
					
						
							|  |  |  |     new(executable, **options).run! | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def self.run!(command, **options) | 
					
						
							|  |  |  |     run(command, **options, must_succeed: true) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def run! | 
					
						
							| 
									
										
										
										
											2020-05-23 14:03:43 +01:00
										 |  |  |     puts redact_secrets(command.shelljoin.gsub('\=', "="), @secrets) if verbose? || Homebrew.args.debug? | 
					
						
							| 
									
										
										
										
											2018-07-24 18:25:59 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-29 19:56:32 +02:00
										 |  |  |     @output = [] | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     each_output_line do |type, line| | 
					
						
							|  |  |  |       case type | 
					
						
							|  |  |  |       when :stdout | 
					
						
							| 
									
										
										
										
											2018-07-30 10:11:00 +02:00
										 |  |  |         $stdout << line if print_stdout? | 
					
						
							| 
									
										
										
										
											2018-08-29 19:56:32 +02:00
										 |  |  |         @output << [:stdout, line] | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |       when :stderr | 
					
						
							| 
									
										
										
										
											2018-07-30 10:11:00 +02:00
										 |  |  |         $stderr << line if print_stderr? | 
					
						
							| 
									
										
										
										
											2018-08-29 19:56:32 +02:00
										 |  |  |         @output << [:stderr, line] | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-04 09:17:26 +02:00
										 |  |  |     result = Result.new(command, @output, @status, secrets: @secrets) | 
					
						
							|  |  |  |     result.assert_success! if must_succeed? | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |     result | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-24 18:43:20 +02:00
										 |  |  |   def initialize(executable, args: [], sudo: false, env: {}, input: [], must_succeed: false, | 
					
						
							| 
									
										
										
										
											2019-06-28 14:50:38 +08:00
										 |  |  |                  print_stdout: false, print_stderr: true, verbose: false, secrets: [], **options) | 
					
						
							| 
									
										
										
										
											2018-07-24 18:43:20 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-07-13 23:22:18 +08:00
										 |  |  |     require "extend/ENV" | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |     @executable = executable | 
					
						
							|  |  |  |     @args = args | 
					
						
							|  |  |  |     @sudo = sudo | 
					
						
							|  |  |  |     @input = [*input] | 
					
						
							|  |  |  |     @print_stdout = print_stdout | 
					
						
							|  |  |  |     @print_stderr = print_stderr | 
					
						
							| 
									
										
										
										
											2018-07-24 18:25:59 +02:00
										 |  |  |     @verbose = verbose | 
					
						
							| 
									
										
										
										
											2019-07-13 23:22:18 +08:00
										 |  |  |     @secrets = (Array(secrets) + ENV.sensitive_environment.values).uniq | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |     @must_succeed = must_succeed | 
					
						
							|  |  |  |     options.assert_valid_keys!(:chdir) | 
					
						
							|  |  |  |     @options = options | 
					
						
							|  |  |  |     @env = env | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @env.keys.grep_v(/^[\w&&\D]\w*$/) do |name| | 
					
						
							|  |  |  |       raise ArgumentError, "Invalid variable name: '#{name}'" | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def command | 
					
						
							|  |  |  |     [*sudo_prefix, *env_args, executable.to_s, *expanded_args] | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-24 00:09:11 +02:00
										 |  |  |   attr_reader :executable, :args, :input, :options, :env | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-24 18:25:59 +02:00
										 |  |  |   attr_predicate :sudo?, :print_stdout?, :print_stderr?, :verbose?, :must_succeed? | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |   def env_args | 
					
						
							| 
									
										
										
										
											2018-07-30 10:11:00 +02:00
										 |  |  |     set_variables = env.reject { |_, value| value.nil? } | 
					
						
							|  |  |  |                        .map do |name, value| | 
					
						
							|  |  |  |                          sanitized_name = Shellwords.escape(name) | 
					
						
							|  |  |  |                          sanitized_value = Shellwords.escape(value) | 
					
						
							|  |  |  |                          "#{sanitized_name}=#{sanitized_value}" | 
					
						
							|  |  |  |                        end | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-30 10:11:00 +02:00
										 |  |  |     return [] if set_variables.empty? | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-08 17:39:31 +01:00
										 |  |  |     ["/usr/bin/env", *set_variables] | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def sudo_prefix | 
					
						
							|  |  |  |     return [] unless sudo? | 
					
						
							| 
									
										
										
										
											2018-09-17 02:45:00 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |     askpass_flags = ENV.key?("SUDO_ASKPASS") ? ["-A"] : [] | 
					
						
							|  |  |  |     ["/usr/bin/sudo", *askpass_flags, "-E", "--"] | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def expanded_args | 
					
						
							|  |  |  |     @expanded_args ||= args.map do |arg| | 
					
						
							|  |  |  |       if arg.respond_to?(:to_path) | 
					
						
							|  |  |  |         File.absolute_path(arg) | 
					
						
							| 
									
										
										
										
											2018-07-30 10:11:00 +02:00
										 |  |  |       elsif arg.is_a?(Integer) || arg.is_a?(Float) || arg.is_a?(URI) | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |         arg.to_s | 
					
						
							|  |  |  |       else | 
					
						
							|  |  |  |         arg.to_str | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def each_output_line(&b) | 
					
						
							|  |  |  |     executable, *args = command | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     raw_stdin, raw_stdout, raw_stderr, raw_wait_thr = | 
					
						
							| 
									
										
										
										
											2018-08-29 19:23:30 +02:00
										 |  |  |       Open3.popen3(env, [executable, executable], *args, **options) | 
					
						
							| 
									
										
										
										
											2018-09-20 10:57:27 +01:00
										 |  |  |     @pid = raw_wait_thr.pid | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     write_input_to(raw_stdin) | 
					
						
							|  |  |  |     raw_stdin.close_write | 
					
						
							|  |  |  |     each_line_from [raw_stdout, raw_stderr], &b | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-24 00:09:11 +02:00
										 |  |  |     @status = raw_wait_thr.value | 
					
						
							|  |  |  |   rescue SystemCallError => e | 
					
						
							|  |  |  |     @status = $CHILD_STATUS | 
					
						
							| 
									
										
										
										
											2018-08-29 19:56:32 +02:00
										 |  |  |     @output << [:stderr, e.message] | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def write_input_to(raw_stdin) | 
					
						
							|  |  |  |     input.each(&raw_stdin.method(:write)) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def each_line_from(sources) | 
					
						
							|  |  |  |     loop do | 
					
						
							|  |  |  |       readable_sources, = IO.select(sources) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       readable_sources = readable_sources.reject(&:eof?) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       break if readable_sources.empty? | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       readable_sources.each do |source| | 
					
						
							| 
									
										
										
										
											2019-10-13 10:03:26 +01:00
										 |  |  |         line = source.readline_nonblock || "" | 
					
						
							|  |  |  |         type = (source == sources[0]) ? :stdout : :stderr | 
					
						
							|  |  |  |         yield(type, line) | 
					
						
							|  |  |  |       rescue IO::WaitReadable, EOFError | 
					
						
							|  |  |  |         next | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sources.each(&:close_read) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   class Result | 
					
						
							| 
									
										
										
										
											2018-08-29 19:56:32 +02:00
										 |  |  |     attr_accessor :command, :status, :exit_status | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-04 09:17:26 +02:00
										 |  |  |     def initialize(command, output, status, secrets:) | 
					
						
							| 
									
										
										
										
											2018-08-29 19:56:32 +02:00
										 |  |  |       @command       = command | 
					
						
							|  |  |  |       @output        = output | 
					
						
							|  |  |  |       @status        = status | 
					
						
							|  |  |  |       @exit_status   = status.exitstatus | 
					
						
							| 
									
										
										
										
											2019-10-04 09:17:26 +02:00
										 |  |  |       @secrets       = secrets | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def assert_success! | 
					
						
							|  |  |  |       return if @status.success? | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       raise ErrorDuringExecution.new(command, status: @status, output: @output, secrets: @secrets) | 
					
						
							| 
									
										
										
										
											2018-08-29 19:56:32 +02:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def stdout | 
					
						
							|  |  |  |       @stdout ||= @output.select { |type,| type == :stdout } | 
					
						
							|  |  |  |                          .map { |_, line| line } | 
					
						
							|  |  |  |                          .join | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def stderr | 
					
						
							|  |  |  |       @stderr ||= @output.select { |type,| type == :stderr } | 
					
						
							|  |  |  |                          .map { |_, line| line } | 
					
						
							|  |  |  |                          .join | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-19 03:09:07 +02:00
										 |  |  |     def merged_output | 
					
						
							|  |  |  |       @merged_output ||= @output.map { |_, line| line } | 
					
						
							|  |  |  |                                 .join | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |     def success? | 
					
						
							| 
									
										
										
										
											2018-09-20 10:57:27 +01:00
										 |  |  |       return false if @exit_status.nil? | 
					
						
							| 
									
										
										
										
											2019-02-19 13:12:52 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |       @exit_status.zero? | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-30 10:11:00 +02:00
										 |  |  |     def to_ary | 
					
						
							|  |  |  |       [stdout, stderr, status] | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |     def plist | 
					
						
							|  |  |  |       @plist ||= begin | 
					
						
							|  |  |  |         output = stdout | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if /\A(?<garbage>.*?)<\?\s*xml/m =~ output | 
					
						
							|  |  |  |           output = output.sub(/\A#{Regexp.escape(garbage)}/m, "") | 
					
						
							|  |  |  |           warn_plist_garbage(garbage) | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if %r{<\s*/\s*plist\s*>(?<garbage>.*?)\Z}m =~ output | 
					
						
							|  |  |  |           output = output.sub(/#{Regexp.escape(garbage)}\Z/, "") | 
					
						
							|  |  |  |           warn_plist_garbage(garbage) | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Plist.parse_xml(output) | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def warn_plist_garbage(garbage) | 
					
						
							| 
									
										
										
										
											2020-02-01 13:32:39 +01:00
										 |  |  |       return unless Homebrew.args.verbose? | 
					
						
							| 
									
										
										
										
											2019-10-13 19:26:39 +01:00
										 |  |  |       return unless garbage.match?(/\S/) | 
					
						
							| 
									
										
										
										
											2018-09-17 02:45:00 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |       opoo "Received non-XML output from #{Formatter.identifier(command.first)}:" | 
					
						
							|  |  |  |       $stderr.puts garbage.strip | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |     private :warn_plist_garbage | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | end |