#! /usr/bin/perl -w
# $Header: /global/src/usr.bin/src/RCS/krenew.pl,v 1.60 2022/03/22 20:53:54 pb22 Exp pb22 $

# Use "pod2text" for info

=head1 NAME

B<cl-krenew> -- keep a kerberos ticket renewed

=head1 SYNOPSIS

B<cl-krenew> [-qfn] [-l lifetime] [-r renewtime] [ --copy ] [ --link ] [--maxout] [--longest] [--cron1] [--cron2] [--cron3] [--ensuretgt] [--ensurehome] [--rsync] [--syncmins mins] [--hosts host1,host2,...] [--slavefile filename] [--status] [--cronboot|--cronprod|--refreshmaster|--int1rsync] [--purge] [--one|1] [--suffix suffix]           [-bhtv] [-c child pid file] [-H minutes] [-K minutes] [-k ticket file] [-p pid file] [--xdg] [command ...]

=head1 OPTIONS

=over 4

=item B<--verbose|-v>

show what it's doing.

=item B<--help|-h>

generate help info.

=item B<--quiet|-q>

be quiet.

=item B<--cron1> B<--cron2> B<--cron3>

perform the actions required by various standard cron jobs.

=item B<--longest>

select the longest listing TGT and link to it.

=item B<--status>

show the current status.
Displays the status of any running "B<refresh>" and "B<distributing>" processes,
and obvious tickets in B</tmp/krb5cc_>I<$USERNO>B<*>.

=item B<--one|1>

do a single "kinit -R" on all tickets, and B<--purge>.

=item B<--suffix>

select only files ending in I<suffix>

=item B<--cronboot>

add a call with this option to crontab to ensure that "B<refresh>" and "B<distributing>" processes are
restarted for all keys if the workstation reboots. See also B<--cronprod> and B<--refreshmaster>.
Use "B<crontab -e>" to add a line of the form:

	@reboot cl-krenew --cronboot

=item B<--cronprod>

add a call with this option to crontab to ensure that "B<refresh>" and "B<distributing>" processes are
still running for all keys on a workstation. See also B<--cronboot> and B<--refreshmaster>.
Use "B<crontab -e>" to add a line of the form:

	*/60 * * * * cl-krenew --cronprod

=item B<--refreshmaster>

refresh the B<master> ticket (B</tmp/krb5_>I<104>B<_master>) as used by B<--cronboot> and B<--cronprod>.
See B<DESCRIPTION> below.

=item B<--purge>

purge any expired tickets.

=item B<--copy>

copy B<$KRB5CCNAME> to the value of the B<-k> flag, or if no such flag, B<$KRB5CCNAME> with the I<_$RANDOM> removed (e.g. copy B</tmp/krb5cc_104_ywwXD23312> to B</tmp/krb5cc_104>).

=item B<--link>

link B<$KRB5CCNAME> to the value of the B<-k> flag, or if no such flag, B<$KRB5CCNAME> with the I<_$RANDOM> removed (e.g. link B</tmp/krb5cc_104_ywwXD23312> to B</tmp/krb5cc_104>).

=item B<--rsync>

rsync the cache file to the specified hosts (command line B<--hosts> list, env var B<$CL_KRENEW_HOSTS>, or 'B<hosts=>' value in B<~/.cl-krenew>) whenever the ticket is refreshed, and every B<$syncmins> minutes.

=item B<--slavefile> I<filename>

when syncing the file to other machines, use I<filename> ((default: env var B<$CL_KRENEW_SLAVEFILE>, or 'B<slavefile=>' value in B<~/.cl-krenew>, or B</tmp/krb5cc_>I<$USERNO>B<_>I<$HOSTNAME>).

=item B<--syncmins> I<mins>

Sync the cache file every I<mins> minutes (default: env var B<$CL_KRENEW_SYNCMINS>, or 'B<syncmins=>' value in B<~/.cl-krenew>, or 5).

=item B<--hosts> I<hosts>

Sync the cache file to I<hosts>.
If the first host is 'B<localhost>', the code uses a local 'B<cp>' command rather than rsync.

See also B<--rsync>

=item B<--noaction|-n>

take no major action.
Will (e.g.) do a B<kinit -R>, but will not get a new ticket.

=item B<--norefresh>

do no do any refreshes.

=item B<--freshticket|-f>

get a fresh ticket (requires a password).
This is normally used with B<-l>, B<-r> or B<--maxout> to obtain a ticket which
lasts longer than the default of one day.

=item B<--lifetime|-l> I<duration>

if a new ticket is needed, request its lifetime be I<duration>.
Policy may reduce this.
The format is an integer followed by a letter B<s>, B<m>, B<h> or B<d>.
(default is I<renew> if set, or 1d).

=item B<--renew|-r> I<duration>

As for B<lifetime> but sets the renew time.
(default is I<lifetime> if set, or 1d).

=item B<--ensuretgt>

Ensure that there is a Ticket Granting Ticket.
Does a B<--status> to display the current available tickets.
If there are no valid tickets, it does a B<--maxout> B<--fresh> to get a new ticket.

=item B<--ensurehome>

Ensure that there is a Ticket Granting Ticket.
Does a B<--status> to display the current available tickets.
If there are no valid tickets, it does a B<--maxout> B<--fresh> to get a new ticket,
changes directory to B</home/>I<$USER> and then exec's B<bash -l> to do a standard login shell setup.

=item B<--maxout>

local addition to max out everything -- get a ticket for as long as possible
(may need B<-f> to get a fresh long lasting ticket),
and keep refreshing it (sets "B<-b -K 10>").
Do not set either of the B<-bt> flags, any of the B<cHKp> keys,
or pass a command -- if you want any of them, do it all.

Note that B<klist> may give pessimistic answers, as it is likely to use a
different cache file, so that it is not deleted when the ssh session ends.
If B<-f> is given, it will unset B<KRB5CCNAME> before obtaining the new ticket
(so it will use the default, e.g. B</tmp/krb5cc_>I<104>),
and if not, it will run a B<sleep> command which will have its own cache.

To show them all use "B<for f in /tmp/krb5cc_>I<104>B<*; do klist $f; done>",
where I<104> is the user's number.

=item B<--int1rsync>

(internal use: do a single rsync)

=item B<--xdg>

Use the B<XDG_SESSION_ID> environment variable and work out what to do

=item B<[-bhtv] [-c child pid file] [-H minutes] [-K minutes] [-k ticket file] [-p pid file] [command ...]>

as per the standard B<krenew> command -- see "B<man krenew>"

=back

=head1 DESCRIPTION

The standard B<krenew> renews a kerberos ticket.

The CL wrapper also has the ability to acquire a new ticket (if no valid ticket or 'B<-f>' flag given),
and to distribute it to other machines (if 'B<--rsync>', 'B<--hosts>' or 'B<--syncmins>' are set).

If hosts are given (command line B<--hosts> list, env var B<$CL_KRENEW_HOSTS>, or 'B<hosts=>' value in B<~/.cl-krenew>)
the command can attempt to B<distrubute> the keys to other fies. This facility for expert users only.

A B<master> file (default B</tmp/krb5_>I<104>B<_master>) can be maintained which may be in a non standard location
(e.g. not in B</tmp/> on Ubuntu systems which clear them on reboot),
which can be distributed to other machines or the localhost. This facility for expert users only.

Note that if a user workstation reboots, B<krenew> will no longer be running,
so the ticket will not be refreshed.
To allow refreshing to persist across reboots, and to recover from a crashed krenew process, use "B<crontab -e>" to add the lines

	@reboot cl-krenew --cronboot
	*/60 * * * * cl-krenew --cronprod

On some systems (e.g. Ubuntu) the default directory (/tmp)
is cleared on reboot, so the cache file location should be set explicitly (using B<-k> or the env var B<$KRB5CCNAME>) e.g. B</local/scratch/>I<$USER/>B<krb5cc_>I<$UID>.

Note that central servers such as B<slogin-serv> and B<ssh-remote> have tickets auto refreshed daily,
so there is no need for cron jobs or krenew processes.
As such, on such machines, the maximum renew period should be set accordingly.

=head1 EXAMPLES

=over 4

=item B<cl-krenew --hosts greta,ra4>

copy the current ticket (or create a new one if not valid) to hosts greta and ra4.

=item B<cl-krenew --maxout>

arrange to refresh the existing ticket (or create a new one if not valid) for as long as possible.

=item B<cl-krenew --maxout -f>

obtain a fresh ticket for as long as possible,
and arrange to keep refreshing it.

=item B<cl-krenew --maxout -f --hosts localhost,greta,ra4>

obtain a fresh ticket for as long as possible,
and arrange to keep refreshing it
and to copy the result to B</tmp/> on localhost, greta and ra4.

For greater resilience, this can be run on multiple hosts (e.g. the two
B<slogin-serv> machines).

=item B<cl-krenew --maxout --rsync -k /tmp/krb5cc_104_slogin-serv7>

arrange to keep refreshed the ticket created on B<slogin-serv7>,
and arrange to copy the result to B</tmp/> on the hosts listed in
B<~/.cl-krenew>.

This ensures that even if B<slogin-serv7> fails, the ticket is still refreshed and distributed, but still only requires the user to type a password once per maximum ticket lifetime.

=back

=head1 SEE ALSO

krenew(1), krb5-auth-dialog(1)

=cut

$cc_tmp	= "/tmp";
$cc_tgt	= "/var/spool/tgt";

umask 0027;
# if CL_KRENEW_LOGFILE is set, open it.
# Useful for commands such as "krenew -t" which loses stdout and stderr
if ($ENV{'CL_KRENEW_LOGFILE'}) {
	close(STDOUT);
	open(STDOUT, ">>$ENV{'CL_KRENEW_LOGFILE'}");
	close(STDERR);
	open(STDERR, ">&STDOUT");
	# print STDOUT "<<$0: $$ reopened log file to STDOUT>>\n";
	# print STDERR "<<$0: $$ reopened log file to STDERR>>\n";
}

$kinit_R = "kinit -R";
$krenew = "krenew";
$kinit = "kinit";
$ttl = "1d";
$fpref	= "krb5cc_";
$suffix	= "*";	# by default include all sufficies
$ppref	= "$cc_tmp/$fpref";
$krenew_K = 60;	# default krenew -K inteval (mins)
$FILE = "FILE:";	# std prefix
$valid	= 0;	# How many valid tickets found?

$last_klist = '<UnSet>';	# log of last klist command
$freebsd = -e "/etc/freebsd-update.conf"; # Better way??
$osuser	= ($freebsd) ? "-U" : "-u";
$osfor	= ($freebsd) ? "-v -c " : "";

# file to be used as the master copy of a file refreshed and distributed
$master_keyfile = "$ppref$<_master";

$USER = $ENV{'USER'} || getpwuid($<);
$HOME= $ENV{'HOME'} || "";
$dotfile = "$HOME/.cl-krenew";

$kf_c, $kf_k, $kf_b, $kf_p, $kf_H, $kf_t, $kf_K, $krenew, $kf_v if 0;
$do_krenew = 1;

# use Date::Parse;
# use Date::Parse;
use Date::Parse;
# BEGIN { unless(eval "use Date::Parse") { die "$0: BEGIN: couldn't load Date::Parse - load 'libtimedate-perl' (or on FreeBSD, 'p5-TimeDate')\n$@"; } }
#eval "use Date::Parse" || die "$0: Sigh couldn't load Date::Parse - load 'libtimedate-perl' (or on FreeBSD, 'p5-TimeDate')\n$@";


# BUGS
#   When both the month and the date are specified in the date as numbers they
#   are always parsed assuming that the month number comes before the date.
#   This is the usual format used in American dates.
#
#   The reason why it is like this and not dynamic is that it must be
#   deterministic. Several people have suggested using the current locale,
#   but this will not work as the date being parsed may not be in the format
#   of the current locale.
#
#   My plans to address this, which will be in a future release, is to allow
#   the programmer to state what order they want these values parsed in.
# 
# SO: force LC_TIME to something which is compaitble - YUCK!
# RT#101530 - works with LC_ALL for U16.04
$ENV{'LC_TIME'} = "C";
$ENV{'LC_ALL'} = "C";

use Getopt::Long;
# abuse bundling_override to allow -K and -k (and H/h) as different
use Getopt::Long qw(:config require_order bundling_override);
use Pod::Usage;
%opts = (
	"copy"			=> \$copy,
	"cronprobe|cronprod"	=> \$cronprobe,
	"cronboot"		=> \$cronboot,
	"cron1"			=> \$cron1,
	"cron2"			=> \$cron2,
	"cron3"			=> \$cron3,
	"freshticket|f"		=> \$freshticket,
	"help|h"		=> \$kf_h,
	"hosts:s"		=> \$hosts,
	"krenews_b|b"		=> \$kf_b,
	"krenews_cap_H|H:s"	=> \$kf_H,
	"krenews_cap_K|K:s"	=> \$kf_K,	# probably :i would work
	"krenews_c|c:s"		=> \$kf_c,
	"krenews_k|k:s"		=> \$kf_k,
	"krenews_t|t"		=> \$kf_t,
	"int1rsync"		=> \$int1rsync,
	"lifetime|l:s"		=> \$lifetime,
	"link"			=> \$link,
	"longest"		=> \$longest,
	"purge"			=> \$purge,
	"maxout"		=> \$maxout,
	"noaction|n"		=> \$noaction,
	"norefresh"		=> \$norefresh,
	"one|1"			=> \$one,
	"quiet|q"		=> \$quiet,
	"renewtime|r:s"		=> \$renewtime,
	"refreshmaster"		=> \$refreshmaster,
	"ensuretgt"		=> \$ensuretgt,
	"ensurehome"		=> \$ensurehome,
	"rsync"			=> \$rsync,
	"slavefile:s"		=> \$slavefile,
	"status"		=> \$status,
	"suffix:s"		=> \$suffix,
	"syncmins:s"		=> \$syncmins,
	"verbose|v"		=> \$kf_v,
	"krenews_p|p:s"		=> \$kf_p,
	"xdg|x"			=> \$xdg,
);

$trace = $ENV{'TRACE'};
@krenew_flags = ( "b", "h", "t", "v" );
@krenew_args = ( "c", "H", "K", "k", "p" );

$refreshable_time = -1;
$ticket_lifetime = -1;
$ticket_file = "NEVER";
$defsyncmins = 5;
$localhost = 0;		# should a local copy be made

$xdg = 0;

$cmd = $0;
$cmd =~ s=^/usr/bin/==;
$cmd =~ s=^/auto/homes/pb22/pl/==;

$lhost = eval 'use Sys::Hostname;  hostname' || `uname -n`;
chomp $lhost;
$lhost =~ s/.cl.cam.ac.uk$//;

GetOptions (%opts) || pod2usage();
pod2usage(-verbose => 2) if $kf_h;
$rsync = 1 if $syncmins || $slavefile;
$ensuretgt =  $ensurehome if $ensurehome;
$freshticket = $maxout = 1 if $refreshmaster;
$freshticket = $maxout = 1 if $ensuretgt;
$status = $purge = $refresh = $longest = 1 if $cron1;
$status = $purge = $refresh = 1 if $cron2;
$status = $purge = $refresh = 1 if $cron3;
$purge = $one if $one;
&xdg if $xdg;

# Set KINIT_PROG so that if cl-krenew calls "krenew -t", this command is called
$ENV{'KINIT_PROG'} = "$0 --int1rsync";

# internal call from "krenew -t"
# should have set all required env vars
# system('env | egrep CL\|KRB | sort'), 
&display_longest("N2", 0);
&read_rsync_info(1), exit(&sync_hosts) if $int1rsync;

# Arrange that the kerberos commands can be found -- some people ZAP PATH
$kerbdir = "/usr/kerberos/bin";
unless ($ENV{'PATH'} =~ /$kerbdir/) {
	$ENV{'PATH'} .= ":$kerbdir" if -e $kerbdir;
	print STDERR "# PATH now '$ENV{'PATH'}'\n" if $trace;
}

&set_kf_k($master_keyfile) if !$kf_k && ($cronprobe || $cronboot || $refreshmaster);

# Assumed to be called from cron, either as machine has just rebooted,
# or to probe that suitable daemons are running
&cron($cronprobe) if $cronprobe || $cronboot;
%refresh = ();
%kkey = ();

if ($ensuretgt || $status || $purge || $one || $longest) {
	# If just doing this due to "--purge", then do not refresh
	$norefresh = 1 if $purge && ! ($ensuretgt || $status || $one || $longest);
	if ($ensuretgt || $status || $purge || $longest) {
	    my($cl_krenew, $krenewing, $self, $what, $kkeys) = &status;
	    die("$cmd: internal lookup failed\n") unless defined $cl_krenew;
	    print STDERR "$cmd: info suspect - did not find self\n" unless $self;

	    # If operating on "all tickets", scan /tmp etc ...
	    if ($status || $purge || $longest) {
		# Build a hash of tickets which are being refreshed
		print STDERR "# watching '$what' of '$kkeys'\n" if $kf_v;
		foreach $key (split(/ /, $what)) { $refresh{$key} = 1; print "#r '$key'\n" if $kf_v}
		foreach $key (split(/ /, $kkeys)) { $kkey{$key} = 1; print "#k '$key'\n" if $kf_v}

		# Display what refreshing is going on
		if (! $status) {
		} elsif ($cl_krenew && $krenewing) {
			$dup = ($cl_krenew > 1 || $krenewing > 1) ? " ($krenewing $cl_krenew -$what)" : " $what";
			print "master: refreshing and distributing keys$dup\n";
		} elsif ($krenewing) {
			$dup = ($krenewing > 1) ? " ($krenewing -$what)" : " $what";
			print "refreshing keys$dup\n";
		} elsif ($cl_krenew) {
			$dup = ($cl_krenew > 1) ? " ($cl_krenew -$what)" : "";
			print "distributing keys only$dup\n";
		} else {
			print "no processes found refreshing or distributing keys\n";
		}
	    }
	}

	# First procss the user's current ticket,
	&display_ticket_life_and_refresh(undef, $ensuretgt || $status, $one, $norefresh);
	($seen = $ticket_file) =~ s/^.://;

	# then any in /tmp/
	$pat = "$ppref$<$suffix $cc_tgt/$fpref$<$suffix ";
	$pat = "$ppref$suffix /tmp/krb5ccmachin* $cc_tgt/$fpref$suffix" if $USER eq "root";
	print STDERR "# Use: '$pat' for $USER\n" if $kf_v;
	foreach $k (eval("<$pat>")) {
		my ($islink) = (-l $k) || 0;
		print STDERR "# Try $k ($seen) ($islink)\n" if $kf_v;
		unless ($k =~ /$seen$/) {
			if ($islink) {
				my $to = readlink($k) || "<nothing>";
				$to = "/tmp/$to" unless $to =~ /^\//;
				print "# $k is a symbolic link to $to\n";
			} else {
				&display_ticket_life_and_refresh($k, $status, $ensuretgt || $one, $norefresh);
			}
		}
		print STDERR "# Done $k ($seen)\n" if $kf_v;
	}

	# All done, so exit "OK".
	print STDERR "# ensuretgt=$ensuretgt valid=$valid\n" if $ensuretgt && $kf_v;
&display_longest("N4", 0);
	&sync_hosts, exit((($ensuretgt || $status || $longest) && $valid == 0) ? 1 : 0) unless $ensuretgt && ($valid == 0);
}


# If wanting to rsync, find the required info: $syncmins and $hosts
&read_rsync_info(1) if ($rsync || $hosts);

&display_ticket_life_and_refresh unless $quiet;

# maxout -- check no flags set?
if ($maxout) {
	pod2usage(-verbose => 2) if $kf_b || $kf_t || $kf_c || $kf_H || $kf_p || $#ARGV >= 0;
	$ttl = 365 * 10 . "d";	# it's changed without notice, so make it large
	$kf_b = 1;
	# set command to "sleep <a while>" if not $freshticket
	unless ($kf_K)	{ # refresh hourly
		$kf_K = &time2min($lifetime) || 0;
		$kf_K = int(($kf_K +1)/2);
		$kf_K = $krenew_K unless $kf_K || ($kf_K > $krenew_K);
	}
}

$lifetime = $lifetime || $renewtime || $ttl;
$renewtime = $renewtime || $lifetime;

if ($copy || $link) {
	die("$cmd: --copy and --link are not compatible\n") if $copy && $link;
	$op = ($copy) ? "copy" : "link";
	die("$cmd: \$KRB5CCNAME unset when trying to $op\n")
		unless $src = $ENV{'KRB5CCNAME'};
	die("$cmd: \$KRB5CCNAME ($src) not in expected form\n")
		unless $src =~ s/^$FILE//;
	die("$cmd: no $src (from \$KRB5CCNAME = $ENV{'KRB5CCNAME'}) $!\n")
		unless -e $src;
	die("$cmd: cannot read $src (from \$KRB5CCNAME = $ENV{'KRB5CCNAME'}) $!\n")
		unless -r $src;
	if ($kf_k) { $dst = $kf_k }
	else {	die("$cmd: \$KRB5CCNAME ($src) not in expected form\n")
		unless ($dst = $src) =~ s/($fpref\d+)_\S+$/$1/;
	}
	$command = ($link) ? "ln -f" : "cp -p";
	$command .= "$src $dst";
	my($pref) = (defined $ENV{'KRB5CCNAME'}) ? "KRB5CCNAME=$ENV{'KRB5CCNAME'}" : "unset KRB5CCNAME;";
	print "$cmd: call: ($pref $command)\n" unless $quiet;
	system($command) unless $noaction;
	print STDERR "# clear KRB5CCNAME\n" if $trace && ! $kf_k;
	delete($ENV{'KRB5CCNAME'}) unless $kf_k;
}

# Honour the -k flag
&set_kf_k($kf_k) if $kf_k;
print STDERR "# KRB5CCNAME now $ENV{'KRB5CCNAME'}\n" if $trace && $kf_k;

if (!$freshticket && !$cronboot && !$cronprobe) {
	# Is there an existing ticket? Try ...
	print STDERR "# run: $kinit_R 2>&1\n" if $kf_v;
	chomp($res = `$kinit_R 2>&1`);
	$res = "[$kinit_R not called]" unless defined $res;
	die("#$cmd: $kinit_R gave '$res': $!\n") if $!;
	# Command at least attempted to run -- see if RC was set:
	if ($?) {
		if ($res =~ /No credentials cache found/ || $res =~ /You have no tickets cached/ || $res =~ /Ticket expired while renewing credentials/) {
			printf "$kinit_R gave $? '$!' / %d ($res)\n", $!;
			$freshticket = 1;
		} elsif ($res =~ /KDC can't fulfill requested option while renewing credentials/) {
			if (defined $refreshable_time && ($refreshable_time == 0) && $ticket_lifetime && ($ticket_lifetime > 0)) {
				printf "$cmd: ticket cannot be refreshed as no extra refresh time\n";
			} else {
				printf "$kinit_R cannot refresh: ($refreshable_time:$ticket_lifetime) $? '$!' / %d ($res)\n", $!;
			}
		} else {
			printf "# def: $kinit_R gave $? '$!' / %d ($res)\n", $!;
		}
	}
}
if ($freshticket) {
	print STDERR "# clear KRB5CCNAME\n" if $trace && ! $kf_k;
	delete($ENV{'KRB5CCNAME'}) unless $kf_k || $xdg;
	$command = "$kinit -l $lifetime -r $renewtime";
	my($KRB5CCNAME) = $ENV{'KRB5CCNAME'};
	my($pref) = (defined $KRB5CCNAME) ? "KRB5CCNAME=$KRB5CCNAME" : "unset KRB5CCNAME;";
	print "$cmd: call: ($pref $command)\n" unless $quiet;
	system($command) unless $noaction;
	$command = "klist -5";
	print STDERR "# $cmd: call: ($pref $command)\n" if $kf_v;
	system("$command") unless $quiet;
	&display_ticket_life_and_refresh unless $quiet;
	&cron(0) if $refreshmaster;
}

unless ($kf_k) {
	$kf_k = $ENV{'KRB5CCNAME'} || "$ppref$<";
	$kf_k =~ s/^$FILE// if $kf_k;
}

# rsync: force a push now, and if requested, start a backgroud process
&sync_hosts;
$ENV{'CL_KRENEW_LOGFILE'} = "/tmp/k_$<_$$";
if ($rsync && $kf_K && !fork) {
	print "$cmd: fork $$ to sync '", join(' ', @hosts), "' ($localhost) every $syncmins mins\n" unless $quiet;
	unless ($kf_v) {
		close(STDOUT); open(STDOUT, ">>$ENV{'CL_KRENEW_LOGFILE'}");
		close(STDERR); open(STDERR, ">&STDOUT");
		close(STDIN);
	}
	while (sleep $syncmins * 60 && -e $kf_k) { &sync_hosts }
	# key file has gone, so exit.
&display_longest("N6", 0);
	exit;
}

&display_longest("N1", 0);

sub display_longest {
	my ($why, $do) = @_;
	my ($old_uid) = $>;
	$do = 1 unless defined $do;
	print STDERR "#", join(", ", @_), ": $do / $kf_v\n" if $do || $kf_v;
	foreach $key (sort keys %longeste) {
		-l "/tmp/krb5cc_$key" || next;
		$x = readlink( "/tmp/krb5cc_$key");
		$y = $longestf{$key};
		$y =~ s=^/tmp/==;
		next if $y eq $x;
		printf STDERR "$ why=$_[0]: key=%-12s longestf=%-25s x=%-25s longeste=%6d longestr=%6d\n", $key, $longestf{$key}, $x, $longeste{$key}, $longestr{$key} if $do || $kf_v;
		next unless $do || $longest;
		$> = $key;
		# $) = $gid;
		print STDERR "# u/g IDS = $> $) / $< $(\n" if $kf_v;
		if (symlink("./$y", "/tmp/krb5cc_$key")) {	# Fails as it pre-exists.
			print STDERR "(symlink(./$y, /tmp/krb5cc_$key) ok\n";
		} elsif (! system("ln -svf $y /tmp/krb5cc_$key")) {	# Seems to work ...
			print STDERR "'ln -svf $y /tmp/krb5cc_$key' ok\n" if $verbose;
		} else {
			symlink($y, "/tmp/krb5cc_$key-") || warn("'symlink($y, /tmp/krb5cc_$key-' failed $?\n");
			rename("/tmp/krb5cc_$key-", "/tmp/krb5cc_$key") && warn("$0: ' rename(/tmp/krb5cc_$key-, /tmp/krb5cc_$key' gave $?\n");
			unlink("/tmp/krb5cc_$key-"); # Just in case ...
		}
		$> = $old_uid;
		# $) = $old_gid;
	}
}

# all that's left is to exec krenew (if needed)
exit (0) unless $do_krenew;

# Generate a command line for krenew
@args = ();
foreach $flag (@krenew_flags) { $f = "kf_$flag"; push(@args, "-$flag") if $$f; }
foreach $flag (@krenew_args)  { $f = "kf_$flag"; push(@args, "-$flag", $$f) if $$f; }
# If we have a host to which to rsync it when it's regenerated, do.
push(@args, "-t") if ($rsync || $hosts);

$mayfail = (defined $refreshable_time && ($refreshable_time == 0) && $ticket_lifetime && ($ticket_lifetime > 0)) ? " (note that this may fail as ticket cannot be refreshed)" : "";
print "$cmd: system '$krenew ", join(' ', @args), ($#ARGV >= 0) ? " " : "", join(' ', @ARGV), "'$mayfail\n" unless $quiet;
system($krenew, @args, @ARGV) unless $noaction;
if ($ensurehome) {
	chdir("/home/$USER") || warn("$cmd: failed to 'cd /home/$USER': $!\n");
	print "$cmd: exec: 'exec bash -l'\n";
	exec("bash", "-l") unless $noaction;
	die("$cmd: 'exec bash -l' failed\n") unless $noaction;
} else { exit(0); }
# Not reached ...

# =============================================================================

# Display info about the current ticket in a user friendly format
sub refresh {
	my($for) = @_;
	# my ($user) = '';
	my ($setuid, $setgid) = (0, 0);
	my ($was) = $ENV{'KRB5CCNAME'};
	my $kf = $was || '';
	$kf =~ s/^$FILE//;
	my ($test) = $for || $kf;
	printf STDERR "# refresh <%s> <%s> <%s>\n", $for || "<UnSet>",  $kf, $test if $kf_v;

	# Asked to refresh a symb link? Use the link instead
	if (-l $test) {
		$for = readlink($test);
		$for = "/tmp/$for" unless $for =~ /^\//;
		$ticket_file = $for;
		print STDERR "# '$test' was a symb link, so using $for\n" if $kf_v;
	}
	# If running as root, the new file is owned by root.
	if ($USER eq "root") {
		($for = $ENV{'KRB5CCNAME'}) =~ s/^$FILE// unless $for;
		if (@stat = stat($for)) {
			# print STDERR "#< stat of $for gave: @stat\n";
			# $user = "; chown $stat[4] $for" if $stat[4];
			$setuid = $stat[4] if $stat[4];
			$setgid = $stat[5] if $stat[5];
		} elsif ($kf_v) {
			print STDERR "## Failed to stat $for: $!\n";
		}
	}

	$command = $kinit_R;
	$ENV{'KRB5CCNAME'} = "$FILE$for" if $for;
	# $command .= $user if $user;
	print STDERR "# $cmd: call KRB5CCNAME=$ENV{'KRB5CCNAME'} '$command'\n" if $kf_v;
	system($command);
	print STDERR "## $cmd: chown $setuid, $setgid, $for\n" if $kf_v && $for && ($setuid || $setgid);
	chown $setuid, $setgid, $for if $for && ($setuid || $setgid);
	if (defined $was) { $ENV{'KRB5CCNAME'} = $was; } else { delete $ENV{'KRB5CCNAME'} }
}

# -----------------------------------------------------------------------------

# Display info about the current ticket in a user friendly format
sub display_ticket_life_and_refresh {
	my($for, $display, $refresh, $donorefresh) = @_;
	my($islink) = (! $for) ? '<env>' : (-l $for) ? '<link>' : '<file>';
	printf STDERR "# display_ticket_life_and_refresh as %s with %s called with %s (%s), %s, %s\n", $USER,  $ENV{'KRB5CCNAME'} || "", $for || "", $islink, $display || "", $refresh || "" if $kf_v;
	# If called with no "action" args, assume "display".
	$display = 1 unless $refresh || $donorefresh;

	# May have the side effect of purging
	my($exp, $ref, $file, $principal, $texp, $tref) = &ticket_life($for);
	$refreshable_time = $ref if defined $ref && $exp;
	$ticket_lifetime = $exp if $exp;
	$ticket_file = $file if $file;

	if ($exp || $ref || $file || $principal) {
		$file =~ s/(machine_..)\.CL.CAM.AC.UK$/$1.../;
		$file = sprintf("%-15s", $file);
		$prins = $principal;
		$prins =~ s=^nfs/(\S+).cl.cam.ac.uk=$1...=;
		$prin = sprintf("%-10s", $prins);
		$tex = sprintf("%6s", $texp);
		if ($tref && $tref ne $texp) {
			if ($exp < 0) {
				$texp =~ s/^-//;
				$tex = sprintf("%6s", $texp);
				print "Ticket for $prin in $file EXPIRED $tex ago (refresh for $tref)\n## klist gave:\n$last_klist" if $display;
			} else {
				my ($fileid) = $file;
				$fileid =~ s/\s+$//;
				my ($ing) = ($refresh{$fileid}) ? "ing" : "";
				print "Ticket for $prin in $file expires in  $tex, refresh$ing for $tref\n" if $display;
				&refresh($for) unless $norefresh || $donorefresh;
				$valid++;
			}
		} elsif ($exp < 0) {
			$texp =~ s/^-//;
			$tex = sprintf("%6s", $texp);
			print "Ticket for $prin in $file EXPIRED $tex ago, no refresh possible\n## klist gave:\n$last_klist" if $display;
		} else {
			print "Ticket for $prin in $file expires in  $tex, no refresh possible\n" if $display;
			$valid++;
		}

	}
}

# -----------------------------------------------------------------------------

# unpick info about the current ticket
sub ticket_life {
	my ($for) = @_;
	my ($exp, $ref, $next_is_renew, $file, $principal, $path) = (0,0,0,"","","");
	my $server = '';

	# return if no file to process
	return 0 unless defined $ENV{'KRB5CCNAME'} || defined $for;
	my ($klistargs) = ($for) ? " $osfor$for" : "";
	$command = "klist -5$klistargs|";
	print STDERR "# set '$command'\n" if $trace;
	printf STDERR "# $cmd: call KRB5CCNAME=%s '%s'\n", $ENV{'KRB5CCNAME'} || '<UnSet>', $command if $kf_v;

	# return if the file does not exist
	return 0 if $f && ! -e $f;

	# Hardened kernels block access by other users to symb links in sticky dirs: http://lwn.net/Articles/502621/
	if ($for && ! -r $for && -l $for) {
		my ($u) = (getpwuid( (lstat($for))[4] ))[0];
		$command = "sudo -u $u klist -5$klistargs|";
		print STDERR "# reset '$command'\n" if $trace;
		printf STDERR "# $cmd: call KRB5CCNAME=%s '%s'\n", $ENV{'KRB5CCNAME'} || '<UnSet>', $command if $kf_v;
	}

	if (! $for) {
		my ($t) =  $ENV{'KRB5CCNAME'};
		$t =~ s/^$FILE//;
		print "### test for '$t'\n" if $trace;
		return 0 if ! -e $t;
	}

	print STDERR "# open '$command'\n" if $trace;
	open(KLIST, $command) || return 0;
	# Ticket cache: FILE:/tmp/krb5cc_104_cfqVvm
	# Default principal: pb22@AD.CL.CAM.AC.UK
	# Valid starting     Expires            Service principal
	# 04/23/09 08:24:45  04/23/09 18:24:45  krbtgt/AD.CL.CAM.AC.UK@AD.CL.CAM.AC.UK
	# renew until 04/30/09 08:24:45
	# 04/23/09 08:24:46  04/23/09 18:24:45  nfs/elmer.cl.cam.ac.uk@AD.CL.CAM.AC.UK
	# renew until 04/30/09 08:24:45
	# 04/23/09 08:39:15  04/23/09 18:24:45  host/thalassa.cl.cam.ac.uk@AD.CL.CAM.AC.UK
	# renew until 04/30/09 08:24:45
	#
	# OR on FreeBSD
	#
	# ebel:~: klist -5 -c /tmp/krb5cc_104
	# Credentials cache: FILE:/tmp/krb5cc_104
	#         Principal: pb22@AD.CL.CAM.AC.UK
	# 
	#   Issued           Expires          Principal
	# Jun 19 19:13:46  Jun 20 05:13:46  krbtgt/AD.CL.CAM.AC.UK@AD.CL.CAM.AC.UK
	# Jun 19 19:13:49  Jun 20 05:13:46  nfs/elmer.cl.cam.ac.uk@AD.CL.CAM.AC.UK
	# Jun 19 19:30:16  Jun 20 05:13:46  host/squacco.cl.cam.ac.uk@AD.CL.CAM.AC.UK
	#
	# or with -v
	#
	#     Cache version: 4
	# 
	# Server: krbtgt/AD.CL.CAM.AC.UK@AD.CL.CAM.AC.UK
	# Client: pb22@AD.CL.CAM.AC.UK
	# Ticket etype: arcfour-hmac-md5, kvno 2
	# Ticket length: 1233
	# Auth time:  Jun 12 09:36:48 2013
	# End time:   Jun 12 19:36:48 2013 (expired)
	# Ticket flags: initial, pre-authenticated
	# Addresses: addressless

	$last_klist = '';
 	while (<KLIST>) {
		print STDERR "## $_" if $trace;
		next if /^\s*$/;
		$last_klist .= "\t" . $_;
		#  06/26/09 18:17:29  06/27/09 04:13:18  nfs/elmer.cl.cam.ac.uk@
		next if /Valid starting\s+Expires\s+Service principal/;
		# Ticket server: host/cron-serv1.cl.cam.ac.uk@DC.CL.CAM.AC.UK
		next if /^\s+Ticket server:/;
		if (/Ticket cache: $FILE$ppref(\S+)/) {
			$path = "$ppref$1";
			$file .= " 1:$1";
			print STDERR "# file is '$file'\n" if $trace;
			next;
		}
		# Credentials cache: FILE:/tmp/krb5cc_104
		if (/Ticket cache: $FILE(\S+)/ || /Credentials cache: $FILE(\S+)/) {
			$path = $file .= " $1";
			$file =~ s= $cc_tgt/$fpref= 2:=;
			$file =~ s= $cc_tmp/krb5cc= 3:=;
			print STDERR "# file is '$file'\n" if $trace;
			next;
		}
	# Client: pb22@AD.CL.CAM.AC.UK
	# Ticket etype: arcfour-hmac-md5, kvno 2
	# Ticket length: 1233
	# Auth time:  Jun 12 09:36:48 2013
	# End time:   Jun 12 19:36:48 2013 (expired)
	# Renew till: Jul 12 05:28:05 2013
	# Ticket flags: initial, pre-authenticated
	# Addresses: addressless
		if (/^(Client|Ticket etype|Ticket length|Auth time|Ticket flags|Addresses|Start time):/) { next; }
		if (/^\s*Cache version:\s*\d+\s*$/) { next; }
		if (/^Server:\s*(\S+)\/(\S+)\@(\S*)/) { $server = $1; next; }
		if (/^End time:\s*([A-Z][a-z][a-z]\s+\d+ \d\d:\d\d:\d\d\s+\d\d\d\d)/) {
			print STDERR "# tkt type = $server End = $1\n" if $trace;
			if ($server eq "krbtgt") {
				my ($when) = $1;
				# $when = &add_decade($when);
				unless ($exp > 0) {
					$exp =  str2time($when) - time;
					print STDERR "# exp is '$exp' for $when " . localtime(str2time($when)) . "\n" if $trace;
					$next_is_renew = 1;
				}
			}
			next; }
		#         Principal: pb22@AD.CL.CAM.AC.UK
		if (/Default principal: (\S+)\@(AD|DC).CL.CAM.AC.UK/ || /^\s* Principal: (\S+)\@(AD|DC).CL.CAM.AC.UK/) {
			$principal .= " $1\@$2";
			print STDERR "# principal is '$principal'\n" if $trace;
			next;
		}
		# 04/23/09 08:24:45  04/23/09 18:24:45  krbtgt/AD.CL.CAM.AC.UK@AD.CL.CAM.AC.UK
		if (/^(\d\d\/\d\d\/\d\d \d\d:\d\d:\d\d)\s+(\d\d\/\d\d\/\d\d \d\d:\d\d:\d\d)\s+(\S+)\/(\S+)\@(\S*)/) {
			print STDERR "# tkt: $1 $2 for $3 $4 $5\n" if $trace;
			if ($3 eq "krbtgt") {
				my ($when) = $2;
				$when = &add_decade($when);
				unless ($exp > 0) {
					$exp =  str2time($when) - time;
					print STDERR "# exp is '$exp' for $when " . localtime(str2time($when)) . "\n" if $trace;
					$next_is_renew = 1;
				}
			}
		# Jun 19 19:13:46  Jun 20 05:13:46  krbtgt/AD.CL.CAM.AC.UK@AD.CL.CAM.AC.UK
		} elsif (/^([A-Z][a-z][a-z]\s+\d+ \d\d:\d\d:\d\d)\s+([A-Z][a-z][a-z]\s+\d+ \d\d:\d\d:\d\d)\s+(\S+)\/(\S+)\@(\S*)/) {
			print STDERR "# tkt: $1 $2 for $3 $4 $5\n" if $trace;
			if ($3 eq "krbtgt") {
				my ($when) = $2;
				$when = &add_decade($when);
				unless ($exp > 0) {
					$exp =  str2time($when) - time;
					print STDERR "# exp is '$exp' for $when " . localtime(str2time($when)) . "\n" if $trace;
					$next_is_renew = 1;
				}
			}
		# Jun 12 09:36:48  >>>Expired<<<  krbtgt/AD.CL.CAM.AC.UK@AD.CL.CAM.AC.UK
		} elsif (/^([A-Z][a-z][a-z]\s+\d+ \d\d:\d\d:\d\d)\s+(>>>Expired<<<)\s+(\S+)\/(\S+)\@(\S*)/) {
			print STDERR "# tkt: $1 '$2' for $3 $4 $5\n" if $trace;
			if ($3 eq "krbtgt") {
				unless ($exp > 0) {
					$exp = -1;
					print STDERR "# exp is '$exp' for $2\n" if $trace;
					$next_is_renew = 1;
				}
			}

		} elsif (/renew until\s+(\d\d\/\d\d\/\d\d \d\d:\d\d:\d\d)/ || 
			 /^Renew till:\s*([A-Z][a-z][a-z]\s+\d+ \d\d:\d\d:\d\d \d\d\d\d)/) {
			my ($when) = $1;
			print STDERR "# renew $next_is_renew: $when\n" if $trace;
			$when = &add_decade($when);
			if ($next_is_renew) {
				$ref = str2time($when) - time;
				print STDERR "# $when gives ", str2time($when), " - ", time, " = $ref\n" if $trace;
			}
			$next_is_renew = 0;
		} elsif (/^\s*Issued\s+Expires\s+Principal\s*$/) {;
		} else { $next_is_renew = 0; print "# unexpected: $_" unless $quiet; }
	}

	$file =~ s/^ //;
	$principal =~ s/^ //;
	if ($path && $purge && $exp < 0) {
		print STDERR "# unlink($path) as EXPIRED ($exp, $ref, $file, $principal)\n";
		unlink($path) 
	}
	$slop_sec = 10;
	@stat = stat($path);
	$user = $stat[4];
	# print STDERR "[[ <$user> (<$longeste{$user}> vs <$exp> +/- <$slop_sec>) <$longestr{$user}> vs <$ref> ]]\n";
	if (defined($user) && 
	    ( (! $longeste{$user}) # It is the first
	      || ($longeste{$user} < ($exp - $slop_sec)) # the expiry date is better than before
	      || ( ($longeste{$user} < ($exp+ $slop_sec)) && ($longestr{$user} < $ref))
	    ) && ($path =~ m=^/tmp/krb5cc_${user}(-DC)*_kt=)) {
		$longeste{$user} = $exp;
		$longestr{$user} = $ref;
		$longestf{$user} = $path;
		# print STDERR "[  <$user> <$longeste{$user}> <$exp> <$slop_sec> <$longestr{$user}> <$ref>  ]\n";
	}
	return ($exp, $ref, $file, $principal, &sec2interval($exp), &sec2interval($ref));
}

# -----------------------------------------------------------------------------

# add the decade to a date so that str2time gets it right
# HACK: look for 'this year' with just 2 digits
sub add_decade {
	my ($when) = @_;
	unless ($y2) {
		$y2 = (localtime(time))[5];
		$y2 += 1900 if $y2 >  100 && $y2 <  200;
		$y2 -= 2000 if $y2 > 2000 && $y2 < 3000;
		$y3 = (localtime(time + 60 * 60 * 24 * 21))[5];
		$y3 += 1900 if $y3 >  100 && $y3 <  200;
		$y3 -= 2000 if $y3 > 2000 && $y3 < 3000;
	}
	# Look for dd/mm/yy and then yy/mm/dd
	$when =~ s=^(\d\d/\d\d)/($y2 ..:..:..)$=$1/20$2=;
	$when =~ s=^($y2/\d\d/\d\d ..:..:..)$=20$1=;
	# Look also for next year near the end of the year - 15/01/16 and 16/01/15 should not clash ...
	$when =~ s=^(\d\d/01)/($y3 ..:..:..)$=$1/20$2=;
	$when =~ s=^($y3/01/\d\d ..:..:..)$=20$1=;
	return $when;
}

# -----------------------------------------------------------------------------

# get the value of a user set value, reading the file if necessary
sub dot_val {
	my ($key) = @_;
	my ($magic) = "MaGiC";
	unless ($dotval{$magic}) {
		print STDERR "# open $dotfile\n" if $kf_v;
		if (open(DOTFILE, $dotfile)) {
			print STDERR "# opened $dotfile\n" if $kf_v;
			while (<DOTFILE>) {
				chomp;
				s/\s*#.*//;
				s/\s+$//;
				$dotval{$1} = $2 if /^\s*(\S+)\s*=\s*(.*)/;
				print STDERR "## $1='$2'\n" if $1 && $kf_v;
			}
			close(DOTFILE);
		}
		$dotval{$magic} = 1;
	}
	return $dotval{$key};
}

# -----------------------------------------------------------------------------

# sync a file to other machines
sub sync_hosts {
	my ($rc) = 0;
	return $rc unless $hosts;
	&set_kf_k($kf_k);
	return $rc unless -r $kf_k;

	system("date") if $verbose; # unless $quiet;
	&read_rsync_info(0) unless $slavefile;
	$command = "(for h in @hosts; do rsync --update --times  --timeout=5 $kf_k \$h:$slavefile\&done)&";
	print "$cmd: exec '$command'\n" if $verbose; # unless $quiet;
	$rc += system($command) unless $noaction;

	return $rc unless $localhost;

	$command = "cp -up $kf_k $slavefile";
	print "$cmd: exec '$command'\n" if $verbose; # unless $quiet;
	$rc += system($command) unless $noaction;
	return $rc;
}

# -----------------------------------------------------------------------------

# convert a number of seconds into a user friendly format
sub sec2interval {
	my ($sec) = @_;
	my ($minus) = "";
	return $sec unless $sec;

	($sec, $minus) = (-$sec, "-") if $sec < 0;
	return $minus . $sec . "s" if $sec < 120;
	return $minus . int($sec/6)/10 . "m" if $sec < (120 * 60);
	return $minus . int($sec/(60*6))/10 . "h" if $sec < (48 * 60 * 60);
	return $minus . int($sec/(60*6*24))/10 . "d";
}

# -----------------------------------------------------------------------------

# See what's running and available
sub status {
	my ($krenew, $cl_krenew, $self) = (0,0,0);
	my ($user) = $USER;
	my($psargs);
	my(%krenewing) = ();
	my(%kkeys) = ();
	$psargs = (! $user || ($user eq "root")) ? "axfww" : "$osuser $user -fww";
	print STDERR "# Use: ps $psargs|\n" if $trace;
	return undef unless open(PS, "ps $psargs|");
	while (<PS>) {
		print STDERR "#PS: $_" if $trace;
		if (/cl-krenew/ || /krenew.pl/) {
			# Is this the current process?
			if (/^(\d+)/ || / (\d+)/ && $1 == $$) { $self++; }
			else { $cl_krenew++; }
			# print STDERR "# $self/$cl_krenew: $_";
		}
		elsif (/krenew/) { $krenewing{$1} = 1 if m=-k $cc_tmp/krb5cc_(\S+)=; $krenew++; }
	}
	$pat = "$ppref$<*";
	$pat = "$ppref*" if $USER eq "root";
	foreach $k (eval("<$pat>")) { $kkeys{$1} = 1 if $k =~ /$ppref(\S+)/; }

	return ($cl_krenew, $krenew,$self, join(" ", sort keys %krenewing), join(" ", sort keys %kkeys));
}

# -----------------------------------------------------------------------------
sub fix_krenews {
	my ($krenewing,  $kkeys) = @_;
	my (%k) = ();
	my (%r) = ();
	foreach $v (split(/ /, $krenewing)) { $r{$v} = 1; }
	foreach $v (split(/ /, $kkeys)) { $k{$v} = 1; }
	foreach $v (keys %r) {
		unless ($k{$v}) {
			# print STDERR "# refreshing non available key $v\n";
		}
	}
	foreach $v (keys %k) {
		unless ($r{$v}) {
			print STDERR "## fix_krenews: ticket_life('$ppref$v')\n" if $trace;
			my($exp, $ref, $file, $principal, $texp, $tref) = &ticket_life("$ppref$v");
			if ($texp =~ /^-/) {
				system("rm -f $ppref$v");
				print STDERR "# not refreshing EXPIRED ticket $v - ($exp, $ref, $file, $principal, $texp, $tref)\n";
			} elsif ($tref eq "0") {
				print STDERR "# was not refreshing $v - ($exp, $ref, $file, $principal, $texp, $tref) - no refersh possible ($tref)\n" if $trace;
				
			} else {
				system("krenew -b -K 60 -k $ppref$v");
				print STDERR "# was not refreshing $v - ($exp, $ref, $file, $principal, $texp, $tref)\n";
			}
		}
	}
}
# -----------------------------------------------------------------------------

# Set things up to perform a cron operation (boot or prod) -- check that relevant jobs are running.
# May exit if nothing to do - otherwise it sets things up and returns
sub cron {
	my($check) = @_;
	my($cl_krenew, $krenew, $self, $krenewing, $kkeys) = &status;

	# *** Check that all existing keys are being renewed if they can be
	# All is well, refresh and distribute processes running, exctly as expected
	if ($self && $cl_krenew && $krenew && $krenewing eq $kkeys) {
		print STDERR "$cmd: processes already running ($cl_krenew, $krenew, $self,$krenewing,$kkeys)\n" unless $check;
		exit(0) 
	}

	# There is not an exact match between available keys and watched keys.
	# 1) key expired - delete it
	# 2) key not being renewed (may not be possible!) - watch it (unless cannot be refreshed?)
	if ($self && $krenew) {
		&fix_krenews($krenewing,  $kkeys);

		print STDERR "$cmd: processes already running ($cl_krenew, $krenew, $self,$krenewing,$kkeys)\n" unless $check;
		exit(0) 
	}

	print STDERR "$cmd: info suspect - did not find self\n" unless $self;

	# Look at available key files to see which are usable and refreshable
	$pat = "$ppref$<*";
	$lastkeys = $lastrefresh='NoNe';
	$def_keys = $def_refresh = $keys = $refresh = 0;
	foreach $k (eval("<$pat>")) {
		my($exp, $ref, $file, $principal, $texp, $tref) = &ticket_life($k);
		$lastkeys = $file, $keys++ if $exp > 0;
		$lastrefresh=$file, $refresh++ if $ref > 0;
		# print STDERR "got: $exp, $ref, $file, $principal, $texp, $tref\n";
	}
	my($exp, $ref, $file, $principal, $texp, $tref) = &ticket_life();
	# print STDERR "Got: $exp, $ref, $file, $principal, $texp, $tref\n";
	$def_keys++, $keys++ if $exp > 0;
	$def_refresh++, $refresh++ if $ref && $ref > 0;

	# If there are keys but none are refreshable, nothing to do
	if ($self && $cl_krenew && $keys && ! $refresh) {
		print STDERR "$cmd: distribute process already running\n" unless $check;
		exit(0) 
	}
&display_longest("N3", 0);
	exit(0) if $check && ! $keys;

	# Report if not expected.
	# For probe we should never get here, as there should be keys.
	# For boot, there should be no processes.
	print "$cmd: status gave: $cl_krenew, $krenew, $self; $keys/$refresh $def_keys/$def_refresh $lastkeys/$lastrefresh '$krenewing' '$kkeys'\n" if $check || $cl_krenew || $krenew;

	&fix_krenews($krenewing,  $kkeys);

	# If there are no keys, there is nothing to do. Complain and exit
	die("$cmd: exiting as no keys found\n") unless $keys;

	# Arrange to distribute if any hosts set
	if ($rsync = &read_rsync_info(0)) {
		if ($def_keys) { print "$cmd: distribute default keys\n"; }
		else { print "$cmd: distribute $lastkeys\n"; }
	}

	# Arrange to refresh if possible
	if ($def_refresh) { $maxout=1; print "$cmd: refresh default keys\n"; }
	elsif ($refresh) { $maxout=1; print "$cmd: refresh $lastrefresh\n"; }
	else { $do_krenew = 0; print "$cmd: no keys to refresh\n"; }

	# fall through and do the business ...
}

# -----------------------------------------------------------------------------

sub read_rsync_info {
	my ($force) = @_;
	$syncmins = $ENV{'CL_KRENEW_SYNCMINS'} unless $syncmins;
	$syncmins = &dot_val('syncmins') unless $syncmins;
	$syncmins = $defsyncmins unless $syncmins;
	$ENV{'CL_KRENEW_SYNCMINS'} = $syncmins;

	$slavefile = $ENV{'CL_KRENEW_SLAVEFILE'} unless $slavefile;
	$slavefile = &dot_val('slavefile') unless $slavefile;
	$slavefile = "$fpref$<_$lhost" unless $slavefile;
	$slavefile = "/tmp/$slavefile" unless $slavefile =~ /^\//;
	$ENV{'CL_KRENEW_SLAVEFILE'} = $slavefile;

	$hosts = $ENV{'CL_KRENEW_HOSTS'} unless $hosts;
	$hosts = &dot_val('hosts') unless $hosts;
	if (! $force && ! $hosts) {
		warn("$cmd: no 'hosts' found, so not distributing\n");
		return 0;
	}
	die("$cmd: '--rsync' specified, but no hosts found\n") if ! $hosts;
	@hosts = split(/[\s,]/, $hosts);
	print STDERR "# rsync ($syncmins) to $#hosts:$hosts (@hosts)\n" if ($rsync || $hosts) && $kf_v;
	$localhost = $ENV{'CL_KRENEW_LOCALHOST'} || 0;
	$localhost = 1, shift @hosts if $hosts[0] && $hosts[0] eq "localhost";
	$ENV{'CL_KRENEW_HOSTS'} = $hosts;
	$ENV{'CL_KRENEW_LOCALHOST'} = $localhost;
	return 1;
}

# -----------------------------------------------------------------------------

# Set $kf_k and set KRB5CCNAME
sub set_kf_k {
	my($new) = @_;
	($new = $ENV{'KRB5CCNAME'} || '') =~ s/^$FILE// unless $new;
	$new = "$ppref$<" unless $new;
	$kf_k = $new;
	$ENV{'KRB5CCNAME'} ="$FILE$new";
}

# -----------------------------------------------------------------------------

sub time2min {
	my ($t) = @_;
	%mult = (
		's'	=> 1/60,
		'm'	=> 1,
		'h'	=> 60,
		'd'	=> 60 * 24,
	);
	return $t unless defined $t;
	$t .= "s" if $t =~ /^\d+$/;
	return undef unless $t =~ /^(\d+)([smhd])/;
	$res = $1 * $mult{$2};
	$res++ if $res < 1;	# don't truncate 30s to 0m
	return $res;
}

sub xdg {
	my($xdg) = &get_xdg;
	&read_dot;
	&start_kinit_xdg;
	return $xdg;
}

sub get_xdg {
	my $val = $ENV{'XDG_SESSION_ID'} || "";
	my $krb = $ENV{'KRB5CCNAME'} || "";
	my $suf = ($krb && $krb =~ m=FILE:/tmp/krb5cc_\d+_kt\d+-XDG(\d+)$=) ? $1 : '';
	print STDERR "# <$val> and <$suf> ($krb)\n" if $kf_v;
	return $val || $suf;
}

sub read_dot {
	my $cmd = "cl-tgt.dot --verify";
	open(IN, "$cmd|") || return warn("$0: failed to open '$cmd': $?\n");
	$access{''} = '';
	while (<IN>) {
		# pamallow  = sandy.cl.cam.ac.uk, ely.cl.cam.ac.uk (slogin-new)
		if (/^(\S+)\s*=\s*(.*)\s*\(.*\)\s*$/) {
			print STDERR "# $1 = $2\n" if $kf_v;
			$access{$1} = $2;
		}
	}
	close(IN);
}

sub start_kinit_xdg {
	my($xdg) = &get_xdg;
	my($user) = $ENV{'USER'};
	unless ($xdg) {
		warn("$0: failed to find the XDG_SESSION_ID\n");
		return 0;
	}
	my($file) = "/tmp/.pid.krenew_$user.XDG$xdg";
	if (-e $file) {
		warn("$0: Found $file\n") if $kf_v;
		my ($kpid) = `cat $file`;
		chomp $kpid;
		if ($kpid =~ /^\d+$/) {
			my ($rc);
			if ($rc= kill(0, $kpid)) {
				warn("$0: Process $kpid is already renewing $xdg for $user\n");
				return;
			} else {
				warn("$0: kill($kpid,0) gave $rc: $? : $!\n");
			}
		} else {
			warn("$@: $file did not contain just a process ID\n");
		}
	}
	my ($cmd) = "krenew -vaK1 -p$file";
	my ($cpid) = fork || exec($cmd);
	warn("$0: started $cpid: $cmd\n");
}
