| 
									
										
										
										
											2020-11-25 17:03:23 +01:00
										 |  |  | # typed: true | 
					
						
							| 
									
										
										
										
											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" | 
					
						
							| 
									
										
										
										
											2020-10-10 17:53:31 +02:00
										 |  |  | require "extend/predicable" | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  | require "extend/hash_validator" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-24 10:55:33 +01:00
										 |  |  | require "extend/time" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-10 17:53:31 +02:00
										 |  |  | # Class for running sub-processes and capturing their output and exit status. | 
					
						
							| 
									
										
										
										
											2020-08-19 07:33:07 +02:00
										 |  |  | # | 
					
						
							|  |  |  | # @api private | 
					
						
							| 
									
										
										
										
											2020-10-10 17:53:31 +02:00
										 |  |  | class SystemCommand | 
					
						
							| 
									
										
										
										
											2020-10-20 12:03:48 +02:00
										 |  |  |   extend T::Sig | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-24 10:55:33 +01:00
										 |  |  |   using TimeRemaining | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-05 17:17:03 -05:00
										 |  |  |   # Helper functions for calling {SystemCommand.run}. | 
					
						
							| 
									
										
										
										
											2020-10-10 17:53:31 +02:00
										 |  |  |   module Mixin | 
					
						
							| 
									
										
										
										
											2020-12-17 15:45:50 +01:00
										 |  |  |     extend T::Sig | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-10 17:53:31 +02:00
										 |  |  |     def system_command(*args) | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |       T.unsafe(SystemCommand).run(*args) | 
					
						
							| 
									
										
										
										
											2020-10-10 17:53:31 +02:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2018-07-22 23:13:32 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-10 17:53:31 +02:00
										 |  |  |     def system_command!(*args) | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |       T.unsafe(SystemCommand).run!(*args) | 
					
						
							| 
									
										
										
										
											2020-10-10 17:53:31 +02:00
										 |  |  |     end | 
					
						
							| 
									
										
										
										
											2018-10-01 12:29:21 +02:00
										 |  |  |   end | 
					
						
							| 
									
										
										
										
											2018-07-22 23:13:32 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-02 14:32:31 +02:00
										 |  |  |   include Context | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |   extend Predicable | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def self.run(executable, **options) | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |     T.unsafe(self).new(executable, **options).run! | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def self.run!(command, **options) | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |     T.unsafe(self).run(command, **options, must_succeed: true) | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-20 12:03:48 +02:00
										 |  |  |   sig { returns(SystemCommand::Result) } | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |   def run! | 
					
						
							| 
									
										
										
										
											2020-12-17 15:45:50 +01:00
										 |  |  |     $stderr.puts redact_secrets(command.shelljoin.gsub('\=', "="), @secrets) if verbose? || 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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-17 22:45:55 -08:00
										 |  |  |   sig { | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |     params( | 
					
						
							|  |  |  |       executable:   T.any(String, Pathname), | 
					
						
							|  |  |  |       args:         T::Array[T.any(String, Integer, Float, URI::Generic)], | 
					
						
							|  |  |  |       sudo:         T::Boolean, | 
					
						
							|  |  |  |       env:          T::Hash[String, String], | 
					
						
							|  |  |  |       input:        T.any(String, T::Array[String]), | 
					
						
							|  |  |  |       must_succeed: T::Boolean, | 
					
						
							|  |  |  |       print_stdout: T::Boolean, | 
					
						
							|  |  |  |       print_stderr: T::Boolean, | 
					
						
							| 
									
										
										
										
											2020-12-15 21:15:43 -05:00
										 |  |  |       debug:        T.nilable(T::Boolean), | 
					
						
							|  |  |  |       verbose:      T.nilable(T::Boolean), | 
					
						
							| 
									
										
										
										
											2020-11-27 17:45:18 +11:00
										 |  |  |       secrets:      T.any(String, T::Array[String]), | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |       chdir:        T.any(String, Pathname), | 
					
						
							| 
									
										
										
										
											2021-03-24 10:55:33 +01:00
										 |  |  |       timeout:      T.nilable(T.any(Integer, Float)), | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |     ).void | 
					
						
							| 
									
										
										
										
											2021-01-17 22:45:55 -08:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2021-03-24 10:55:33 +01:00
										 |  |  |   def initialize( | 
					
						
							|  |  |  |     executable, | 
					
						
							|  |  |  |     args: [], | 
					
						
							|  |  |  |     sudo: false, | 
					
						
							|  |  |  |     env: {}, | 
					
						
							|  |  |  |     input: [], | 
					
						
							|  |  |  |     must_succeed: false, | 
					
						
							|  |  |  |     print_stdout: false, | 
					
						
							|  |  |  |     print_stderr: true, | 
					
						
							|  |  |  |     debug: nil, | 
					
						
							|  |  |  |     verbose: false, | 
					
						
							|  |  |  |     secrets: [], | 
					
						
							|  |  |  |     chdir: T.unsafe(nil), | 
					
						
							|  |  |  |     timeout: nil | 
					
						
							|  |  |  |   ) | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |     env.each_key do |name| | 
					
						
							|  |  |  |       next if /^[\w&&\D]\w*$/.match?(name) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-26 15:21:24 -05:00
										 |  |  |       raise ArgumentError, "Invalid variable name: #{name}" | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |     end | 
					
						
							|  |  |  |     @env = env | 
					
						
							| 
									
										
										
										
											2020-07-13 22:48:53 +10:00
										 |  |  |     @input = Array(input) | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |     @must_succeed = must_succeed | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |     @print_stdout = print_stdout | 
					
						
							|  |  |  |     @print_stderr = print_stderr | 
					
						
							| 
									
										
										
										
											2020-12-14 12:36:32 -05:00
										 |  |  |     @debug = debug | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |     @chdir = chdir | 
					
						
							| 
									
										
										
										
											2021-03-24 10:55:33 +01:00
										 |  |  |     @timeout = timeout | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |   sig { returns(T::Array[String]) } | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |   def command | 
					
						
							|  |  |  |     [*sudo_prefix, *env_args, executable.to_s, *expanded_args] | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |   attr_reader :executable, :args, :input, :chdir, :env | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-02 14:32:31 +02:00
										 |  |  |   attr_predicate :sudo?, :print_stdout?, :print_stderr?, :must_succeed? | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |   sig { returns(T::Boolean) } | 
					
						
							| 
									
										
										
										
											2020-12-14 12:36:32 -05:00
										 |  |  |   def debug? | 
					
						
							|  |  |  |     return super if @debug.nil? | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @debug | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   sig { returns(T::Boolean) } | 
					
						
							| 
									
										
										
										
											2020-08-02 14:32:31 +02:00
										 |  |  |   def verbose? | 
					
						
							|  |  |  |     return super if @verbose.nil? | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @verbose | 
					
						
							|  |  |  |   end | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |   sig { returns(T::Array[String]) } | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |   def env_args | 
					
						
							| 
									
										
										
										
											2020-11-09 20:15:28 +11:00
										 |  |  |     set_variables = env.compact.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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |   sig { returns(T::Array[String]) } | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |   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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |   sig { returns(T::Array[String]) } | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |   def expanded_args | 
					
						
							|  |  |  |     @expanded_args ||= args.map do |arg| | 
					
						
							|  |  |  |       if arg.respond_to?(:to_path) | 
					
						
							|  |  |  |         File.absolute_path(arg) | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |       elsif arg.is_a?(Integer) || arg.is_a?(Float) || arg.is_a?(URI::Generic) | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |         arg.to_s | 
					
						
							|  |  |  |       else | 
					
						
							|  |  |  |         arg.to_str | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-03 22:22:34 +01:00
										 |  |  |   class ProcessTerminatedInterrupt < StandardError; end | 
					
						
							|  |  |  |   private_constant :ProcessTerminatedInterrupt | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-17 15:45:50 +01:00
										 |  |  |   sig { params(block: T.proc.params(type: Symbol, line: String).void).void } | 
					
						
							|  |  |  |   def each_output_line(&block) | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |     executable, *args = command | 
					
						
							| 
									
										
										
										
											2020-12-17 15:45:50 +01:00
										 |  |  |     options = { | 
					
						
							|  |  |  |       # Create a new process group so that we can send `SIGINT` from | 
					
						
							|  |  |  |       # parent to child rather than the child receiving `SIGINT` directly. | 
					
						
							| 
									
										
										
										
											2020-12-19 19:30:33 +01:00
										 |  |  |       pgroup: sudo? ? nil : true, | 
					
						
							| 
									
										
										
										
											2020-12-17 15:45:50 +01:00
										 |  |  |     } | 
					
						
							|  |  |  |     options[:chdir] = chdir if chdir | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     pid = T.let(nil, T.nilable(Integer)) | 
					
						
							|  |  |  |     raw_stdin, raw_stdout, raw_stderr, raw_wait_thr = ignore_interrupts do | 
					
						
							|  |  |  |       T.unsafe(Open3).popen3(env, [executable, executable], *args, **options) | 
					
						
							|  |  |  |        .tap { |*, wait_thr| pid = wait_thr.pid } | 
					
						
							|  |  |  |     end | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     write_input_to(raw_stdin) | 
					
						
							|  |  |  |     raw_stdin.close_write | 
					
						
							| 
									
										
										
										
											2021-05-03 22:22:34 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-11 13:27:26 +01:00
										 |  |  |     thread_ready_queue = Queue.new | 
					
						
							|  |  |  |     thread_done_queue = Queue.new | 
					
						
							| 
									
										
										
										
											2021-05-03 22:22:34 +01:00
										 |  |  |     line_thread = Thread.new do | 
					
						
							|  |  |  |       Thread.handle_interrupt(ProcessTerminatedInterrupt => :never) do | 
					
						
							| 
									
										
										
										
											2021-05-11 13:27:26 +01:00
										 |  |  |         thread_ready_queue << true | 
					
						
							| 
									
										
										
										
											2021-05-03 22:22:34 +01:00
										 |  |  |         each_line_from [raw_stdout, raw_stderr], &block | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2021-05-11 13:27:26 +01:00
										 |  |  |       thread_done_queue.pop | 
					
						
							| 
									
										
										
										
											2021-05-03 22:22:34 +01:00
										 |  |  |     rescue ProcessTerminatedInterrupt | 
					
						
							|  |  |  |       nil | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     end_time = Time.now + @timeout if @timeout | 
					
						
							|  |  |  |     raise Timeout::Error if raw_wait_thr.join(end_time&.remaining).nil? | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-24 00:09:11 +02:00
										 |  |  |     @status = raw_wait_thr.value | 
					
						
							| 
									
										
										
										
											2021-05-03 22:22:34 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-11 13:27:26 +01:00
										 |  |  |     thread_ready_queue.pop | 
					
						
							| 
									
										
										
										
											2021-05-03 22:22:34 +01:00
										 |  |  |     line_thread.raise ProcessTerminatedInterrupt.new | 
					
						
							| 
									
										
										
										
											2021-05-11 13:27:26 +01:00
										 |  |  |     thread_done_queue << true | 
					
						
							| 
									
										
										
										
											2021-05-03 22:22:34 +01:00
										 |  |  |     line_thread.join | 
					
						
							| 
									
										
										
										
											2020-12-17 15:45:50 +01:00
										 |  |  |   rescue Interrupt | 
					
						
							| 
									
										
										
										
											2020-12-19 19:30:33 +01:00
										 |  |  |     Process.kill("INT", pid) if pid && !sudo? | 
					
						
							| 
									
										
										
										
											2020-12-17 15:45:50 +01:00
										 |  |  |     raise Interrupt | 
					
						
							| 
									
										
										
										
											2018-07-24 00:09:11 +02:00
										 |  |  |   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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-17 15:45:50 +01:00
										 |  |  |   sig { params(raw_stdin: IO).void } | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |   def write_input_to(raw_stdin) | 
					
						
							|  |  |  |     input.each(&raw_stdin.method(:write)) | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-17 15:45:50 +01:00
										 |  |  |   sig { params(sources: T::Array[IO], _block: T.proc.params(type: Symbol, line: String).void).void } | 
					
						
							|  |  |  |   def each_line_from(sources, &_block) | 
					
						
							| 
									
										
										
										
											2021-04-03 06:03:59 +02:00
										 |  |  |     sources = { | 
					
						
							|  |  |  |       sources[0] => :stdout, | 
					
						
							|  |  |  |       sources[1] => :stderr, | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-03-24 10:55:33 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-03 22:22:34 +01:00
										 |  |  |     pending_interrupt = T.let(false, T::Boolean) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     until pending_interrupt | 
					
						
							|  |  |  |       readable_sources = T.let([], T::Array[IO]) | 
					
						
							|  |  |  |       begin | 
					
						
							|  |  |  |         Thread.handle_interrupt(ProcessTerminatedInterrupt => :on_blocking) do | 
					
						
							|  |  |  |           readable_sources = T.must(IO.select(sources.keys)).fetch(0) | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  |       rescue ProcessTerminatedInterrupt | 
					
						
							|  |  |  |         readable_sources = sources.keys | 
					
						
							|  |  |  |         pending_interrupt = true | 
					
						
							|  |  |  |       end | 
					
						
							| 
									
										
										
										
											2021-04-03 05:17:08 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-03 06:03:59 +02:00
										 |  |  |       break if readable_sources.none? do |source| | 
					
						
							| 
									
										
										
										
											2021-05-03 22:22:34 +01:00
										 |  |  |         loop do | 
					
						
							|  |  |  |           line = source.readline_nonblock || "" | 
					
						
							|  |  |  |           yield(sources.fetch(source), line) | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2021-04-01 15:42:16 +01:00
										 |  |  |       rescue EOFError | 
					
						
							|  |  |  |         source.close_read | 
					
						
							| 
									
										
										
										
											2021-04-03 06:03:59 +02:00
										 |  |  |         sources.delete(source) | 
					
						
							|  |  |  |         sources.any? | 
					
						
							| 
									
										
										
										
											2021-04-01 15:42:16 +01:00
										 |  |  |       rescue IO::WaitReadable | 
					
						
							| 
									
										
										
										
											2021-04-03 06:03:59 +02:00
										 |  |  |         true | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-03 06:03:59 +02:00
										 |  |  |     sources.each_key(&:close_read) | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |   end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-19 07:33:07 +02:00
										 |  |  |   # Result containing the output and exit status of a finished sub-process. | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |   class Result | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |     extend T::Sig | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-02 14:32:31 +02:00
										 |  |  |     include Context | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-29 19:56:32 +02:00
										 |  |  |     attr_accessor :command, :status, :exit_status | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-17 22:45:55 -08:00
										 |  |  |     sig { | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |       params( | 
					
						
							|  |  |  |         command: T::Array[String], | 
					
						
							|  |  |  |         output:  T::Array[[Symbol, String]], | 
					
						
							|  |  |  |         status:  Process::Status, | 
					
						
							|  |  |  |         secrets: T::Array[String], | 
					
						
							|  |  |  |       ).void | 
					
						
							| 
									
										
										
										
											2021-01-17 22:45:55 -08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |     sig { void } | 
					
						
							| 
									
										
										
										
											2019-10-04 09:17:26 +02:00
										 |  |  |     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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |     sig { returns(String) } | 
					
						
							| 
									
										
										
										
											2018-08-29 19:56:32 +02:00
										 |  |  |     def stdout | 
					
						
							|  |  |  |       @stdout ||= @output.select { |type,| type == :stdout } | 
					
						
							|  |  |  |                          .map { |_, line| line } | 
					
						
							|  |  |  |                          .join | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |     sig { returns(String) } | 
					
						
							| 
									
										
										
										
											2018-08-29 19:56:32 +02:00
										 |  |  |     def stderr | 
					
						
							|  |  |  |       @stderr ||= @output.select { |type,| type == :stderr } | 
					
						
							|  |  |  |                          .map { |_, line| line } | 
					
						
							|  |  |  |                          .join | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |     sig { returns(String) } | 
					
						
							| 
									
										
										
										
											2018-09-19 03:09:07 +02:00
										 |  |  |     def merged_output | 
					
						
							|  |  |  |       @merged_output ||= @output.map { |_, line| line } | 
					
						
							|  |  |  |                                 .join | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |     sig { returns(T::Boolean) } | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |     sig { returns([String, String, Process::Status]) } | 
					
						
							| 
									
										
										
										
											2018-07-30 10:11:00 +02:00
										 |  |  |     def to_ary | 
					
						
							|  |  |  |       [stdout, stderr, status] | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |     sig { returns(T.nilable(T.any(Array, Hash))) } | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |     def plist | 
					
						
							|  |  |  |       @plist ||= begin | 
					
						
							|  |  |  |         output = stdout | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |         output = output.sub(/\A(.*?)(\s*<\?\s*xml)/m) do | 
					
						
							|  |  |  |           warn_plist_garbage(T.must(Regexp.last_match(1))) | 
					
						
							|  |  |  |           Regexp.last_match(2) | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |         output = output.sub(%r{(<\s*/\s*plist\s*>\s*)(.*?)\Z}m) do | 
					
						
							|  |  |  |           warn_plist_garbage(T.must(Regexp.last_match(2))) | 
					
						
							|  |  |  |           Regexp.last_match(1) | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Plist.parse_xml(output) | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-23 02:05:50 +01:00
										 |  |  |     sig { params(garbage: String).void } | 
					
						
							| 
									
										
										
										
											2018-07-19 23:56:51 +02:00
										 |  |  |     def warn_plist_garbage(garbage) | 
					
						
							| 
									
										
										
										
											2020-08-02 14:32:31 +02:00
										 |  |  |       return unless 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 | 
					
						
							| 
									
										
										
										
											2020-10-10 17:53:31 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | # Make `system_command` available everywhere. | 
					
						
							|  |  |  | # FIXME: Include this explicitly only where it is needed. | 
					
						
							|  |  |  | include SystemCommand::Mixin # rubocop:disable Style/MixinUsage |