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.
@@ -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
+
@@ -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