ruby-cute 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|