popen.rb 1.52 KB
Newer Older
1 2
# frozen_string_literal: true

3
require 'fileutils'
4
require 'open3'
5

6 7
module Gitlab
  module Popen
8 9
    extend self

10 11 12 13 14 15
    Result = Struct.new(:cmd, :stdout, :stderr, :status, :duration)

    # Returns [stdout + stderr, status]
    def popen(cmd, path = nil, vars = {}, &block)
      result = popen_with_detail(cmd, path, vars, &block)

16
      ["#{result.stdout}#{result.stderr}", result.status&.exitstatus]
17 18 19 20
    end

    # Returns Result
    def popen_with_detail(cmd, path = nil, vars = {})
21 22 23 24
      unless cmd.is_a?(Array)
        raise "System commands must be given as an array of strings"
      end

25
      path ||= Dir.pwd
26
      vars['PWD'] = path
27
      options = { chdir: path }
28

29
      unless File.directory?(path)
Dale Hamel's avatar
Dale Hamel committed
30
        FileUtils.mkdir_p(path)
31 32
      end

33 34
      cmd_stdout = ''
      cmd_stderr = ''
35
      cmd_status = nil
36 37
      start = Time.now

38
      Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
39 40 41 42 43
        # stderr and stdout pipes can block if stderr/stdout aren't drained: https://bugs.ruby-lang.org/issues/9082
        # Mimic what Ruby does with capture3: https://github.com/ruby/ruby/blob/1ec544695fa02d714180ef9c34e755027b6a2103/lib/open3.rb#L257-L273
        out_reader = Thread.new { stdout.read }
        err_reader = Thread.new { stderr.read }

44
        yield(stdin) if block_given?
45
        stdin.close
46

47 48
        cmd_stdout = out_reader.value
        cmd_stderr = err_reader.value
49
        cmd_status = wait_thr.value
50 51
      end

52
      Result.new(cmd, cmd_stdout, cmd_stderr, cmd_status, Time.now - start)
53 54 55
    end
  end
end