#!/usr/local/bin/perl5 use SGI::FAM; use MD5; use IO::File; use Getopt::Mixed qw(getOptions); use File::Basename qw(basename dirname fileparse); use File::PathConvert qw(realpath); my $have_vc_rcs_file=1; eval {require VC::Rcs::File}; if ($@ and $@ =~ /Can\'t locate/) { $have_vc_rcs_file=0; } elsif ($@) { die; } use strict; use integer; use vars qw($opt_h $opt_v $opt_d $opt_s $opt_r $opt_t $opt_u $opt_g $opt_c $opt_x $opt_w $opt_l $opt_dwim $syslevel $sig); # $syslevel is current syslog seriousness level # $sig is current signal my ($logseq, $fam, $md5, %md5, %monitor, %discard)=(0, undef, new MD5); # $logseq is logging sequencer # $fam is FAM handle # $md5 is MD5 context (reusable) # %md5 is hash from file names to last-seen MD5 hashes # %monitor is map from monitored file or dir names, to 1. # %discard is map from full path names to time_t of last modification. sub msg($;$;) { my ($text, $reqd)=@_; return unless $reqd or $opt_v; if ($opt_s) { syslog($syslevel, "$0: (%04d) $text", ++$logseq); } else { print STDERR "$0 (" . localtime() . "): $text\n"; } } sub excluded($;) { my ($path)=@_; msg("Excluding $path (nonexistent)"), return 1 unless -e $path; msg("Excluding $path (special file)"), return 1 unless -d _ or -f _; msg("Excluding $path (RCS directory)"), return 1 if -d _ and basename($path) eq 'RCS'; msg("Excluding $path (binary file)"), return 1 if -f _ and $opt_t and -s _ and -B _; msg("Excluding $path (regex match)"), return 1 if $opt_x and $path =~ /$opt_x/o; 0; } sub chmog($$$) { my ($path, $mode, $serious)=@_; return unless $opt_u or $opt_g or $mode; msg sprintf "Chmog $path: mode=%s%06o user=%s group=%s", ($mode < 0 ? '&' : '|'), abs($mode), (defined $opt_u ? $opt_u : ''), (defined $opt_g ? $opt_g : ''); my $warn_or_die=sub { my ($msg)=@_; $msg .= ": $!" if $!; if ($serious) { die $msg; } else { warn $msg; } }; my @stat=stat $path or (&$warn_or_die("Stat $path"), return); if ($opt_u or $opt_g) { my ($user, $group)=@stat[4, 5]; $user=$opt_u if $opt_u; $group=$opt_g if $opt_g; chown $user, $group, $path or (&$warn_or_die("Chown $user.$group $path"), return); $discard{$path}{change}=time; } if ($mode) { use integer; my $omode=$stat[2] & 077777; if ($mode > 0) { $omode |= $mode; } else { $omode &= ~(-$mode); } chmod $omode, $path or (&$warn_or_die(sprintf("Chmod %06o $path", $omode)), return); $discard{$path}{change}=time; } } sub rcsperm($;) { my ($file)=@_; chmog $file, 0200 | (defined($opt_g) ? 0020 : 0), 1; my ($base, $dir)=fileparse($file); chmog "${dir}RCS/$base,v", 0, 0; } sub md5($;) { my ($file)=@_; $md5->reset; $md5->addfile(IO::File->new($file) or die "Open $file: $?$!"); $md5->hexdigest; } sub rcscommit($;) { my ($file)=@_; my $newmd5=md5 $file; if ($md5{$file} eq $newmd5) { msg "Skipping commit on $file (unchanged: $newmd5)"; return; } msg "Committing changes to $file ($md5{$file} -> $newmd5)"; my ($author, $message); if ($opt_l) { my $log=new IO::File "$file.log"; if ($log) { my @lines=<$log>; if (@lines) { if ($lines[0] =~ /^-(.*)$/) { shift @lines; $author=$1; } if (@lines) { $message=join '', @lines; } } $log=new IO::File ">$file.log"; if ($log) { print $log "-$author\n" if $author; } else { warn "Clearing of $file.log failed: $!"; } } else { msg "No log file available for $file: $!"; } } $author ||= 'magicrcs'; $message ||= '# (magicrcs)'; system qw{ci -u -q}, "-w$author", "-m$message", $file and die "RCS checkin of $file failed: $?"; $discard{$file}{change}=time; rcsperm $file; # Might have keyword substs... my $newnewmd5=$md5{$file}=md5 $file; msg "New MD5 for $file (post-checkin): $newnewmd5" unless $newmd5 eq $newnewmd5; } sub rcsinit($;) { my ($file)=@_; my ($base, $dir)=fileparse($file); my $rcs="${dir}RCS/$base,v"; if (-f $rcs) { # Appears to already be under control. Ensure it is unlocked. my $locked; if ($have_vc_rcs_file) { my $locks=VC::Rcs::File->new($rcs)->{locks}; $locked=$locks && %$locks; } else { # Alternate. $locked=`rlog -L -R $rcs`; } if ($locked) { msg "Stealing lock on $file", 1; system qw{rcs -u -M -q}, $file and die "RCS stealing lock on $file failed: $?"; } msg "$file is already under RCS control"; system qw{rcs -U -q}, $file and die "RCS setting to non-strict of $file failed: $?"; msg "RCS-restored MD5 for $file: " . ($md5{$file}=md5 "co -r -q -p $file |"); rcscommit $file; rcsperm $file; } else { msg "Putting $file under RCS control"; system qw{ci -i -u -q -wmagicrcs -t-(magicrcs)}, $file and die "RCS initial checkin of $file failed: $?"; msg "Initial MD5 for $file: " . ($md5{$file}=md5 $file); $discard{$file}{change}=time; rcsperm $file; } if ($opt_l) { new IO::File ">$file.log" or die "Creation of $file.log failed: $!"; chmog "$file.log", 0220, 1; $discard{"$file.log"}{change}=time; } system qw{rcs -U -q}, $file and die "RCS setting to non-strict of $file failed: $?"; } sub monitor($$) { my ($_path, $why)=@_; my $path=realpath($_path) or die "Resolution error at $File::PathConvert::resolved"; my $again=($monitor{$path} ? " (again)" : ''); msg "Monitoring $path ($why)$again"; $monitor{$path}=$why unless $again; sub make_rcs($;) { my ($basedir)=@_; mkdir "$basedir/RCS", 0777 or die "mkdir $basedir/RCS 0777: $!" unless -d "$basedir/RCS"; chmog "$basedir/RCS", 0, 0; } if (-d $path) { $fam->monitor($path); chmog $path, 0, 0; make_rcs $path; } else { my $dir=dirname($path); make_rcs $dir; $fam->monitor($dir); rcsinit $path; } } sub cleanup() { undef $fam; # Close connection my @_files=keys %monitor; undef %monitor; undef %md5; undef %discard; return unless $opt_c; my @files=grep {-f $_} @_files; msg "Cleaning up: @files"; foreach (@files) { system qw{rcs -L -q}, $_ and die "RCS reverting to strict mode for $_ failed: $?"; rcsperm $_; chmog $_, -0222, 0; unlink "$_.log" or warn "Delete $_.log failed: $!" if $opt_l; } } sub init() { $fam=new SGI::FAM; foreach (@ARGV) { monitor $_, 'explicit'; } if ($opt_r) { require File::Find; $File::Find::name=$File::Find::name; $File::Find::prune=$File::Find::prune; # -w File::Find::find (sub { my $test=excluded $_; if ($test) { $File::Find::prune=1 if -d; } else { monitor $File::Find::name, 'implicit'; } }, grep { if (-d) { msg "Searching $_"; 1; } else { 0; } } @ARGV); } msg 'Ready', 1; } sub event() { my $ev=$fam->next_event; my ($file, $type, $which)=($ev->filename, $ev->type, $fam->which($ev)); my $path=realpath($file eq $which ? $file : "$which/$file") or die "Resolution failure at $File::PathConvert::resolved"; return unless $type eq 'change' and -f $path or $type eq 'create'; return if $file eq 'RCS' and -d $path; if (exists $discard{$path}{$type} and $discard{$path}{$type} >= time - $opt_w) { msg "Discarding recent $type event for $path"; return; } if ($path =~ s/\.log$//) { if ($type eq 'create') { msg "New log file for $path"; } else { msg "Log file for $path changed"; } return; } if ($type eq 'change' and not $monitor{$path}) { msg "$type event on $path ignored"; return; } # Dispatching. msg "Received $type event for $path"; if ($type eq 'create') { return if excluded $path; monitor $path, 'new'; } if (-f $path) { if ($type eq 'create') { rcsinit $path; } else { # Change. rcscommit $path; } } elsif (not -d $path) { msg "Ignoring special file $path"; } } sub debug_dump() { require Data::Dumper; my $dump=new Data::Dumper [$fam, \%md5, \%monitor, \%discard], [qw(fam *md5 *monitor *discard)]; # $dump->Indent(0); $dump->Useqq(1); my $msg=$dump->Dump; $msg =~ s/%/%%/g if $opt_s; msg 'Dumping internal data...', 1; foreach (split /\n/, $msg) { msg $_, 1; } } sub handler { $sig=shift; # Trigger an event somewhere to speed response. If it fails, oh well. unless ($fam and $fam->pending) { my ($file)=grep {-f} keys %monitor; if ($file) { my $mode=(stat $file)[2]; if ($mode) { chmod $mode, $file; } } } } getOptions(q(h help>h d detach>d v verbose>v s:s syslog>s r recursize>r dwim l log>l t text>t u=s user>u g:s group>g c cleanup>c x=s exclude>x w=i wait>w)); if ($opt_dwim) { $opt_c=$opt_r=$opt_d=$opt_t=$opt_l=1; $opt_s ||= ''; $opt_g ||= ''; # Dotfiles are bogus. _ files may be RCS crap. # Emacs backups, sundry crap. # Junk dirs should be skipped. my $re=q/^[._]|([~\#]|\.(bak|tmp|old|log))$|^junk$/; if ($opt_x) { $opt_x=qq/($opt_x)|$re/; } else { $opt_x=$re; } } $opt_s ||= q(info) if defined $opt_s; $syslevel=$opt_s; $opt_w ||= 1; use autouse Pod::Usage => qw(pod2usage); pod2usage(VERBOSE => 1) if $opt_h or not @ARGV or ($opt_s and not $opt_d); if ($opt_u and $opt_u =~ /\D/) { defined($opt_u=(getpwnam $opt_u)[2]) or die "Get numeric UID: $!"; } if ($opt_g and $opt_g =~ /\D/) { defined($opt_g=(getgrnam $opt_g)[2]) or die "Get numeric GID: $!"; } if ($opt_d) { my $pid=fork; if ($pid) { print STDERR "Detached process ID: $pid\n"; exit; } if ($opt_s) { require Sys::Syslog; import Sys::Syslog; openlog($0, q(pid cons), q(user)); $SIG{__DIE__}=sub { $syslevel=q(err); my $err=shift; msg "$err (terminating)", 1; closelog(); die "magicrcs[$$]: $err (terminating)"; }; $SIG{__WARN__}=sub { local($syslevel)=q(warning); my $err=shift; msg $err, 1; }; } setpgrp 0, 0 or die "Detaching: $!"; msg "Starting (@ARGV)", 1; } init; $SIG{USR1}=$SIG{INT}=$SIG{TERM}=$SIG{TSTP}=$SIG{ALRM}=\&handler; $sig=0; msg 'Beginning event loop'; while (1) { my $sig2=$sig; $sig=0; if ($sig2) { local($SIG{USR1}, $SIG{INT}, $SIG{TERM}, $SIG{TSTP}, $SIG{ALRM}); if ($sig2 eq 'TSTP') { cleanup; msg 'Suspending', 1; $SIG{ALRM}=\&handler; sleep; next; } elsif ($sig2 eq 'INT' or $sig2 eq 'TERM') { cleanup; msg 'Stopping', 1; closelog() if $opt_s; exit; } elsif ($sig2 eq 'ALRM') { msg 'Resuming', 1; init; next; } else { # USR1 debug_dump; next; } } else { event; } } __END__ =head1 NAME B - automatically monitor directories & files and keep them under RCS =head1 SYNOPSIS Recommended usage: F B<--dwim> I | I ... =head1 OPTIONS =over 4 =item B<--cleanup> I B<-c> Try to clean up file permissions before script exits, assuming this is done with a polite B or B. (See L.) Specifically, write permission is turned off RCS-controlled files, so that Emacs VC-mode will not subsequently be confused; RCS files are reverted to strict mode; and log files (under B<--log>) are deleted. =item B<--user=>I I B<-u> I Try to change ownership of modified working files back to user I. May fail unless run as root. File will be made user-writable whether this option is specified or not. =item B<--group> [ B<=>I ] I B<-g>[I] Like B<--user>, but change group ownership, and also make sure file is group-writable. If I is omitted, just change the permission bit. =item B<--recursive> I B<-r> Recursively monitor all subdirectories and files within specified directories. This flag only takes effect when this script starts up, and is analogous to something like: magicrcs `find dir1/ dir2/ ... -print` file1 file2 ... except F subdirectories are excluded, &c. =item B<--text> I B<-t> Only I start monitoring text files, not binary files. Useful if there are graphics, tar files, core dumps, &c. in the vicinity. Does not affect files explicitly mentioned on the command line. =item B<--exclude=>I I B<-x> I Do not I start monitoring files or subdirectories whose names match this (Perl 5) regular expression. Should only be specified once, but that is what regex alternatives are for. Like B<--text>. You will almost certainly want this to include Emacs backup and autosave files; things like F<*.bak>; etc. =item B<--help> I B<-h> Show a synopsis. =item B<--detach> I B<-d> Detach after starting, in daemon fashion. This is the recommended usage. =item B<--syslog> [ B<=>I ] I B<-s>[I] Log messages to SYSLOG rather than standard error. Only meaningful in daemon mode (B<--detach>). Optional I (default B) sets logging level, which may affect visibility on the system log depending on its configuration (F). =item B<--log> I B<-l> Permit external-file log messages. See L. =item B<--verbose> I B<-v> Generate verbose messages about what is happening. =item B<--wait=>I I B<-w> I Wait for I seconds of quiescence (default one second) before acknowledging further events on a file. This option may improve performance in some cases, though probably not dramatically. If it is too large, some changes to a file may not be noticed. =item B<--dwim> Do What I Mean. Sets B<--cleanup>, B<--log>, B<--group>, B<--recursive>, B<--text>, B<--exclude> (to a reasonable value, as well as your own excludes if any), B<--detach>, and B<--syslog>. =back =head1 DESCRIPTION F is intended to run as a background process that will use the SGI File Access Monitor library to observe given files or directory trees and keep them transparently under RCS control. Files remain writable, but unlocked. Thus, the archival aspects of RCS can be retained in an environment where manual checkins and other operations are impractical. Selecting the correct invoking user, and combination of B<--user> and B<--group> options, may require experimentation if this script is to be useful in a multiuser environment, due to complexities of permissions interactions. These two options will also attempt to set ownership of monitored directories and RCS control files. You should be able to edit these files in Emacs even with VC Mode enabled (as it is by default); the RCS version at the time of first visitation will appear, and should be ignored as it will not stay in synch (unless you do a C). But do not try to check monitored files in or out explicitly with Emacs, or all hell will break loose; just save the buffer whenever you want (which should not be write-protected assuming you have user or group access to the file). Hopefully, there will not be problems. If you need to log a message, though, or perform any fancy RCS operations, you will want to shut down this script first (and probably wanted B<--cleanup> specified to begin with). =head2 For each file monitored... =over 4 =item Startup The file is put under RCS if it is not already. This script expects the RCS file for F to be named F and may not work correctly if it is not. =item File Changed Each change event, i.e. each time the contents of the file are changed, will deposit a new RCS revision. The author and log message indicate that the checkin was automated. Permissions on the file should be untouched, so you can continue editing freely. =item File Deleted Nothing happens to the RCS file. If the file is recreated, however, deposits will resume where they left off. =back =head2 For each directory monitored... =over 4 =item Startup This script will ensure the existence of an RCS subdirectory. Files currently in the directory, and subdirectories recursively, are also automatically monitored if you provide the B<--recursive> flag, just as if they had been specified on the command line. =item New File or Subdirectory Monitoring begins on any new files or subdirectories (unless a file is binary and B<--text> was specified, or it matches B<--exclude>). Note that this happens whether B<--recursive> was given or not. If you never want I new files to be put under RCS, you should not specify any directory names on the command line. =item Subdirectory Deleted Nothing special is done. =back =head2 Log Messages If B<--log> is given, the script will permit external files to be used to create RCS log messages. For each file F placed under RCS control, a separate file F will be created. If you want to register a log message with an RCS deposit of F, I write it into F, save F, then save F. The contents of F will be used for the RCS log message. If the first line is of the form: B< ->I (a dash followed by some text) then I will be used as the RCS author, as well. The F file is cleared (except for the author line, if present) after making the deposit. You may save the log file at any time. It will only be used if the message has been re-entered since the last deposit. =head1 SIGNALS =over 4 =item B or B (I if undetached; else B ...) Stops the script, after cleaning up if requested. =item B and B (B ... and B ...) Suspend and resume (resp.); clean up and reinitialize as if the script were actually restarted. Handy if you need to do some special RCS operation but do not want to manually restart. =item B (B ...) Log internal debugging information. =back =head1 BUGS Due to polling delays, monitoring NFS-mounted areas may be problematic if the NFS server is not also running Irix (or if it is just not running the FAM daemon). Try adjusting B<--wait> to a larger amount according to the poll settings on your FAM daemon; this is typically set to six seconds to provide a balance of load minimization and accuracy, so B<--wait> should be somewhat larger. See L. There are probably a variety of miscellaneous bugs here and there. The script is not as efficient as it could be; it sometimes invokes RCS gratuitously, usually at startup. This should really use some RCS XSUB library. =head1 SEE ALSO See L for the workings; for RCS, see L; for logging, L and the resultant logfile, typically F; regular expressions, see L. =head1 AUTHORS J. Glick B. =head1 REVISION X<$Format: "F<$Source$> last modified $Date$ release $ProjectRelease$. $Copyright$"$> F last modified Thu, 25 Sep 1997 22:56:42 -0400 release 1.002. Copyright (c) 1997 Strategic Interactive Group. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut