1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
|
require 'ffi'
require 'fcntl'
module PTY
module LibUtil
extend FFI::Library
ffi_lib FFI::Library::LIBC
attach_function :openpty, [ :buffer_out, :buffer_out, :buffer_in, :buffer_in, :buffer_in ], :int
end
class ChildExited < RuntimeError
attr_reader :status
def initialize(status)
@status = status
end
def inspect
"<#{self.class.name}: #{status}>"
end
end
class << self
def spawn(*args, &block)
if args.size > 0
exec_pty(args, &block)
else
exec_pty(get_shell, &block)
end
end
alias :getpty :spawn
def open
masterfd, slavefd, slave_name = openpty
master = IO.for_fd(masterfd, IO::RDWR)
slave = File.for_fd(slavefd, IO::RDWR)
File.chmod(0600, slave_name)
slave.define_singleton_method(:path) do
slave_name
end
slave.define_singleton_method(:tty?) do
true
end
fds = [master, slave]
fds.each do |fd|
fd.sync = true
fd.close_on_exec = true
end
return fds unless block_given?
begin
retval = yield(fds.dup)
ensure
fds.reject(&:closed?).each(&:close)
end
retval
end
def check(target_pid, exception = false)
pid, status = Process.waitpid2(target_pid, Process::WNOHANG|Process::WUNTRACED)
# I sometimes see #<Process::Status: pid 0 signal 36> here.
# See Github issue #3117
if pid == target_pid && status
if exception
raise ChildExited.new(status)
else
return status
end
end
rescue SystemCallError => e
nil
end
private
def openpty
master = FFI::Buffer.alloc_out :int
slave = FFI::Buffer.alloc_out :int
name = FFI::Buffer.alloc_out 1024
result = LibUtil.openpty(master, slave, name, nil, nil)
if result != 0
raise(exception_for_errno(FFI.errno) || SystemCallError.new("errno=#{FFI.errno}"))
end
[master.get_int(0), slave.get_int(0), name.get_string(0)]
end
def exec_pty(args)
master, slave = open
read, write = IO.pipe
pid = Process.spawn(*args, in: read, out: slave, err: slave, close_others: true, pgroup: true)
[read, slave].each(&:close)
master.close_on_exec = true
write.close_on_exec = true
ret = [master, write, pid]
if block_given?
begin
retval = yield(ret.dup)
ensure
ret[0, 2].reject(&:closed?).each(&:close)
end
retval
else
ret
end
end
def get_shell
if shell = ENV['SHELL']
return shell
elsif pwent = Etc.getpwuid(Process.uid) && pwent.shell
return pwent.shell
else
"/bin/sh"
end
end
def exception_for_errno(errno)
Errno.constants.each do |name|
err = Errno.const_get(name)
if err.constants.include?(:Errno) && err.const_get(:Errno) == errno
return err
end
end
SystemCallError.new("errno=#{errno}")
end
end
end
|