1require 'open3' 2 3module Gitlab 4 module Git 5 module Popen 6 FAST_GIT_PROCESS_TIMEOUT = 15.seconds 7 8 def popen(cmd, path, vars = {}, include_stderr: true, lazy_block: nil) 9 raise "System commands must be given as an array of strings" unless cmd.is_a?(Array) 10 11 path ||= Dir.pwd 12 vars['PWD'] = path 13 options = { chdir: path } 14 15 cmd_output = "" 16 cmd_status = 0 17 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| 18 stdout.set_encoding(Encoding::ASCII_8BIT) 19 stderr.set_encoding(Encoding::ASCII_8BIT) 20 21 # stderr and stdout pipes can block if stderr/stdout aren't drained: https://bugs.ruby-lang.org/issues/9082 22 # Mimic what Ruby does with capture3: https://github.com/ruby/ruby/blob/1ec544695fa02d714180ef9c34e755027b6a2103/lib/open3.rb#L257-L273 23 err_reader = Thread.new { stderr.read } 24 25 begin 26 yield(stdin) if block_given? 27 stdin.close 28 29 if lazy_block 30 cmd_output = lazy_block.call(stdout.lazy) 31 cmd_status = 0 32 break 33 else 34 cmd_output << stdout.read 35 end 36 37 cmd_output << err_reader.value if include_stderr 38 cmd_status = wait_thr.value.exitstatus 39 ensure 40 # When Popen3.open3 returns, the stderr reader gets closed, which causes 41 # an exception in the err_reader thread. Kill the thread before 42 # returning from Popen3.open3. 43 err_reader.kill 44 end 45 end 46 47 [cmd_output, cmd_status] 48 end 49 50 def popen_with_timeout(cmd, timeout, path, vars = {}) 51 raise "System commands must be given as an array of strings" unless cmd.is_a?(Array) 52 53 path ||= Dir.pwd 54 vars['PWD'] = path 55 56 FileUtils.mkdir_p(path) unless File.directory?(path) 57 58 rout, wout = IO.pipe 59 rerr, werr = IO.pipe 60 61 pid = Process.spawn(vars, *cmd, out: wout, err: werr, chdir: path, pgroup: true) 62 # stderr and stdout pipes can block if stderr/stdout aren't drained: https://bugs.ruby-lang.org/issues/9082 63 # Mimic what Ruby does with capture3: https://github.com/ruby/ruby/blob/1ec544695fa02d714180ef9c34e755027b6a2103/lib/open3.rb#L257-L273 64 out_reader = Thread.new { rout.read } 65 err_reader = Thread.new { rerr.read } 66 67 begin 68 # close write ends so we could read them 69 wout.close 70 werr.close 71 72 status = process_wait_with_timeout(pid, timeout) 73 74 cmd_output = out_reader.value 75 cmd_output << err_reader.value # Copying the behaviour of `popen` which merges stderr into output 76 77 [cmd_output, status.exitstatus] 78 rescue Timeout::Error => e 79 kill_process_group_for_pid(pid) 80 81 raise e 82 ensure 83 wout.close unless wout.closed? 84 werr.close unless werr.closed? 85 86 # rout is shared with out_reader. To prevent an exception in that 87 # thread, kill the thread before closing rout. The same goes for rerr 88 # below. 89 out_reader.kill 90 rout.close 91 92 err_reader.kill 93 rerr.close 94 end 95 end 96 97 def process_wait_with_timeout(pid, timeout) 98 deadline = timeout.seconds.from_now 99 wait_time = 0.01 100 101 while deadline > Time.now 102 sleep(wait_time) 103 _, status = Process.wait2(pid, Process::WNOHANG) 104 105 return status unless status.nil? 106 end 107 108 raise Timeout::Error, "Timeout waiting for process ##{pid}" 109 end 110 111 def kill_process_group_for_pid(pid) 112 Process.kill("KILL", -pid) 113 Process.wait(pid) 114 rescue Errno::ESRCH 115 end 116 end 117 end 118end 119