summaryrefslogtreecommitdiff
path: root/lib/gitlab/git/popen.rb
blob: b45da6020ee43bdc67abc32ae71f71c8a59fc51c (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
# Gitaly note: JV: no RPC's here.

require 'open3'

module Gitlab
  module Git
    module Popen
      FAST_GIT_PROCESS_TIMEOUT = 15.seconds

      def popen(cmd, path, vars = {})
        unless cmd.is_a?(Array)
          raise "System commands must be given as an array of strings"
        end

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

        @cmd_output = ""
        @cmd_status = 0
        Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
          yield(stdin) if block_given?
          stdin.close

          @cmd_output << stdout.read
          @cmd_output << stderr.read
          @cmd_status = wait_thr.value.exitstatus
        end

        [@cmd_output, @cmd_status]
      end

      def popen_with_timeout(cmd, timeout, path, vars = {})
        unless cmd.is_a?(Array)
          raise "System commands must be given as an array of strings"
        end

        path ||= Dir.pwd
        vars['PWD'] = path

        unless File.directory?(path)
          FileUtils.mkdir_p(path)
        end

        rout, wout = IO.pipe
        rerr, werr = IO.pipe

        pid = Process.spawn(vars, *cmd, out: wout, err: werr, chdir: path, pgroup: true)

        begin
          status = process_wait_with_timeout(pid, timeout)

          # close write ends so we could read them
          wout.close
          werr.close

          cmd_output = rout.readlines.join
          cmd_output << rerr.readlines.join # Copying the behaviour of `popen` which merges stderr into output

          [cmd_output, status.exitstatus]
        rescue Timeout::Error => e
          kill_process_group_for_pid(pid)

          raise e
        ensure
          wout.close unless wout.closed?
          werr.close unless werr.closed?

          rout.close
          rerr.close
        end
      end

      def process_wait_with_timeout(pid, timeout)
        deadline = timeout.seconds.from_now
        wait_time = 0.01

        while deadline > Time.now
          sleep(wait_time)
          _, status = Process.wait2(pid, Process::WNOHANG)

          return status unless status.nil?
        end

        raise Timeout::Error, "Timeout waiting for process ##{pid}"
      end

      def kill_process_group_for_pid(pid)
        Process.kill("KILL", -pid)
        Process.wait(pid)
      rescue Errno::ESRCH
      end
    end
  end
end