
# Copyright (C) 2006, David Muir Sharnoff <muir@idiom.com>

package Daemon::Generic;

use strict;
use warnings;
require Exporter;
require POSIX;
use Getopt::Long;
use File::Slurp;
use File::Flock;
our @ISA = qw(Exporter);
our @EXPORT = qw(newdaemon);

our $VERSION = 0.51;

our $force_quit_delay = 15;
our $package = __PACKAGE__;
our $caller;

sub newdaemon
{
	my (%args) = @_;
	my $pkg = $caller || caller() || 'main';

	my $foo = bless {}, $pkg;

	unless ($foo->isa($package)) {
		no strict qw(refs);
		my $isa = \@{"${pkg}::ISA"};
		unshift(@$isa, $package);
	}

	bless $foo, 'This::Package::Does::Not::Exist';
	undef $foo;

	new($pkg, %args);
}

sub new
{
	my ($pkg, %args) = @_;

	if ($pkg eq __PACKAGE__) {
		$pkg = caller() || 'main';
	}

	srand(time ^ ($$ << 5))
		unless $args{no_srand};

	my $av0 = $0;
	$av0 =~ s!/!/.!g;

	my $self = {
		gd_args		=> \%args,
		gd_pidfile	=> $args{pidfile},
		gd_logpriority	=> $args{logpriority},
		gd_progname	=> $args{progname}
					? $args{progname}
					: $0,
		gd_pidbase	=> $args{pidbase}
					? $args{pidbase}
					: ($args{progname} 
						? "/var/run/$args{progname}"
						: "/var/run/$av0"),
		gd_foreground	=> $args{foreground} || 0,
		configfile	=> $args{configfile}
					? $args{configfile}
					: ($args{progname}
						? "/etc/$args{progname}.conf"
						: "/etc/$av0"),
		debug		=> $args{debug} || 0,
	};
	bless $self, $pkg;

	$self->gd_getopt;
	$self->gd_parse_argv;

	my $do = $self->{do} = $ARGV[0];

	$self->gd_help		if $do eq 'help';
	$self->gd_version	if $do eq 'version';
	$self->gd_install	if $do eq 'install';
	$self->gd_uninstall	if $do eq 'uninstall';

	$self->gd_pidfile unless $self->{gd_pidfile};

	my %newconfig = $self->gd_preconfig;

	$self->{gd_pidfile} = $newconfig{pidfile} if $newconfig{pidfile};

	print "Configuration looks okay\n" if $do eq 'check';

	my $pidfile = $self->{gd_pidfile};
	my $killed = 0;
	my $locked = 0;
	if (-e $pidfile) {
		if ($locked = lock($pidfile, undef, 'nonblocking')) {
			# old process is dead
		} else {
			sleep(2) if -M $pidfile < 2/86400;
			my $oldpid = read_file($pidfile);
			chomp($oldpid);
			if ($oldpid) {
				if ($do eq 'stop' or $do eq 'restart') {
					$killed = $self->gd_kill($oldpid);
					exit if $do eq 'stop';
					$locked = lock($pidfile);
				} elsif ($do eq 'reload') {
					if (kill(1,$oldpid)) {
						print "Requested reconfiguration\n";
						exit;
					} else {
						print "Kill failed: $!\n";
					}
				} elsif ($do eq 'check') {
					if (kill(0,$oldpid)) {
						print "$0 running - pid $oldpid\n";
						$self->gd_check($pidfile, $oldpid);
						exit;
					} 
				} elsif ($do eq 'start') {
					print "\u$self->{gd_progname} is already running (pid $oldpid)\n";
					exit(3);
				}
			} else {
				$self->gd_error("Pid file $pidfile is invalid but locked, exiting\n");
			}
		}
	} else {
		$locked = lock($pidfile, undef, 'nonblocking') 
			or die "Could not lock pid file $pidfile: $!";
	}

	if ($do eq 'reload' || $do eq 'stop' || $do eq 'check' || ($do eq 'restart' && ! $killed)) {
		print "No $0 running\n";
	}

	exit if $do eq 'stop';
	if ($do eq 'check') {
		$self->gd_check($pidfile);
		exit 
	}

	unless ($do eq 'reload' || $do eq 'restart' || $do eq 'start') {
		$self->gd_other_cmd($do, $locked);
	}

	unless ($self->{gd_foreground}) {
		$self->gd_daemonize;
	}

	$locked or lock($pidfile, undef, 'nonblocking') 
		or die "Could not lock PID file $pidfile: $!";

	write_file($pidfile, "$$\n");

	print STDERR "Starting up...\n";

	$self->gd_postconfig(%newconfig);

	$self->gd_setup_signals;

	$self->gd_run;

	unlink($pidfile);
	exit(0);
}

sub gd_check {}

sub gd_more_opt { return() }

sub gd_getopt
{
	my $self = shift;
	Getopt::Long::Configure("auto_version");
	GetOptions(
		'configfile=s'	=> \$self->{configfile},
		'foreground!'	=> \$self->{gd_foreground},
		'debug!'	=> \$self->{debug},
		$self->{gd_args}{options}
			? %{$self->{gd_args}{options}}
			: (),
		$self->gd_more_opt(),
	) or exit($self->gd_usage());

	if (@ARGV < ($self->{gd_args}{minimum_args} || 1)) {
		exit($self->gd_usage());
	}
	if (@ARGV > ($self->{gd_args}{maximum_args} || 1)) {
		exit($self->gd_usage());
	}
}

sub gd_parse_argv { }

sub gd_help
{
	my $self = shift;
	exit($self->gd_usage($self->{gd_args}));
}

sub gd_version
{
	my $self = shift;
	no strict qw(refs);
	my $v = $self->{gd_args}{version} 
		|| ${ref($self)."::VERSION"} 
		|| $::VERSION 
		|| $main::VERSION 
		|| "?";
	print "$self->{gd_progname} - version $v\n";;
	exit;
} 

sub gd_pidfile
{
	my $self = shift;
	my $x = $self->{configfile};
	$x =~ s!/!.!g;
	$self->{gd_pidfile} = "$self->{gd_pidbase}$x.pid";
}

sub gd_other_cmd
{
	my $self = shift;
	$self->gd_usage;
	exit(1);
}

sub gd_redirect_output
{
	my $self = shift;
	return if $self->{gd_foreground};
	my $logname = $self->gd_logname;
	my $p = $self->{gd_logpriority} ? "-p $self->{gd_logpriority}" : "";
	open(STDERR, "|logger $p -t '$logname'") or (print "could not open stderr: $!" && exit(1));
	close(STDOUT);
	open(STDOUT, ">&STDERR") or die "redirect STDOUT -> STDERR: $!";
}

sub gd_daemonize
{
	my $self = shift;
	print "Starting $self->{gd_progname} server\n";
	$self->gd_redirect_output();
	my $pid;
	POSIX::_exit(0) if $pid = fork;
	die "Could not fork: $!" unless defined $pid;
	POSIX::_exit(0) if $pid = fork;
	die "Could not fork: $!" unless defined $pid;

	POSIX::setsid();
	select(STDERR);
	$| = 1;
	print "Sucessfully daemonized\n";
}

sub gd_logname
{
	my $self = shift;
	return $self->{gd_progname}."[$$]";
}

sub gd_reconfig_event
{
	my $self = shift;
	print STDERR "Reconfiguration requested\n";
	$self->gd_postconfig($self->gd_preconfig());
}

sub gd_quit_event
{
	my $self = shift;
	print STDERR "Quitting...\n";
	exit(0);
}

sub gd_setup_signals
{
	my $self = shift;
	$SIG{INT} = sub { $self->gd_quit_event() };
	$SIG{HUP} = sub { $self->gd_reconfig_event() };
}

sub gd_run { die "must defined gd_run()" }

sub gd_error
{
	my $self = shift;
	my $e = shift;
	my $do = $self->{do};
	if ($do && $do eq 'stop') {
		warn $e;
	} else {
		die $e;
	}
}

sub gd_flags_more { return () }

sub gd_flags
{
	my $self = shift;
	return (
		'-c file'	=> "Specify configuration file (instead of $self->{configfile})",
		'-f'		=> "Run in the foreground (don't detach)",
		$self->gd_flags_more
	);
}

sub gd_commands_more { return () }

sub gd_commands
{
	my $self = shift;
	return (
		start		=> "Starts a new $self->{gd_progname} if there isn't one running already",
		stop		=> "Stops a running $self->{gd_progname}",
		reload		=> "Causes a running $self->{gd_progname} to reload it's config file.  Starts a new one if none is running.",
		restart		=> "Stops a running $self->{gd_progname} if one is running.  Starts a new one.",
		$self->gd_commands_more(),
		($self->gd_can_install()
			? ('install' => "Setup $self->{gd_progname} to run automatically after reboot")
			: ()),
		($self->gd_can_uninstall()
			? ('uninstall' => "Do not run $self->{gd_progname} after reboots")
			: ()),
		check		=> "Check the configuration file and report the daemon state",
		help		=> "Display this usage info",
		version		=> "Display the version of $self->{gd_progname}",
	)
}

sub gd_positional_more { return() }

sub gd_alts
{
	my $offset = shift;
	my @results;
	for (my $i = $offset; $i <= $#_; $i += 2) {
		push(@results, $_[$i]);
	}
	return @results;
}

sub gd_usage
{
	my $self = shift;

	require Text::Wrap;
	import Text::Wrap;

	my $col = 15;

	my @flags = $self->gd_flags;
	my @commands = $self->gd_commands;
	my @positional = $self->gd_positional_more;

	my $summary = "Usage: $self->{gd_progname} ";
	my $details = '';
	for my $i (gd_alts(0, @flags)) {
		$summary .= "[ $i ] ";
	}
	$summary .= "{ ";
	$summary .= join(" | ", gd_alts(0, @commands));
	$summary .= " } ";
	$summary .= join(" ", gd_alts(0, @positional));

	my (@all) = (@flags, @commands, @positional);
	while (@all) {
		my ($key, $desc) = splice(@all, 0, 2);
		local($Text::Wrap::columns) = 79;
		$details .= wrap(
			sprintf(" %-${col}s ", $key),
			" " x ($col + 2),
			$desc);
		$details .= "\n";
	}

	print "$summary\n$details";
	return 0;
}

sub gd_install_pre {}
sub gd_install_post {}

sub gd_can_install
{
	my $self = shift;
	require File::Basename;
	my $basename = File::Basename::basename($0);
	if (
		-x "/usr/sbin/update-rc.d"
		&& 
		-x $0
		&& 
		$0 !~ m{^(?:/usr|/var)?/tmp/}
		&&
		eval { symlink("",""); 1 }
		&& 
		-d "/etc/init.d"
		&&
		! -e "/etc/init.d/$basename"
	) {
		return sub {
			$self->gd_install_pre("update-rc.d");
			require Cwd;
			my $abs_path = Cwd::abs_path($0);
			symlink($abs_path, "/etc/init.d/$basename")
				or die "Install failed: symlink /etc/init.d/$basename -> $abs_path: $!\n";
			print "+ /usr/sbin/update-rc.d $basename defaults\n";
			system("/usr/sbin/update-rc.d", $basename, "defaults");
			my $exit = $? >> 8;
			$self->gd_install_post("update-rc.d");
			exit($exit) if $exit;
		};
	}

	return 0;
}

sub gd_install
{
	my $self = shift;
	my $ifunc = $self->gd_can_install();
	die "Install command not supported\n" unless $ifunc;
	&$ifunc($self);
	exit(0);
}

sub gd_uninstall_pre {}
sub gd_uninstall_post {}

sub gd_can_uninstall
{
	my $self = shift;
	require File::Basename;
	my $basename = File::Basename::basename($0);
	require Cwd;
	my $abs_path = Cwd::abs_path($0) || 'no abs path';
	my $link = readlink("/etc/init.d/$basename") || 'no link';
	if (
		$link eq $abs_path
		&& 
		-x "/usr/sbin/update-rc.d"
	) {
		return sub {
			$self->gd_uninstall_pre("update-rc.d");
			unlink("/etc/init.d/$basename");
			print "+ /usr/sbin/update-rc.d $basename remove\n";
			system("/usr/sbin/update-rc.d", $basename, "remove");
			my $exit = $? >> 8;
			$self->gd_uninstall_post("update-rc.d");
			exit($exit) if $exit;
		}
	}
	return 0;
}

sub gd_uninstall
{
	my $self = shift;
	my $ufunc = $self->gd_can_uninstall();
	die "Cannot uninstall\n" unless $ufunc;
	&$ufunc($self);
	exit(0);
}

sub gd_kill
{
	my ($self, $pid) = @_;

	my $talkmore = 0;
	my $killed = 0;
	if (kill(0, $pid)) {
		$killed = 1;
		kill(2,$pid);
		print "Killing $pid\n";
		my $t = time;
		sleep(1) if kill(0, $pid);
		if ($force_quit_delay && kill(0, $pid)) {
			print "Waiting for $pid to die...\n";
			$talkmore = 1;
			while(kill(0, $pid) && time - $t < $force_quit_delay) {
				sleep(1);
			}
		}
		if (kill(15, $pid)) {
			print "Killing $pid with -TERM...\n";
			if ($force_quit_delay) {
				while(kill(0, $pid) && time - $t < $force_quit_delay * 2) {
					sleep(1);
				}
			} else {
				sleep(1) if kill(0, $pid);
			}
		}
		if (kill(9, $pid)) {
			print "Killing $pid with -KILL...\n";
			my $k9 = time;
			my $max = $force_quit_delay * 4;
			$max = 60 if $max < 60;
			while(kill(0, $pid)) {
				if (time - $k9 > $max) {
					print "Giving up on $pid ever dying.\n";
					exit(1);
				}
				print "Waiting for $pid to die...\n";
				sleep(1);
			}
		}
		print "Process $pid is gone\n" if $talkmore;
	} else {
		print "Process $pid no longer running\n";
	}
	return $killed;
}

sub gd_preconfig { die "gd_preconfig() must be redefined"; }

sub gd_postconfig { }


1;
