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
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
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 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 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 {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 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 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 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 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 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