Class: Muxr::PTYProcess

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

Instance Method Summary collapse

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

#colsObject (readonly)

Returns the value of attribute cols.



7
8
9
# File 'lib/muxr/pty_process.rb', line 7

def cols
  @cols
end

#ioObject (readonly)

Returns the value of attribute io.



7
8
9
# File 'lib/muxr/pty_process.rb', line 7

def io
  @io
end

#pidObject (readonly)

Returns the value of attribute pid.



7
8
9
# File 'lib/muxr/pty_process.rb', line 7

def pid
  @pid
end

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

Returns:

  • (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

#closeObject



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

#cwdObject

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

#drainObject

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

Returns:

  • (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

#reapObject



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_ioObject



61
62
63
# File 'lib/muxr/pty_process.rb', line 61

def writer_io
  @writer
end