Class: Holder::Handle
- Inherits:
-
Object
- Object
- Holder::Handle
- Defined in:
- lib/holder/handle.rb
Overview
A live process handle.
A regular class, not a Data: a handle is stateful (it gets torn down) and has internals to hide. Data.define exposes every field as a public reader, which is the leak we're closing -- so the wait_thread, pump threads, owned pipes, and mutex live in ivars with no readers.
Public surface: stdin/stdout/stderr (only the streams you did NOT redirect; a redirected stream is talked to via your own IO, so its accessor is nil), pid, and terminate/interrupt/wait.
Teardown is plain method calls guarded by a mutex, so it's idempotent, thread-safe, and callable from any thread. terminate and interrupt are the same robust teardown differing only in the first signal: send it, wait GRACE for the group to die, then guarantee death with SIGKILL, reap the child, join the pumps, and close the pipes we own. Concurrent teardowns share one escalation clock -- the earliest deadline any caller asked for wins -- so a terminate(grace: 0) is never wedged behind another caller's longer grace. wait is the passive counterpart: no first signal, leftovers killed outright, and a redirected out: sink drained for a caller-tunable drain_grace (defaulting well past a teardown's PUMP_GRACE) -- bounded so a sink that never drains can't wedge wait, with a cut-short drain surfaced as a StalledSinkError in pump_error rather than a silent truncation.
Constant Summary collapse
- GRACE =
seconds to wait after the first signal before escalating to SIGKILL
5- PUMP_GRACE =
seconds to wait for a pump to drain after the process exits before interrupting it (see reap_pumps)
1- POLL =
interval between process-group liveness polls while awaiting the grace window
0.02- WAIT_DRAIN_GRACE =
default for wait's drain_grace: seconds wait keeps delivering a redirected out: sink's buffered output -- patient well past a teardown's PUMP_GRACE -- before giving up on a sink that never drains, so a wedged sink cannot block wait forever
2- EPERM_MEANS_GONE =
Whether an EPERM from a group signal means the group is gone. macOS/BSD report a zombie-only group (its last survivor a defunct process) as EPERM, so there it means gone. Linux never reports EPERM for a group we own -- there EPERM means the group is alive but unsignalable (e.g. a child that dropped privilege), which must surface rather than be mistaken for an empty group.
!RUBY_PLATFORM.include?("linux")
Instance Attribute Summary collapse
-
#pid ⇒ Object
readonly
Returns the value of attribute pid.
-
#pump_error ⇒ Object
readonly
Returns the value of attribute pump_error.
-
#stderr ⇒ Object
readonly
Returns the value of attribute stderr.
-
#stdin ⇒ Object
readonly
Returns the value of attribute stdin.
-
#stdout ⇒ Object
readonly
Returns the value of attribute stdout.
Instance Method Summary collapse
-
#initialize(stdin:, stdout:, stderr:, wait_thread:, pump_threads:, owned_ios:, drain_pump: nil) ⇒ Handle
constructor
rubocop:disable Metrics/MethodLength, Metrics/ParameterLists.
- #interrupt(grace: GRACE) ⇒ Object
- #terminate(grace: GRACE) ⇒ Object
-
#wait(drain_grace: WAIT_DRAIN_GRACE) ⇒ Object
Wait for the process to exit on its own, then reap its group and finalize.
Constructor Details
#initialize(stdin:, stdout:, stderr:, wait_thread:, pump_threads:, owned_ios:, drain_pump: nil) ⇒ Handle
rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/holder/handle.rb', line 51 def initialize(stdin:, stdout:, stderr:, wait_thread:, pump_threads:, owned_ios:, drain_pump: nil) # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists @stdin = stdin @stdout = stdout @stderr = stderr @wait_thread = wait_thread @pump_threads = pump_threads @drain_pump = drain_pump @owned_ios = owned_ios @pid = wait_thread.pid @pump_error = nil @mutex = Mutex.new @escalation_tick = ConditionVariable.new @kill_deadline = nil end |
Instance Attribute Details
#pid ⇒ Object (readonly)
Returns the value of attribute pid.
49 50 51 |
# File 'lib/holder/handle.rb', line 49 def pid @pid end |
#pump_error ⇒ Object (readonly)
Returns the value of attribute pump_error.
49 50 51 |
# File 'lib/holder/handle.rb', line 49 def pump_error @pump_error end |
#stderr ⇒ Object (readonly)
Returns the value of attribute stderr.
49 50 51 |
# File 'lib/holder/handle.rb', line 49 def stderr @stderr end |
#stdin ⇒ Object (readonly)
Returns the value of attribute stdin.
49 50 51 |
# File 'lib/holder/handle.rb', line 49 def stdin @stdin end |
#stdout ⇒ Object (readonly)
Returns the value of attribute stdout.
49 50 51 |
# File 'lib/holder/handle.rb', line 49 def stdout @stdout end |
Instance Method Details
#interrupt(grace: GRACE) ⇒ Object
68 |
# File 'lib/holder/handle.rb', line 68 def interrupt(grace: GRACE) = teardown("INT", grace) |
#terminate(grace: GRACE) ⇒ Object
66 |
# File 'lib/holder/handle.rb', line 66 def terminate(grace: GRACE) = teardown("TERM", grace) |
#wait(drain_grace: WAIT_DRAIN_GRACE) ⇒ Object
Wait for the process to exit on its own, then reap its group and finalize. A redirected out: sink gets drain_grace seconds to finish draining (see deliver_redirected_output).
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/holder/handle.rb', line 73 def wait(drain_grace: WAIT_DRAIN_GRACE) # Block on the waiter OUTSIDE the mutex: Thread#value is itself # thread-safe, and holding the mutex across the block would wedge a # concurrent terminate -- it could not acquire the mutex to signal until # the process exited on its own. finalize runs under the mutex and is # idempotent, so a terminate signal that finalizes first is harmless. status = @wait_thread.value begin @mutex.synchronize { kill_group_leftovers } deliver_redirected_output(drain_grace) ensure # finalize even if reaping the group raised (an unsignalable group surfaces # EPERM), so the owned pipes are always closed. @mutex.synchronize { finalize } end status end |