Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add :timeout? attribute to the status returned by ProcessExecuter.spawn #43

Merged
merged 1 commit into from
Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 4 additions & 3 deletions lib/process_executer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require 'process_executer/monitored_pipe'
require 'process_executer/options'
require 'process_executer/status'

require 'timeout'

Expand Down Expand Up @@ -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
44 changes: 44 additions & 0 deletions lib/process_executer/status.rb
Original file line number Diff line number Diff line change
@@ -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
12 changes: 5 additions & 7 deletions spec/process_executer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
Loading