ruby-cute 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.yardopts +2 -0
- data/Gemfile +6 -0
- data/README.md +137 -6
- data/Rakefile +48 -0
- data/bin/cute +22 -0
- data/debian/changelog +5 -0
- data/debian/compat +1 -0
- data/debian/control +15 -0
- data/debian/copyright +33 -0
- data/debian/ruby-cute.docs +2 -0
- data/debian/ruby-tests.rb +2 -0
- data/debian/rules +19 -0
- data/debian/source/format +1 -0
- data/debian/watch +2 -0
- data/examples/distem-bootstrap +516 -0
- data/examples/g5k_exp1.rb +41 -0
- data/examples/g5k_exp_virt.rb +129 -0
- data/lib/cute.rb +7 -2
- data/lib/cute/bash.rb +337 -0
- data/lib/cute/configparser.rb +404 -0
- data/lib/cute/execute.rb +272 -0
- data/lib/cute/extensions.rb +38 -0
- data/lib/cute/g5k_api.rb +1190 -0
- data/lib/cute/net-ssh.rb +144 -0
- data/lib/cute/net.rb +29 -0
- data/lib/cute/synchronization.rb +89 -0
- data/lib/cute/taktuk.rb +554 -0
- data/lib/cute/version.rb +3 -0
- data/ruby-cute.gemspec +32 -0
- data/spec/extensions_spec.rb +17 -0
- data/spec/g5k_api_spec.rb +192 -0
- data/spec/spec_helper.rb +66 -0
- data/spec/taktuk_spec.rb +129 -0
- data/test/test_bash.rb +71 -0
- metadata +204 -47
@@ -0,0 +1,404 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Cute
|
5
|
+
class ParserError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
###
|
9
|
+
# Sample file:
|
10
|
+
###
|
11
|
+
# database:
|
12
|
+
# user: myser
|
13
|
+
# password: thepassword
|
14
|
+
# ip: 127.0.0.1
|
15
|
+
# cache:
|
16
|
+
# size: 1234
|
17
|
+
# # default directory: /tmp
|
18
|
+
# # default strict: true
|
19
|
+
# # default values for environments fields
|
20
|
+
#
|
21
|
+
###
|
22
|
+
# Parser
|
23
|
+
###
|
24
|
+
# cp = ConfigParser.new(yamlstr)
|
25
|
+
# conf = {:db=>{}, :cache=>{}, :env=>{}, :pxe => {}}
|
26
|
+
# cp.parse('database',true) do
|
27
|
+
# # String with default value
|
28
|
+
# conf[:db][:user] = cp.value('user',String,nil,'defaultvalue')
|
29
|
+
# # Mandatory String
|
30
|
+
# conf[:db][:password] = cp.value('password',String)
|
31
|
+
# # String with multiple possible values
|
32
|
+
# conf[:db][:kind] = cp.value('kind',String,nil,['MySQL','PostGRE','Oracle'])
|
33
|
+
# # Regexp
|
34
|
+
# conf[:db][:ip] = cp.value('ip',String,'127.0.0.1',
|
35
|
+
# /\A\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}\Z/
|
36
|
+
# )
|
37
|
+
# end
|
38
|
+
# cp.parse('cache',true) do
|
39
|
+
# # Integer with default value
|
40
|
+
# conf[:cache][:size] = cp.value('size',Fixnum,nil,100)
|
41
|
+
# # Directory that need to exist and be r/w
|
42
|
+
# conf[:cache][:directory] = cp.value('directory',String,'/tmp',
|
43
|
+
# {
|
44
|
+
# :type => 'dir',
|
45
|
+
# :readable => true,
|
46
|
+
# :writable => true,
|
47
|
+
# :create => true,
|
48
|
+
# :mode => 0700
|
49
|
+
# }
|
50
|
+
# )
|
51
|
+
# # Boolean
|
52
|
+
# conf[:cache][:strict] = cp.value('strict',[TrueClass,FalseClass],true)
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# # Non-mandatory field
|
56
|
+
# cp.parse('environments') do
|
57
|
+
# # Specification of a unix path
|
58
|
+
# conf[:env][:tar_dir] = cp.value('tarball_dir',String,'/tmp',Pathname)
|
59
|
+
# # Add a prefix to a value
|
60
|
+
# conf[:env][:user_dir] = cp.value('user_dir',String,'/tmp',
|
61
|
+
# {:type => 'dir', :prefix => '/home/'}
|
62
|
+
# )
|
63
|
+
# end
|
64
|
+
|
65
|
+
class ConfigParser
|
66
|
+
attr_reader :basehash
|
67
|
+
PATH_SEPARATOR = '/'
|
68
|
+
|
69
|
+
def initialize(confighash)
|
70
|
+
@basehash = confighash
|
71
|
+
# The current path
|
72
|
+
@path = []
|
73
|
+
# The current value
|
74
|
+
@val = confighash
|
75
|
+
end
|
76
|
+
|
77
|
+
def push(fieldname, val=nil)
|
78
|
+
@path.push(fieldname)
|
79
|
+
@val = (val.nil? ? curval() : val)
|
80
|
+
end
|
81
|
+
|
82
|
+
def pop(val=nil)
|
83
|
+
@path.pop
|
84
|
+
@val = (val.nil? ? curval() : val)
|
85
|
+
end
|
86
|
+
|
87
|
+
def depth
|
88
|
+
@path.size
|
89
|
+
end
|
90
|
+
|
91
|
+
def path(val=nil)
|
92
|
+
ConfigParser.pathstr(@path + [val])
|
93
|
+
end
|
94
|
+
|
95
|
+
def curval
|
96
|
+
ret = @basehash
|
97
|
+
@path.compact.each do |field|
|
98
|
+
begin
|
99
|
+
field = Integer(field)
|
100
|
+
rescue ArgumentError
|
101
|
+
end
|
102
|
+
|
103
|
+
if ret[field]
|
104
|
+
ret = ret[field]
|
105
|
+
else
|
106
|
+
ret = nil
|
107
|
+
break
|
108
|
+
end
|
109
|
+
end
|
110
|
+
ret
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.errmsg(field,message)
|
114
|
+
"#{message} [field: #{field}]"
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.pathstr(array)
|
118
|
+
array.compact.join(PATH_SEPARATOR)
|
119
|
+
end
|
120
|
+
|
121
|
+
def check_field(fieldname,mandatory,type)
|
122
|
+
begin
|
123
|
+
if @val.is_a?(Hash)
|
124
|
+
if !@val[fieldname].nil?
|
125
|
+
if type.is_a?(Class)
|
126
|
+
typeok = @val[fieldname].is_a?(type)
|
127
|
+
elsif type.is_a?(Array)
|
128
|
+
type.each do |t|
|
129
|
+
typeok = @val[fieldname].is_a?(t)
|
130
|
+
break if typeok
|
131
|
+
end
|
132
|
+
else
|
133
|
+
raise 'Internal Error'
|
134
|
+
end
|
135
|
+
|
136
|
+
if typeok
|
137
|
+
yield(@val[fieldname])
|
138
|
+
else
|
139
|
+
$,=','
|
140
|
+
typename = type.to_s
|
141
|
+
$,=nil
|
142
|
+
raise ParserError.new(
|
143
|
+
"The field should have the type #{typename}"
|
144
|
+
)
|
145
|
+
end
|
146
|
+
elsif mandatory
|
147
|
+
raise ParserError.new("The field is mandatory")
|
148
|
+
else
|
149
|
+
yield(nil)
|
150
|
+
end
|
151
|
+
elsif mandatory
|
152
|
+
if @val.nil?
|
153
|
+
raise ParserError.new("The field is mandatory")
|
154
|
+
else
|
155
|
+
raise ParserError.new("The field has to be a Hash")
|
156
|
+
end
|
157
|
+
else
|
158
|
+
yield(nil)
|
159
|
+
end
|
160
|
+
rescue ParserError => pe
|
161
|
+
raise ArgumentError.new(
|
162
|
+
ConfigParser.errmsg(path(fieldname),pe.message)
|
163
|
+
)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def check_array(val, array, fieldname)
|
168
|
+
unless array.include?(val)
|
169
|
+
raise ParserError.new(
|
170
|
+
"Invalid value '#{val}', allowed value"\
|
171
|
+
"#{(array.size == 1 ? " is" : "s are")}: "\
|
172
|
+
"#{(array.size == 1 ? '' : "'#{array[0..-2].join("', '")}' or ")}"\
|
173
|
+
"'#{array[-1]}'"
|
174
|
+
)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def check_hash(val, hash, fieldname)
|
179
|
+
self.send("customcheck_#{hash[:type].downcase}".to_sym,val,fieldname,hash)
|
180
|
+
end
|
181
|
+
|
182
|
+
def check_range(val, range, fieldname)
|
183
|
+
check_array(val, range.entries, fieldname)
|
184
|
+
end
|
185
|
+
|
186
|
+
def check_regexp(val, regexp, fieldname)
|
187
|
+
unless val =~ regexp
|
188
|
+
raise ParserError.new(
|
189
|
+
"Invalid value '#{val}', the value must have the form (ruby-regexp): "\
|
190
|
+
"#{regexp.source}"
|
191
|
+
)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# A file, checking if exists (creating it otherwise) and writable
|
196
|
+
def check_file(val, file, fieldname)
|
197
|
+
if File.exists?(val)
|
198
|
+
unless File.file?(val)
|
199
|
+
raise ParserError.new("The file '#{val}' is not a regular file")
|
200
|
+
end
|
201
|
+
else
|
202
|
+
raise ParserError.new("The file '#{val}' does not exists")
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# A directory, checking if exists (creating it otherwise) and writable
|
207
|
+
def check_dir(val, dir, fieldname)
|
208
|
+
if File.exist?(val)
|
209
|
+
unless File.directory?(val)
|
210
|
+
raise ParserError.new("'#{val}' is not a regular directory")
|
211
|
+
end
|
212
|
+
else
|
213
|
+
raise ParserError.new("The directory '#{val}' does not exists")
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# A pathname, checking if exists (creating it otherwise) and writable
|
218
|
+
def check_pathname(val, pathname, fieldname)
|
219
|
+
begin
|
220
|
+
Pathname.new(val)
|
221
|
+
rescue
|
222
|
+
raise ParserError.new("Invalid pathname '#{val}'")
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def check_string(val, str, fieldname)
|
227
|
+
unless val == str
|
228
|
+
raise ParserError.new(
|
229
|
+
"Invalid value '#{val}', allowed values are: '#{str}'"
|
230
|
+
)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def customcheck_code(val, fieldname, args)
|
235
|
+
begin
|
236
|
+
eval("#{args[:prefix]}#{args[:code]}#{args[:suffix]}")
|
237
|
+
rescue
|
238
|
+
raise ParserError.new("Invalid expression '#{args[:code]}'")
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def customcheck_file(val, fieldname, args)
|
243
|
+
return if args[:disable]
|
244
|
+
val = File.join(args[:prefix],val) if args[:prefix]
|
245
|
+
val = File.join(val,args[:suffix]) if args[:suffix]
|
246
|
+
if File.exists?(val)
|
247
|
+
if File.file?(val)
|
248
|
+
if args[:writable]
|
249
|
+
unless File.stat(val).writable?
|
250
|
+
raise ParserError.new("The file '#{val}' is not writable")
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
if args[:readable]
|
255
|
+
unless File.stat(val).readable?
|
256
|
+
raise ParserError.new("The file '#{val}' is not readable")
|
257
|
+
end
|
258
|
+
end
|
259
|
+
else
|
260
|
+
raise ParserError.new("The file '#{val}' is not a regular file")
|
261
|
+
end
|
262
|
+
else
|
263
|
+
if args[:create]
|
264
|
+
begin
|
265
|
+
puts "The file '#{val}' does not exists, let's create it"
|
266
|
+
tmp = FileUtils.touch(val)
|
267
|
+
raise if tmp.is_a?(FalseClass)
|
268
|
+
rescue
|
269
|
+
raise ParserError.new("Cannot create the file '#{val}'")
|
270
|
+
end
|
271
|
+
else
|
272
|
+
raise ParserError.new("The file '#{val}' does not exists")
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def customcheck_dir(val, fieldname, args)
|
278
|
+
return if args[:disable]
|
279
|
+
val = File.join(args[:prefix],val) if args[:prefix]
|
280
|
+
val = File.join(val,args[:suffix]) if args[:suffix]
|
281
|
+
if File.exist?(val)
|
282
|
+
if File.directory?(val)
|
283
|
+
if args[:writable]
|
284
|
+
unless File.stat(val).writable?
|
285
|
+
raise ParserError.new("The directory '#{val}' is not writable")
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
if args[:readable]
|
290
|
+
unless File.stat(val).readable?
|
291
|
+
raise ParserError.new("The directory '#{val}' is not readable")
|
292
|
+
end
|
293
|
+
end
|
294
|
+
else
|
295
|
+
raise ParserError.new("'#{val}' is not a regular directory")
|
296
|
+
end
|
297
|
+
else
|
298
|
+
if args[:create]
|
299
|
+
begin
|
300
|
+
puts "The directory '#{val}' does not exists, let's create it"
|
301
|
+
tmp = FileUtils.mkdir_p(val, :mode => (args[:mode] || 0700))
|
302
|
+
raise if tmp.is_a?(FalseClass)
|
303
|
+
rescue
|
304
|
+
raise ParserError.new("Cannot create the directory '#{val}'")
|
305
|
+
end
|
306
|
+
else
|
307
|
+
raise ParserError.new("The directory '#{val}' does not exists")
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
|
313
|
+
def parse(fieldname, mandatory=false, type=Hash)
|
314
|
+
check_field(fieldname,mandatory,type) do |curval|
|
315
|
+
oldval = @val
|
316
|
+
push(fieldname, curval)
|
317
|
+
|
318
|
+
if curval.is_a?(Array)
|
319
|
+
curval.each_index do |i|
|
320
|
+
push(i)
|
321
|
+
yield({
|
322
|
+
:val => curval,
|
323
|
+
:empty => curval.nil?,
|
324
|
+
:path => path,
|
325
|
+
:iter => i,
|
326
|
+
})
|
327
|
+
pop()
|
328
|
+
end
|
329
|
+
curval.clear
|
330
|
+
else
|
331
|
+
yield({
|
332
|
+
:val => curval,
|
333
|
+
:empty => curval.nil?,
|
334
|
+
:path => path,
|
335
|
+
:iter => 0,
|
336
|
+
})
|
337
|
+
end
|
338
|
+
|
339
|
+
oldval.delete(fieldname) if curval and curval.empty?
|
340
|
+
|
341
|
+
pop(oldval)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
# if no defaultvalue defined, field is mandatory
|
346
|
+
def value(fieldname,type,defaultvalue=nil,expected=nil)
|
347
|
+
ret = nil
|
348
|
+
check_field(fieldname,defaultvalue.nil?,type) do |val|
|
349
|
+
if val.nil?
|
350
|
+
ret = defaultvalue
|
351
|
+
else
|
352
|
+
ret = val
|
353
|
+
@val.delete(fieldname)
|
354
|
+
end
|
355
|
+
#ret = (val.nil? ? defaultvalue : val)
|
356
|
+
|
357
|
+
if expected
|
358
|
+
classname = (
|
359
|
+
expected.class == Class ? expected.name : expected.class.name
|
360
|
+
).split('::').last
|
361
|
+
self.send(
|
362
|
+
"check_#{classname.downcase}".to_sym,
|
363
|
+
ret,
|
364
|
+
expected,
|
365
|
+
fieldname
|
366
|
+
)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
ret
|
370
|
+
end
|
371
|
+
|
372
|
+
def unused(result = [],curval=nil,curpath=nil)
|
373
|
+
curval = @basehash if curval.nil?
|
374
|
+
curpath = [] unless curpath
|
375
|
+
|
376
|
+
if curval.is_a?(Hash)
|
377
|
+
curval.each do |key,value|
|
378
|
+
curpath << key
|
379
|
+
if value.nil?
|
380
|
+
result << ConfigParser.pathstr(curpath)
|
381
|
+
else
|
382
|
+
unused(result,value,curpath)
|
383
|
+
end
|
384
|
+
curpath.pop
|
385
|
+
end
|
386
|
+
elsif curval.is_a?(Array)
|
387
|
+
curval.each_index do |i|
|
388
|
+
curpath << i
|
389
|
+
if curval[i].nil?
|
390
|
+
result << ConfigParser.pathstr(curpath)
|
391
|
+
else
|
392
|
+
unused(result,curval[i],curpath)
|
393
|
+
end
|
394
|
+
curpath.pop
|
395
|
+
end
|
396
|
+
else
|
397
|
+
result << ConfigParser.pathstr(curpath)
|
398
|
+
end
|
399
|
+
|
400
|
+
result
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
data/lib/cute/execute.rb
ADDED
@@ -0,0 +1,272 @@
|
|
1
|
+
# To be used as you're using Open3.popen3 in ruby 1.9.2
|
2
|
+
module Cute
|
3
|
+
|
4
|
+
class Execute
|
5
|
+
require 'thread'
|
6
|
+
require 'fcntl'
|
7
|
+
attr_reader :command, :exec_pid, :stdout, :stderr, :status,:emptypipes
|
8
|
+
@@forkmutex = Mutex.new
|
9
|
+
|
10
|
+
def initialize(*cmd)
|
11
|
+
@command = *cmd
|
12
|
+
|
13
|
+
@exec_pid = nil
|
14
|
+
|
15
|
+
@stdout = nil
|
16
|
+
@stderr = nil
|
17
|
+
@status = nil
|
18
|
+
@run_thread = nil
|
19
|
+
@killed = false
|
20
|
+
|
21
|
+
@child_io = nil
|
22
|
+
@parent_io = nil
|
23
|
+
@lock = Mutex.new
|
24
|
+
@emptypipes = false
|
25
|
+
end
|
26
|
+
|
27
|
+
# Free the command, stdout stderr string.
|
28
|
+
def free
|
29
|
+
@command = nil
|
30
|
+
@stdout = nil
|
31
|
+
@stderr = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
# Same as new function
|
35
|
+
def self.[](*cmd)
|
36
|
+
self.new(*cmd)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Initialize the pipes and return one array for parent and one array for child.
|
40
|
+
def self.init_ios(opts={:stdin => false})
|
41
|
+
if opts[:stdin]
|
42
|
+
in_r, in_w = IO::pipe
|
43
|
+
in_w.sync = true
|
44
|
+
else
|
45
|
+
in_r, in_w = [nil,nil]
|
46
|
+
end
|
47
|
+
|
48
|
+
out_r, out_w = opts[:stdout] == false ? [nil,nil] : IO::pipe
|
49
|
+
err_r, err_w = opts[:stderr] == false ? [nil,nil] : IO::pipe
|
50
|
+
|
51
|
+
[ [in_r,out_w,err_w], [in_w,out_r,err_r] ]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Launch the command provided by the constructor
|
55
|
+
# @param [Hash] opts run options
|
56
|
+
# @option opts [Boolean] :stdin enable or disable pipe in stdin
|
57
|
+
# @option opts [Boolean] :stdout enable or disable pipe in stdout
|
58
|
+
# @option opts [Boolean] :stderr enable or disable pipe in stderr
|
59
|
+
# @option opts [Fixnum] :stdout_size number to limit the number of byte read by execute stdout
|
60
|
+
# @option opts [Fixnum] :stderr_size number to limit the number of byte read by execute stderr
|
61
|
+
def run(opts={:stdin => false})
|
62
|
+
@lock.synchronize do
|
63
|
+
if @run_thread
|
64
|
+
raise "Already launched"
|
65
|
+
else
|
66
|
+
begin
|
67
|
+
ensure #We can't interrupt this process here before run was launched.
|
68
|
+
@child_io, @parent_io = Execute.init_ios(opts)
|
69
|
+
@@forkmutex.synchronize do
|
70
|
+
@exec_pid = fork do
|
71
|
+
run_fork()
|
72
|
+
end
|
73
|
+
end
|
74
|
+
@run_thread = Thread.new do
|
75
|
+
@child_io.each do |io|
|
76
|
+
io.close if io and !io.closed?
|
77
|
+
end
|
78
|
+
@child_io = nil
|
79
|
+
emptypipes = true
|
80
|
+
|
81
|
+
@stdout,emptypipes = read_parent_io(1,opts[:stdout_size],emptypipes)
|
82
|
+
@stderr,emptypipes = read_parent_io(2,opts[:stderr_size],emptypipes)
|
83
|
+
|
84
|
+
_, @status = Process.wait2(@exec_pid)
|
85
|
+
@exec_pid = nil
|
86
|
+
|
87
|
+
@parent_io.each do |io|
|
88
|
+
io.close if io and !io.closed?
|
89
|
+
end
|
90
|
+
|
91
|
+
@parent_io = nil
|
92
|
+
@emptypipes = emptypipes
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
[@exec_pid, *@parent_io]
|
98
|
+
end
|
99
|
+
|
100
|
+
# Write to stdin
|
101
|
+
# @param str [String] string passed to process stdin.
|
102
|
+
def write_stdin(str)
|
103
|
+
@lock.synchronize do
|
104
|
+
if @parent_io and @parent_io[0] and !@parent_io[0].closed?
|
105
|
+
@parent_io[0].write(str)
|
106
|
+
else
|
107
|
+
raise "Stdin is closed"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Close stdin of programme if it opened.
|
113
|
+
def close_stdin()
|
114
|
+
@lock.synchronize do
|
115
|
+
if @parent_io and @parent_io[0] and !@parent_io[0].closed?
|
116
|
+
@parent_io[0].close
|
117
|
+
@parent_io[0] = nil
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Run the command and return the Execute object.
|
123
|
+
# @param [Hash] opts
|
124
|
+
def run!(opts={:stdin => false})
|
125
|
+
run(opts)
|
126
|
+
self
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
# Wait the end of process
|
131
|
+
# @param [Hash] opts wait options
|
132
|
+
# @option opts [Boolean] :checkstatus if it is true at end of process it raises an exception if the result is not null.
|
133
|
+
# @return [Array] Process::Status, stdout String, stderr String, emptypipe
|
134
|
+
def wait(opts={:checkstatus => true})
|
135
|
+
begin
|
136
|
+
wkilled=true
|
137
|
+
close_stdin()
|
138
|
+
@run_thread.join
|
139
|
+
wkilled=false
|
140
|
+
ensure
|
141
|
+
@lock.synchronize do
|
142
|
+
if wkilled && !@killed
|
143
|
+
kill!()
|
144
|
+
end
|
145
|
+
end
|
146
|
+
@run_thread.join
|
147
|
+
if !@killed
|
148
|
+
# raise SignalException if the process was terminated by a signal and the kill function was not called.
|
149
|
+
raise SignalException.new(@status.termsig) if @status and @status.signaled?
|
150
|
+
raise "Command #{@command.inspect} exited with status #{@status.exitstatus}" if opts[:checkstatus] and !@status.success?
|
151
|
+
end
|
152
|
+
end
|
153
|
+
[ @status, @stdout, @stderr, @emptypipes ]
|
154
|
+
end
|
155
|
+
|
156
|
+
EXECDEBUG = false
|
157
|
+
# kill a tree of processes. The killing is done in three steps:
|
158
|
+
# 1) STOP the target process
|
159
|
+
# 2) recursively kill all children
|
160
|
+
# 3) KILL the target process
|
161
|
+
def self.kill_recursive(pid)
|
162
|
+
puts "Killing PID #{pid} from PID #{$$}" if EXECDEBUG
|
163
|
+
|
164
|
+
# SIGSTOPs the process to avoid it creating new children
|
165
|
+
begin
|
166
|
+
Process.kill('STOP',pid)
|
167
|
+
rescue Errno::ESRCH # "no such process". The process was already killed, return.
|
168
|
+
puts "got ESRCH on STOP" if EXECDEBUG
|
169
|
+
return
|
170
|
+
end
|
171
|
+
# Gather the list of children before killing the parent in order to
|
172
|
+
# be able to kill children that will be re-attached to init
|
173
|
+
children = `ps --ppid #{pid} -o pid=`.split("\n").collect!{|p| p.strip.to_i}
|
174
|
+
children.compact!
|
175
|
+
puts "Children: #{children}" if EXECDEBUG
|
176
|
+
# Check that the process still exists
|
177
|
+
# Directly kill the process not to generate <defunct> children
|
178
|
+
children.each do |cpid|
|
179
|
+
kill_recursive(cpid)
|
180
|
+
end if children
|
181
|
+
|
182
|
+
begin
|
183
|
+
Process.kill('KILL',pid)
|
184
|
+
rescue Errno::ESRCH # "no such process". The process was already killed, return.
|
185
|
+
puts "got ESRCH on KILL" if EXECDEBUG
|
186
|
+
return
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
#Kill the launched process.
|
191
|
+
def kill()
|
192
|
+
@lock.synchronize{ kill! }
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
private
|
197
|
+
|
198
|
+
# Launch kill_recurcive if in launched and it not already killed
|
199
|
+
# killed becomes true.
|
200
|
+
def kill!()
|
201
|
+
if @exec_pid && !@killed
|
202
|
+
@killed = true
|
203
|
+
Execute.kill_recursive(@exec_pid)
|
204
|
+
# This function do not wait the PID since the thread that use wait() is supposed to be running and to do so
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Read pipe and return out and boolean which indicate if pipe are empty.
|
209
|
+
# @param num [Fixnum] number of file descriptor
|
210
|
+
# @param size [Fixnum] Maximum number of bytes must be read 0 is unlimited.
|
211
|
+
# @param emptypipes [Fixnum] Previous value of emptypipe the new value was obtained with logical and.
|
212
|
+
# @return [Array] output: String, emptypipes: Boolean
|
213
|
+
def read_parent_io(num,size,emptypipes)
|
214
|
+
out=''
|
215
|
+
if @parent_io and @parent_io[num]
|
216
|
+
if size and size > 0
|
217
|
+
out = @parent_io[num].read(size) unless @parent_io[num].closed?
|
218
|
+
emptypipes = false if !@parent_io[num].closed? and !@parent_io[num].eof?
|
219
|
+
unless @parent_io[num].closed?
|
220
|
+
@parent_io[num].readpartial(4096) until @parent_io[num].eof?
|
221
|
+
end
|
222
|
+
else
|
223
|
+
out = @parent_io[num].read unless @parent_io[num].closed?
|
224
|
+
end
|
225
|
+
end
|
226
|
+
[out,emptypipes]
|
227
|
+
end
|
228
|
+
|
229
|
+
# This function is made by children.
|
230
|
+
# It redirect the stdin,stdout,stderr
|
231
|
+
# Close another descriptor if we are in ruby < 2.0
|
232
|
+
# And launch the command with exec.
|
233
|
+
def run_fork()
|
234
|
+
begin
|
235
|
+
#stdin
|
236
|
+
STDIN.reopen(@child_io[0] || '/dev/null')
|
237
|
+
|
238
|
+
#stdout
|
239
|
+
STDOUT.reopen(@child_io[1] || '/dev/null')
|
240
|
+
|
241
|
+
#stderr
|
242
|
+
STDERR.reopen(@child_io[2] || '/dev/null')
|
243
|
+
|
244
|
+
|
245
|
+
# Close useless file descriptors.
|
246
|
+
# Since ruby 2.0, FD_CLOEXEC is set when ruby opens a descriptor.
|
247
|
+
# After performing exec(), all file descriptors are closed excepted 0,1,2
|
248
|
+
# https://bugs.ruby-lang.org/issues/5041
|
249
|
+
if RUBY_VERSION < "2.0"
|
250
|
+
Dir.foreach('/proc/self/fd') do |opened_fd|
|
251
|
+
begin
|
252
|
+
fd=opened_fd.to_i
|
253
|
+
if fd>2
|
254
|
+
f_IO=IO.new(fd)
|
255
|
+
f_IO.close if !f_IO.closed?
|
256
|
+
end
|
257
|
+
rescue Exception
|
258
|
+
#Some file descriptor are reserved for the rubyVM.
|
259
|
+
#So the function 'IO.new' raises an exception. We ignore that.
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
exec(*@command)
|
264
|
+
rescue SystemCallError, Exception => e
|
265
|
+
STDERR.puts "Fork Error: #{e.message} (#{e.class.name})"
|
266
|
+
STDERR.puts e.backtrace
|
267
|
+
end
|
268
|
+
exit! 1
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
end
|