#!/usr/bin/perl -w
# $Header: /var/lib/cvs/csyncstuff/csync2id.pl,v 1.5 2008/10/03 15:22:27 ard Exp $
use strict;
use Linux::Inotify2;
use Data::Dumper;
use File::Find;
use POSIX qw(uname :sys_wait_h);
use Sys::Syslog;
use Net::Server::Daemonize qw(daemonize);

#local @main::dirs;
# my ($sysname, $nodename, $release, $version, $machine) = POSIX::uname();
#my @dirs;
# /etc/csync2id.cfg should contain something like:
# @::dirs= qw ( list of dirs )
# 1;
my $program="csync2id";
my $daemonize=1;
my $pidfile='/var/run/csync2id.pid';
my $pidfileboot='/var/run/csync2id.boot.pid';
@::csyncdirhint=("/usr/sbin/csync2", "-B","-A","-rh");
@::csyncfilehint=("/usr/sbin/csync2", "-B","-A","-h");
$::debug=1;
$::statsdir="/dev/shm/csyncstats/dirs";
$::statschanges="/dev/shm/csyncstats/changes";
$::statsretry="/dev/shm/csyncstats/retry";
@::dirs=();
require "/etc/csync2id.cfg";

$daemonize && daemonize(0,0,$pidfileboot);
openlog("$program",'pid','daemon');

$::debug && syslog('debug',Dumper(\@::dirs));


my $inotify = new Linux::Inotify2 or die "Unable to create new inotify object: $!";

# For stats
my $globaldirs=0;
my $globalevents=0;
my $globalhintretry=0;

sub logstatsline {
	my ($file,$line)=@_;
	open STATS,"> $file";
	print STATS $line;
	close STATS;
}

################################################################################
# Subtree parser
# Adds subtrees to an existing watch
# globals: $globaldirs for stats.
# Logs to syslog
################################################################################
sub watchtree {
	my ($inotifier,$tree,$inotifyflags) = @_;
	$inotifier->watch ($tree, $inotifyflags);
	$globaldirs++;
	find(
		sub {
			if(! m/^\.\.?$/) {
				my ($dev, $ino, $mode, $nlink, $uid, $gid) = lstat($_) ;
				if(-d _ ) {
					if ($nlink==2) {
						$File::Find::prune = 1;
					}
					$inotifier->watch ($File::Find::dir.'/'.$_, $inotifyflags) or die "watch creation failed (maybe increase the number of watches?)" ;
					$globaldirs++;
					$::debug && syslog('debug',"directory #%d %s", $globaldirs,$File::Find::dir.'/'.$_);
				}
			}
		},
		$tree
	);
	logstatsline($::statsdir,$globaldirs);
}


################################################################################
# CSYNC2 HINT RUNNER
# Run csync2 -h in the background and restart if necessary
# This should be generalized.
################################################################################
use constant { CSHSIDLE => 0, CSHSRUN => 1, CSHSREAPED =>2 };
my $csynchintstatus = CSHSIDLE;
my $csynchintexitstatus = 0;
my $csynchintpid;
my @hintline;
sub reaper {
	my $backupstatus=$?;
	if(waitpid($csynchintpid,WNOHANG)) {
		$csynchintstatus=CSHSREAPED;
		$csynchintexitstatus=$?;
	}
	$?=$backupstatus;
}
sub runcsynchint {
	$csynchintstatus=CSHSRUN;
	$::debug && syslog("debug","about to run:".join(' ','>', @hintline,'<'));
	$SIG{CHLD} = \&reaper;
	$csynchintpid=fork();
	if(!$csynchintpid) {
		exec(@hintline);
		warn "Unable to exec syncer";
		# Exec failure restart grace, fix it here:
		sleep 0.5;
		exit 1;
	}
}
sub checkcsynchint {
	if(CSHSREAPED == $csynchintstatus) {
		if($csynchintexitstatus) {
			$::debug && syslog("info","Hinter got $csynchintexitstatus, retrying hints:",@hintline);
			$globalhintretry++;
			logstatsline($::statsretry,$globalhintretry);
			runcsynchint();
		} else {
			$::debug && syslog("debug","Hints successfully given");
			$csynchintstatus = CSHSIDLE;
		}
	}
	return $csynchintstatus;
}
sub csynchint {
	if(CSHSIDLE == $csynchintstatus) {
		@hintline=@_;
		runcsynchint();
	} else {
		syslog("error", "Serialization failure: trying to run a running program.");
	}
}

################################################################################
# HINTFIFO RUNNER
# groups queued hints into single csync commands
# 
################################################################################

my @hintfifo;
sub givehints {
	if(checkcsynchint() == CSHSIDLE && @hintfifo) {
		# PREPARE JOB
		# Directories should be treated with care, one at a time.
		my @hintcommand;
		if($hintfifo[0]->{'recurse'}) {
			my $filename=$hintfifo[0]->{'filename'};
			@hintcommand=(@::csyncdirhint,$filename);
			shift(@hintfifo) while (@hintfifo && $filename eq $hintfifo[0]->{'filename'} );
		} else {
			# Files can be bulked, until the next directory
			@hintcommand=(@::csyncfilehint);
			while(@hintfifo && !$hintfifo[0]->{'recurse'}) {
				my $filename=$hintfifo[0]->{'filename'};
				push(@hintcommand,$filename);
				shift(@hintfifo) while (@hintfifo && $filename eq $hintfifo[0]->{'filename'} );
			}
		}
		csynchint(@hintcommand);
	}
}


################################################################################
# Start watching the directories
syslog("info", "traversing directories");
watchtree($inotify,$_,IN_MOVE|IN_DELETE|IN_CLOSE_WRITE|IN_ATTRIB|IN_CREATE) foreach(@::dirs);
syslog("info","ready for events");

# Kill other daemon because we are ready
if($daemonize) {
	if ( -e $pidfile ) {
		my $thepid;
		@ARGV=($pidfile);
		$thepid=<>;
		syslog("info", "about to kill previous incarnation $thepid");
		kill(15,$thepid);
		sleep 0.5;
	}
	rename($pidfileboot,$pidfile);
}

# Main loop
while () {
	my @events = $inotify->read;

	unless (@events > 0) {
		$::debug && syslog('debug',"zero events, must be our child");
	}
	foreach(@events) {
		if($_->IN_Q_OVERFLOW) {
			syslog("error","inotify queue overflow: csync2id was to slow to handle events");
		}
		if( $_->IN_ISDIR) {
			my $recurse=0;
			# We want to recurse only for new, renamed or deleted directories
			$recurse=$_->IN_DELETE||$_->IN_CREATE||$_->IN_MOVED_TO||$_->IN_MOVED_FROM;
			watchtree($inotify,$_->fullname,IN_MOVE|IN_DELETE|IN_CLOSE_WRITE|IN_ATTRIB|IN_CREATE) if $_->IN_CREATE||$_->IN_MOVED_TO;
			push(@hintfifo,{ "filename" => $_->fullname , "recurse" => $recurse });
			$::debug && syslog("debug","DIR: %s(%d), %04x",$_->fullname,$recurse,$_->mask);
		} else {
			# Accumulate single file events:
			next if(@hintfifo && $hintfifo[-1]->{"filename"} eq $_->fullname);
			push(@hintfifo,{ "filename" => $_->fullname , "recurse" => 0 });
			$::debug && syslog("debug","FILE: %s, %04x",$_->fullname,$_->mask);
		}
		$globalevents++;
	}
	givehints();
	logstatsline($::statschanges,$globalevents);
}
