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