Class: TTY::Command::Window::ChildSession

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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pid:, master:, rows:) ⇒ ChildSession

Returns a new instance of ChildSession.

Parameters:

  • pid (Integer)

    child process id (session/pgroup leader)

  • master (IO)

    PTY master fd

  • rows (Integer)

    emulator/PTY row count (fixed for the lifetime of the session)



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

.liveObject (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_mutexObject (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

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

#closeObject

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_waitProcess::Status?

Non-blocking waitpid poll. Marks the session as reaped on success so subsequent #signal calls are inert.

Returns:

  • (Process::Status, nil)


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_blockingObject

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.

Returns:

  • (IO, nil)

    truthy when readable, nil on timeout



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