Class: LittleGhost::Sandbox::ProcessRunner

Inherits:
Object
  • Object
show all
Defined in:
lib/little_ghost/sandbox/process_runner.rb

Overview

Runs one bounded child process and terminates its entire process group when the Run is cancelled or the command exceeds its timeout.

Constant Summary collapse

POLL_INTERVAL =

:nodoc:

0.01
TERMINATION_GRACE =
0.5

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(command:, timeout:, context: nil, max_output_bytes: 1_000_000, environment: {}, inherit_environment: false, chdir: nil) ⇒ ProcessRunner

Returns a new instance of ProcessRunner.

Raises:



15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/little_ghost/sandbox/process_runner.rb', line 15

def initialize(command:, timeout:, context: nil, max_output_bytes: 1_000_000,
  environment: {}, inherit_environment: false, chdir: nil)
  @command = Array(command).map(&:to_s)
  @timeout = Float(timeout)
  @context = context
  @max_output_bytes = Integer(max_output_bytes)
  @environment = environment.transform_keys(&:to_s).transform_values(&:to_s)
  @inherit_environment = inherit_environment
  @chdir = chdir

  raise ToolError, "Command must contain an executable" if @command.empty? || @command.first.empty?
  raise ArgumentError, "timeout must be positive" unless @timeout.positive? && @timeout.finite?
  raise ArgumentError, "max_output_bytes must be positive" unless @max_output_bytes.positive?
end

Class Method Details

.runObject



13
# File 'lib/little_ghost/sandbox/process_runner.rb', line 13

def self.run(...) = new(...).run

Instance Method Details

#runObject



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/little_ghost/sandbox/process_runner.rb', line 30

def run
  result = nil
  options = {pgroup: true, unsetenv_others: !@inherit_environment}
  options[:chdir] = @chdir if @chdir
  Open3.popen3(@environment, *@command, **options) do |stdin, stdout, stderr, wait_thread|
    stdin.close
    stdout_reader = Thread.new { drain(stdout) }
    stderr_reader = Thread.new { drain(stderr) }
    wait_for(wait_thread, [stdout_reader, stderr_reader])
    result = Execution.new(
      stdout: stdout_reader.value,
      stderr: stderr_reader.value,
      exit_code: wait_thread.value.exitstatus
    )
  ensure
    stdout_reader&.kill
    stderr_reader&.kill
  end
  result
rescue Errno::ENOENT
  Execution.new(stdout: "", stderr: "#{@command.first}: command not found\n", exit_code: 127)
end