#! /usr/bin/env ruby # Rswatch, ruby implementation of swatch # # require File::Tail http://file-tail.rubyforge.org/ require 'getoptlong' begin require 'rubygems' rescue LoadError ensure $LOAD_PATH.push( File.dirname( __FILE__ ) ) end require 'file/tail' Version = "0.0.1" module Rswatch class Controller def initialize @libfile = nil @cfgfile = nil @yamlfile = nil @conf = nil # conf-self @recipes = {} @dryrun = false @detach = false end def run if ( parse_opts( optionparser ) ) if ( @libfile ) load( @libfile ) end if ( @yamlfile || @cfgfile ) if ( @detach and !@dryrun ) fork { do_exec } else do_exec end else usage( 'full' ) end else usage() end end def do_exec create_recipes() if ( @dryrun ) puts "config OK" exit end if ( @recipes.size > 0 ) if ( @detach ) Process.setsid end sleep end end def create_recipes load_conf().each_pair { |name, recipe| if ( recipe.class == Hash ) recipe['name'] = name r = Recipe.new( recipe ) if ( r.ready ) @recipes.store( name, r ) if ( !@dryrun ) r.watch() end end end } end def optionparser parser = GetoptLong.new() parser.set_options( ['--dry-run', '-n', GetoptLong::NO_ARGUMENT], ['--lib-file', '-l', GetoptLong::REQUIRED_ARGUMENT], ['--config-file', '-f', GetoptLong::REQUIRED_ARGUMENT], ['--config-yaml', '-y', GetoptLong::REQUIRED_ARGUMENT], ['--daemon', '-d', GetoptLong::NO_ARGUMENT], ['--debug', '-D', GetoptLong::NO_ARGUMENT], ['--help', '-h', GetoptLong::NO_ARGUMENT], ['--version', '-v', GetoptLong::NO_ARGUMENT] ) return parser end def parse_opts( parser ) ret = false parser.each_option { |name, val| case name when '--dry-run' @dryrun = true when '--lib-file' if ( File.exists?( val ) ) @libfile = val ret = true else puts "#{val} does not exist." abort end when '--config-file' if ( File.exist?( val ) ) @cfgfile = val ret = true else puts "#{val} does not exist." abort end when '--config-yaml' if ( File.exist?( val ) ) @yamlfile = val ret = true else puts "#{val} does not exist." abort end when '--daemon' @detach = true when '--debug' $DEBUG = true when '--help' usage( true ) exit when '--version' usage() exit end } return ret end def usage( full = nil ) puts <= '1.8.2' and @yamlfile ) require 'yaml' conf = YAML::load_file( @yamlfile ) elsif ( @cfgfile ) File.open( @cfgfile ) { |f| conf = eval( f.read() ) } end return conf end end # of class Controller # # Recipe for watching logfiles with some rules and a action # class Recipe @@items = nil def initialize( recipe = nil ) @name = nil @ready = false @watchfiles = nil @rules = nil @backline = 1 @action = nil @ignore_first = true @max_repeat = nil if ( recipe ) if ( recipe.has_key?( 'disabled' ) ) ; else set_recipe( recipe ) @ready = true end end end attr_reader( :ready ) attr_accessor( :name, :ignore_first ) def set_recipe( recipe = nil ) if ( recipe.class == Hash ) %w( name watchfiles rules backline action ignore_first max_repeat ).each { |var| if ( recipe.has_key?( var ) ) send( "#{var}=", recipe[var] ) end } return true else return false end end def watch action = Action.new rule = Rule.new @watchfiles.each { |file| Thread.start { alerts = 0 first = true backlog = Array.new( @backline, '' ) File.open( file ) { |log| log.extend( File::Tail ) log.backward( @backline ) log.tail { |line| backlog.shift backlog.push( line ) do_act = true @rules.each { |rulename| if ( !rule.send( rulename, backlog ) ) do_act = false break end } if ( do_act ) if ( (@ignore_first and first) or (!@max_repeat.nil? and alerts >= @max_repeat) ) ; else action.send( @action, log.path, backlog ) alerts += 1 end first = false backlog = Array.new( @backline, '' ) end } } } } end # # [Return] Array # def wrong_keys( conf ) ret = [] if ( conf.class == Hash ) conf.keys.each { |key| if ( !items.include?( key ) ) ret.push( key ) end } end return ret end # # [Return] Array # def items if ( !@@items.frozen? ) @@items = [] instance_variables.each { |var| @@items.push( var.sub( /\A@(.*)\z/, '\1' ) ) } @@items.freeze end return @@items end def defined_rules return RuleBase.instance_methods( false ) + Rule.instance_methods( false ) end def defined_actions return ActionBase.instance_methods( false ) + Action.instance_methods( false ) end attr_reader( :watchfiles ) protected # # set or die # def watchfiles=( files ) if ( files.class != Array ) files = [ files ] end @watchfiles = [] files.each { |file| if ( File.exists?( file ) ) @watchfiles.push( file ) else puts "A File `#{file}' does not exist. Cannot watch." abort end } return true end # # set or die # def rules=( rules ) if ( rules.class != Array ) rules = [ rules ] end methods = defined_rules @rules = [] rules.each { |rule| if ( methods.include?( rule ) ) @rules.push( rule ) else puts "Undefined rule `#{rule}'" abort end } return true end # # [Return] boolean # def backline=( num ) num = num.to_i if ( 0 < num and num < 100 ) # irresponsible limit @backline = num return true else return false end end # # set or die # def action=( action ) methods = defined_actions if ( methods.include?( action ) ) @action = action else puts "Undefined action #{action}" abort end return true end def max_repeat=( num ) num = num.to_i if ( num > 0 ) @max_repeat = num return true else return false end end end # of class Recipe # # boolean methods for analyzing characteristic pattern # class RuleBase # # example # # [Param] Array backlog # def nonzero?( backlog ) return ( backlog.last.chomp.size > 0 ) ? true : false end end # of class RuleBase # # actions invoked when some rules detect pattern # class ActionBase # # example # # [Param] String logname # [Param] Array backlog # def echo( logname, backlog ) puts "---- " + logname puts backlog.join end end # of class ActionBase # # user-defined rules # class Rule < RuleBase; end # # user-define actions # class Action < ActionBase; end end # of module Rswatch # # Run the app # if ( __FILE__ == $0 ) app = Rswatch::Controller.new() app.run() end