Class: Vagrant::Util::SubprocessPatched

Inherits:
Object
  • Object
show all
Defined in:
lib/vagrant-reflect/patched/subprocess.rb

Overview

Execute a command in a subprocess, gathering the results and exit status.

This class also allows you to read the data as it is outputted from the subprocess in real time, by simply passing a block to the execute method.

Defined Under Namespace

Classes: LaunchError, Result, TimeoutExceeded

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*command) ⇒ SubprocessPatched

Returns a new instance of SubprocessPatched.



25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/vagrant-reflect/patched/subprocess.rb', line 25

def initialize(*command)
  @options = command.last.is_a?(Hash) ? command.pop : {}
  @command = command.dup
  @command = @command.map { |s| s.encode(Encoding.default_external) }
  @command[0] = Which.which(@command[0]) if !File.file?(@command[0])
  if !@command[0]
    raise Errors::CommandUnavailableWindows, file: command[0] if Platform.windows?
    raise Errors::CommandUnavailable, file: command[0]
  end

  @logger  = Log4r::Logger.new("vagrant::util::subprocess")
end

Class Method Details

.execute(*command, &block) ⇒ Object

Convenience method for executing a method.



21
22
23
# File 'lib/vagrant-reflect/patched/subprocess.rb', line 21

def self.execute(*command, &block)
  new(*command).execute(&block)
end

Instance Method Details

#executeObject



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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/vagrant-reflect/patched/subprocess.rb', line 38

def execute
  # Get the timeout, if we have one
  timeout = @options[:timeout]

  # Get the working directory
  workdir = @options[:workdir] || Dir.pwd

  # Get what we're interested in being notified about
  notify  = @options[:notify] || []
  notify  = [notify] if !notify.is_a?(Array)
  if notify.empty? && block_given?
    # If a block is given, subscribers must be given, otherwise the
    # block is never called. This is usually NOT what you want, so this
    # is an error.
    message = "A list of notify subscriptions must be given if a block is given"
    raise ArgumentError, message
  end

  # Let's get some more useful booleans that we access a lot so
  # we're not constantly calling an `include` check
  notify_table = {}
  notify_table[:stderr] = notify.include?(:stderr)
  notify_table[:stdout] = notify.include?(:stdout)
  notify_stdin  = notify.include?(:stdin)

  # Build the ChildProcess
  @logger.info("Starting process: #{@command.inspect}")
  process = ChildProcess.build(*@command)

  # Create the pipes so we can read the output in real time as
  # we execute the command.
  stdout, stdout_writer = ::IO.pipe
  stderr, stderr_writer = ::IO.pipe
  process.io.stdout = stdout_writer
  process.io.stderr = stderr_writer
  process.duplex = true

  # Special installer-related things
  if Vagrant.in_installer?
    installer_dir = ENV["VAGRANT_INSTALLER_EMBEDDED_DIR"].to_s.downcase

    # If we're in an installer on Mac and we're executing a command
    # in the installer context, then force DYLD_LIBRARY_PATH to look
    # at our libs first.
    if Platform.darwin?
      if @command[0].downcase.include?(installer_dir)
        @logger.info("Command in the installer. Specifying DYLD_LIBRARY_PATH...")
        process.environment["DYLD_LIBRARY_PATH"] =
          "#{installer_dir}/lib:#{ENV["DYLD_LIBRARY_PATH"]}"
      else
        @logger.debug("Command not in installer, not touching env vars.")
      end

      if File.setuid?(@command[0]) || File.setgid?(@command[0])
        @logger.info("Command is setuid/setgid, clearing DYLD_LIBRARY_PATH")
        process.environment["DYLD_LIBRARY_PATH"] = ""
      end
    end

    # If the command that is being run is not inside the installer, reset
    # the original environment - this is required for shelling out to
    # other subprocesses that depend on environment variables (like Ruby
    # and $GEM_PATH for example)
    internal = [installer_dir, Vagrant.user_data_path.to_s.downcase].
      any? { |path| @command[0].downcase.include?(path) }
    if !internal
      @logger.info("Command not in installer, restoring original environment...")
      jailbreak(process.environment)
    end
  else
    @logger.info("Vagrant not running in installer, restoring original environment...")
    jailbreak(process.environment)
  end

  # Set the environment on the process if we must
  if @options[:env]
    @options[:env].each do |k, v|
      process.environment[k] = v
    end
  end

  # Start the process
  begin
    SafeChdir.safe_chdir(workdir) do
      process.start
    end
  rescue ChildProcess::LaunchError => ex
    # Raise our own version of the error so that users of the class
    # don't need to be aware of ChildProcess
    raise LaunchError.new(ex.message)
  end

  # Make sure the stdin does not buffer
  process.io.stdin.sync = true

  if RUBY_PLATFORM != "java"
    # On Java, we have to close after. See down the method...
    # Otherwise, we close the writers right here, since we're
    # not on the writing side.
    stdout_writer.close
    stderr_writer.close
  end

  # Create a dictionary to store all the output we see.
  io_data = { stdout: "", stderr: "" }

  # Record the start time for timeout purposes
  start_time = Time.now.to_i

  @logger.debug("Selecting on IO")
  while true
    # PATCH BEGINS HERE TO FIX STDIN CLOSURE
    writers = notify_stdin && !process.io.stdin.closed? ? [process.io.stdin] : []
    # PATCH END
    results = ::IO.select([stdout, stderr], writers, nil, 0.1)
    results ||= []
    readers = results[0]
    writers = results[1]

    # Check if we have exceeded our timeout
    raise TimeoutExceeded, process.pid if timeout && (Time.now.to_i - start_time) > timeout

    # Check the readers to see if they're ready
    if readers && !readers.empty?
      readers.each do |r|
        # Read from the IO object
        data = IO.read_until_block(r)

        # We don't need to do anything if the data is empty
        next if data.empty?

        io_name = r == stdout ? :stdout : :stderr
        @logger.debug("#{io_name}: #{data.chomp}")

        io_data[io_name] += data
        yield io_name, data if block_given? && notify_table[io_name]
      end
    end

    # Break out if the process exited. We have to do this before
    # attempting to write to stdin otherwise we'll get a broken pipe
    # error.
    break if process.exited?

    # Check the writers to see if they're ready, and notify any listeners
    if writers && !writers.empty? && block_given?
      yield :stdin, process.io.stdin
    end
  end

  # Wait for the process to end.
  begin
    remaining = (timeout || 32000) - (Time.now.to_i - start_time)
    remaining = 0 if remaining < 0
    @logger.debug("Waiting for process to exit. Remaining to timeout: #{remaining}")

    process.poll_for_exit(remaining)
  rescue ChildProcess::TimeoutError
    raise TimeoutExceeded, process.pid
  end

  @logger.debug("Exit status: #{process.exit_code}")

  # Read the final output data, since it is possible we missed a small
  # amount of text between the time we last read data and when the
  # process exited.
  [stdout, stderr].each do |io|
    # Read the extra data, ignoring if there isn't any
    extra_data = IO.read_until_block(io)
    next if extra_data == ""

    # Log it out and accumulate
    io_name = io == stdout ? :stdout : :stderr
    io_data[io_name] += extra_data
    @logger.debug("#{io_name}: #{extra_data.chomp}")

    # Yield to any listeners any remaining data
    yield io_name, extra_data if block_given? && notify_table[io_name]
  end

  if RUBY_PLATFORM == "java"
    # On JRuby, we need to close the writers after the process,
    # for some reason. See GH-711.
    stdout_writer.close
    stderr_writer.close
  end

  # Return an exit status container
  return Result.new(process.exit_code, io_data[:stdout], io_data[:stderr])
ensure
  if process && process.alive?
    # Make sure no matter what happens, the process exits
    process.stop(2)
  end
end