lrun-ruby 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/LICENSE +19 -0
- data/README.md +32 -0
- data/Rakefile +10 -0
- data/lib/lrun.rb +381 -0
- data/lib/lrun/runner.rb +84 -0
- data/lrun-ruby.gemspec +17 -0
- data/spec/lrun_runner_spec.rb +59 -0
- data/spec/lrun_spec.rb +144 -0
- metadata +55 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NzllZjNiZGQzZWZhOWVjMjlmOTliNGUxYTExYzE1MzI2ZmQ0ZDc1Yg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
YzM1YTU5ZjkwNTRlYzE5OTI5NWNmZWFmYzljYzNlNmQ0NTdiYjQ1Yg==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MmEwNjQ0NGUwYWM0YWJhMTVkOGI0Y2E4N2ViMmI4MWI5YWMxMjg0N2FiNmQ1
|
10
|
+
N2ZkM2I0YmVjZmQ1Mjc2YTc3MWQ0YWRlYTU4MjNmODM3MjI4MTQxMjViZWQ2
|
11
|
+
NTc3YzlhMjk3MmQ4MjU2MDQxZTQzNTEwMzY5OWI2ODRkNzI4ODQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MTZmOTE1NTdiNDdhZDkwNzdkYmZmZmRlMWE0ZmJjMzk0NzJmYWM4MDM2MzRj
|
14
|
+
NzQyOTQ5ZTk1M2EzMzRjNTM1M2EzZDA3MGI2ZmIwZDY1Mjk4YzFkZDYzZGY1
|
15
|
+
Y2I0MmJmMjhkNjYwMWIyMjNhYTlkYWVkMDI3M2JlNjMwYzI0YzA=
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (C) 2012-2013 WU Jun <quark@zju.edu.cn>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# lrun-ruby
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/quark-zju/lrun-ruby.png)](https://travis-ci.org/quark-zju/lrun-ruby)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/quark-zju/lrun-ruby.png)](https://codeclimate.com/github/quark-zju/lrun-ruby)
|
5
|
+
|
6
|
+
Ruby binding for [lrun](https://github.com/quark-zju/lrun)
|
7
|
+
|
8
|
+
## Dependencies
|
9
|
+
|
10
|
+
* [lrun](https://github.com/quark-zju/lrun)
|
11
|
+
* Ruby 1.9 or above
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
1. Install lrun binary. For detailed steps, please refer to [lrun project page](https://github.com/quark-zju/lrun).
|
16
|
+
2. Install `lrun-ruby` gem:
|
17
|
+
|
18
|
+
```bash
|
19
|
+
gem install lrun-ruby
|
20
|
+
```
|
21
|
+
|
22
|
+
## Documentation
|
23
|
+
|
24
|
+
lrun-ruby is documented with examples.
|
25
|
+
|
26
|
+
[Browse online](http://rdoc.info/github/quark-zju/lrun-ruby).
|
27
|
+
|
28
|
+
Alternatively, run `rake doc` and open `doc/index.html`.
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
See examples in documentation.
|
data/Rakefile
ADDED
data/lib/lrun.rb
ADDED
@@ -0,0 +1,381 @@
|
|
1
|
+
################################################################################
|
2
|
+
# Copyright (C) 2012-2013 WU Jun <quark@zju.edu.cn>
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
9
|
+
# furnished to do so, subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
12
|
+
# all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
# THE SOFTWARE.
|
21
|
+
################################################################################
|
22
|
+
|
23
|
+
require 'tempfile'
|
24
|
+
require 'shellwords'
|
25
|
+
|
26
|
+
# Run program using <tt>lrun</tt>. Require external <tt>lrun</tt> binary.
|
27
|
+
#
|
28
|
+
# @see Lrun.run
|
29
|
+
# @see https://github.com/quark-zju/lrun lrun project page
|
30
|
+
#
|
31
|
+
# = Example
|
32
|
+
#
|
33
|
+
# Lrun.run('foo', Lrun.merge_options({:max_memory => 2 ** 20, :max_cpu_time => 5}, {:network => false}))
|
34
|
+
#
|
35
|
+
module Lrun
|
36
|
+
|
37
|
+
# Name of lrun executable
|
38
|
+
LRUN_BINARY = 'lrun'
|
39
|
+
|
40
|
+
# Full path of lrun executable, automatically detected using {LRUN_BINARY} and <tt>PATH</tt> environment variable
|
41
|
+
LRUN_PATH = ENV['PATH'].split(':').map{|p| File.join(p, LRUN_BINARY)}.find{|p| File.executable? p}
|
42
|
+
|
43
|
+
# Available lrun options, and whether they can occur multiple times (1: no, 2: yes)
|
44
|
+
LRUN_OPTIONS = {
|
45
|
+
:max_cpu_time => 1,
|
46
|
+
:max_real_time => 1,
|
47
|
+
:max_memory => 1,
|
48
|
+
:max_output => 1,
|
49
|
+
:max_nprocess => 1,
|
50
|
+
:max_rtprio => 1,
|
51
|
+
:max_nfile => 1,
|
52
|
+
:max_stack => 1,
|
53
|
+
:isolate_process => 1,
|
54
|
+
:basic_devices => 1,
|
55
|
+
:reset_env => 1,
|
56
|
+
:network => 1,
|
57
|
+
:chroot => 1,
|
58
|
+
:chdir => 1,
|
59
|
+
:nice => 1,
|
60
|
+
:umask => 1,
|
61
|
+
:uid => 1,
|
62
|
+
:gid => 1,
|
63
|
+
:interval => 1,
|
64
|
+
:cgname => 1,
|
65
|
+
:bindfs => 2,
|
66
|
+
:cgroup_option => 2,
|
67
|
+
:tmpfs => 2,
|
68
|
+
:env => 2,
|
69
|
+
:fd => 2,
|
70
|
+
:group => 2,
|
71
|
+
:cmd => 2,
|
72
|
+
}
|
73
|
+
|
74
|
+
# Keep how many bytes of stdout and stderr, can be overrided using <tt>options[:truncate]</tt>
|
75
|
+
TRUNCATE_OUTPUT_LENGTH = 4096
|
76
|
+
|
77
|
+
# Error related to lrun
|
78
|
+
class LrunError < RuntimeError; end
|
79
|
+
|
80
|
+
# Result of {Lrun.run}.
|
81
|
+
class Result < Struct.new(:memory, :cputime, :exceed, :exitcode, :signal, :stdout, :stderr)
|
82
|
+
|
83
|
+
# @!attribute memory
|
84
|
+
# @return [Integer] peak memory used, in bytes
|
85
|
+
#
|
86
|
+
# @!attribute cputime
|
87
|
+
# @return [Float] CPU time consumed, in seconds
|
88
|
+
#
|
89
|
+
# @!attribute exceed
|
90
|
+
# @return [Symbol] what limit exceed,
|
91
|
+
# <tt>:time</tt> if time limit exceeded,
|
92
|
+
# <tt>:memory</tt> if memory limit exceeded,
|
93
|
+
# <tt>:output</tt> if output limit exceeded,
|
94
|
+
# or <tt>nil</tt> if no limit exceeded
|
95
|
+
#
|
96
|
+
# @!attribute exitcode
|
97
|
+
# @return [Integer] exit code
|
98
|
+
#
|
99
|
+
# @!attribute signal
|
100
|
+
# @return [Integer] signal number received, or <tt>nil</tt> if exited normally
|
101
|
+
#
|
102
|
+
# @!attribute stdout
|
103
|
+
# @return [String] standard output, or <tt>nil</tt> if it is redirected in options
|
104
|
+
#
|
105
|
+
# @!attribute stderr
|
106
|
+
# @return [String] standard error output, or <tt>nil</tt> if stderr is redirected in options
|
107
|
+
|
108
|
+
# @return [Boolean] whether the program exited without crash and has a zero exit code
|
109
|
+
def clean?
|
110
|
+
exitcode.to_i == 0 && !crashed?
|
111
|
+
end
|
112
|
+
|
113
|
+
# @return [Boolean] whether the program crashed (exited by signal)
|
114
|
+
def crashed?
|
115
|
+
!signal.nil?
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
# Merge options so that it can be used in {Lrun.run}.
|
121
|
+
#
|
122
|
+
# @param [Array<Hash>] options options to be merged
|
123
|
+
# @return [Hash] merged options, can be used again in {Lrun.merge_options}
|
124
|
+
#
|
125
|
+
# = Example
|
126
|
+
#
|
127
|
+
# Lrun.merge_options({:uid => 1000}, {:gid => 100})
|
128
|
+
# # => {:uid=>1000, :gid=>100}
|
129
|
+
#
|
130
|
+
# Lrun.merge_options({:nice => 1, :uid => 1001}, {:nice => 2})
|
131
|
+
# # => {:nice=>2, :uid=>1000}
|
132
|
+
#
|
133
|
+
# Lrun.merge_options({:fd => [4, 6]}, {:fd => 5}, {:fd => 7})
|
134
|
+
# # => {:fd=>[4, 6, 5, 7]}
|
135
|
+
#
|
136
|
+
# Lrun.merge_options({:env => {'A'=>'1', 'B' => '2'}}, {:env => {'C' => '3'}})
|
137
|
+
# # => {:env=>[["A", "1"], ["B", "2"], ["C", "3"]]}
|
138
|
+
#
|
139
|
+
# Lrun.merge_options({:uid => 1000}, {:uid => nil})
|
140
|
+
# # => {}
|
141
|
+
#
|
142
|
+
# Lrun.merge_options({:fd => [4]}, {:fd => 5}, {:fd => nil})
|
143
|
+
# # => {}
|
144
|
+
#
|
145
|
+
# Lrun.merge_options({:network => true, :chdir => '/tmp', :bindfs => {'/a' => '/b'}},
|
146
|
+
# {:network => nil, :bindfs => {'/c' => '/d'}})
|
147
|
+
# # => {:chdir=>"/tmp", :bindfs=>[["/a", "/b"], ["/c", "/d"]]}
|
148
|
+
def self.merge_options(*options)
|
149
|
+
# Remove nil
|
150
|
+
options.compact!
|
151
|
+
|
152
|
+
# Check type of options
|
153
|
+
raise TypeError, 'options should be Hash' unless options.all? { |o| o.is_a? Hash }
|
154
|
+
|
155
|
+
# Merge options
|
156
|
+
options.inject({}) do |result, option|
|
157
|
+
option.each do |k, v|
|
158
|
+
# Remove an option using nil
|
159
|
+
if v.nil?
|
160
|
+
result.delete k
|
161
|
+
next
|
162
|
+
end
|
163
|
+
|
164
|
+
# Append to or Replace an option
|
165
|
+
case LRUN_OPTIONS[k]
|
166
|
+
when 2
|
167
|
+
# Append to previous options
|
168
|
+
result[k] ||= []
|
169
|
+
result[k] += [*v]
|
170
|
+
else
|
171
|
+
# Overwrite previous option
|
172
|
+
result[k] = v
|
173
|
+
end
|
174
|
+
end
|
175
|
+
result
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Run program using <tt>lrun</tt> binary.
|
180
|
+
#
|
181
|
+
# @param [Array<String>, String] commands
|
182
|
+
# commands to be executed
|
183
|
+
#
|
184
|
+
# @param [Hash] options
|
185
|
+
# options for lrun.
|
186
|
+
# Besides options in {Lrun.LRUN_OPTIONS}, there are some additional options available:
|
187
|
+
#
|
188
|
+
# truncate::
|
189
|
+
# maximum bytes read for stderr and stdout (default: {Lrun.TRUNCATE_OUTPUT_LENGTH}).
|
190
|
+
# stdin::
|
191
|
+
# stdin file path (default: no input).
|
192
|
+
# stdout::
|
193
|
+
# stdout file path (default: a tempfile, will be deleted automatically).
|
194
|
+
# If this option is set, the returned result will have no stdout,
|
195
|
+
# you should read and delete stdout file manually.
|
196
|
+
# stderr::
|
197
|
+
# stderr file path (default: a tempfile, will be deleted automatically).
|
198
|
+
# If this option is set, the returned result will have no stderr,
|
199
|
+
# you should read and delete stderr file manually.
|
200
|
+
#
|
201
|
+
# Note: lrun chroot and mounts does not affect above paths.
|
202
|
+
#
|
203
|
+
# @return [Lrun::Result]
|
204
|
+
#
|
205
|
+
# = Example
|
206
|
+
#
|
207
|
+
# Lrun.run('echo hello')
|
208
|
+
# # => #<struct Lrun::Result
|
209
|
+
# # memory=262144, cputime=0.002,
|
210
|
+
# # exceed=nil, exitcode=0, signal=nil,
|
211
|
+
# # stdout="hello\n", stderr="">
|
212
|
+
#
|
213
|
+
# Lrun.run('java', :max_memory => 2 ** 19, :stdout => '/tmp/out.txt')
|
214
|
+
# # => #<struct Lrun::Result
|
215
|
+
# # memory=524288, cputime=0.006,
|
216
|
+
# # exceed=:memory, exitcode=0, signal=nil,
|
217
|
+
# # stdout=nil, stderr="">
|
218
|
+
#
|
219
|
+
# Lrun.run('sleep 30', :max_real_time => 1, :stderr => '/dev/null')
|
220
|
+
# # => #<struct Lrun::Result
|
221
|
+
# # memory=262144, cputime=0.002,
|
222
|
+
# # exceed=:time, exitcode=0, signal=nil,
|
223
|
+
# # stdout="", stderr=nil>
|
224
|
+
#
|
225
|
+
# Lrun.run('cat', :max_output => 100, :stdin => '/dev/urandom', :truncate => 2)
|
226
|
+
# # => #<struct Lrun::Result
|
227
|
+
# # memory=782336, cputime=0.05,
|
228
|
+
# # exceed=:output, exitcode=0, signal=nil,
|
229
|
+
# # stdout="U\xE1", stderr="">
|
230
|
+
#
|
231
|
+
def self.run(commands, options = {})
|
232
|
+
# Make sure lrun binary is available
|
233
|
+
available!
|
234
|
+
|
235
|
+
# Temp files storing stdout and stderr of target process
|
236
|
+
tmp_out = tmp_err = nil
|
237
|
+
|
238
|
+
# Create temp stdout, stderr files if user does not redirect them
|
239
|
+
options = options.dup
|
240
|
+
options[:stdout] ||= (tmp_out = Tempfile.new("lrun.#{$$}.out")).path
|
241
|
+
options[:stderr] ||= (tmp_err = Tempfile.new("lrun.#{$$}.err")).path
|
242
|
+
|
243
|
+
IO.pipe do |rfd, wfd|
|
244
|
+
# Keep pid of lrun process for checking its status
|
245
|
+
pid = spawn_lrun commands, options, wfd
|
246
|
+
|
247
|
+
# Read fd 3, where lrun write its report
|
248
|
+
wfd.close
|
249
|
+
report = rfd.read
|
250
|
+
|
251
|
+
# Check if lrun exits normally
|
252
|
+
stat = Process.wait2(pid)[-1]
|
253
|
+
if stat.signaled? || stat.exitstatus != 0
|
254
|
+
raise LrunError, "lrun exits abnormally: #{stat}. #{tmp_err.read unless tmp_err.nil?}"
|
255
|
+
end
|
256
|
+
|
257
|
+
# Build and return result
|
258
|
+
build_result report, tmp_out, tmp_err, options[:truncate]
|
259
|
+
end
|
260
|
+
ensure
|
261
|
+
clean_tmpfile [tmp_out, tmp_err]
|
262
|
+
end
|
263
|
+
|
264
|
+
# Check if lrun binary exists
|
265
|
+
#
|
266
|
+
# @return [Bool] whether lrun binary is found
|
267
|
+
def self.available?
|
268
|
+
!LRUN_PATH.nil?
|
269
|
+
end
|
270
|
+
|
271
|
+
# Complain if lrun binary is not available
|
272
|
+
def self.available!
|
273
|
+
raise LrunError, "#{LRUN_BINARY} not found in PATH. Please install lrun first." unless available?
|
274
|
+
end
|
275
|
+
|
276
|
+
private
|
277
|
+
|
278
|
+
# Clean temp files
|
279
|
+
#
|
280
|
+
# @param [Array<Tempfile>] temp_files temp files to be cleaned
|
281
|
+
def self.clean_tmpfile(temp_files)
|
282
|
+
temp_files.each do |file|
|
283
|
+
file.unlink rescue nil
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# Expand options to be used in command line
|
288
|
+
#
|
289
|
+
# @param [Hash] options single options hash returned by {Lrun.merge_options}
|
290
|
+
# @return [Array<String>] command line arguments
|
291
|
+
#
|
292
|
+
# = Example
|
293
|
+
#
|
294
|
+
# Lrun.format_options({:chdir=>"/tmp", :bindfs=>[["/a", "/b"], ["/c", "/d"]], :fd => [2, 3]})
|
295
|
+
# # => ["--chdir", "/tmp", "--bindfs", "/a", "/b", "--bindfs", "/c", "/d", "--fd", "2", "--fd", "3"]
|
296
|
+
def self.expand_options(options)
|
297
|
+
raise TypeError, 'expect options to be a Hash' unless options.is_a? Hash
|
298
|
+
|
299
|
+
command_arguments = options.map do |key, values|
|
300
|
+
expand_option key, values
|
301
|
+
end
|
302
|
+
|
303
|
+
command_arguments.compact.flatten.map(&:to_s)
|
304
|
+
end
|
305
|
+
|
306
|
+
# Expand a single option to be used in command line
|
307
|
+
#
|
308
|
+
# @param [Symbol] key option name
|
309
|
+
# @param [Array, #to_s] values option value(s)
|
310
|
+
# @return [Array<String>] arguments used in command line
|
311
|
+
def self.expand_option(key, values)
|
312
|
+
return nil unless LRUN_OPTIONS.has_key? key
|
313
|
+
|
314
|
+
[*values].map do |value|
|
315
|
+
["--#{key.to_s.gsub('_', '-')}", *value]
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
# Spawn lrun process.
|
320
|
+
#
|
321
|
+
# @param [IO:fd] report_fd
|
322
|
+
# file descriptor used to receive lrun report
|
323
|
+
#
|
324
|
+
# @return [Integer] pid spawned process id of lrun
|
325
|
+
def self.spawn_lrun(commands, options, report_fd)
|
326
|
+
# Expand commands if commands is a string
|
327
|
+
commands = Shellwords.split(commands) if commands.is_a? String
|
328
|
+
raise ArgumentError, 'commands should not be empty' if commands.nil? || commands.empty?
|
329
|
+
|
330
|
+
# Build command line
|
331
|
+
command_line = [LRUN_PATH, *expand_options(options), *commands]
|
332
|
+
spawn_options = {0 => options[:stdin] || :close,
|
333
|
+
1 => options[:stdout] || (tmp_out = Tempfile.new("lrun.#{$$}.out")).path,
|
334
|
+
2 => options[:stderr] || (tmp_err = Tempfile.new("lrun.#{$$}.err")).path,
|
335
|
+
3 => report_fd.fileno}
|
336
|
+
|
337
|
+
# Keep pid of lrun process for checking its status
|
338
|
+
Process.spawn(*command_line, spawn_options)
|
339
|
+
end
|
340
|
+
|
341
|
+
# Build {Lrun::Result} from essential information.
|
342
|
+
#
|
343
|
+
# @return [Lrun::Result]
|
344
|
+
def self.build_result(lrun_report, stdout = nil, stderr = nil, truncate = TRUNCATE_OUTPUT_LENGTH)
|
345
|
+
report = Hash[lrun_report.lines.map{ |l| l.chomp.split(' ', 2)}]
|
346
|
+
|
347
|
+
# Collect information
|
348
|
+
memory = report['MEMORY'].to_i
|
349
|
+
cputime = report['CPUTIME'].to_f
|
350
|
+
exceed = parse_exceed(report['EXCEED'])
|
351
|
+
exitcode = report['EXITCODE'].to_i
|
352
|
+
signal = report['SIGNALED'].to_i == 0 ? nil : report['TERMSIG'].to_i
|
353
|
+
stdout &&= stdout.read(truncate) || ''
|
354
|
+
stderr &&= stderr.read(truncate) || ''
|
355
|
+
|
356
|
+
# Build Result
|
357
|
+
Result.new(memory, cputime, exceed, exitcode, signal, stdout, stderr)
|
358
|
+
end
|
359
|
+
|
360
|
+
# Parse exceed information from lrun report
|
361
|
+
#
|
362
|
+
# @param [String] report_exceed exceed reported by lrun
|
363
|
+
# @return [Symbol, nil] exceeded limit in symbol, or <tt>nil</tt> if no limit exceeded
|
364
|
+
def self.parse_exceed(report_exceed)
|
365
|
+
case report_exceed
|
366
|
+
when 'none'
|
367
|
+
nil
|
368
|
+
when /TIME/
|
369
|
+
:time
|
370
|
+
when /OUTPUT/
|
371
|
+
:output
|
372
|
+
when /MEMORY/
|
373
|
+
:memory
|
374
|
+
else
|
375
|
+
raise LrunError, "unexpected EXCEED returned by lrun: #{report['EXCEED']}"
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
# Autoload {Lrun::Runner}
|
380
|
+
autoload :Runner, 'lrun/runner'
|
381
|
+
end
|
data/lib/lrun/runner.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
################################################################################
|
2
|
+
# Copyright (C) 2012-2013 WU Jun <quark@zju.edu.cn>
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
9
|
+
# furnished to do so, subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
12
|
+
# all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
# THE SOFTWARE.
|
21
|
+
################################################################################
|
22
|
+
|
23
|
+
require 'lrun'
|
24
|
+
|
25
|
+
# {Lrun} provides essential methods to run program with different options
|
26
|
+
# using <tt>lrun</tt> binary.
|
27
|
+
#
|
28
|
+
# {Lrun::Runner} makes it easier to run many programs with same options.
|
29
|
+
#
|
30
|
+
# = Example
|
31
|
+
#
|
32
|
+
# runner = Lrun::Runner.new(:max_cpu_time=>1, :tmpfs=>[["/tmp", 2**20]], :chdir=>"/tmp")
|
33
|
+
# # or:
|
34
|
+
# runner = Lrun::Runner.new.where(:max_cpu_time=>1, :tmpfs=>[["/tmp", 2**20]], :chdir=>"/tmp")
|
35
|
+
# # or:
|
36
|
+
# runner = Lrun::Runner.new.max_cpu_time(1).tmpfs('/tmp' => 2**20).chdir('/tmp')
|
37
|
+
#
|
38
|
+
# runner.options
|
39
|
+
# # => {:max_cpu_time=>1, :tmpfs=>[["/tmp", 1048576]], :chdir=>"/tmp"}
|
40
|
+
# runner.max_cpu_time(nil).options
|
41
|
+
# # => {:tmpfs=>[["/tmp", 1048576]], :chdir=>"/tmp"}
|
42
|
+
#
|
43
|
+
# runner.run('pwd').stdout
|
44
|
+
# # => "/tmp\n"
|
45
|
+
# runner.cmd("touch `seq 1 4`").run('ls').stdout
|
46
|
+
# # => "1\n2\n3\n4\n"
|
47
|
+
# runner.cmd("echo 'puts ENV[?A]' > a.rb").env('A' => 'Hello').run('ruby a.rb').stdout
|
48
|
+
# # => "Hello\n"
|
49
|
+
class Lrun::Runner
|
50
|
+
|
51
|
+
# @!attribute [rw] options
|
52
|
+
# @return [Hash] options used in {#run}
|
53
|
+
attr_accessor :options
|
54
|
+
|
55
|
+
# @param [Hash] options options for the runner
|
56
|
+
def initialize(options = {})
|
57
|
+
@options = options
|
58
|
+
end
|
59
|
+
|
60
|
+
# Methods for easily applying custom options
|
61
|
+
[:stdin, :stdout, :stderr, *Lrun::LRUN_OPTIONS.keys].each do |name|
|
62
|
+
define_method name do |value| where(name => value) end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Run commands using current {#options}.
|
66
|
+
#
|
67
|
+
# @param [Array<String>, String] commands commands to be executed
|
68
|
+
#
|
69
|
+
# @see options
|
70
|
+
# @see Lrun.run
|
71
|
+
def run(commands)
|
72
|
+
Lrun.run commands, @options
|
73
|
+
end
|
74
|
+
|
75
|
+
# Create a new runner with new options
|
76
|
+
#
|
77
|
+
# @param [Hash] options new options to be merged
|
78
|
+
# @return [Lrun::Runner] new runner created with merged options
|
79
|
+
def where(options)
|
80
|
+
raise TypeError, 'expect options to be a Hash' unless options.is_a? Hash
|
81
|
+
Lrun::Runner.new(Lrun.merge_options(@options, options))
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
data/lrun-ruby.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'lrun-ruby'
|
3
|
+
s.version = '0.1.1'
|
4
|
+
s.date = Date.civil(2013,3,23)
|
5
|
+
s.summary = 'Ruby binding for lrun'
|
6
|
+
s.description = 'Ruby binding for lrun, a standalone executable designed to run programs with limited resources under Linux.'
|
7
|
+
s.authors = ["Wu Jun"]
|
8
|
+
s.email = 'quark@zju.edu.cn'
|
9
|
+
s.homepage = 'https://github.com/quark-zju/lrun-ruby'
|
10
|
+
s.require_paths = ['lib']
|
11
|
+
s.licenses = ['MIT']
|
12
|
+
s.has_rdoc = 'yard'
|
13
|
+
s.files = %w(LICENSE README.md Rakefile lrun-ruby.gemspec)
|
14
|
+
s.files += Dir.glob("lib/**/*.rb")
|
15
|
+
s.files += Dir.glob("spec/**/*.rb")
|
16
|
+
s.test_files = Dir['spec/**/*.rb']
|
17
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'lrun/runner'
|
2
|
+
|
3
|
+
describe Lrun::Runner, '#new' do
|
4
|
+
|
5
|
+
it 'creates a runner' do
|
6
|
+
Lrun::Runner.new.class.should == Lrun::Runner
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'accepts options' do
|
10
|
+
Lrun::Runner.new(:uid => 2, :gid => 3).options.should == {:uid => 2, :gid => 3}
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'rejects too many options' do
|
14
|
+
lambda { Lrun::Runner.new({:uid => 2}, {:gid => 3}) }.should raise_error(ArgumentError)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
describe Lrun::Runner, '#where' do
|
20
|
+
|
21
|
+
let(:runner) { Lrun::Runner.new(:uid => 2, :fd => 2, :tmpfs => {'/tmp' => 0}) }
|
22
|
+
|
23
|
+
it 'change options' do
|
24
|
+
runner.where(:uid => 3).options[:uid].should == 3
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'add options' do
|
28
|
+
runner.where(:fd => 3).options[:fd].should == [2, 3]
|
29
|
+
runner.where(:fd => [4, 5]).options[:fd].should == [2, 4, 5]
|
30
|
+
runner.where(:tmpfs => {'/usr/bin' => 1}).options[:tmpfs].should == [["/tmp", 0], ["/usr/bin", 1]]
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'delete options' do
|
34
|
+
runner.where(:uid => nil).options[:fd].should_not include(:uid)
|
35
|
+
runner.where(:fd => nil).options.keys.should_not include(:fd)
|
36
|
+
runner.where(:fd => nil, :uid => nil, :tmpfs => nil).options.should be_empty
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'does not change original options' do
|
40
|
+
lambda {
|
41
|
+
runner.where(:uid => 5, :fd => nil, :tmpfs => {'/a' => 2}, :gid => 6)
|
42
|
+
}.should_not change(runner, :options)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
describe Lrun::Runner, '#run', :if => Lrun.available? do
|
48
|
+
|
49
|
+
let(:runner) { Lrun::Runner.new(:uid => 2, :fd => 2, :tmpfs => {'/tmp' => 0}) }
|
50
|
+
|
51
|
+
it 'runs' do
|
52
|
+
runner.run('echo a').stdout.should == "a\n"
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'passes options' do
|
56
|
+
runner.env('A' => 42).run(['sh', '-c', 'echo $A']).stdout.chomp.should == '42'
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
data/spec/lrun_spec.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'lrun'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
describe Lrun do
|
5
|
+
|
6
|
+
it 'can not be instantiated' do
|
7
|
+
lambda { Lrun.new }.should raise_error(NoMethodError)
|
8
|
+
end
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
describe Lrun, '#merge_options' do
|
13
|
+
|
14
|
+
it 'accepts empty options' do
|
15
|
+
Lrun.merge_options(nil).should == {}
|
16
|
+
Lrun.merge_options({}).should == {}
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'rejects non-Hash' do
|
20
|
+
lambda { Lrun.merge_options(1) }.should raise_error(TypeError)
|
21
|
+
lambda { Lrun.merge_options('a', 'b') }.should raise_error(TypeError)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'merges options' do
|
25
|
+
Lrun.merge_options({:uid => 1000}, {:gid => 100, :interval => 2}, {:network => false}).should == {:network => false, :uid => 1000, :gid => 100, :interval => 2}
|
26
|
+
Lrun.merge_options({:fd => [4, 6]}, {:fd => 5}, {:fd => 7}).should == {:fd=>[4, 6, 5, 7]}
|
27
|
+
Lrun.merge_options({:bindfs => {'/a' => '/b'}}, {:bindfs => {'/c' => '/d'}}).should == {:bindfs => [["/a", "/b"], ["/c", "/d"]]}
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'overwrite options' do
|
31
|
+
Lrun.merge_options({:uid => 1}, {:uid => 3}, {:uid => 4}).should == {:uid => 4}
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'removes options' do
|
35
|
+
Lrun.merge_options({:uid => 1000}, {:uid => nil}).should == {}
|
36
|
+
Lrun.merge_options({:fd => [4, 5, 6]}, {:fd => 7}, {:fd => nil}).should == {}
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
describe Lrun, '#run', :if => Lrun.available? do
|
42
|
+
|
43
|
+
context "when running true and false" do
|
44
|
+
let(:true_result) { Lrun.run('true') }
|
45
|
+
let(:false_result) { Lrun.run('false') }
|
46
|
+
|
47
|
+
it 'returns exitcode' do
|
48
|
+
true_result.exitcode.should == 0
|
49
|
+
false_result.exitcode.should_not == 0
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'returns cpu and memory usage' do
|
53
|
+
true_result.memory.should > 0
|
54
|
+
true_result.cputime.should >= 0
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'returns exceed' do
|
58
|
+
true_result.exceed.should be_nil
|
59
|
+
false_result.exceed.should be_nil
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'returns stdout and stderr' do
|
63
|
+
true_result.stdout.should == ""
|
64
|
+
false_result.stdout.should == ""
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'returns stderr' do
|
68
|
+
true_result.stderr.should == ""
|
69
|
+
false_result.stderr.should == ""
|
70
|
+
end
|
71
|
+
|
72
|
+
it '.clean?' do
|
73
|
+
true_result.clean?.should be_true
|
74
|
+
false_result.clean?.should be_false
|
75
|
+
end
|
76
|
+
|
77
|
+
it '.crashed?' do
|
78
|
+
true_result.crashed?.should be_false
|
79
|
+
false_result.crashed?.should be_false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'when running cat' do
|
84
|
+
it 'can redirect stdin' do
|
85
|
+
begin
|
86
|
+
tmpfile = Tempfile.new("input")
|
87
|
+
tmpfile.write("foo bar")
|
88
|
+
tmpfile.close
|
89
|
+
Lrun.run('cat', stdin: tmpfile.path).stdout.should == "foo bar"
|
90
|
+
ensure
|
91
|
+
tmpfile.unlink rescue nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'does not alter options' do
|
96
|
+
options = {}
|
97
|
+
Lrun.run('cat', options)
|
98
|
+
options.should == {}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'when running echo' do
|
103
|
+
it 'captures stdout' do
|
104
|
+
Lrun.run('echo Hello').stdout.should == "Hello\n"
|
105
|
+
Lrun.run(['echo', 'World']).stdout.should == "World\n"
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'can redirect stdout' do
|
109
|
+
begin
|
110
|
+
tmpfile = Tempfile.new("output")
|
111
|
+
result = Lrun.run('echo blabla', stdout: tmpfile.path)
|
112
|
+
result.stderr.should == ""
|
113
|
+
result.stdout.should be_nil
|
114
|
+
tmpfile.read.should == "blabla\n"
|
115
|
+
ensure
|
116
|
+
tmpfile.unlink rescue nil
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context 'when setting limit options' do
|
122
|
+
it 'returns exceed' do
|
123
|
+
Lrun.run('sleep 1', :max_real_time => 0.1).exceed.should == :time
|
124
|
+
Lrun.run(['cat', '/dev/full'], :stdout => '/dev/null', :max_cpu_time => 0.1).exceed.should == :time
|
125
|
+
Lrun.run(['cpp', '/dev/full', '-o', '/dev/null'], :max_memory => 1_000_000).exceed.should == :memory
|
126
|
+
Lrun.run(['cat', '/dev/full'], :stdout => '/dev/null', :max_output => 1_000).exceed.should == :output
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context 'when parameters are illegal' do
|
131
|
+
it 'rejects empty command' do
|
132
|
+
lambda { Lrun.run(nil) }.should raise_error(ArgumentError)
|
133
|
+
lambda { Lrun.run('') }.should raise_error(ArgumentError)
|
134
|
+
lambda { Lrun.run([]) }.should raise_error(ArgumentError)
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'handles lrun errors' do
|
138
|
+
lambda { Lrun.run('strange_non_existed_executable') }.should raise_error(Lrun::LrunError)
|
139
|
+
lambda { Lrun.run(['--unsupported-options', 'true']) }.should raise_error(Lrun::LrunError)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lrun-ruby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Wu Jun
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-03-23 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Ruby binding for lrun, a standalone executable designed to run programs
|
14
|
+
with limited resources under Linux.
|
15
|
+
email: quark@zju.edu.cn
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- LICENSE
|
21
|
+
- README.md
|
22
|
+
- Rakefile
|
23
|
+
- lrun-ruby.gemspec
|
24
|
+
- lib/lrun.rb
|
25
|
+
- lib/lrun/runner.rb
|
26
|
+
- spec/lrun_spec.rb
|
27
|
+
- spec/lrun_runner_spec.rb
|
28
|
+
homepage: https://github.com/quark-zju/lrun-ruby
|
29
|
+
licenses:
|
30
|
+
- MIT
|
31
|
+
metadata: {}
|
32
|
+
post_install_message:
|
33
|
+
rdoc_options: []
|
34
|
+
require_paths:
|
35
|
+
- lib
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
requirements: []
|
47
|
+
rubyforge_project:
|
48
|
+
rubygems_version: 2.0.3
|
49
|
+
signing_key:
|
50
|
+
specification_version: 4
|
51
|
+
summary: Ruby binding for lrun
|
52
|
+
test_files:
|
53
|
+
- spec/lrun_spec.rb
|
54
|
+
- spec/lrun_runner_spec.rb
|
55
|
+
has_rdoc: yard
|