Commit 93b8cae9 authored by Stan Hu's avatar Stan Hu

Fix Popen not always returning error code

`Process#waitpid` returns `Process::Status`, which holds a 16-bit value.

* The higher-order 8 bits hold the exit() code (`exitstatus`).
* The lower-order bits holds whether the process was terminated.

Previously if the process didn't exit normally, `Gitlab::Popen#popen`
would return a status of `nil` since `exitstatus` would be `nil`.

This isn't informative because we want to know what signal killed the
process. We can get this by calling `Process::Status.to_i`.

Relates to https://gitlab.com/gitlab-org/gitlab/-/issues/351155

Changelog: fixed
parent 87357df6
...@@ -10,10 +10,19 @@ module Gitlab ...@@ -10,10 +10,19 @@ module Gitlab
Result = Struct.new(:cmd, :stdout, :stderr, :status, :duration) Result = Struct.new(:cmd, :stdout, :stderr, :status, :duration)
# Returns [stdout + stderr, status] # Returns [stdout + stderr, status]
# status is either the exit code or the signal that killed the process
def popen(cmd, path = nil, vars = {}, &block) def popen(cmd, path = nil, vars = {}, &block)
result = popen_with_detail(cmd, path, vars, &block) result = popen_with_detail(cmd, path, vars, &block)
["#{result.stdout}#{result.stderr}", result.status&.exitstatus] # Process#waitpid returns Process::Status, which holds a 16-bit value.
# The higher-order 8 bits hold the exit() code (`exitstatus`).
# The lower-order bits holds whether the process was terminated.
# If the process didn't exit normally, `exitstatus` will be `nil`,
# but we still want a non-zero code, even if the value is
# platform-dependent.
status = result.status&.exitstatus || result.status.to_i
["#{result.stdout}#{result.stderr}", status]
end end
# Returns Result # Returns Result
......
...@@ -40,6 +40,17 @@ RSpec.describe Gitlab::Popen do ...@@ -40,6 +40,17 @@ RSpec.describe Gitlab::Popen do
it { expect(@output).to include('No such file or directory') } it { expect(@output).to include('No such file or directory') }
end end
context 'non-zero status with a kill' do
let(:cmd) { [Gem.ruby, "-e", "thr = Thread.new { sleep 5 }; Process.kill(9, Process.pid); thr.join"] }
before do
@output, @status = @klass.new.popen(cmd)
end
it { expect(@status).to eq(9) }
it { expect(@output).to be_empty }
end
context 'unsafe string command' do context 'unsafe string command' do
it 'raises an error when it gets called with a string argument' do it 'raises an error when it gets called with a string argument' do
expect { @klass.new.popen('ls', path) }.to raise_error(RuntimeError) expect { @klass.new.popen('ls', path) }.to raise_error(RuntimeError)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment