module Lrun

Run program using lrun. Require external lrun binary.

@see Lrun.run @see github.com/quark-zju/lrun lrun project page

Example

Lrun.run('foo', Lrun.merge_options({:max_memory => 2 ** 20, :max_cpu_time => 5}, {:network => false}))

Constants

LRUN_BINARY

Name of lrun executable

LRUN_OPTIONS

Available lrun options, and whether they can occur multiple times (1: no, 2: yes)

LRUN_PATH

Full path of lrun executable, automatically detected using {LRUN_BINARY} and PATH environment variable

TRUNCATE_OUTPUT_LENGTH

Keep how many bytes of stdout and stderr, can be overrided using options[:truncate]

Public Class Methods

available!() click to toggle source

Complain if lrun binary is not available

# File lib/lrun.rb, line 272
def self.available!
  raise LrunError, "#{LRUN_BINARY} not found in PATH. Please install lrun first." unless available?
end
available?() click to toggle source

Check if lrun binary exists

@return [Bool] whether lrun binary is found

# File lib/lrun.rb, line 267
def self.available?
  !LRUN_PATH.nil?
end
merge_options(*options) click to toggle source

Merge options so that it can be used in {Lrun.run}.

@param [Array<Hash>] options options to be merged @return [Hash] merged options, can be used again in {Lrun.merge_options}

Example

Lrun.merge_options({:uid => 1000}, {:gid => 100})
# => {:uid=>1000, :gid=>100}

Lrun.merge_options({:nice => 1, :uid => 1001}, {:nice => 2})
# => {:nice=>2, :uid=>1000}

Lrun.merge_options({:fd => [4, 6]}, {:fd => 5}, {:fd => 7})
# => {:fd=>[4, 6, 5, 7]}

Lrun.merge_options({:env => {'A'=>'1', 'B' => '2'}}, {:env => {'C' => '3'}})
# => {:env=>[["A", "1"], ["B", "2"], ["C", "3"]]}

Lrun.merge_options({:uid => 1000}, {:uid => nil})
# => {}

Lrun.merge_options({:fd => [4]}, {:fd => 5}, {:fd => nil})
# => {}

Lrun.merge_options({:network => true, :chdir => '/tmp', :bindfs => {'/a' => '/b'}},
                   {:network => nil, :bindfs => {'/c' => '/d'}})
# => {:chdir=>"/tmp", :bindfs=>[["/a", "/b"], ["/c", "/d"]]}
# File lib/lrun.rb, line 148
def self.merge_options(*options)
  # Remove nil
  options.compact!

  # Check type of options
  raise TypeError, 'options should be Hash' unless options.all? { |o| o.is_a? Hash }

  # Merge options
  options.inject({}) do |result, option|
    option.each do |k, v|
      # Remove an option using nil
      if v.nil?
        result.delete k
        next
      end

      # Append to or Replace an option
      case LRUN_OPTIONS[k]
      when 2
        # Append to previous options
        result[k] ||= []
        result[k] += [*v]
      else
        # Overwrite previous option
        result[k] = v
      end
    end
    result
  end
end
run(commands, options = {}) click to toggle source

Run program using lrun binary.

@param [Array<String>, String] commands

commands to be executed

@param [Hash] options

options for lrun.
Besides options in {Lrun.LRUN_OPTIONS}, there are some additional options available:

truncate::
  maximum bytes read for stderr and stdout (default: {Lrun.TRUNCATE_OUTPUT_LENGTH}).
stdin::
  stdin file path (default: no input).
stdout::
  stdout file path (default: a tempfile, will be deleted automatically).
  If this option is set, the returned result will have no stdout,
  you should read and delete stdout file manually.
stderr::
  stderr file path (default: a tempfile, will be deleted automatically).
  If this option is set, the returned result will have no stderr,
  you should read and delete stderr file manually.

Note: lrun chroot and mounts does not affect above paths.

@return [Lrun::Result]

Example

Lrun.run('echo hello')
# => #<struct Lrun::Result
#             memory=262144, cputime=0.002,
#             exceed=nil, exitcode=0, signal=nil,
#             stdout="hello\n", stderr="">

Lrun.run('java', :max_memory => 2 ** 19, :stdout => '/tmp/out.txt')
# => #<struct Lrun::Result
#             memory=524288, cputime=0.006,
#             exceed=:memory, exitcode=0, signal=nil,
#             stdout=nil, stderr="">

Lrun.run('sleep 30', :max_real_time => 1, :stderr => '/dev/null')
#  => #<struct Lrun::Result
#              memory=262144, cputime=0.002,
#              exceed=:time, exitcode=0, signal=nil,
#              stdout="", stderr=nil>

Lrun.run('cat', :max_output => 100, :stdin => '/dev/urandom', :truncate => 2)
# => #<struct Lrun::Result
#             memory=782336, cputime=0.05,
#             exceed=:output, exitcode=0, signal=nil,
#             stdout="U\xE1", stderr="">
# File lib/lrun.rb, line 231
def self.run(commands, options = {})
  # Make sure lrun binary is available
  available!

  # Temp files storing stdout and stderr of target process
  tmp_out = tmp_err = nil

  # Create temp stdout, stderr files if user does not redirect them
  options = options.dup
  options[:stdout] ||= (tmp_out = Tempfile.new("lrun.#{$$}.out")).path
  options[:stderr] ||= (tmp_err = Tempfile.new("lrun.#{$$}.err")).path

  IO.pipe do |rfd, wfd|
    # Keep pid of lrun process for checking its status
    pid = spawn_lrun commands, options, wfd

    # Read fd 3, where lrun write its report
    wfd.close
    report = rfd.read

    # Check if lrun exits normally
    stat = Process.wait2(pid)[-1]
    if stat.signaled? || stat.exitstatus != 0
      raise LrunError, "lrun exits abnormally: #{stat}. #{tmp_err.read unless tmp_err.nil?}"
    end

    # Build and return result
    build_result report, tmp_out, tmp_err, options[:truncate]
  end
ensure
  clean_tmpfile [tmp_out, tmp_err]
end

Private Class Methods

build_result(lrun_report, stdout = nil, stderr = nil, truncate = TRUNCATE_OUTPUT_LENGTH) click to toggle source

Build {Lrun::Result} from essential information.

@return [Lrun::Result]

# File lib/lrun.rb, line 344
def self.build_result(lrun_report, stdout = nil, stderr = nil, truncate = TRUNCATE_OUTPUT_LENGTH)
  report = Hash[lrun_report.lines.map{ |l| l.chomp.split(' ', 2)}]

  # Collect information
  memory = report['MEMORY'].to_i
  cputime = report['CPUTIME'].to_f
  exceed = parse_exceed(report['EXCEED'])
  exitcode = report['EXITCODE'].to_i
  signal = report['SIGNALED'].to_i == 0 ? nil : report['TERMSIG'].to_i
  stdout &&= stdout.read(truncate) || ''
  stderr &&= stderr.read(truncate) || ''

  # Build Result
  Result.new(memory, cputime, exceed, exitcode, signal, stdout, stderr)
end
clean_tmpfile(temp_files) click to toggle source

Clean temp files

@param [Array<Tempfile>] temp_files temp files to be cleaned

# File lib/lrun.rb, line 281
def self.clean_tmpfile(temp_files)
  temp_files.each do |file|
    file.unlink rescue nil
  end
end
expand_option(key, values) click to toggle source

Expand a single option to be used in command line

@param [Symbol] key option name @param [Array, to_s] values option value(s) @return [Array<String>] arguments used in command line

# File lib/lrun.rb, line 311
def self.expand_option(key, values)
  return nil unless LRUN_OPTIONS.has_key? key

  [*values].map do |value|
    ["--#{key.to_s.gsub('_', '-')}", *value]
  end
end
expand_options(options) click to toggle source

Expand options to be used in command line

@param [Hash] options single options hash returned by {Lrun.merge_options} @return [Array<String>] command line arguments

Example

Lrun.format_options({:chdir=>"/tmp", :bindfs=>[["/a", "/b"], ["/c", "/d"]], :fd => [2, 3]})
# => ["--chdir", "/tmp", "--bindfs", "/a", "/b", "--bindfs", "/c", "/d", "--fd", "2", "--fd", "3"]
# File lib/lrun.rb, line 296
def self.expand_options(options)
  raise TypeError, 'expect options to be a Hash' unless options.is_a? Hash

  command_arguments = options.map do |key, values|
    expand_option key, values
  end

  command_arguments.compact.flatten.map(&:to_s)
end
parse_exceed(report_exceed) click to toggle source

Parse exceed information from lrun report

@param [String] report_exceed exceed reported by lrun @return [Symbol, nil] exceeded limit in symbol, or nil if no limit exceeded

# File lib/lrun.rb, line 364
def self.parse_exceed(report_exceed)
  case report_exceed
  when 'none'
    nil
  when /TIME/
    :time
  when /OUTPUT/
    :output
  when /MEMORY/
    :memory
  else
    raise LrunError, "unexpected EXCEED returned by lrun: #{report['EXCEED']}"
  end
end
spawn_lrun(commands, options, report_fd) click to toggle source

Spawn lrun process.

@param [IO:fd] report_fd

file descriptor used to receive lrun report

@return [Integer] pid spawned process id of lrun

# File lib/lrun.rb, line 325
def self.spawn_lrun(commands, options, report_fd)
  # Expand commands if commands is a string
  commands = Shellwords.split(commands) if commands.is_a? String
  raise ArgumentError, 'commands should not be empty' if commands.nil? || commands.empty?

  # Build command line
  command_line = [LRUN_PATH, *expand_options(options), *commands]
  spawn_options = {0 => options[:stdin] || :close,
                   1 => options[:stdout] || (tmp_out = Tempfile.new("lrun.#{$$}.out")).path,
                   2 => options[:stderr] || (tmp_err = Tempfile.new("lrun.#{$$}.err")).path,
                   3 => report_fd.fileno}

  # Keep pid of lrun process for checking its status
  Process.spawn(*command_line, spawn_options)
end