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

group[R]

Process group.

log_path[RW]

Process log output

options[RW]

Process options

output[R]

Process output

owner[R]

Process owner.

pid[R]

Process id.

pwd[R]

Directory where process was called from.

Public Class Methods

included(klass) click to toggle source
# File lib/runnable.rb, line 38
def self.included(klass)
  klass.extend ClassMethods
end
processes() click to toggle source

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

bandwidth( iface, sample_lapse = 0.1 ) click to toggle source

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
command() click to toggle source

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
command_style() click to toggle source

Parameter style used for the command. @return [Symbol] Command style.

# File lib/runnable.rb, line 143
def command_style
  :gnu
end
cpu() click to toggle source

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
input=( opt ) click to toggle source

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
join() click to toggle source

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() click to toggle source

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
mem() click to toggle source

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
method_missing( method, *params, &block ) click to toggle source

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]

Calls superclass method
# 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
output=( opt ) click to toggle source

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
run(name = nil, opts = nil, log_path = nil) click to toggle source

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
running?() click to toggle source

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_signal( signal ) click to toggle source

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
std_err() click to toggle source

Standar error output of the command @return [String] Standar error output

# File lib/runnable.rb, line 273
def std_err
  @std_err ||= ""
end
std_out() click to toggle source

Standar output of command @return [String] Standar output

# File lib/runnable.rb, line 267
def std_out
  @std_out ||= ""
end
stop() click to toggle source

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

parse_hash( hash ) click to toggle source

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_streams( output_streams = {}, stream_processors = nil ) click to toggle source

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

compose_command() click to toggle source
# 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
save_log(output_name, line) click to toggle source
# 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
threaded_output_processor(output_name, pipes, stream_processors) click to toggle source
# 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
threaded_process(pid, output_threads) click to toggle source
# 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