aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Homebrew/cask/lib/hbc/system_command.rb
blob: bb8d91504e46f7ee031817bcafcda058a52d0b23 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
require "open3"
require "shellwords"

class Hbc::SystemCommand
  attr_reader :command

  def self.run(executable, options = {})
    new(executable, options).run!
  end

  def self.run!(command, options = {})
    run(command, options.merge(must_succeed: true))
  end

  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
    end

    assert_success if options[:must_succeed]
    result
  end

  def initialize(executable, options)
    @executable = executable
    @options = options
    process_options!
  end

  private

  attr_reader :executable, :options, :processed_output, :processed_status

  def process_options!
    options.assert_valid_keys :input, :print_stdout, :print_stderr, :args, :must_succeed, :sudo, :bsexec
    sudo_prefix = %w[/usr/bin/sudo -E --]
    bsexec_prefix = ["/bin/launchctl", "bsexec", options[:bsexec] == :startup ? "/" : options[:bsexec]]
    @command = [executable]
    options[:print_stderr] = true    unless options.key?(:print_stderr)
    @command.unshift(*bsexec_prefix) if  options[:bsexec]
    @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

  def assert_success
    return if processed_status && processed_status.success?
    raise Hbc::CaskCommandFailedError.new(command.utf8_inspect, processed_output[:stdout], processed_output[:stderr], processed_status)
  end

  def expanded_command
    @expanded_command ||= command.map { |arg|
      if arg.respond_to?(:to_path)
        File.absolute_path(arg)
      else
        String(arg)
      end
    }
  end

  def each_output_line(&b)
    raw_stdin, raw_stdout, raw_stderr, raw_wait_thr =
      Open3.popen3(*expanded_command)

    write_input_to(raw_stdin) if options[:input]
    raw_stdin.close_write
    each_line_from [raw_stdout, raw_stderr], &b

    @processed_status = raw_wait_thr.value
  end

  def write_input_to(raw_stdin)
    Array(options[:input]).each { |line| raw_stdin.puts line }
  end

  def each_line_from(sources)
    loop do
      readable_sources = IO.select(sources)[0]
      readable_sources.delete_if(&:eof?).first(1).each do |source|
        type = (source == sources[0] ? :stdout : :stderr)
        begin
          yield(type, source.readline_nonblock || "")
        rescue IO::WaitReadable, EOFError
          next
        end
      end
      break if readable_sources.empty?
    end
    sources.each(&:close_read)
  end

  def result
    Hbc::SystemCommand::Result.new(command,
                                   processed_output[:stdout],
                                   processed_output[:stderr],
                                   processed_status.exitstatus)
  end
end

class Hbc::SystemCommand::Result
  attr_accessor :command, :stdout, :stderr, :exit_status

  def initialize(command, stdout, stderr, exit_status)
    @command     = command
    @stdout      = stdout
    @stderr      = stderr
    @exit_status = exit_status
  end

  def plist
    @plist ||= self.class._parse_plist(@command, @stdout.dup)
  end

  def success?
    @exit_status == 0
  end

  def merged_output
    @merged_output ||= @stdout + @stderr
  end

  def to_s
    @stdout
  end

  def self._warn_plist_garbage(command, garbage)
    return true unless garbage =~ %r{\S}
    external = File.basename(command.first)
    lines = garbage.strip.split("\n")
    opoo "Non-XML stdout from #{external}:"
    $stderr.puts lines.map { |l| "    #{l}" }
  end

  def self._parse_plist(command, output)
    raise Hbc::CaskError, "Empty plist input" unless output =~ %r{\S}
    output.sub!(%r{\A(.*?)(<\?\s*xml)}m, '\2')
    _warn_plist_garbage(command, Regexp.last_match[1]) if Hbc.debug
    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 Hbc::CaskError, <<-EOS
Empty result parsing plist output from command.
  command was:
  #{command.utf8_inspect}
  output we attempted to parse:
  #{output}
      EOS
    end
    xml
  rescue Plist::ParseError => e
    raise Hbc::CaskError, <<-EOS
Error parsing plist output from command.
  command was:
  #{command.utf8_inspect}
  error was:
  #{e}
  output we attempted to parse:
  #{output}
    EOS
  end
end