File: pty.rb

package info (click to toggle)
jruby 9.3.9.0%2Bds-8
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 80,856 kB
  • sloc: ruby: 517,823; java: 260,094; xml: 31,930; ansic: 5,777; yacc: 4,973; sh: 1,163; makefile: 105; jsp: 48; tcl: 40; exp: 11
file content (142 lines) | stat: -rw-r--r-- 3,093 bytes parent folder | download
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