2016-08-18 22:11:42 +03:00
|
|
|
require "open3"
|
|
|
|
require "shellwords"
|
2017-02-28 16:31:25 +01:00
|
|
|
require "vendor/plist/plist"
|
2016-08-18 22:11:42 +03:00
|
|
|
|
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)
|
2016-12-31 18:02:42 +01:00
|
|
|
.assert_valid_keys :input, :print_stdout, :print_stderr, :args, :must_succeed, :sudo
|
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
|
|
|
@command = [executable]
|
|
|
|
options[:print_stderr] = true unless options.key?(:print_stderr)
|
|
|
|
@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
|
|
|
|
2017-03-08 03:03:49 +01:00
|
|
|
write_input_to(raw_stdin)
|
2016-09-24 13:52:43 +02:00
|
|
|
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)
|
2017-03-08 03:03:49 +01:00
|
|
|
[*options[:input]].each { |line| raw_stdin.print line }
|
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_line_from(sources)
|
|
|
|
loop do
|
2017-04-21 14:12:16 +02:00
|
|
|
selected_sources = IO.select(sources, [], [], 10)
|
2017-04-21 07:11:40 +02:00
|
|
|
|
2017-04-21 14:12:16 +02:00
|
|
|
break if selected_sources.nil?
|
2017-04-21 07:11:40 +02:00
|
|
|
|
|
|
|
readable_sources = selected_sources[0].delete_if(&:eof?)
|
|
|
|
|
|
|
|
readable_sources.each do |source|
|
2016-09-24 13:52:43 +02:00
|
|
|
type = (source == sources[0] ? :stdout : :stderr)
|
2017-04-21 07:11:40 +02:00
|
|
|
|
2016-09-24 13:52:43 +02:00
|
|
|
begin
|
|
|
|
yield(type, source.readline_nonblock || "")
|
|
|
|
rescue IO::WaitReadable, EOFError
|
|
|
|
next
|
|
|
|
end
|
2016-08-18 22:11:42 +03:00
|
|
|
end
|
2017-04-21 07:11:40 +02:00
|
|
|
|
2016-09-24 13:52:43 +02:00
|
|
|
break if readable_sources.empty?
|
2016-08-18 22:11:42 +03:00
|
|
|
end
|
2017-04-21 07:11:40 +02:00
|
|
|
|
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')
|
2017-03-06 20:37:13 +01:00
|
|
|
_warn_plist_garbage(command, Regexp.last_match[1]) if CLI.debug?
|
2016-09-24 13:52:43 +02:00
|
|
|
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
|
|
|
|
end
|
2016-08-18 22:11:42 +03:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|