Class: Holder::Handle

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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

#pidObject (readonly)

Returns the value of attribute pid.



49
50
51
# File 'lib/holder/handle.rb', line 49

def pid
  @pid
end

#pump_errorObject (readonly)

Returns the value of attribute pump_error.



49
50
51
# File 'lib/holder/handle.rb', line 49

def pump_error
  @pump_error
end

#stderrObject (readonly)

Returns the value of attribute stderr.



49
50
51
# File 'lib/holder/handle.rb', line 49

def stderr
  @stderr
end

#stdinObject (readonly)

Returns the value of attribute stdin.



49
50
51
# File 'lib/holder/handle.rb', line 49

def stdin
  @stdin
end

#stdoutObject (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