runnable 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown ADDED
@@ -0,0 +1,93 @@
1
+ # Runnable
2
+ A Ruby gem that allow programmer to control UNIX system commands as a Ruby class.
3
+
4
+ # Usage
5
+ All you have to do is to create a class named exactly as command and make it inherit from class Runnable.
6
+
7
+ ```ruby
8
+ class LS < Runnable
9
+ end
10
+ ```
11
+
12
+ That gives you the basics to control the execution of `ls` command.
13
+
14
+ Now you can create an instance like this:
15
+
16
+ ```ruby
17
+ my_command = LS.new
18
+ ```
19
+
20
+ And run the command as follows
21
+
22
+ ```ruby
23
+ my_command.run
24
+ ```
25
+
26
+ Many other options are available; you can stop the command, kill it or look
27
+ for some important information about the command and its process. Entire
28
+ documentation of this gem can be found under `./doc` directory or been generated
29
+ by `yardoc`.
30
+
31
+ ## Return values
32
+ Runnable uses another gems called `Publisher`. It allow Runnable to fire
33
+ events that can be processed or ignored. When a command ends its execution,
34
+ Runnable always fire and event: `:finish` if commands finalized in a correct way
35
+ or `:fail` if an error ocurred. In case something went wrong and a `:fail`
36
+ events was fired, Runnable also provide an array containing the command return
37
+ value as the parameter of a SystemCallError exception and optionally others
38
+ exceptions ocurred at runtime.
39
+
40
+ This is an example of how can we receive the return value of a command:
41
+
42
+ ```ruby
43
+ class LS < Runnable
44
+ end
45
+
46
+ my_command = LS.new
47
+
48
+ my_command.when :finish do
49
+ puts "Everything went better than expected :)"
50
+ end
51
+
52
+ my_command.when :fail do |exceptions|
53
+ puts "Something went wrong"
54
+ exceptions.each do |exception|
55
+ puts exception.message
56
+ end
57
+ end
58
+
59
+ my_command.run
60
+ ```
61
+
62
+ ## Custom exceptions
63
+ As we saw in previous chapter, if a command execution does not ends
64
+ succesfully, Runnable fires a `:fail` event whit an exceptions array. We can
65
+ add exceptions to that array based on the output of command. For example, we
66
+ can controll that parameters passed to a command are valids if we know the
67
+ command output for an invalid parameters.
68
+
69
+ First we have to do is override the method `exceptions` defined in runnable
70
+ as follows
71
+
72
+ ```ruby
73
+ class LS < Runnable
74
+ def exceptions
75
+ { /ls: (invalid option.*)/ => ArgumentError }
76
+ end
77
+ end
78
+ ```
79
+
80
+ `exceptions` method should return a hash containing a regular expression
81
+ which will be match against the command output, and a value which will be the
82
+ exception added to exception array. This means that if the command output match
83
+ the regular expression, a new exception will be include in `:fail` event parameter.
84
+
85
+ # About
86
+ Runnable is a gem develop by [NoSoloSoftware](http://nosolosoftware.biz)
87
+
88
+ # License
89
+ Runnable is Copyright 2011 NoSoloSoftware, it is free software.
90
+
91
+ Runnable is distributed under GPLv3 license. More details can be found at COPYING
92
+ file.
93
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -0,0 +1,42 @@
1
+ # Copyright 2011 NoSoloSoftware
2
+
3
+ # This file is part of Runnable.
4
+ #
5
+ # Runnable is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # Runnable is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Runnable. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+
19
+ # <p>Base class to create a command-line parameter parser.</p>
20
+ # <p>It holds that parameters in a hash and the child has
21
+ # to be the one who return the formatted string according
22
+ # to the standard used.</p>
23
+ class Command_parser
24
+ # Create a new instance of the parser.
25
+ def initialize
26
+ @params = {}
27
+ end
28
+
29
+ # Add params and value to the params hash to be parsed.
30
+ # @param [String] param Parameter name.
31
+ # @param [Object] value Parameter value.
32
+ # @return [nil]
33
+ def add_param( param, value = nil )
34
+ @params[param] = value
35
+ end
36
+
37
+ # This method has to be overwritten in the child
38
+ # @abstract
39
+ # @return [String]
40
+ def parse
41
+ end
42
+ end
@@ -0,0 +1,33 @@
1
+ # Copyright 2011 NoSoloSoftware
2
+
3
+ # This file is part of Runnable.
4
+ #
5
+ # Runnable is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # Runnable is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Runnable. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ require 'command_parser'
19
+
20
+ # <p>Parse the parameter hash using the extended standard.</p>
21
+ class Extended < Command_parser
22
+
23
+ # Convert a hash in a Extended style string options.
24
+ # @return [String] Extended-style parsed params in a raw character array.
25
+ def parse
26
+ options = ""
27
+ @params.each do | param , value |
28
+ options = "#{options} -#{param} #{value} "
29
+ end
30
+ options.strip
31
+ end
32
+
33
+ end
@@ -0,0 +1,46 @@
1
+ # Copyright 2011 NoSoloSoftware
2
+
3
+ # This file is part of Runnable.
4
+ #
5
+ # Runnable is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # Runnable is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Runnable. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ require 'command_parser'
19
+
20
+ # <p>Parse the parameter hash using the GNU standard.</p>
21
+ class Gnu < Command_parser
22
+
23
+ # This method convert a hash in a string ready to
24
+ # be passed to a command that uses GNU style to parse command line
25
+ # parameters.
26
+ # @return [String] Gnu-style parsed params in a raw character array.
27
+ def parse
28
+ result = ""
29
+
30
+ @params.each do |param, value|
31
+ # We assume that an one character words is preceed by one
32
+ # lead and two or more characters words are preceed by two
33
+ # leads
34
+ result << ( param.length == 1 ? "-#{param} " : "--#{param} " )
35
+
36
+ # In case the param have parameter we use the correct assignation
37
+ # -Param followed by value (without whitespace) to one character params
38
+ # -Param followed by '=' and value to more than one character params
39
+ if( value != nil )
40
+ result << ( param.length == 1 ? "#{value}" : "=#{value}" )
41
+ end
42
+ end
43
+
44
+ return result.strip
45
+ end
46
+ end
data/lib/runnable.rb ADDED
@@ -0,0 +1,415 @@
1
+ # Copyright 2011 NoSoloSoftware
2
+
3
+ # This file is part of Runnable.
4
+ #
5
+ # Runnable is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # Runnable is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Runnable. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+
19
+ # Convert a executable command in a Ruby-like class
20
+ # you are able to start, define params and send signals (like kill, or stop)
21
+ #
22
+ # @example Usage:
23
+ # class LS < Runnable
24
+ # command_style :extended
25
+ # end
26
+ #
27
+ # ls = LS.new
28
+ # ls.alh
29
+ # ls.run
30
+ #
31
+ $LOAD_PATH << File.expand_path( './runnable', __FILE__ )
32
+
33
+ require 'publisher'
34
+
35
+ class Runnable
36
+ extend Publisher
37
+
38
+ # Fires to know whats happening inside
39
+ can_fire :fail, :finish
40
+
41
+ # Process id.
42
+ attr_reader :pid
43
+ # Process owner.
44
+ attr_reader :owner
45
+ # Process group.
46
+ attr_reader :group
47
+ # Directory where process was called from.
48
+ attr_reader :pwd
49
+
50
+ # Metaprogramming part of the class
51
+
52
+ # Define the parameter style to be used.
53
+ # @return [nil]
54
+ def self.command_style( style )
55
+ define_method( :command_style ) do
56
+ style
57
+ end
58
+ end
59
+
60
+ # Parameter style used for the command.
61
+ # @return [Symbol] Command style.
62
+ def command_style
63
+ :gnu
64
+ end
65
+
66
+ # List of runnable instances running on the system order by pid.
67
+ @@processes = Hash.new
68
+
69
+ # Constant to calculate cpu usage.
70
+ HERTZ = 100
71
+
72
+ # Create a new instance of a runnable command.
73
+ # @param [Hash] option_hash Options.
74
+ # @option option_hash :delete_log (true) Delete the log after execution.
75
+ # @option option_hash :command_options ("") Command options.
76
+ # @option option_hash :log_path ("/var/log/runnable") Path for the log files.
77
+ def initialize( option_hash = {} )
78
+ # keys :delete_log
79
+ # :command_options
80
+ # :log_path
81
+
82
+ # If we have the command class in a namespace, we need to remove
83
+ # the namespace name
84
+ @command = self.class.to_s.split( "::" ).last.downcase
85
+
86
+ # Set the default command option
87
+ # Empty by default
88
+ option_hash[:command_options] ||= ""
89
+ @options = option_hash[:command_options]
90
+
91
+ # Set the log path
92
+ # Default path is "/var/log/runnable"
93
+ option_hash[:log_path] ||= "/var/log/runnable/"
94
+ @log_path = option_hash[:log_path]
95
+
96
+ # Set the delete_log option
97
+ # true by default
98
+ if option_hash[:delete_log] == nil
99
+ @delete_log = true
100
+ else
101
+ @delete_log = option_hash[:delete_log]
102
+ end
103
+
104
+ # Store input options
105
+ @input = Array.new
106
+
107
+ # Store output options
108
+ @output = Array.new
109
+
110
+ # @todo: checks that command is in the PATH
111
+ # ...
112
+
113
+ # we dont set the pid, because we dont know until run
114
+ @pid = nil
115
+ @excep_array = []
116
+
117
+
118
+ # Metaprogramming part
119
+ # Require the class to parse the command line options
120
+ require command_style.to_s.downcase
121
+ # Create a new instance of the parser class
122
+ @command_line_interface = Object.const_get( command_style.to_s.capitalize.to_sym ).new
123
+ # End Metaprogramming part
124
+
125
+ #End of initialize instance variables
126
+
127
+ create_log_directory
128
+ end
129
+
130
+ # Start the execution of the command.
131
+ # @return [nil]
132
+ # @fire :finish
133
+ # @fire :fail
134
+ def run
135
+ # Create a new mutex
136
+ @pid_mutex = Mutex.new
137
+
138
+ # Create pipes to redirect Standar I/O
139
+ out_rd, out_wr = IO.pipe
140
+ # Redirect Error I/O
141
+ err_rd, err_wr = IO.pipe
142
+
143
+ #
144
+ @pid = Process.spawn( "#{@command} #{@input.join( " " )} \
145
+ #{@options} #{@command_line_interface.parse} \
146
+ #{@output.join( " " )}", { :out => out_wr, :err => err_wr } )
147
+
148
+ # Include instance in class variable
149
+ @@processes[@pid] = self
150
+
151
+ # Prepare the process info file to be read
152
+ file_status = File.open( "/proc/#{@pid}/status" ).read.split( "\n" )
153
+ # Owner: Read the owner of the process from /proc/@pid/status
154
+ @owner = file_status[6].split( " " )[1]
155
+ # Group: Read the Group owner from /proc/@pid/status
156
+ @group = file_status[7].split( " " )[1]
157
+
158
+ # Set @output_thread with new threads
159
+ # wich execute the input/ouput loop
160
+ create_logs(:out => [out_wr, out_rd], :err => [err_wr, err_rd])
161
+
162
+ # Create a new thread to avoid blocked processes
163
+ @run_thread = Thread.new do
164
+ # Wait to get the pid process even if it has finished
165
+ Process.wait( @pid, Process::WUNTRACED )
166
+
167
+ # Wait each I/O thread
168
+ @output_threads.each { |thread| thread.join }
169
+ # Delete log if its necesary
170
+ delete_log
171
+
172
+ # Get the exit code from command
173
+ exit_status = $?.exitstatus
174
+
175
+ # In case of error add an Exception to the @excep_array
176
+ @excep_array << SystemCallError.new( exit_status ) if exit_status != 0
177
+
178
+ # Fire signals according to the exit code
179
+ if @excep_array.empty?
180
+ fire :finish
181
+ else
182
+ fire :fail, @excep_array
183
+ end
184
+
185
+ # This instance is finished and we remove it
186
+ @@processes.delete( @pid )
187
+ end
188
+
189
+ # Satuts Variables
190
+ # PWD: Current Working Directory get by /proc/@pid/cwd
191
+ # @rescue If a fast process is runned there isn't time to get
192
+ # the correct PWD. If the readlink fails, we retry, if the process still alive
193
+ # until the process finish.
194
+ begin
195
+ @pwd = File.readlink( "/proc/#{@pid}/cwd" )
196
+ rescue
197
+ # If cwd is not available rerun @run_thread
198
+ if @run_thread.alive?
199
+ #If it is alive, we retry to get cwd
200
+ @run_thread.run
201
+ retry
202
+ else
203
+ #If process has terminated, we set pwd to current working directory of ruby
204
+ @pwd = Dir.getwd
205
+ end
206
+ end
207
+ end
208
+
209
+ # Stop the command.
210
+ # @return [nil]
211
+ # @todo Raise an exception if process is not running.
212
+ def stop
213
+ send_signal( :stop )
214
+
215
+ # In order to maintain consistency of @@processes
216
+ # we must assure that @run_thread finish correctly
217
+ @run_thread.run if @run_thread.alive?
218
+ end
219
+
220
+ # Kill the comand.
221
+ # @return [nil]
222
+ # @todo Raise an exeption if process is not running.
223
+ def kill
224
+ send_signal( :kill )
225
+
226
+ # In order to maintain consistency of @@processes
227
+ # we must assure that @run_thread finish correctly
228
+ join
229
+ end
230
+
231
+ # Wait for command thread to finish it execution.
232
+ # @return [nil]
233
+ def join
234
+ @run_thread.join if @run_thread.alive?
235
+ end
236
+
237
+ # Calculate the estimated memory usage in Kb.
238
+ # @return [Number] Estimated mem usage in Kb.
239
+ def mem
240
+ File.open( "/proc/#{@pid}/status" ).read.split( "\n" )[11].split( " " )[1].to_i
241
+ end
242
+
243
+ # Estimated CPU usage in %.
244
+ # @return [Number] The estimated cpu usage.
245
+ def cpu
246
+ # Open the proc stat file
247
+ begin
248
+ stat = File.open( "/proc/#{@pid}/stat" ).read.split
249
+
250
+ # Get time variables
251
+ # utime = User Time
252
+ # stime = System Time
253
+ # start_time = Time passed from process starting
254
+ utime = stat[13].to_f
255
+ stime = stat[14].to_f
256
+ start_time = stat[21].to_f
257
+
258
+ # uptime = Time passed from system starting
259
+ uptime = File.open( "/proc/uptime" ).read.split[0].to_f
260
+
261
+ # Total time that the process has been executed
262
+ total_time = utime + stime # in jiffies
263
+
264
+ # Seconds passed between start the process and now
265
+ seconds = uptime - ( start_time / HERTZ )
266
+ # Percentage of used CPU ( ESTIMATED )
267
+ (total_time / seconds.to_f)
268
+ rescue IOError
269
+ # Fails to open file
270
+ 0
271
+ rescue ZeroDivisionError
272
+ # Seconds is Zero!
273
+ 0
274
+ end
275
+
276
+ end
277
+
278
+ # Set the input files.
279
+ # @param [String] param Input to be parsed as command options.
280
+ # @return [nil]
281
+ def input( param )
282
+ @input << param
283
+ end
284
+
285
+ # Set the output files.
286
+ # @param [String] param Output to be parsed as command options.
287
+ # @return [nil]
288
+ def output( param )
289
+ @output << param
290
+ end
291
+
292
+ # Convert undefined methods (ruby-like syntax) into parameters
293
+ # to be parsed at the execution time.
294
+ # This only convert methods with zero or one parameters. A hash can be passed
295
+ # and each key will define a new method and method name will be ignored.
296
+ #
297
+ # @example Valid calls:
298
+ # find.depth #=> find -depth
299
+ # find.iname( '"*.rb"') #=> find -iname "*.rb"
300
+ # find.foo( { :iname => '"*.rb"', :type => '"f"' } ) #=> find -iname "*.rb" - type "f"
301
+ # @example Invalid calls:
302
+ # sleep.5 #=> Incorrect. "5" is not a valid call to a ruby method so method_missing will not be invoked and will
303
+ # raise a tINTEGER exception
304
+ #
305
+ # @param [Symbol] method Method called that is missing
306
+ # @param [Array] params Params in the call
307
+ # @param [Block] block Block code in method
308
+ # @return [nil]
309
+ # @override
310
+ def method_missing( method, *params, &block )
311
+ if params.length > 1
312
+ super( method, params, block )
313
+ else
314
+ if params[0].class == Hash
315
+ # If only one param is passed and its a Hash
316
+ # we need to expand the hash and call each key as a method with value as params
317
+ # @see parse_hash for more information
318
+ parse_hash( params[0] )
319
+ else
320
+ @command_line_interface.add_param( method.to_s,
321
+ params != nil ? params.join(",") : nil )
322
+ end
323
+ end
324
+ end
325
+
326
+ # List of runnable instances running on the system.
327
+ # @return [Hash] Using process pids as keys and instances as values.
328
+ def self.processes
329
+ @@processes
330
+ end
331
+
332
+ # @abstract
333
+ # Returns a hash of regular expressions and exceptions associated to them.
334
+ # Command output is match against those regular expressions, if it does match
335
+ # an appropiate exception is included in the return value of execution.
336
+ # @note This method should be overwritten in child classes.
337
+ # @example Usage:
338
+ # class ls < Runnable
339
+ # def exceptions
340
+ # { /ls: (invalid option.*)/ => ArgumentError }
341
+ # end
342
+ # end
343
+ #
344
+ # @return [Hash] Using regular expressions as keys and exceptions that should
345
+ # be raised as values.
346
+ def exceptions
347
+ {}
348
+ end
349
+
350
+ protected
351
+
352
+ # Send the desired signal to the command.
353
+ # @param [Symbol] Signal to be send to the command.
354
+ # @todo raise ESRCH if pid is not in system
355
+ # or EPERM if pid is not from user.
356
+ def send_signal( signal )
357
+ if signal == :stop
358
+ Process.kill( :SIGINT, @pid )
359
+ elsif signal == :kill
360
+ Process.kill( :SIGKILL, @pid )
361
+ end
362
+ end
363
+
364
+ # Redirect command I/O to log files.
365
+ # These files are located in /var/log/runnable.
366
+ # @param [Hash] Outputs options.
367
+ # @option outputs stream [Symbol] Stream name.
368
+ # @option outputs pipes [IO] I/O stream to be redirected.
369
+ # @return [nil]
370
+ def create_logs( outputs = {} )
371
+ # Create an empty file for logging
372
+ FileUtils.touch "#{@log_path}#{@command}_#{@pid}.log"
373
+
374
+ @output_threads = []
375
+ # for each io stream we create a thread wich read that
376
+ # stream and write it in a log file
377
+ outputs.each do |output_name, pipes|
378
+ @output_threads << Thread.new do
379
+ pipes[0].close
380
+
381
+ pipes[1].each_line do |line|
382
+ File.open("#{@log_path}#{@command}_#{@pid}.log", "a") do |log_file|
383
+ log_file.puts( "[#{Time.new.inspect} || [STD#{output_name.to_s.upcase} || [#{@pid}]] #{line}" )
384
+ end
385
+ # Match custom exceptions
386
+ # if we get a positive match, add it to the exception array
387
+ # in order to inform the user of what had happen
388
+ exceptions.each do | reg_expr, value |
389
+ @excep_array<< value.new( $1 ) if reg_expr =~ line
390
+ end
391
+ end
392
+ end
393
+ end
394
+ end
395
+
396
+ def create_log_directory
397
+ Dir.mkdir( @log_path ) unless Dir.exist?( @log_path )
398
+ end
399
+
400
+ def delete_log
401
+ File.delete( "#{@log_path}#{@command}_#{@pid}.log" ) if @delete_log == true
402
+ end
403
+
404
+ # Expand a parameter hash calling each key as method and value as param
405
+ # forcing method misssing to be called.
406
+ # @param [Hash] hash Parameters to be expand and included in command execution
407
+ # @return [nil]
408
+ def parse_hash( hash )
409
+ hash.each do |key, value|
410
+ # Call to a undefined method which trigger overwritten method_missing
411
+ # unless its named as a runnable method
412
+ self.public_send( key.to_sym, value ) unless self.respond_to?( key.to_sym )
413
+ end
414
+ end
415
+ end