class Toys::Utils::Exec::Executor

An object that manages the execution of a subcommand @private

Public Class Methods

new(exec_opts, spawn_cmd, block) click to toggle source
# File lib/toys/utils/exec.rb, line 938
def initialize(exec_opts, spawn_cmd, block)
  @fork_func = spawn_cmd.respond_to?(:call) ? spawn_cmd : nil
  @spawn_cmd = spawn_cmd.respond_to?(:call) ? nil : spawn_cmd
  @config_opts = exec_opts.config_opts
  @spawn_opts = exec_opts.spawn_opts
  @captures = {}
  @controller_streams = {}
  @join_threads = []
  @child_streams = []
  @parent_streams = []
  @block = block
  @default_stream = @config_opts[:background] ? :null : :inherit
  @mutex = ::Mutex.new
end

Public Instance Methods

execute() click to toggle source
# File lib/toys/utils/exec.rb, line 953
def execute
  setup_in_stream
  setup_out_stream(:out)
  setup_out_stream(:err)
  log_command
  controller = start_with_controller
  return controller if @config_opts[:background]
  begin
    @block&.call(controller)
    controller.result
  ensure
    controller.close_streams(:both)
  end
end

Private Instance Methods

capture_stream_thread(key) click to toggle source
# File lib/toys/utils/exec.rb, line 1299
def capture_stream_thread(key)
  stream = make_out_pipe(key)
  @join_threads << ::Thread.new do
    begin
      data = stream.read
      @mutex.synchronize do
        @captures[key] = data
      end
    ensure
      stream.close
    end
  end
end
copy_from_out_thread(key, io) click to toggle source
# File lib/toys/utils/exec.rb, line 1287
def copy_from_out_thread(key, io)
  stream = make_out_pipe(key)
  @join_threads << ::Thread.new do
    begin
      ::IO.copy_stream(stream, io)
    ensure
      stream.close
      io.close
    end
  end
end
copy_to_in_thread(io) click to toggle source
# File lib/toys/utils/exec.rb, line 1275
def copy_to_in_thread(io)
  stream = make_in_pipe
  @join_threads << ::Thread.new do
    begin
      ::IO.copy_stream(io, stream)
    ensure
      stream.close
      io.close
    end
  end
end
default_log_str(spawn_cmd) click to toggle source
# File lib/toys/utils/exec.rb, line 978
def default_log_str(spawn_cmd)
  return nil unless spawn_cmd
  return spawn_cmd.first if spawn_cmd.size == 1 && spawn_cmd.first.is_a?(::String)
  cmd_binary = spawn_cmd.first
  cmd_binary = cmd_binary.first if cmd_binary.is_a?(::Array)
  ([cmd_binary] + spawn_cmd[1..-1]).inspect
end
interpret_in_array(setting) click to toggle source
# File lib/toys/utils/exec.rb, line 1132
def interpret_in_array(setting)
  case setting.first
  when ::Symbol
    setup_in_stream_of_type(setting.first, setting[1..-1])
  when ::String
    setup_in_stream_of_type(:file, setting)
  else
    raise "Unknown value for in: #{setting.inspect}"
  end
end
interpret_in_file(args) click to toggle source
# File lib/toys/utils/exec.rb, line 1168
def interpret_in_file(args)
  raise "Expected only file name" unless args.size == 1 && args.first.is_a?(::String)
  @spawn_opts[:in] = args + [::File::RDONLY]
end
interpret_in_io(setting) click to toggle source
# File lib/toys/utils/exec.rb, line 1124
def interpret_in_io(setting)
  if setting.fileno.is_a?(::Integer)
    setup_in_stream_of_type(:parent, [setting.fileno])
  else
    setup_in_stream_of_type(:copy_io, [setting])
  end
end
interpret_out_array(key, setting) click to toggle source
# File lib/toys/utils/exec.rb, line 1199
def interpret_out_array(key, setting)
  case setting.first
  when ::Symbol
    setup_out_stream_of_type(key, setting.first, setting[1..-1])
  when ::String
    setup_out_stream_of_type(key, :file, setting)
  else
    raise "Unknown value for #{key}: #{setting.inspect}"
  end
end
interpret_out_array_within_fork(stream) click to toggle source
# File lib/toys/utils/exec.rb, line 1092
def interpret_out_array_within_fork(stream)
  if stream.first == :child
    case stream[1]
    when :err
      $stderr
    when :out
      $stdout
    end
  else
    ::File.open(*stream)
  end
end
interpret_out_file(key, args) click to toggle source
# File lib/toys/utils/exec.rb, line 1235
def interpret_out_file(key, args)
  raise "Expected file name" if args.empty? || !args.first.is_a?(::String)
  raise "Too many file arguments" if args.size > 3
  @spawn_opts[key] = args.size == 1 ? args.first : args
end
interpret_out_io(key, setting) click to toggle source
# File lib/toys/utils/exec.rb, line 1191
def interpret_out_io(key, setting)
  if setting.fileno.is_a?(::Integer)
    setup_out_stream_of_type(key, :parent, [setting.fileno])
  else
    setup_out_stream_of_type(key, :copy_io, [setting])
  end
end
log_command() click to toggle source
# File lib/toys/utils/exec.rb, line 970
def log_command
  logger = @config_opts[:logger]
  if logger && @config_opts[:log_level] != false
    cmd_str = @config_opts[:log_cmd] || default_log_str(@spawn_cmd)
    logger.add(@config_opts[:log_level] || ::Logger::INFO, cmd_str) if cmd_str
  end
end
make_in_pipe() click to toggle source
# File lib/toys/utils/exec.rb, line 1247
def make_in_pipe
  r, w = ::IO.pipe
  @spawn_opts[:in] = r
  @child_streams << r
  @parent_streams << w
  w.sync = true
  w
end
make_null_stream(key, mode) click to toggle source
# File lib/toys/utils/exec.rb, line 1241
def make_null_stream(key, mode)
  f = ::File.open(::File::NULL, mode)
  @spawn_opts[key] = f
  @child_streams << f
end
make_out_pipe(key) click to toggle source
# File lib/toys/utils/exec.rb, line 1256
def make_out_pipe(key)
  r, w = ::IO.pipe
  @spawn_opts[key] = w
  @child_streams << w
  @parent_streams << r
  r
end
run_fork_func() click to toggle source
# File lib/toys/utils/exec.rb, line 1022
def run_fork_func
  catch(:result) do
    if @spawn_opts[:chdir]
      ::Dir.chdir(@spawn_opts[:chdir]) { @fork_func.call(@config_opts) }
    else
      @fork_func.call(@config_opts)
    end
    0
  end
end
setup_env_within_fork() click to toggle source
# File lib/toys/utils/exec.rb, line 1033
def setup_env_within_fork
  if @config_opts[:unsetenv_others]
    ::ENV.each_key do |k|
      ::ENV.delete(k) unless @config_opts.key?(k)
    end
  end
  (@config_opts[:env] || {}).each { |k, v| ::ENV[k.to_s] = v.to_s }
end
setup_in_stream() click to toggle source
# File lib/toys/utils/exec.rb, line 1105
def setup_in_stream
  setting = @config_opts[:in] || @default_stream
  return unless setting
  case setting
  when ::Symbol
    setup_in_stream_of_type(setting, [])
  when ::Integer
    setup_in_stream_of_type(:parent, [setting])
  when ::String
    setup_in_stream_of_type(:file, [setting])
  when ::IO, ::StringIO
    interpret_in_io(setting)
  when ::Array
    interpret_in_array(setting)
  else
    raise "Unknown value for in: #{setting.inspect}"
  end
end
setup_in_stream_of_type(type, args) click to toggle source
# File lib/toys/utils/exec.rb, line 1143
def setup_in_stream_of_type(type, args)
  case type
  when :controller
    @controller_streams[:in] = make_in_pipe
  when :null
    make_null_stream(:in, "r")
  when :inherit
    @spawn_opts[:in] = :in
  when :close
    @spawn_opts[:in] = type
  when :parent
    @spawn_opts[:in] = args.first
  when :child
    @spawn_opts[:in] = [:child, args.first]
  when :string
    write_string_thread(args.first.to_s)
  when :copy_io
    copy_to_in_thread(args.first)
  when :file
    interpret_in_file(args)
  else
    raise "Unknown type for in: #{type.inspect}"
  end
end
setup_in_stream_within_fork(stream, stdstream) click to toggle source
# File lib/toys/utils/exec.rb, line 1049
def setup_in_stream_within_fork(stream, stdstream)
  in_stream =
    case stream
    when ::Integer
      ::IO.open(stream)
    when ::Array
      ::File.open(*stream)
    when ::String
      ::File.open(stream, "r")
    when :close
      :close
    else
      stream if stream.respond_to?(:write)
    end
  if in_stream == :close
    stdstream.close
  elsif in_stream
    stdstream.reopen(in_stream)
  end
end
setup_out_stream(key) click to toggle source
# File lib/toys/utils/exec.rb, line 1173
def setup_out_stream(key)
  setting = @config_opts[key] || @default_stream
  case setting
  when ::Symbol
    setup_out_stream_of_type(key, setting, [])
  when ::Integer
    setup_out_stream_of_type(key, :parent, [setting])
  when ::String
    setup_out_stream_of_type(key, :file, [setting])
  when ::IO, ::StringIO
    interpret_out_io(key, setting)
  when ::Array
    interpret_out_array(key, setting)
  else
    raise "Unknown value for #{key}: #{setting.inspect}"
  end
end
setup_out_stream_of_type(key, type, args) click to toggle source
# File lib/toys/utils/exec.rb, line 1210
def setup_out_stream_of_type(key, type, args)
  case type
  when :controller
    @controller_streams[key] = make_out_pipe(key)
  when :null
    make_null_stream(key, "w")
  when :inherit
    @spawn_opts[key] = key
  when :close, :out, :err
    @spawn_opts[key] = type
  when :parent
    @spawn_opts[key] = args.first
  when :child
    @spawn_opts[key] = [:child, args.first]
  when :capture
    capture_stream_thread(key)
  when :copy_io
    copy_from_out_thread(key, args.first)
  when :file
    interpret_out_file(key, args)
  else
    raise "Unknown type for #{key}: #{type.inspect}"
  end
end
setup_out_stream_within_fork(stream, stdstream) click to toggle source
# File lib/toys/utils/exec.rb, line 1070
def setup_out_stream_within_fork(stream, stdstream)
  out_stream =
    case stream
    when ::Integer
      ::IO.open(stream)
    when ::Array
      interpret_out_array_within_fork(stream)
    when ::String
      ::File.open(stream, "w")
    when :close
      :close
    else
      stream if stream.respond_to?(:write)
    end
  if out_stream == :close
    stdstream.close
  elsif out_stream
    stdstream.reopen(out_stream)
    stdstream.sync = true
  end
end
setup_streams_within_fork() click to toggle source
# File lib/toys/utils/exec.rb, line 1042
def setup_streams_within_fork
  @parent_streams.each(&:close)
  setup_in_stream_within_fork(@spawn_opts[:in], $stdin)
  setup_out_stream_within_fork(@spawn_opts[:out], $stdout)
  setup_out_stream_within_fork(@spawn_opts[:err], $stderr)
end
start_fork() click to toggle source
# File lib/toys/utils/exec.rb, line 1005
def start_fork
  pid = ::Process.fork
  return pid unless pid.nil?
  exit_code = -1
  begin
    setup_env_within_fork
    setup_streams_within_fork
    exit_code = run_fork_func
  rescue ::SystemExit => e
    exit_code = e.status
  rescue ::Exception => e # rubocop:disable Lint/RescueException
    warn(([e.inspect] + e.backtrace).join("\n"))
  ensure
    ::Kernel.exit!(exit_code)
  end
end
start_process() click to toggle source
# File lib/toys/utils/exec.rb, line 998
def start_process
  args = []
  args << @config_opts[:env] if @config_opts[:env]
  args.concat(@spawn_cmd)
  ::Process.spawn(*args, @spawn_opts)
end
start_with_controller() click to toggle source
# File lib/toys/utils/exec.rb, line 986
def start_with_controller
  pid =
    begin
      @fork_func ? start_fork : start_process
    rescue ::StandardError => e
      e
    end
  @child_streams.each(&:close)
  Controller.new(@config_opts[:name], @controller_streams, @captures, pid,
                 @join_threads, @config_opts[:result_callback], @mutex)
end
write_string_thread(string) click to toggle source
# File lib/toys/utils/exec.rb, line 1264
def write_string_thread(string)
  stream = make_in_pipe
  @join_threads << ::Thread.new do
    begin
      stream.write string
    ensure
      stream.close
    end
  end
end