Class: TTY::Command::Window::ChildSession
- Inherits:
-
Object
- Object
- TTY::Command::Window::ChildSession
- Defined in:
- lib/tty/command/window/child_session.rb
Overview
Handle to the child process spawned inside the PTY.
Owns the process id and the PTY master file descriptor and exposes the operations the rest of the gem needs to perform against a live child: write user input, resize the terminal, forward a signal to its process group, reap the child, and close the master fd.
Orphan protection. Every live session is tracked in a
class-level set. A one-time at_exit hook SIGKILLs the process
group of any session still alive when the host process exits
(follow-up review P1-8). This covers the case where the host
dies before Runner#cleanup runs — SIGKILL to the host, an
interpreter abort, or exit!.
Class Attribute Summary collapse
- .live ⇒ Object readonly private
- .live_mutex ⇒ Object readonly private
Instance Attribute Summary collapse
-
#rows ⇒ Object
readonly
Returns the value of attribute rows.
Class Method Summary collapse
- .reap_orphans! ⇒ Object
-
.track(session) ⇒ Object
Register the session as live and ensure the
at_exitorphan sweeper is installed. -
.untrack(session) ⇒ Object
Remove the session from the live-set (called on close).
Instance Method Summary collapse
-
#close ⇒ Object
Close the PTY master.
-
#initialize(pid:, master:, rows:) ⇒ ChildSession
constructor
A new instance of ChildSession.
-
#kill_group! ⇒ Object
Send SIGKILL to the process group.
-
#read_nonblock(length) ⇒ Object
Read up to
lengthbytes of child output without blocking. -
#resize(cols) ⇒ Object
Resize the PTY to the new column count.
-
#signal(name) ⇒ Object
Forward a signal to the child's process group.
-
#try_wait ⇒ Process::Status?
Non-blocking waitpid poll.
-
#wait_blocking ⇒ Object
Blocking waitpid — used when the caller has already decided to give up polling (e.g. after SIGKILL).
-
#wait_readable(timeout) ⇒ IO?
Block until the child has output or
timeoutseconds elapse. -
#write(data) ⇒ Object
Write user keystrokes / bytes to the child.
Constructor Details
#initialize(pid:, master:, rows:) ⇒ ChildSession
Returns a new instance of ChildSession.
62 63 64 65 66 67 68 69 |
# File 'lib/tty/command/window/child_session.rb', line 62 def initialize(pid:, master:, rows:) @pid = pid @master = master @rows = rows @closed = false @reaped = false self.class.track(self) end |
Class Attribute Details
.live ⇒ Object (readonly)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
29 30 31 |
# File 'lib/tty/command/window/child_session.rb', line 29 def live @live end |
.live_mutex ⇒ Object (readonly)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
29 30 31 |
# File 'lib/tty/command/window/child_session.rb', line 29 def live_mutex @live_mutex end |
Instance Attribute Details
#rows ⇒ Object (readonly)
Returns the value of attribute rows.
71 72 73 |
# File 'lib/tty/command/window/child_session.rb', line 71 def rows @rows end |
Class Method Details
.reap_orphans! ⇒ Object
44 45 46 47 |
# File 'lib/tty/command/window/child_session.rb', line 44 def reap_orphans! sessions = @live_mutex.synchronize { @live.keys.dup } sessions.each(&:kill_group!) end |
.track(session) ⇒ Object
Register the session as live and ensure the at_exit orphan
sweeper is installed.
33 34 35 36 |
# File 'lib/tty/command/window/child_session.rb', line 33 def track(session) install_at_exit_once @live_mutex.synchronize { @live[session] = true } end |
.untrack(session) ⇒ Object
Remove the session from the live-set (called on close).
39 40 41 |
# File 'lib/tty/command/window/child_session.rb', line 39 def untrack(session) @live_mutex.synchronize { @live.delete(session) } end |
Instance Method Details
#close ⇒ Object
Close the PTY master. Idempotent. Also removes the session from the orphan-tracking set.
142 143 144 145 146 147 148 149 150 151 |
# File 'lib/tty/command/window/child_session.rb', line 142 def close return if @closed @closed = true @master.close if @master && !@master.closed? rescue IOError, Errno::EBADF nil ensure self.class.untrack(self) end |
#kill_group! ⇒ Object
Send SIGKILL to the process group. Used by the at_exit
orphan sweeper; safe to call on a reaped session (no-op).
155 156 157 |
# File 'lib/tty/command/window/child_session.rb', line 155 def kill_group! signal("KILL") end |
#read_nonblock(length) ⇒ Object
Read up to length bytes of child output without blocking.
Raises like IO#read_nonblock (IO::WaitReadable, EOFError,
Errno::EIO on PTY hangup) — callers own the EOF handling.
110 111 112 |
# File 'lib/tty/command/window/child_session.rb', line 110 def read_nonblock(length) @master.read_nonblock(length) end |
#resize(cols) ⇒ Object
Resize the PTY to the new column count. Rows are fixed.
81 82 83 84 85 |
# File 'lib/tty/command/window/child_session.rb', line 81 def resize(cols) @master&.winsize = [@rows, cols] rescue Errno::EIO, Errno::EBADF, IOError nil end |
#signal(name) ⇒ Object
Forward a signal to the child's process group. No-op once the child has been reaped so a stale session cannot deliver to a recycled PID (belt-and-braces alongside the coordinator's unconditional @sessions.delete — see follow-up review P0-2).
91 92 93 94 95 96 97 98 |
# File 'lib/tty/command/window/child_session.rb', line 91 def signal(name) return if @reaped return unless @pid Process.kill(name, -@pid) rescue Errno::ESRCH, Errno::EPERM nil end |
#try_wait ⇒ Process::Status?
Non-blocking waitpid poll. Marks the session as reaped on success so subsequent #signal calls are inert.
118 119 120 121 122 123 124 125 126 127 |
# File 'lib/tty/command/window/child_session.rb', line 118 def try_wait pid, status = Process.waitpid2(@pid, Process::WNOHANG) return nil unless pid @reaped = true status rescue Errno::ECHILD @reaped = true nil end |
#wait_blocking ⇒ Object
Blocking waitpid — used when the caller has already decided to give up polling (e.g. after SIGKILL). Marks reaped.
131 132 133 134 135 136 137 138 |
# File 'lib/tty/command/window/child_session.rb', line 131 def wait_blocking _, status = Process.waitpid2(@pid) @reaped = true status rescue Errno::ECHILD @reaped = true nil end |
#wait_readable(timeout) ⇒ IO?
Block until the child has output or timeout seconds elapse.
103 104 105 |
# File 'lib/tty/command/window/child_session.rb', line 103 def wait_readable(timeout) @master.wait_readable(timeout) end |
#write(data) ⇒ Object
Write user keystrokes / bytes to the child.
74 75 76 77 78 |
# File 'lib/tty/command/window/child_session.rb', line 74 def write(data) @master.write(data) if @master && !@master.closed? rescue Errno::EIO, IOError nil end |