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
|