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!
|
2019-06-28 14:50:38 +08:00
|
|
|
puts redact_secrets(command.shelljoin.gsub('\=', "="), @secrets) if verbose? || ARGV.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
|
|
|
|
|
|
|
|
assert_success if must_succeed?
|
|
|
|
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
|
|
|
|
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-06-28 14:50:38 +08:00
|
|
|
@secrets = Array(secrets)
|
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
|
|
|
|
2018-07-30 10:11:00 +02:00
|
|
|
["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 assert_success
|
2018-07-24 00:09:11 +02:00
|
|
|
return if @status.success?
|
2018-09-17 02:45:00 +02:00
|
|
|
|
2019-06-28 14:50:38 +08:00
|
|
|
raise ErrorDuringExecution.new(command, status: @status, output: @output, secrets: @secrets)
|
2018-07-19 23:56:51 +02:00
|
|
|
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|
|
|
|
|
begin
|
|
|
|
line = source.readline_nonblock || ""
|
|
|
|
type = (source == sources[0]) ? :stdout : :stderr
|
|
|
|
yield(type, line)
|
|
|
|
rescue IO::WaitReadable, EOFError
|
|
|
|
next
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
sources.each(&:close_read)
|
|
|
|
end
|
|
|
|
|
|
|
|
def result
|
2018-08-29 19:56:32 +02:00
|
|
|
Result.new(command, @output, @status)
|
2018-07-19 23:56:51 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
class Result
|
2018-08-29 19:56:32 +02:00
|
|
|
attr_accessor :command, :status, :exit_status
|
|
|
|
|
|
|
|
def initialize(command, output, status)
|
|
|
|
@command = command
|
|
|
|
@output = output
|
|
|
|
@status = status
|
|
|
|
@exit_status = status.exitstatus
|
|
|
|
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)
|
|
|
|
return unless ARGV.verbose?
|
|
|
|
return unless garbage =~ /\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
|