runnable 0.1.1

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