Class: Muxr::PTYProcess
- Inherits:
-
Object
- Object
- Muxr::PTYProcess
- Defined in:
- lib/muxr/pty_process.rb
Overview
Owns a single pseudo-terminal pair plus the child shell process attached to the slave side. The parent side is exposed via #io / #read_nonblock / #write.
Instance Attribute Summary collapse
-
#cols ⇒ Object
readonly
Returns the value of attribute cols.
-
#io ⇒ Object
readonly
Returns the value of attribute io.
-
#pid ⇒ Object
readonly
Returns the value of attribute pid.
-
#rows ⇒ Object
readonly
Returns the value of attribute rows.
Instance Method Summary collapse
- #alive? ⇒ Boolean
- #close ⇒ Object
-
#cwd ⇒ Object
Best-effort cwd of the child process.
-
#drain ⇒ Object
Push as much of @write_buffer through the PTY as it’ll take without blocking.
-
#initialize(command: nil, rows: 24, cols: 80, cwd: nil, env_overrides: {}) ⇒ PTYProcess
constructor
A new instance of PTYProcess.
- #pending_write? ⇒ Boolean
- #read_nonblock(max = 8192) ⇒ Object
- #reap ⇒ Object
- #resize(rows, cols) ⇒ Object
-
#write(data) ⇒ Object
Appends bytes to the per-process outgoing buffer and tries to flush as many as the PTY will accept right now.
- #writer_io ⇒ Object
Constructor Details
#initialize(command: nil, rows: 24, cols: 80, cwd: nil, env_overrides: {}) ⇒ PTYProcess
Returns a new instance of PTYProcess.
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# File 'lib/muxr/pty_process.rb', line 9 def initialize(command: nil, rows: 24, cols: 80, cwd: nil, env_overrides: {}) @rows = rows @cols = cols @exited = false @write_buffer = +"".b shell = command || ENV["SHELL"] || "/bin/sh" env = ENV.to_h.merge("TERM" => "xterm-256color").merge(env_overrides) env["LINES"] = rows.to_s env["COLUMNS"] = cols.to_s chdir = (cwd && File.directory?(cwd)) ? cwd : Dir.pwd @reader, @writer, @pid = PTY.spawn(env, shell, chdir: chdir) @io = @reader resize(rows, cols) end |
Instance Attribute Details
#cols ⇒ Object (readonly)
Returns the value of attribute cols.
7 8 9 |
# File 'lib/muxr/pty_process.rb', line 7 def cols @cols end |
#io ⇒ Object (readonly)
Returns the value of attribute io.
7 8 9 |
# File 'lib/muxr/pty_process.rb', line 7 def io @io end |
#pid ⇒ Object (readonly)
Returns the value of attribute pid.
7 8 9 |
# File 'lib/muxr/pty_process.rb', line 7 def pid @pid end |
#rows ⇒ Object (readonly)
Returns the value of attribute rows.
7 8 9 |
# File 'lib/muxr/pty_process.rb', line 7 def rows @rows end |
Instance Method Details
#alive? ⇒ Boolean
84 85 86 87 88 89 90 91 |
# File 'lib/muxr/pty_process.rb', line 84 def alive? return false if @exited Process.kill(0, @pid) true rescue Errno::ESRCH, Errno::EPERM @exited = true false end |
#close ⇒ Object
99 100 101 102 103 104 105 106 |
# File 'lib/muxr/pty_process.rb', line 99 def close reap Process.kill("TERM", @pid) if alive? @reader.close unless @reader.closed? @writer.close if @writer != @reader && !@writer.closed? rescue Errno::ESRCH, Errno::EBADF, IOError # already gone end |
#cwd ⇒ Object
Best-effort cwd of the child process. Used to inherit cwd when opening the drawer or for session save/restore. Falls back to nil if the system doesn’t expose the information.
111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/muxr/pty_process.rb', line 111 def cwd if File.directory?("/proc/#{@pid}") File.readlink("/proc/#{@pid}/cwd") else # macOS / BSD fallback via lsof. out = `lsof -a -p #{@pid} -d cwd -Fn 2>/dev/null` line = out.lines.find { |l| l.start_with?("n/") } line && line[1..].strip end rescue StandardError nil end |
#drain ⇒ Object
Push as much of @write_buffer through the PTY as it’ll take without blocking. Safe to call repeatedly — both from write() and from the event loop when select reports the writer fd writable.
43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/muxr/pty_process.rb', line 43 def drain return if @exited || @write_buffer.empty? loop do n = @writer.write_nonblock(@write_buffer) @write_buffer = @write_buffer.byteslice(n..-1) || +"".b break if @write_buffer.empty? end rescue IO::WaitWritable # Kernel buffer full; the rest stays queued. rescue Errno::EIO, IOError, Errno::EPIPE @exited = true @write_buffer.clear end |
#pending_write? ⇒ Boolean
57 58 59 |
# File 'lib/muxr/pty_process.rb', line 57 def pending_write? !@write_buffer.empty? end |
#read_nonblock(max = 8192) ⇒ Object
65 66 67 68 69 70 71 72 |
# File 'lib/muxr/pty_process.rb', line 65 def read_nonblock(max = 8192) @reader.read_nonblock(max) rescue IO::WaitReadable nil rescue EOFError, Errno::EIO @exited = true nil end |
#reap ⇒ Object
93 94 95 96 97 |
# File 'lib/muxr/pty_process.rb', line 93 def reap Process.waitpid(@pid, Process::WNOHANG) rescue Errno::ECHILD nil end |
#resize(rows, cols) ⇒ Object
74 75 76 77 78 79 80 81 82 |
# File 'lib/muxr/pty_process.rb', line 74 def resize(rows, cols) @rows = rows @cols = cols begin @reader.winsize = [rows, cols, 0, 0] rescue StandardError # Some platforms reject zero pixel sizes; ignore. end end |
#write(data) ⇒ Object
Appends bytes to the per-process outgoing buffer and tries to flush as many as the PTY will accept right now. Any remainder stays buffered; the event loop drains it later when the writer fd is reported writable. This avoids deadlocking the single-threaded server when the inner program is slow to read (large pastes were the original motivating case — see CHANGELOG 0.1.3).
33 34 35 36 37 38 |
# File 'lib/muxr/pty_process.rb', line 33 def write(data) return if @exited return if data.nil? || data.empty? @write_buffer << data.b drain end |
#writer_io ⇒ Object
61 62 63 |
# File 'lib/muxr/pty_process.rb', line 61 def writer_io @writer end |