#!/usr/bin/perl use strict; use English qw/ $PID $PROGRAM_NAME /; use FileHandle; use File::Basename qw/ basename /; use Getopt::Long qw/ :config no_ignore_case /; use Jcode; use Net::SMTP; use POSIX qw/ EAGAIN strftime /; use Sys::Syslog qw/ openlog syslog closelog /; # バージョン番号 my $VERSION = sprintf( '0.%d.%02d', q$Revision: 1.15 $ =~ /(\d+)\.(\d+)/ ); my $BASENAME = &basename( $PROGRAM_NAME, ".perl" ); =head1 NAME polling - ファイルが変化したときに指定されたコマンドを実行する =head1 SYNOPSIS polling [OPTIONS] COMMAND [ARGUMENTS...] =head1 DESCRIPTION 一定時間おきに指定されたファイルの更新時間を調べ,変化があった場合には 指定されたコマンドを実行する.コマンドが標準出力または標準エラー出力に 何らかの出力を行った場合は,メールでその結果を通知する. =head1 OPTIONS =over 4 =item -f C =item --file=C 指定された C の変化を監視する.オプションによる指定がなければ, F を監視対象とする. =cut my $FILE = "/var/log/messages"; =item -i C =item --interval=C ファイルの変化を検出する時間間隔を指定する.オプションによる指定がなけ れば,30 秒おきに検出を行う. =cut my $INTERVAL = 30; =item -c C =item --command-interval=C コマンドを実行する最低間隔を指定する.ファイルの変化が検出された場合で あっても,前回コマンド実行時から指定された秒数が経過していなければ,コ マンドの実行を行わない.オプションによる指定がなければ,900 秒を最低間 隔とする. =cut my $COMMANDINTERVAL = 900; # 900秒 = 15分 =item -r =item --reentrant コマンドが reentrant であることを指定する.このオプションが指定されて いると,コマンドの終了を待たずにファイルの変更日時を検出するループに戻 るため,複数のコマンドが同時に実行されることが有り得る. =cut my $REENTRANT = 0; =item -p C =item --pidfile=C background で動作するプロセスの PID を記録するファイル名を指定する.オ プションによる指定がなければ,F に記録する. =cut my $PIDFILE = "/var/run/polling.pid"; =item -P C =item --priority=C background で動作するプロセスの process priority を指定する.オプショ ンによる指定がなければ,12 を用いる. =cut my $PRIORITY = 12; =item -l C =item --log-facility=C syslog(3) に記録するときに用いる facility を指定する.オプションによる 指定がなければ,I が用いられる. =cut my $FACILITY = "local6"; =item -a C
=item --address=C
コマンドの実行結果を通知するアドレスを指定する.オプションによる指定が なければ,このスクリプトを実行したユーザーに対して通知を試みる. =cut my $FROM = sprintf( "%s\@%s", ( $ENV{'USER'} || "root" ), ( $ENV{'HOST'} || $ENV{'HOSTNAME'} || "localhost" ) ); my $TO = $FROM; # 子プロセスの起動を再試行する回数 my $RETRY = 5; # デバッグモード my $DEBUG; GetOptions( 'file|f=s' => \$FILE, 'interval|i=i' => \$INTERVAL, 'command-interval|c=i' => \$COMMANDINTERVAL, 'reentrant|r!' => \$REENTRANT, 'pidfile|p=s' => \$PIDFILE, 'priority|P=i' => \$PRIORITY, 'log-facility|l=s' => \$FACILITY, 'address|a=s' => \$TO, 'debug|d+' => \$DEBUG ); die "No command is given\n" unless @ARGV; die "No target file is specified\n" unless $FILE; die "Can't find file: $FILE\n" unless -f $FILE; # strftime() の出力文字列が英語になるように locale を無効化しておく eval { use locale; use POSIX qw/ setlocale LC_TIME /; setlocale( &LC_TIME, "C" ); }; if( $DEBUG ){ &polling( @ARGV ); } else { for( my $i; $i < $RETRY; $i++ ){ if ( my $pid = fork ) { eval { setpriority( 0, $pid, $PRIORITY ); }; exit 0; } elsif ( defined $pid ) { # 子プロセス側の処理 STDIN->close; STDOUT->close; STDERR->close; &polling( @ARGV ); exit 0; # Not reach. } elsif ( $! == &EAGAIN ) { sleep 5; next; } else { die "Can't fork daemon process: $!\n"; } } die "Can't fork daemon process: retry conter exceeded\n"; } #---------------------------------------------------------------------- # 本体 #---------------------------------------------------------------------- my $PPID; sub polling (@) { my( @argv ) = @_; $PPID = $PID; $PROGRAM_NAME = sprintf( '%s %s to execute "%s"', $BASENAME, $FILE, join( " ", @argv ) ); &printinfo( $PROGRAM_NAME ); my $fh = new FileHandle( $PIDFILE, "w" ); if( $fh ){ $fh->print( "$PID\n" ); $fh->close; $SIG{'INT'} = \&handler; $SIG{'QUIT'} = \&handler; $SIG{'TERM'} = \&handler; $SIG{'HUP'} = 'IGNORE'; } else { &printerror( "Can't open %s to write: %s", $PIDFILE, $! ); $PIDFILE = undef; } my $last; my $mtime = &mtime( $FILE ); while(1){ my $sleep = $COMMANDINTERVAL + $last - time; sleep( ( $sleep < $INTERVAL )||( $sleep > $COMMANDINTERVAL )? $INTERVAL : $sleep ); my $x = &mtime( $FILE ); if( $x != $mtime ){ $mtime = $x; $last = time; &printinfo( "%s is changed at %s", $FILE, strftime( "%T", localtime( $mtime ) ) ); if( $REENTRANT ){ &child( $mtime, @argv ); } else { &start( $mtime, @argv ); } } } } # ファイルの変更時間を検出する関数 sub mtime ($) { my( $file ) = @_; ( stat( $file ) )[9]; } sub printlog ($$@) { my( $priority, $format, @arg ) = @_; if( $DEBUG ){ printf "%s %s %s[%d]: ", strftime( "%b %d %T", localtime ), $priority, $BASENAME, $PID; printf $format, @arg; print "\n"; STDOUT->flush; } else { &openlog( $BASENAME, 'pid', $FACILITY ); &syslog( $priority, $format, @arg ); &closelog(); } } sub printinfo ($@) { &printlog( "info", @_ ); } sub printerror ($@) { &printlog( "err", @_ ); } sub handler ($) { my( $sig ) = @_; &printinfo( "Caught a SIG%s -- shutting down", $sig ); unlink $PIDFILE if $PIDFILE; exit 0; } #---------------------------------------------------------------------- # コマンドを実行する関数 #---------------------------------------------------------------------- sub child ($@) { my( $mtime, @argv ) = @_; for( my $i; $i < $RETRY; $i++ ){ if( my $pid = fork ){ # 親プロセス側の処理 return wait; } elsif( defined $pid ){ # 子プロセス側の処理 &grandchild( $mtime, @argv ); exit 0; } elsif( $! == &EAGAIN ){ sleep 5; next; } else { &printerror( "Can't fork a child process: %s", $! ); } } &printerror( "Can't fork a child process: retry conter exceeded" ); } sub grandchild ($@) { my( $mtime, @argv ) = @_; for( my $i; $i < $RETRY; $i++ ){ if( my $pid = fork ){ # 親プロセス側の処理 exit 0; } elsif( defined $pid ){ # 子プロセス側の処理 &printinfo( "Fork from %s[%s]", $BASENAME, $PPID ); &start( $mtime, @argv ); exit 0; } elsif( $! == &EAGAIN ){ sleep 5; next; } else { &printerror( "Can't fork a grandchild process: %s", $! ); } } &printerror( "Can't fork a grandchild process: retry conter exceeded" ); } sub start ($@) { my( $mtime, @argv ) = @_; local $PROGRAM_NAME = sprintf( '%s %s and executing "%s"', $BASENAME, $FILE, join( ' ', @argv ) ); &printinfo( 'Execute "%s"', join( ' ', @argv ) ); my $result = &run( @argv ); wait; if( $result ){ &printinfo( "Send the result mail to %s", $TO ); if( $DEBUG ){ print $result; print "\n" unless $result =~ m/\n\Z/m; STDOUT->flush; } else { &sendmail( $mtime, join( ' ', @argv ), $result ); } } else { &printinfo( "Normal ended" ); } } # コマンドの標準出力と標準エラー出力をまとめて,1つの出力文字列として # 受け取るために,標準の IPC::Open3 ではなく自前の関数を使っている. sub run (@) { my( @argv ) = @_; # 子プロセスと通信するためのパイプを生成する my( $read, $write ) = FileHandle::pipe; # 子プロセスを fork する for( my $i; $i < $RETRY; $i++ ){ if( my $pid = fork ){ # 親プロセス側の処理 close $write; return join( '', $read->getlines ); } elsif( defined $pid ){ # 子プロセス側の処理 close $read; STDOUT->fdopen( $write, "w" ); STDERR->fdopen( $write, "w" ); my $null = new FileHandle( "/dev/null", "r" ); if( $null ){ STDIN->fdopen( $null, "r" ); exec @argv; } else { &printerror( "Can't open /dev/null: $!" ); } exit 0; # Not reach. } elsif( $! == &EAGAIN ){ sleep 5; next; } else { &printerror( "Can't fork the command(%s): %s", join( ' ', @argv ), $! ); return undef; } } &printerror( "Can't fork the command(%s): retry conter exceeded", join( ' ', @argv ) ); undef; } #---------------------------------------------------------------------- # メールを送信する関数 #---------------------------------------------------------------------- sub sendmail ($$$) { my( $time, $command, $str ) = @_; my $smtp = Net::SMTP->new( "localhost" ) or die; $smtp->mail( $FROM ); $smtp->to( $TO ); $smtp->data(); $smtp->datasend( &mailheader( $time, $command ) ); $smtp->datasend( Jcode->new( $str )->iso_2022_jp ); $smtp->dataend(); $smtp->quit; } sub mailheader ($$) { my( $time, $command ) = @_; $time = strftime( "%a, %d %b %Y %T %z", localtime( $time ) ); my $ppid = $REENTRANT ? sprintf( "\nX-Polling-Parent-Pid: %d", $PPID ) : ""; sprintf( <<"__header__", $FROM, $BASENAME, $TO, $FILE, $command, $time, $FILE, $command, $PID, $ppid ); From: %s (%s) To: %s Subject: Poll [%s] %s Date: %s X-Polling-File: %s X-Polling-Command: %s X-Polling-Pid: %d%s MIME-Version: 1.0 Content-Type: text/plain; charset=iso-2022-jp __header__ } =back =head1 USAGE このスクリプトは,元々,dhcpd によって管理されているホストが接続された 時点で特定のコマンドを実行することを目的に作成された. 例えば,動的な IP アドレスの割り当てを受けている普通のホストであれば, アドレスが貸し出された時点で F に記録さ れるので,このファイルをスクリプトで監視していれば,ホストが接続された 時点でコマンドを実行することができる. ただし,MAC アドレスに基づいて静的な IP アドレスを付与しているホストの 場合は,F が変化しないため,もう少し工夫 が必要である. F に記述されている I の設定値を確認し, dhcpd の動作記録を別にするように F に設定する. Example: # For dhcpd local7.info -/var/log/dhcpd.info この後,syslogd に対して HUP シグナルを送る. これで,dhcpd の動作記録が F に保存されるようにな るので,このファイルを監視していれば,ホストが接続された時点でコマンド を実行することができる. 作者の環境では,MTA の queue の処理をホストの接続にあわせて行うように している. =head1 REQUIREMENT Perl に標準的に付属しているモジュール以外に,以下の2つのモジュールが必 要である. =over 4 =item * L =item * L =back =head1 AUTHOR =over 4 =item TSUCHIYA Masatoshi =back =head1 COPYRIGHT This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, you can either send email to this program's maintainer or write to: The Free Software Foundation, Inc.; 59 Temple Place, Suite 330; Boston, MA 02111-1307, USA. Last Update: $Date: 2002/12/02 01:35:31 $ =cut