2020-11-25 17:03:23 +01:00
|
|
|
# typed: true
|
2019-04-19 15:38:03 +09:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2023-12-28 11:45:18 -08:00
|
|
|
require "attrable"
|
2018-07-19 23:56:51 +02:00
|
|
|
require "open3"
|
2018-09-13 15:24:18 +01:00
|
|
|
require "plist"
|
2018-07-19 23:56:51 +02:00
|
|
|
require "shellwords"
|
|
|
|
|
2024-01-26 17:33:55 -08:00
|
|
|
require "context"
|
2018-07-19 23:56:51 +02:00
|
|
|
require "extend/io"
|
2024-01-29 18:14:31 -08:00
|
|
|
require "utils/timer"
|
2021-03-24 10:55:33 +01:00
|
|
|
|
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-11-05 17:17:03 -05:00
|
|
|
# Helper functions for calling {SystemCommand.run}.
|
2020-10-10 17:53:31 +02:00
|
|
|
module Mixin
|
2022-10-08 01:08:15 +01:00
|
|
|
def system_command(executable, **options)
|
|
|
|
SystemCommand.run(executable, **options)
|
2020-10-10 17:53:31 +02:00
|
|
|
end
|
2018-07-22 23:13:32 +02:00
|
|
|
|
2022-10-08 01:08:15 +01:00
|
|
|
def system_command!(command, **options)
|
|
|
|
SystemCommand.run!(command, **options)
|
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
|
2023-12-28 11:45:18 -08:00
|
|
|
extend Attrable
|
2018-07-19 23:56:51 +02:00
|
|
|
|
|
|
|
def self.run(executable, **options)
|
2022-10-08 01:08:15 +01:00
|
|
|
new(executable, **options).run!
|
2018-07-19 23:56:51 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.run!(command, **options)
|
2022-10-08 01:08:15 +01:00
|
|
|
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
|
2023-10-10 02:08:27 +02:00
|
|
|
case @print_stdout
|
|
|
|
when true
|
|
|
|
$stdout << redact_secrets(line, @secrets)
|
|
|
|
when :debug
|
|
|
|
$stderr << redact_secrets(line, @secrets) if debug?
|
|
|
|
end
|
2018-08-29 19:56:32 +02:00
|
|
|
@output << [:stdout, line]
|
2018-07-19 23:56:51 +02:00
|
|
|
when :stderr
|
2023-10-10 02:08:27 +02:00
|
|
|
case @print_stderr
|
|
|
|
when true
|
|
|
|
$stderr << redact_secrets(line, @secrets)
|
|
|
|
when :debug
|
|
|
|
$stderr << redact_secrets(line, @secrets) if debug?
|
|
|
|
end
|
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)],
|
2023-02-17 14:48:13 -08:00
|
|
|
sudo: T::Boolean,
|
2023-03-27 14:06:07 -07:00
|
|
|
sudo_as_root: T::Boolean,
|
2020-11-23 02:05:50 +01:00
|
|
|
env: T::Hash[String, String],
|
|
|
|
input: T.any(String, T::Array[String]),
|
|
|
|
must_succeed: T::Boolean,
|
2023-10-10 02:08:27 +02:00
|
|
|
print_stdout: T.any(T::Boolean, Symbol),
|
|
|
|
print_stderr: T.any(T::Boolean, Symbol),
|
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,
|
2023-03-27 14:06:07 -07:00
|
|
|
sudo_as_root: false,
|
2021-03-24 10:55:33 +01:00
|
|
|
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
|
2023-02-17 14:48:13 -08:00
|
|
|
|
2023-10-10 02:08:27 +02:00
|
|
|
raise ArgumentError, "`sudo_as_root` cannot be set if sudo is false" if !sudo && sudo_as_root
|
|
|
|
|
|
|
|
if print_stdout.is_a?(Symbol) && print_stdout != :debug
|
|
|
|
raise ArgumentError, "`print_stdout` is not a valid symbol"
|
|
|
|
end
|
|
|
|
if print_stderr.is_a?(Symbol) && print_stderr != :debug
|
|
|
|
raise ArgumentError, "`print_stderr` is not a valid symbol"
|
|
|
|
end
|
2023-02-17 14:48:13 -08:00
|
|
|
|
2018-07-19 23:56:51 +02:00
|
|
|
@sudo = sudo
|
2023-03-27 14:06:07 -07:00
|
|
|
@sudo_as_root = sudo_as_root
|
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
|
2023-02-13 18:40:37 -08:00
|
|
|
[*command_prefix, executable.to_s, *expanded_args]
|
2018-07-19 23:56:51 +02:00
|
|
|
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
|
|
|
|
2023-10-10 02:08:27 +02:00
|
|
|
attr_predicate :sudo?, :sudo_as_root?, :must_succeed?
|
2020-08-02 14:32:31 +02:00
|
|
|
|
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
|
|
|
|
2023-02-13 18:40:37 -08:00
|
|
|
set_variables
|
2018-07-19 23:56:51 +02:00
|
|
|
end
|
|
|
|
|
2023-09-29 18:49:44 +01:00
|
|
|
sig { returns(T.nilable(String)) }
|
|
|
|
def homebrew_sudo_user
|
|
|
|
ENV.fetch("HOMEBREW_SUDO_USER", nil)
|
|
|
|
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
|
|
|
|
askpass_flags = ENV.key?("SUDO_ASKPASS") ? ["-A"] : []
|
2023-09-29 18:49:44 +01:00
|
|
|
user_flags = []
|
2023-12-20 09:36:46 -08:00
|
|
|
if Homebrew::EnvConfig.sudo_through_sudo_user?
|
2023-09-29 18:49:44 +01:00
|
|
|
raise ArgumentError, "HOMEBREW_SUDO_THROUGH_SUDO_USER set but SUDO_USER unset!" if homebrew_sudo_user.blank?
|
|
|
|
|
|
|
|
user_flags += ["--prompt", "Password for %p:", "-u", homebrew_sudo_user,
|
|
|
|
*askpass_flags,
|
|
|
|
"-E", *env_args,
|
|
|
|
"--", "/usr/bin/sudo"]
|
2023-12-20 09:39:10 -08:00
|
|
|
end
|
2023-12-21 01:16:09 -08:00
|
|
|
user_flags += ["-u", "root"] if sudo_as_root?
|
2023-02-14 13:24:36 -08:00
|
|
|
["/usr/bin/sudo", *user_flags, *askpass_flags, "-E", *env_args, "--"]
|
2023-02-13 18:40:37 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
sig { returns(T::Array[String]) }
|
2023-02-14 13:02:59 +00:00
|
|
|
def env_prefix
|
2023-02-13 18:40:37 -08:00
|
|
|
["/usr/bin/env", *env_args]
|
|
|
|
end
|
|
|
|
|
|
|
|
sig { returns(T::Array[String]) }
|
|
|
|
def command_prefix
|
2023-02-14 13:02:59 +00:00
|
|
|
sudo? ? sudo_prefix : env_prefix
|
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 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
|
|
|
|
|
|
|
|
raw_stdin, raw_stdout, raw_stderr, raw_wait_thr = ignore_interrupts do
|
2023-04-17 23:30:25 +02:00
|
|
|
Open3.popen3(
|
|
|
|
env.merge({ "COLUMNS" => Tty.width.to_s }),
|
|
|
|
[executable, executable],
|
|
|
|
*args,
|
|
|
|
**options,
|
|
|
|
)
|
2020-12-17 15:45:50 +01:00
|
|
|
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
|
|
|
|
2023-10-10 03:39:42 +02:00
|
|
|
thread_context = Context.current
|
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
|
2023-10-10 03:39:42 +02:00
|
|
|
# Ensure the new thread inherits the current context.
|
|
|
|
Context.current = thread_context
|
|
|
|
|
2021-05-03 22:22:34 +01:00
|
|
|
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
|
2024-01-29 18:14:31 -08:00
|
|
|
raise Timeout::Error if raw_wait_thr.join(Utils::Timer.remaining(end_time)).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
|
2023-04-17 23:30:25 +02:00
|
|
|
Process.kill("INT", raw_wait_thr.pid) if raw_wait_thr && !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)
|
2024-03-03 18:55:56 -08:00
|
|
|
input.each { raw_stdin.write(_1) }
|
2018-07-19 23:56:51 +02:00
|
|
|
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)
|
|
|
|
|
2023-10-29 03:43:52 +00:00
|
|
|
until pending_interrupt || sources.empty?
|
2021-05-03 22:22:34 +01:00
|
|
|
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
|
|
|
|
2023-10-29 03:43:52 +00:00
|
|
|
readable_sources.each 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)
|
2021-04-01 15:42:16 +01:00
|
|
|
rescue IO::WaitReadable
|
2023-10-29 03:43:52 +00:00
|
|
|
# We've got all the data that was ready, but the other end of the stream isn't finished yet
|
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-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
|
|
|
|
|
2023-02-22 22:52:06 +00:00
|
|
|
Plist.parse_xml(output, marshal: false)
|
2018-07-19 23:56:51 +02:00
|
|
|
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
|