#!/usr/bin/perl
# Dayplanner commander
# $Id: commander 1249 2007-03-30 23:52:50Z zero_dogg $
# Copyright (C) Eskild Hustvedt 2006
#
# 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
# of the License, 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, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

use strict;			# Make my coding strict
use warnings;			# Enable perl warnings
use IO::Socket;			# Daemon communication
use Term::ReadLine;		# A more advanced way to get input than <STDIN>
use File::Basename;
use Getopt::Long;

my $socket;			# The socket
my $Is_Interactive;
my $ConfDir = DetectConfDir();
my $SocketName = "$ConfDir/dayplannerd"; # Default socket
my $RCSRev = '$Id: commander 1249 2007-03-30 23:52:50Z zero_dogg $';

# Purpose: Detect the user config  directory
# Usage: DetectConfDir();
sub DetectConfDir {
	# First detect the HOME directory, and set $ENV{HOME} if successfull,
	# if not we just fall back to the value of $ENV{HOME}.
	my $HOME = getpwuid($>);
	if(-d $HOME) {
		$ENV{HOME} = $HOME;
	}
	# Compatibility mode, using the old conf dir
	if(-d "$ENV{HOME}/.dayplanner") {
		return("$ENV{HOME}/.dayplanner");
	}
	# Check for XDG_CONFIG_HOME in the env
	my $XDG_CONFIG_HOME;
	if(defined($ENV{XDG_CONFIG_HOME})) {
		$XDG_CONFIG_HOME = $ENV{XDG_CONFIG_HOME};
	} else {
		if(defined($ENV{HOME}) and length($ENV{HOME})) {
			# Verify that HOME is set properly
			if(not -d $ENV{HOME}) {
				die("$ENV{HOME}: does not exist\n");
			}
			$XDG_CONFIG_HOME = "$ENV{HOME}/.config";
		} else {
			die("The environment variable HOME is missing\n");
		}
	}
	return("$XDG_CONFIG_HOME/dayplanner");
}

# Purpose: Print formatted --help messages
# Usage: PrintHelp("-short", "--long", "Description");
sub PrintHelp ($$$) {
	printf "%-4s %-16s %s\n", "$_[0]", "$_[1]", "$_[2]";
}

# Purpose: Connect to the daemon
# Usage: ConnectToDaemon();
sub ConnectToDaemon () {
	# Verify socket existance
	unless (-e $SocketName) {
		die "$SocketName doesn't exist\n";
	}
	
	unless (-S $SocketName) {
		       if (-d $SocketName and -e "$SocketName/dayplannerd") {
				$SocketName = "$SocketName/dayplannerd";
			}
	}

	# Verify that it actually IS a socket
	unless (-S $SocketName) {
		die "$SocketName is not a socket\n";
	}

	# Open up the connection to the socket or die trying
	$socket = IO::Socket::UNIX->new(Peer	=> $SocketName,
	                                Type	=> SOCK_STREAM,
					Timeout	=> 10 )
	    or die "Couldn't connect to $SocketName: $@\nPerhaps the daemon isn't running?\n";
	
	# Say HI to the daemon and get the reply
	print $socket "$$ HI commander\n";
}

# Purpose: Print a nicely formatted help output for commands available
# Usage: CommandPrint(COMMAND, Description);
sub CommandPrint ($$) {
	printf "%-18s %s\n", $_[0], $_[1];
}

# Purpose: Parse user input and send to daemon if needed
# Usage: ParseInput(INPUT);
sub ParseInput ($) {
	my $COMMAND = $_[0];
	# Make sure we got something other than emptyness or whitespace
	if(defined($COMMAND) and $COMMAND =~ /\S/) {
		chomp($COMMAND);			# Strip newlines
		if ($COMMAND =~ /^!QUIT/) {		# If it's !QUIT then tell the socket BYE and quit
			print $socket "$$ BYE\n";
		} elsif ($COMMAND =~ /^!HELP/) {	# If it's !HELP then we display the help
			print "\nList of internal commander commands:\n";
			CommandPrint("!QUIT", "Quit the commander");
			CommandPrint("!HELP", "Display this help screen");
			CommandPrint("!RESTART_DAEMON","Shut down the current daemon, start a new one and reconnect");
			print "\nList of known daemon commands:\n";
			CommandPrint("HI [client type]", "Initiate a new connection of type 'client type'");
			CommandPrint("", "client type can be \"commander\" (is always allowed)");
			CommandPrint("", "or \"client\" (the type dayplanner clients use");
			CommandPrint("", "only one client can be connected at any time)");
			CommandPrint("PING", "Send a simple PING request to see if the daemon is alive");
			CommandPrint("SHUTDOWN", "Tell the daemon to shut down");
			CommandPrint("RELOAD_DATA", "Tell the daemon to reload the calendar data");
			CommandPrint("BYE", "Tell the daemon to remove the client from its access lists");
			CommandPrint("VERSION", "Get the daemon version number");
			CommandPrint("GET_PATH", "Get the path used to start the daemon");
			CommandPrint("NOTIFICATION [GET|VERIFY|CLEAN] ID", "Perform the action on the notification");
			CommandPrint("", "ID supplied. This can very very rarely be done from this commander as it");
			CommandPrint("", "requires a special condition inside the daemon");
			CommandPrint("DEBUG [COMMAND]", "Run the debugging command [COMMAND]");
			print "\nList of debugging commands: (parameters to DEBUG)\n";
			CommandPrint("DUMP_VARIOUS", "Dump various useful information about the running daemon");
			CommandPrint("DUMP_CLIENTLIST", "Dump the list of connected PIDs");
			CommandPrint("DUMP_SLEEPTIME", "Dump approximately how long the daemon would sleep after");
			CommandPrint("", "this request has been processed (unless a new request is recieved).");
			CommandPrint("", "Returns the value in seconds as returned by the FindSleepDuration() function");
			CommandPrint("KICK [PID]", "Kick the PID specified from the daemon - denies further access until");
			CommandPrint("", "that PID reconnects (PID is removed from the access lists - its connection");
			CommandPrint("", "to the daemon stays open");
			CommandPrint("ENABLE_DEBUGLOG", "Enable debugging output to the logfile");
			CommandPrint("RECALCULATE", "Recalculate todays events");
			CommandPrint("DUMP_HASHES", "Dump all of the data hashes");
			CommandPrint("DUMP_MAINHASH", "Dump the main data hash (\%NotificationHash)");
			CommandPrint("DUMP_CONFIG", "Dump the internal configuration hash (\%Config)");
		} elsif ($COMMAND =~ /^!RESTART_DAEMON/) {
			print "Restarting daemon...\n";
			print $socket "$$ GET_PATH\n";
			my $PATH = <$socket>;
			chomp($PATH);
			print $socket "$$ SHUTDOWN\n";
			my $REPLY = <$socket>;
			chomp($REPLY);
			unless($REPLY eq 'okay') {
				print "Failed to restart daemon, it replied \"$REPLY\" to my SHUTDOWN request\n";
			} else {
				print "Daemon shut down, starting a new one...\n";
				my $DaemonDir = dirname($SocketName);
				if (system("$PATH --force-fork --dayplannerdir $DaemonDir")) {
					print "\nStarting a new daemon failed! I can't continue.\n";
					exit(0);
				}
				ConnectToDaemon();
				my $RESTART_HI = <$socket>;
				chomp($RESTART_HI);
				print "A new daemon has been started. It says $RESTART_HI\n";
			}
		} else {				# Everything else we send unmodified to the daemon
			print $socket "$$ $COMMAND\n";
		}
		unless($COMMAND =~ /^(!HELP|!RESTART_DAEMON)/) {	# If the command isn't !HELP nor !RESTART_DAEMON then get and display the daemon reply
			my $REPLY = <$socket>;
			if (defined($REPLY)) {
				if($REPLY =~ /^BEGIN MULTILINE/) {
					print " Multi line daemon reply:\n";
					print $REPLY;
					my $ML_NotEnded = 1;
					while($ML_NotEnded) {
						foreach(split(/\n/,<$socket>)) {
							if (/^END MULTILINE/) {
								print "$_\n";
								$ML_NotEnded = 0;
								last;
							} else {
								print "	$_\n";
							}
						}
					}
				} else {
					chomp($REPLY);
					print " Daemon replied: $REPLY\n";
				}
			} else {
				print " Daemon didn't reply\n";
			}
			# Handle quitting the program
			if ($COMMAND =~ /^!QUIT/) {
				close($socket);
				exit(0);
			}
		}
	}
}

# Set up the signal handling
$SIG{PIPE} = sub { print "I recieved SIGPIPE - daemon shut down?\n"; };	# Handle SIGPIPE gracefully
$SIG{INT} = sub {				# Handle SIGINT correctly
	if (defined($socket)) {
		print $socket "$$ BYE";
	}
	print("\n");exit(0);};

# Get commandline options
GetOptions(
	'h|help' => sub {
		print "Day Planner commander\n\n";
		print "Usage: " . basename($0) . " [PARAMETERS] [COMMAND]\n\n";
		PrintHelp("-h", "--help", "Display this help screen");
		PrintHelp("-s", "--socket", "Select an alternate socket");
		PrintHelp("-v", "--version", "Print RCS revision and exit");
		PrintHelp("-t", "--test", "Control the daemon of a \"dayplanner --test\" session");
		print "All commandline arguments are optional.\n\n";
		print "For help about the commands the daemon accepts enter the command !HELP\n";
		exit(0);
	},
	's|socket=s' => \$SocketName,
	'r|v|version' => sub {
		print "Day Planner commander $RCSRev\n";
		exit(0);
	},

) or die "See --help for more information\n";

# If there is something left in @ARGV then we are running in non-interactive mode
$Is_Interactive = 1 unless(@ARGV);

# Connect to the daemon and save the reply in $HI_REPLY
ConnectToDaemon();
my $HI_REPLY = <$socket>;

# If we're interactive present the user with a command prompt
if($Is_Interactive) {
	# Print initial warning and welcomming message
	print "WARNING: Use with care, you have RAW access to the daemon and I\n will NOT stop you from destroying data!\n\n";
	print "Welcome to the Day Planner daemon commander\n";
	print "Type !QUIT to quit, !HELP to view known daemon commands\n";
	print "Daemon says $HI_REPLY\n";
	
	# Create our Term::ReadLine object
	my $term = Term::ReadLine->new('Day Planner daemon commander');
	# Get the attribs and set up autocompleteion
	my $attribs = $term->Attribs;
	$attribs->{completion_entry_function} = $attribs->{list_completion_function};
	$attribs->{completion_word} = [qw(HI PING SHUTDOWN RELOAD_DATA BYE VERSION GET_PATH NOTIFICATION GET VERIFY CLEAN DEBUG DUMP_VARIOUS DUMP_CLIENTLIST DUMP_SLEEPTIME ENABLE_DEBUGLOG KICK RECALCULATE DUMP_HASHES DUMP_MAINHASH DUMP_CONFIG !QUIT !HELP !RESTART_DAEMON)];
	
	# Eternal loop getting and parsing user input
	while (1) {
		# Get the input
		my $COMMAND = $term->readline('Command: ');
		ParseInput($COMMAND);
	}
} else {# If we're not interactive just send the commands on the commandline to the
	# daemon in turn.
	print "Sent: HI\n";
	chomp($HI_REPLY);
	print " Daemon replied: $HI_REPLY\n";
	foreach(@ARGV) {
		print "Sent: $_\n";
		ParseInput($_);
	}
	print "Sent: BYE\n";
	ParseInput("!QUIT");
}

# Terminate the connection if we get this far, should never happen though
close($socket);
