Module: ProcessExecuter

Defined in:
lib/process_executer.rb,
lib/process_executer/errors.rb,
lib/process_executer/result.rb,
lib/process_executer/runner.rb,
lib/process_executer/options.rb,
lib/process_executer/version.rb,
lib/process_executer/destinations.rb,
lib/process_executer/options/base.rb,
lib/process_executer/monitored_pipe.rb,
lib/process_executer/destinations/io.rb,
lib/process_executer/destination_base.rb,
lib/process_executer/destinations/tee.rb,
lib/process_executer/destinations/close.rb,
lib/process_executer/destinations/stderr.rb,
lib/process_executer/destinations/stdout.rb,
lib/process_executer/destinations/writer.rb,
lib/process_executer/options/run_options.rb,
lib/process_executer/options/spawn_options.rb,
lib/process_executer/destinations/file_path.rb,
lib/process_executer/options/option_definition.rb,
lib/process_executer/destinations/file_path_mode.rb,
lib/process_executer/destinations/monitored_pipe.rb,
lib/process_executer/destinations/file_descriptor.rb,
lib/process_executer/destinations/child_redirection.rb,
lib/process_executer/options/spawn_and_wait_options.rb,
lib/process_executer/destinations/file_path_mode_perms.rb

Overview

rubocop:disable Layout/LineLength

Defined Under Namespace

Modules: Destinations, Options Classes: CommandError, DestinationBase, Error, FailedError, MonitoredPipe, ProcessIOError, Result, Runner, SignaledError, SpawnError, TimeoutError

Constant Summary collapse

VERSION =

The current Gem version

'3.2.1'

Class Method Summary collapse

Class Method Details

.run(*command, **options_hash) ⇒ ProcessExecuter::Result

Execute the given command as a subprocess blocking until it finishes

Works just like spawn, but does the following in addition:

  1. If nothing is specified for out, stdout is captured to a StringIO object which can be accessed via the Result object in result.options.out. The same applies to err.

  2. out and err are automatically wrapped in a ProcessExecuter::MonitoredPipe object so that any object that implements #write (or an Array of such objects) can be given for out and err.

  3. Raises one of the following errors unless raise_errors is explicitly set to false:

 * `ProcessExecuter::FailedError` if the command returns a non-zero
   exitstatus
 * `ProcessExecuter::SignaledError` if the command exits because of
   an unhandled signal
 * `ProcessExecuter::TimeoutError` if the command times out

 If `raise_errors` is false, the returned Result object will contain the error.
  1. Raises a ProcessExecuter::ProcessIOError if an exception is raised while collecting subprocess output. This can not be turned off.

  2. If a logger is provided, it will be used to log:

 * The command that was executed and its status to `info` level
 * The stdout and stderr output to `debug` level

By default, Logger.new(nil) is used for the logger.

This method takes two forms:

  1. The command is executed via a shell when the command is given as a single string:

    ProcessExecuter.run([env, ] command_line, options = {}) -> Result

  2. The command is executed directly (bypassing the shell) when the command and it arguments are given as an array of strings:

    ProcessExecuter.run([env, ] exe_path, *args, options = {}) -> Result

Optional argument env is a hash that affects ENV for the new process; see Execution Environment.

Argument options is a hash of options for the new process. See the options listed below.

Examples:

Run a command given as a single string (uses shell)

# The command must be properly shell escaped when passed as a single string.
command = 'echo "stdout: `pwd`" && echo "stderr: $HOME" 1>&2'
result = ProcessExecuter.run(command)
result.success? #=> true
result.stdout #=> "stdout: /Users/james/projects/main-branch/process_executer\n"
result.stderr #=> "stderr: /Users/james\n"

Run a command given as an array of strings (does not use shell)

# The command and its args must be provided as separate strings in the array.
# Shell expansions and redirections are not supported.
command = ['git', 'clone', 'https://github.com/main-branch/process_executer']
result = ProcessExecuter.run(*command)
result.success? #=> true
result.stdout #=> ""
result.stderr #=> "Cloning into 'process_executer'...\n"

Run a command with a timeout

command = ['sleep', '1']
result = ProcessExecuter.run(*command, timeout_after: 0.01)
#=> raises ProcessExecuter::TimeoutError which contains the command result

Run a command which fails

command = ['exit 1']
result = ProcessExecuter.run(*command)
#=> raises ProcessExecuter::FailedError which contains the command result

Run a command which exits due to an unhandled signal

command = ['kill -9 $$']
result = ProcessExecuter.run(*command)
#=> raises ProcessExecuter::SignaledError which contains the command result

Do not raise an error when the command fails

command = ['echo "Some error" 1>&2 && exit 1']
result = ProcessExecuter.run(*command, raise_errors: false)
result.success? #=> false
result.exitstatus #=> 1
result.stdout #=> ""
result.stderr #=> "Some error\n"

Set environment variables

env = { 'FOO' => 'foo', 'BAR' => 'bar' }
command = 'echo "$FOO$BAR"'
result = ProcessExecuter.run(env, *command)
result.stdout #=> "foobar\n"

Set environment variables when using a command array

env = { 'FOO' => 'foo', 'BAR' => 'bar' }
command = ['ruby', '-e', 'puts ENV["FOO"] + ENV["BAR"]']
result = ProcessExecuter.run(env, *command)
result.stdout #=> "foobar\n"

Unset environment variables

env = { 'FOO' => nil } # setting to nil unsets the variable in the environment
command = ['echo "FOO: $FOO"']
result = ProcessExecuter.run(env, *command)
result.stdout #=> "FOO: \n"

Reset existing environment variables and add new ones

env = { 'PATH' => '/bin' }
result = ProcessExecuter.run(env, 'echo "Home: $HOME" && echo "Path: $PATH"', unsetenv_others: true)
result.stdout #=> "Home: \n/Path: /bin\n"

Run command in a different directory

command = ['pwd']
result = ProcessExecuter.run(*command, chdir: '/tmp')
result.stdout #=> "/tmp\n"

Capture stdout and stderr into a single buffer

command = ['echo "stdout" && echo "stderr" 1>&2']
result = ProcessExecuter.run(*command, [out:, err:]: StringIO.new)
result.stdout #=> "stdout\nstderr\n"
result.stderr #=> "stdout\nstderr\n"
result.stdout.object_id == result.stderr.object_id #=> true

Capture to an explicit buffer

out = StringIO.new
err = StringIO.new
command = ['echo "stdout" && echo "stderr" 1>&2']
result = ProcessExecuter.run(*command, out: out, err: err)
out.string #=> "stdout\n"
err.string #=> "stderr\n"

Capture to a file

# Same technique can be used for stderr
out = File.open('stdout.txt', 'w')
err = StringIO.new
command = ['echo "stdout" && echo "stderr" 1>&2']
result = ProcessExecuter.run(*command, out: out, err: err)
out.close
File.read('stdout.txt') #=> "stdout\n"
# stderr is still captured to a StringIO buffer internally
result.stderr #=> "stderr\n"

Capture to multiple destinations (e.g. files, buffers, STDOUT, etc.)

# Same technique can be used for stderr
out_buffer = StringIO.new
out_file = File.open('stdout.txt', 'w')
command = ['echo "stdout" && echo "stderr" 1>&2']
result = ProcessExecuter.run(*command, out: [:tee, out_buffer, out_file])
# You must manage closing resources you create yourself
out_file.close
out_buffer.string #=> "stdout\n"
File.read('stdout.txt') #=> "stdout\n"
result.stdout #=> "stdout\n"

Parameters:

  • command (Array<String>)

    The command to run

    If the first element of command is a Hash, it is added to the ENV of the new process. See Execution Environment for more details. The env hash is then removed from the command array.

    If the first and only (remaining) command element is a string, it is passed to a subshell if it begins with a shell reserved word, contains special built-ins, or includes shell metacharacters.

    Care must be taken to properly escape shell metacharacters in the command string.

    Otherwise, the command is run bypassing the shell. When bypassing the shell, shell expansions and redirections are not supported.

  • options_hash (Hash)

    Additional options

Options Hash (**options_hash):

  • :timeout_after (Numeric)

    The maximum seconds to wait for the command to complete

    If zero or nil, the command will not time out. If the command times out, it is killed via a SIGKILL signal. A TimeoutError will be raised if the :raise_errors option is true.

    If the command does not exit when receiving the SIGKILL signal, this method may hang indefinitely.

  • :out (#write) — default: nil

    The object to write stdout to

  • :err (#write) — default: nil

    The object to write stderr to

  • :raise_errors (Boolean) — default: true

    Raise an exception if the command fails

  • :unsetenv_others (Boolean) — default: false

    If true, unset all environment variables before applying the new ones

  • :pgroup (true, Integer, nil) — default: nil

    true or 0: new process group; non-zero: join the group, nil: existing group

  • :new_pgroup (Boolean) — default: nil

    Create a new process group (Windows only)

  • :rlimit_resource_name (Integer) — default: nil

    Set resource limits (see Process.setrlimit)

  • :umask (Integer) — default: nil

    Set the umask (see File.umask)

  • :close_others (Boolean) — default: false

    If true, close non-standard file descriptors

  • :chdir (String) — default: nil

    The directory to run the command in

  • :logger (Logger)

    The logger to use

Returns:

Raises:



298
299
300
301
# File 'lib/process_executer.rb', line 298

def self.run(*command, **options_hash)
  options = ProcessExecuter.run_options(options_hash)
  run_with_options(command, options)
end

.run_options(obj) ⇒ RunOptions

Convert a hash to a RunOptions object

Examples:

options_hash = { out: $stdout }
options = ProcessExecuter.run_options(options_hash) # =>
  #<ProcessExecuter::Options::RunOptions:0x00007f8f9b0b3d20 out: $stdout>
ProcessExecuter.run_options(options) # =>
  #<ProcessExecuter::Options::RunOptions:0x00007f8f9b0b3d20 out: $stdout>

Parameters:

  • obj (Hash, RunOptions)

    the object to be converted

Returns:

  • (RunOptions)

Raises:

  • (ArgumentError)

    if obj is not a Hash or SpawnOptions



431
432
433
434
435
436
437
438
439
440
# File 'lib/process_executer.rb', line 431

def self.run_options(obj)
  case obj
  when ProcessExecuter::Options::RunOptions
    obj
  when Hash
    ProcessExecuter::Options::RunOptions.new(**obj)
  else
    raise ArgumentError, "Expected a Hash or ProcessExecuter::Options::RunOptions but got a #{obj.class}"
  end
end

.run_with_options(command, options) ⇒ ProcessExecuter::Result

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Run a command with the given options

Parameters:

Returns:

See Also:



313
314
315
# File 'lib/process_executer.rb', line 313

def self.run_with_options(command, options)
  ProcessExecuter::Runner.new.call(command, options)
end

.spawn_and_wait(*command, **options_hash) ⇒ ProcessExecuter::Result

Run a command in a subprocess, wait for it to finish, then return the result

This method is a thin wrapper around Process.spawn and blocks until the command terminates.

A timeout may be specified with the :timeout_after option. The command will be sent the SIGKILL signal if it does not terminate within the specified timeout.

Examples:

result = ProcessExecuter.spawn_and_wait('echo hello')
result.exited? # => true
result.success? # => true
result.timed_out? # => false

with a timeout

result = ProcessExecuter.spawn_and_wait('sleep 10', timeout_after: 0.01)
result.exited? # => false
result.success? # => nil
result.signaled? # => true
result.termsig # => 9
result.timed_out? # => true

capturing stdout to a string

stdout_buffer = StringIO.new
stdout_pipe = ProcessExecuter::MonitoredPipe.new(stdout_buffer)
result = ProcessExecuter.spawn_and_wait('echo hello', out: stdout_pipe)
stdout_buffer.string # => "hello\n"

Parameters:

  • command (Array<String>)

    The command to execute

  • options_hash (Hash)

    The options to use when executing the command

Returns:

See Also:



75
76
77
78
# File 'lib/process_executer.rb', line 75

def self.spawn_and_wait(*command, **options_hash)
  options = ProcessExecuter.spawn_and_wait_options(options_hash)
  spawn_and_wait_with_options(command, options)
end

.spawn_and_wait_options(obj) ⇒ SpawnAndWaitOptions

Convert a hash to a SpawnAndWaitOptions object

Examples:

options_hash = { out: $stdout }
options = ProcessExecuter.spawn_and_wait_options(options_hash) # =>
  #<ProcessExecuter::Options::SpawnAndWaitOptions:0x00007f8f9b0b3d20 out: $stdout>
ProcessExecuter.spawn_and_wait_options(options) # =>
  #<ProcessExecuter::Options::SpawnAndWaitOptions:0x00007f8f9b0b3d20 out: $stdout>

Parameters:

  • obj (Hash, SpawnAndWaitOptions)

    the object to be converted

Returns:

  • (SpawnAndWaitOptions)

Raises:

  • (ArgumentError)

    if obj is not a Hash or SpawnOptions



403
404
405
406
407
408
409
410
411
412
# File 'lib/process_executer.rb', line 403

def self.spawn_and_wait_options(obj)
  case obj
  when ProcessExecuter::Options::SpawnAndWaitOptions
    obj
  when Hash
    ProcessExecuter::Options::SpawnAndWaitOptions.new(**obj)
  else
    raise ArgumentError, "Expected a Hash or ProcessExecuter::Options::SpawnAndWaitOptions but got a #{obj.class}"
  end
end

.spawn_and_wait_with_options(command, options) ⇒ ProcessExecuter::Result

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Run a command in a subprocess, wait for it to finish, then return the result

Parameters:

Returns:

See Also:



89
90
91
92
93
94
95
96
# File 'lib/process_executer.rb', line 89

def self.spawn_and_wait_with_options(command, options)
  begin
    pid = Process.spawn(*command, **options.spawn_options)
  rescue StandardError => e
    raise ProcessExecuter::SpawnError, "Failed to spawn process: #{e.message}"
  end
  wait_for_process(pid, command, options)
end

.spawn_options(obj) ⇒ SpawnOptions

Convert a hash to a SpawnOptions object

Examples:

options_hash = { out: $stdout }
options = ProcessExecuter.spawn_options(options_hash) # =>
  #<ProcessExecuter::Options::SpawnOptions:0x00007f8f9b0b3d20 out: $stdout>
ProcessExecuter.spawn_options(options) # =>
  #<ProcessExecuter::Options::SpawnOptions:0x00007f8f9b0b3d20 out: $stdout>

Parameters:

  • obj (Hash, SpawnOptions)

    the object to be converted

Returns:

  • (SpawnOptions)

Raises:

  • (ArgumentError)

    if obj is not a Hash or SpawnOptions



375
376
377
378
379
380
381
382
383
384
# File 'lib/process_executer.rb', line 375

def self.spawn_options(obj)
  case obj
  when ProcessExecuter::Options::SpawnOptions
    obj
  when Hash
    ProcessExecuter::Options::SpawnOptions.new(**obj)
  else
    raise ArgumentError, "Expected a Hash or ProcessExecuter::Options::SpawnOptions but got a #{obj.class}"
  end
end