module Runnable
Convert a executable command in a Ruby-like class you are able to start, define params and send signals (like kill, or stop)
@example Usage:
class LS include Runnable executes :ls command_style :extended end ls = LS.new ls.alh ls.run
Constants
- HERTZ
Constant to calculate cpu usage.
Attributes
Process group.
Process log output
Process options
Process output
Process owner.
Process id.
Directory where process was called from.
Public Class Methods
# File lib/runnable.rb, line 38 def self.included(klass) klass.extend ClassMethods end
List of runnable instances running on the system. @return [Hash] Using process pids as keys and instances as values.
# File lib/runnable.rb, line 383 def self.processes @@processes end
Public Instance Methods
Estimated bandwidth in kb/s. @param [String] iface Interface to be scaned. @param [Number] sample_time Time passed between samples in seconds.
The longest lapse the more accurate stimation.
@return [Number] The estimated bandwidth used.
# File lib/runnable.rb, line 334 def bandwidth( iface, sample_lapse = 0.1 ) file = "/proc/#{@pid}/net/dev" File.open( file ).read =~ /#{iface}:\s+(\d+)\s+/ init = $1.to_i sleep sample_lapse File.open( file ).read =~ /#{iface}:\s+(\d+)\s+/ finish = $1.to_i (finish - init)*(1/sample_lapse)/1024 end
Default command to be executed @return [String] Command to be executed
# File lib/runnable.rb, line 149 def command self.class.to_s.split( "::" ).last.downcase end
Parameter style used for the command. @return [Symbol] Command style.
# File lib/runnable.rb, line 143 def command_style :gnu end
Estimated CPU usage in %. @return [Number] The estimated cpu usage.
# File lib/runnable.rb, line 297 def cpu # Open the proc stat file begin stat = File.open( "/proc/#{@pid}/stat" ).read.split # Get time variables # utime = User Time # stime = System Time # start_time = Time passed from process starting utime = stat[13].to_f stime = stat[14].to_f start_time = stat[21].to_f # uptime = Time passed from system starting uptime = File.open( "/proc/uptime" ).read.split[0].to_f # Total time that the process has been executed total_time = utime + stime # in jiffies # Seconds passed between start the process and now seconds = uptime - ( start_time / HERTZ ) # Percentage of used CPU ( ESTIMATED ) (total_time / seconds.to_f) rescue IOError # Fails to open file 0 rescue ZeroDivisionError # Seconds is Zero! 0 end end
Sets the command input to be passed to the command execution @param [String] opt Command input
# File lib/runnable.rb, line 279 def input=( opt ) @command_input = opt end
Wait for command thread to finish it execution. @return [nil]
# File lib/runnable.rb, line 254 def join @run_thread.join if @run_thread.alive? @output unless @output.empty? end
Kill the comand. @return [nil] @todo Raise an exeption if process is not running.
# File lib/runnable.rb, line 244 def kill send_signal( :kill ) # In order to maintain consistency of @@processes # we must assure that @run_thread finish correctly join end
Calculate the estimated memory usage in Kb. @return [Number] Estimated mem usage in Kb.
# File lib/runnable.rb, line 291 def mem File.open( "/proc/#{@pid}/status" ).read.split( "\n" )[11].split( " " )[1].to_i end
Convert undefined methods (ruby-like syntax) into parameters to be parsed at the execution time. This only convert methods with zero or one parameters. A hash can be passed and each key will define a new method and method name will be ignored.
@example Valid calls:
find.depth #=> find -depth find.iname( '"*.rb"') #=> find -iname "*.rb" find.foo( { :iname => '"*.rb"', :type => '"f"' } ) #=> find -iname "*.rb" - type "f"
@example Invalid calls:
sleep.5 #=> Incorrect. "5" is not a valid call to a ruby method so method_missing will not be invoked and will raise a tINTEGER exception
@param [Symbol] method Method called that is missing @param [Array] params Params in the call @param [Block] block Block code in method @return [nil]
# File lib/runnable.rb, line 364 def method_missing( method, *params, &block ) @command_line_interface ||= Object.const_get( command_style.to_s.capitalize.to_sym ).new if params.length > 1 super( method, params, block ) else if params[0].class == Hash # If only one param is passed and its a Hash # we need to expand the hash and call each key as a method with value as params # @see parse_hash for more information parse_hash( params[0] ) else @command_line_interface.add_param( method.to_s, params != nil ? params.join(",") : nil ) end end end
Sets the command output to be passed to the command execution @param [String] opt Command output
# File lib/runnable.rb, line 285 def output=( opt ) @command_output = opt end
Start the execution of the command. @return [nil]
# File lib/runnable.rb, line 158 def run(name = nil, opts = nil, log_path = nil) return false if @pid # Create a new mutex @pid_mutex = Mutex.new # Log path should be an instance variable to avoid a mess @log_path = log_path || @log_path # Create pipes to redirect Standar I/O out_rd, out_wr = IO.pipe # Redirect Error I/O err_rd, err_wr = IO.pipe # Reset exceptions array to not store exceptions for # past executions command_argument = opts ? opts.split(" ") : compose_command @pid = Process.spawn( command.to_s, *command_argument, { :out => out_wr, :err => err_wr } ) # Include instance in class variable self.class.processes[@pid] = self # Prepare the process info file to be read file_status = File.open( "/proc/#{@pid}/status" ).read.split( "\n" ) # Owner: Read the owner of the process from /proc/@pid/status @owner = file_status[6].split( " " )[1] # Group: Read the Group owner from /proc/@pid/status @group = file_status[7].split( " " )[1] # Set @output_thread with new threads # wich execute the input/ouput loop stream_info = { :out => [out_wr, out_rd], :err => [err_wr, err_rd] } if name cmd_info = self.class.commands[name] stream_processors = { :outputs => cmd_info[:outputs], :exceptions => cmd_info[:exceptions] } end output_threads = process_streams( stream_info, stream_processors ) # Create a new thread to avoid blocked processes @run_thread = threaded_process(@pid, output_threads) # Satuts Variables # PWD: Current Working Directory get by /proc/@pid/cwd # @rescue If a fast process is runned there isn't time to get # the correct PWD. If the readlink fails, we retry, if the process still alive # until the process finish. begin @pwd ||= File.readlink( "/proc/#{@pid}/cwd" ) rescue Errno::ENOENT # If cwd is not available rerun @run_thread if @run_thread.alive? #If it is alive, we retry to get cwd @run_thread.run retry else #If process has terminated, we set pwd to current working directory of ruby @pwd = Dir.getwd end rescue #Errno::EACCESS @pwd = Dir.getwd end end
Check if prcess is running on the system. @return [Bool] True if process is running, false if it is not.
# File lib/runnable.rb, line 261 def running? Dir.exists?( "/proc/#{@pid}") end
Send the desired signal to the command. @param [Symbol] Signal to be send to the command. @todo raise ESRCH if pid is not in system
or EPERM if pid is not from user.
# File lib/runnable.rb, line 391 def send_signal( signal ) if signal == :stop signal = :SIGINT elsif signal == :kill signal = :SIGKILL end `ps -ef`.each_line do |line| line = line.split pid = line[1] ppid = line[2] if ppid.to_i == @pid Process.kill( signal, pid.to_i ) end end begin Process.kill( signal, @pid ) rescue Errno::ESRCH # As we kill child processes, main process may have exit already end end
Standar error output of the command @return [String] Standar error output
# File lib/runnable.rb, line 273 def std_err @std_err ||= "" end
Standar output of command @return [String] Standar output
# File lib/runnable.rb, line 267 def std_out @std_out ||= "" end
Stop the command. @return [nil] @todo Raise an exception if process is not running.
# File lib/runnable.rb, line 233 def stop send_signal( :stop ) # In order to maintain consistency of @@processes # we must assure that @run_thread finish correctly @run_thread.run if @run_thread.alive? end
Protected Instance Methods
Expand a parameter hash calling each key as method and value as param forcing method misssing to be called. @param [Hash] hash Parameters to be expand and included in command execution @return [nil]
# File lib/runnable.rb, line 438 def parse_hash( hash ) hash.each do |key, value| # Add the param parsed to command_line_interface @command_line_interface.add_param( key.to_s, value != nil ? value.to_s : nil ) end end
Process the command I/O. These files are located in /var/log/runnable. @param [Hash] Outputs options. @option outputs stream [Symbol] Stream name. @option outputs pipes [IO] I/O stream to be redirected. @return [Array] output_threads Array containing the output processing threads
# File lib/runnable.rb, line 422 def process_streams( output_streams = {}, stream_processors = nil ) @output = Hash.new @std_output = Hash.new output_threads = [] # for each io stream we create a thread wich read that # stream and write it in a log file output_streams.collect do |output_name, pipes| threaded_output_processor(output_name, pipes, stream_processors) end end
Private Instance Methods
# File lib/runnable.rb, line 455 def compose_command @command_line_interface ||= Object.const_get( command_style.to_s.capitalize.to_sym ).new [ @command_input.to_s, @options.to_s, @command_line_interface.parse, @command_output.to_s ].select do |value| !value.to_s.strip.empty? end.flatten.select{|x| !x.empty?} end
# File lib/runnable.rb, line 447 def save_log(output_name, line) Dir.mkdir( @log_path ) unless Dir.exist?( @log_path ) File.open("#{@log_path}/#{self.command}_#{@pid}.log", "a") do |log_file| log_file.puts( "[#{Time.new.inspect} || [STD#{output_name.to_s.upcase} || [#{@pid}]] #{line}" ) end end
# File lib/runnable.rb, line 487 def threaded_output_processor(output_name, pipes, stream_processors) exception_processors = stream_processors[:exceptions].is_a?(Hash) ? stream_processors[:exceptions] : {} exception_processors.merge!(self.class.processors[:exceptions] || {}) output_processors = stream_processors[:outputs].is_a?(Hash) ? stream_processors[:outputs] : {} output_processors.merge!(self.class.processors[:output] || {}) Thread.new do pipes[0].close pipes[1].each_line do |line| ( output_name == :err ? self.std_err : self.std_out ) << line save_log(output_name, line) if @log_path # Match custom exceptions # if we get a positive match, raise the exception exception_processors.each do | reg_expr, value | raise value.new( line ) if reg_expr =~ line end # Match custom outputs # if we get a positive match, add it to the outputs array output_processors.each do | reg_expr, value | @output[value] ||= Array.new @output[value] << $1 if reg_expr =~ line end end end end
# File lib/runnable.rb, line 467 def threaded_process(pid, output_threads) Thread.new do # Wait to get the pid process even if it has finished Process.wait( pid, Process::WUNTRACED ) # Wait each I/O thread output_threads.each { |thread| thread.join } # Get the exit code from command exit_status = $?.exitstatus # This instance is finished and we remove it self.class.processes.delete( pid ) @pid = nil # In case of error add an Exception to the @excep_array raise SystemCallError.new( exit_status ) if exit_status != 0 end end