diff --git a/README.md b/README.md index 1ee953e..af2f519 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,15 @@ important behaviorial differences: 2. A timeout can be specified using the `:timeout` option If the command does not terminate before the timeout, the process is killed by -sending it the SIGKILL signal. +sending it the SIGKILL signal. The returned status object's `timeout?` attribute will +return `true`. For example: + +```ruby +status = ProcessExecuter.spawn('sleep 10', timeout: 0.01) +status.signaled? #=> true +status.termsig #=> 9 +status.timeout? #=> true +``` ## Installation diff --git a/lib/process_executer.rb b/lib/process_executer.rb index c0e97f7..91bc882 100644 --- a/lib/process_executer.rb +++ b/lib/process_executer.rb @@ -2,6 +2,7 @@ require 'process_executer/monitored_pipe' require 'process_executer/options' +require 'process_executer/status' require 'timeout' @@ -59,16 +60,16 @@ def self.spawn(*command, **options_hash) # @param pid [Integer] the process id # @param options [ProcessExecuter::Options] the options used # - # @return [Process::Status] the status of the process + # @return [ProcessExecuter::Status] the status of the process # # @api private # private_class_method def self.wait_for_process(pid, options) Timeout.timeout(options.timeout) do - Process.wait2(pid).last + ProcessExecuter::Status.new(Process.wait2(pid).last, false) end rescue Timeout::Error Process.kill('KILL', pid) - Process.wait2(pid).last + ProcessExecuter::Status.new(Process.wait2(pid).last, true) end end diff --git a/lib/process_executer/status.rb b/lib/process_executer/status.rb new file mode 100644 index 0000000..9a0d8c7 --- /dev/null +++ b/lib/process_executer/status.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'delegate' + +module ProcessExecuter + # A simple delegator for Process::Status that adds a `timeout?` attribute + # + # @api public + # + class Status < SimpleDelegator + extend Forwardable + + # Create a new Status object from a Process::Status and timeout flag + # + # @param status [Process::Status] the status to delegate to + # @param timeout [Boolean] true if the process timed out + # + # @example + # status = Process.wait2(pid).last + # timeout = false + # ProcessExecuter::Status.new(status, timeout) + # + # @api public + # + def initialize(status, timeout) + super(status) + @timeout = timeout + end + + # @!attribute [r] timeout? + # + # True if the process timed out and was sent the SIGKILL signal + # + # @example + # status = ProcessExecuter.spawn('sleep 10', timeout: 0.01) + # status.timeout? # => true + # + # @return [Boolean] + # + # @api public + # + def timeout? = @timeout + end +end diff --git a/spec/process_executer_spec.rb b/spec/process_executer_spec.rb index 373d833..0b69ece 100644 --- a/spec/process_executer_spec.rb +++ b/spec/process_executer_spec.rb @@ -49,15 +49,15 @@ context 'for a command that does not time out' do let(:command) { %w[false] } let(:options) { {} } - it { is_expected.to be_a(Process::Status) } - it { is_expected.to have_attributes(exitstatus: 1) } + it { is_expected.to be_a(ProcessExecuter::Status) } + it { is_expected.to have_attributes(timeout?: false, exitstatus: 1) } end context 'for a command that times out' do let(:command) { %w[sleep 1] } let(:options) { { timeout: 0.01 } } - it { is_expected.to be_a(Process::Status) } + it { is_expected.to be_a(ProcessExecuter::Status) } it 'should have killed the process' do start_time = Time.now @@ -72,13 +72,11 @@ if (WINDOWS = (RUBY_PLATFORM =~ /mswin|win32|mingw|bccwin|cygwin/) rescue false) # On windows, the status of a process killed with SIGKILL will indicate # that the process exited normally with exitstatus 0. - expect(subject.exited?).to eq(true) - expect(subject.exitstatus).to eq(0) + expect(subject).to have_attributes(exited?: true, exitstatus: 0, timeout?: true) else # On other platforms, the status of a process killed with SIGKILL will indicate # that the process terminated because of the uncaught signal - expect(subject.signaled?).to eq(true) - expect(subject.termsig).to eq(9) + expect(subject).to have_attributes(signaled?: true, termsig: 9, timeout?: true) end # rubocop:enable Style/RescueModifier # :nocov: