lib/process_executer/options.rb
# frozen_string_literal: true require 'forwardable' require 'ostruct' module ProcessExecuter # Validate ProcessExecuter::Executer#spawn options and return Process.spawn options # # Valid options are those accepted by Process.spawn plus the following additions: # # * `:timeout`: # # @api public # class Options # :nocov: # SimpleCov on JRuby seems to hav a bug that causes hashes declared on multiple lines # to not be counted as covered. # These options should be passed to `Process.spawn` # # Additionally, any options whose key is an Integer or an IO object will # be passed to `Process.spawn`. # SPAWN_OPTIONS = %i[ in out err unsetenv_others pgroup new_pgroup rlimit_resourcename umask close_others chdir ].freeze # These options are allowed but should NOT be passed to `Process.spawn` # NON_SPAWN_OPTIONS = %i[ timeout ].freeze # Any `SPAWN_OPTIONS`` set to this value will not be passed to `Process.spawn` # NOT_SET = :not_set # The default values for all options # @return [Hash] DEFAULTS = { in: NOT_SET, out: NOT_SET, err: NOT_SET, unsetenv_others: NOT_SET, pgroup: NOT_SET, new_pgroup: NOT_SET, rlimit_resourcename: NOT_SET, umask: NOT_SET, close_others: NOT_SET, chdir: NOT_SET, timeout: nil }.freeze # :nocov: # All options allowed by this class # ALL_OPTIONS = (SPAWN_OPTIONS + NON_SPAWN_OPTIONS).freeze # Create accessor functions for all options. Assumes that the options are stored # in a hash named `@options` # ALL_OPTIONS.each do |option| define_method(option) do @options[option] end end # Create a new Options object # # @example # options = ProcessExecuter::Options.new(out: $stdout, err: $stderr, timeout: 10) # # @param options [Hash] Process.spawn options plus additional options listed below. # # See [Process.spawn](https://ruby-doc.org/core/Process.html#method-c-spawn) # for a list of valid. # # @option options [Integer, Float, nil] :timeout # Number of seconds to wait for the process to terminate. Any number # may be used, including Floats to specify fractional seconds. A value of 0 or nil # will allow the process to run indefinitely. # def initialize(**options) assert_no_unknown_options(options) @options = DEFAULTS.merge(options) end # Returns the options to be passed to Process.spawn # # @example # options = ProcessExecuter::Options.new(out: $stdout, err: $stderr, timeout: 10) # options.spawn_options # => { out: $stdout, err: $stderr } # # @return [Hash] # def spawn_options {}.tap do |spawn_options| options.each do |option, value| spawn_options[option] = value if include_spawn_option?(option, value) end end end private # @!attribute [r] # # Options with values # # All options have values. If an option is not given in the initializer, it # will have the value `NOT_SET`. # # @return [Hash<Symbol, Object>] # # @api private # attr_reader :options # Determine if the options hash contains any unknown options # @param options [Hash] the hash of options # @return [void] # @raise [ArgumentError] if the options hash contains any unknown options # @api private def assert_no_unknown_options(options) unknown_options = options.keys.reject { |key| valid_option?(key) } raise ArgumentError, "Unknown options: #{unknown_options.join(', ')}" unless unknown_options.empty? end # Determine if the given option is a valid option # @param option [Symbol] the option to be tested # @return [Boolean] true if the given option is a valid option # @api private def valid_option?(option) ALL_OPTIONS.include?(option) || option.is_a?(Integer) || option.respond_to?(:fileno) end # Determine if the given option should be passed to `Process.spawn` # @param option [Symbol, Integer, IO] the option to be tested # @param value [Object] the value of the option # @return [Boolean] true if the given option should be passed to `Process.spawn` # @api private def include_spawn_option?(option, value) (option.is_a?(Integer) || option.is_a?(IO) || SPAWN_OPTIONS.include?(option)) && value != NOT_SET end end end