File: child_process.rb

package info (click to toggle)
ruby-tty-command 0.10.1-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 452 kB
  • sloc: ruby: 1,990; makefile: 4; sh: 4
file content (221 lines) | stat: -rw-r--r-- 5,757 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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# frozen_string_literal: true

require "tempfile"
require "securerandom"
require "io/console"

module TTY
  class Command
    module ChildProcess
      # Execute command in a child process with all IO streams piped
      # in and out. The interface is similar to Process.spawn
      #
      # The caller should ensure that all IO objects are closed
      # when the child process is finished. However, when block
      # is provided this will be taken care of automatically.
      #
      # @param [Cmd] cmd
      #   the command to spawn
      #
      # @return [pid, stdin, stdout, stderr]
      #
      # @api public
      def spawn(cmd)
        process_opts = normalize_redirect_options(cmd.options)
        binmode = cmd.options[:binmode] || false
        pty     = cmd.options[:pty] || false
        verbose = cmd.options[:verbose]

        pty = try_loading_pty(verbose) if pty
        require("pty") if pty # load within this scope

        # Create pipes
        in_rd,  in_wr  = pty ? PTY.open : IO.pipe("utf-8") # reading
        out_rd, out_wr = pty ? PTY.open : IO.pipe("utf-8") # writing
        err_rd, err_wr = pty ? PTY.open : IO.pipe("utf-8") # error
        in_wr.sync = true

        if binmode
          in_wr.binmode
          out_rd.binmode
          err_rd.binmode
        end

        if pty
          in_wr.raw!
          out_wr.raw!
          err_wr.raw!
        end

        # redirect fds
        opts = {
          in: in_rd,
          out: out_wr,
          err: err_wr
        }
        unless TTY::Command.windows?
          close_child_fds = {
            in_wr  => :close,
            out_rd => :close,
            err_rd => :close
          }
          opts.merge!(close_child_fds)
        end
        opts.merge!(process_opts)

        pid = Process.spawn(cmd.to_command, opts)

        # close streams in parent process talking to the child
        close_fds(in_rd, out_wr, err_wr)

        tuple = [pid, in_wr, out_rd, err_rd]

        if block_given?
          begin
            return yield(*tuple)
          ensure
            # ensure parent pipes are closed
            close_fds(in_wr, out_rd, err_rd)
          end
        else
          tuple
        end
      end
      module_function :spawn

      # Close all streams
      # @api private
      def close_fds(*fds)
        fds.each { |fd| fd && !fd.closed? && fd.close }
      end
      module_function :close_fds

      # Try loading pty module
      #
      # @return [Boolean]
      #
      # @api private
      def try_loading_pty(verbose = false)
        require 'pty'
        true
      rescue LoadError
        warn("Requested PTY device but the system doesn't support it.") if verbose
        false
      end
      module_function :try_loading_pty

      # Normalize spawn fd into :in, :out, :err keys.
      #
      # @return [Hash]
      #
      # @api private
      def normalize_redirect_options(options)
        options.reduce({}) do |opts, (key, value)|
          if fd?(key)
            spawn_key, spawn_value = convert(key, value)
            opts[spawn_key] = spawn_value
          elsif key.is_a?(Array) && key.all?(&method(:fd?))
            key.each do |k|
              spawn_key, spawn_value = convert(k, value)
              opts[spawn_key] = spawn_value
            end
          end
          opts
        end
      end
      module_function :normalize_redirect_options

      # Convert option pari to recognized spawn option pair
      #
      # @api private
      def convert(spawn_key, spawn_value)
        key   = fd_to_process_key(spawn_key)
        value = spawn_value

        if key.to_s == "in"
          value = convert_to_fd(spawn_value)
        end

        if fd?(spawn_value)
          value = fd_to_process_key(spawn_value)
          value = [:child, value] # redirect in child process
        end
        [key, value]
      end
      module_function :convert

      # Determine if object is a fd
      #
      # @return [Boolean]
      #
      # @api private
      def fd?(object)
        case object
        when :stdin, :stdout, :stderr, :in, :out, :err,
             STDIN, STDOUT, STDERR, $stdin, $stdout, $stderr, ::IO
          true
        when ::Integer
          object >= 0
        else
          respond_to?(:to_i) && !object.to_io.nil?
        end
      end
      module_function :fd?

      # Convert fd to name :in, :out, :err
      #
      # @api private
      def fd_to_process_key(object)
        case object
        when STDIN, $stdin, :in, :stdin, 0
          :in
        when STDOUT, $stdout, :out, :stdout, 1
          :out
        when STDERR, $stderr, :err, :stderr, 2
          :err
        when Integer
          object >= 0 ? IO.for_fd(object) : nil
        when IO
          object
        when respond_to?(:to_io)
          object.to_io
        else
          raise ExecuteError, "Wrong execute redirect: #{object.inspect}"
        end
      end
      module_function :fd_to_process_key

      # Convert file name to file handle
      #
      # @api private
      def convert_to_fd(object)
        return object if fd?(object)

        if object.is_a?(::String) && ::File.exist?(object)
          return object
        end

        tmp = ::Tempfile.new(::SecureRandom.uuid.split("-")[0])
        content = try_reading(object)
        tmp.write(content)
        tmp.rewind
        tmp
      end
      module_function :convert_to_fd

      # Attempts to read object content
      #
      # @api private
      def try_reading(object)
        if object.respond_to?(:read)
          object.read
        elsif object.respond_to?(:to_s)
          object.to_s
        else
          object
        end
      end
      module_function :try_reading
    end # ChildProcess
  end # Command
end # TTY