#!/usr/bin/perl -w
# $Header: /global/src/usr.bin/src/RCS/cl-isidle.pl,v 1.101 2022/10/17 04:14:23 pb22 Exp $
# Use "pod2text" for info

=head1 NAME

isidle - waits while a machine is busy

=head1 SYNOPSIS

B<isidle.pl> [-v] [-h] [--email $to] [--halt] [--cron] [-n]  [--fork|--foreground]  [--kill] [--sigstop} [--sigcont] [--restart] [--recent] [--quitidletime $seconds] [--except $times] [--nosuspend] [--nohibernate] [--nohybrid] [--nohalt]  [--settings] [--running] [--min_suspend $seconds] [--min_hibernate $seconds] [--pad $ignored] [--testnow]  [--requirexidle] [[ if not under cl-asuer:     [-1|one] [--ignore $user] [--command $cmd] [--resumecmd $cmd] [--now] [--count $loops] [--idletime $seconds] [--sleep $seconds] [--loadavg $sum] [--logfile $file] [--timeoutcmd $timeoutcmd]]

=head1 OPTIONS

=over 4

=item B<-v|--verbose> +

show what it's doing

=item B<-h|--help> +

generate help info.

=item B<--email> I<$to> +

When the machine becomes idle, send email to I<$to>.
If run under B<cl-asuser> or B<sudo>, it must be a simple mailbox,
e.g. "I<pb22>" rather than "I<pb22@cl.cam.ac.uk>".

=item B<--halt> +

halt the machine.
Roughly equivalent of B<--command 'pm-suspend-hybrid || pm-suspend || pm-hibernate || shutdown -h now'>,
but it tries to enable and disable a partition with label B<resume>, etc.

=item B<--n> +

Do not actually run any B<command> or B<halt> the system.
Does send email, print B<cron> etc.

=item B<--cron> +

For use by cron jobs, which email their output, it prints a trace of the LA and active users which caused the idle event.
Also, if the lock file is busy, exits silently with a return code of 0.

=item B<--fork> +

Fork before waiting. (Default: if B<--halt> or B<--command>)

=item B<--forground> +

Do not fork before waiting. (Default: unless B<--halt> or B<--command>)

=item B<--kill> +

Kill the currently running daemon.

=item B<--sigstop> +

Send the currently running daemon a SIGSTOP.
"B<cl-isidle --run>" should include "B<STOPed>", e.g.
"Process 1917 B<STOPed> as user root started 08:46:".


To temporarily disable the system, run "B<cl-asuser cl-isidle --SIGSTOP>".
To re-enable it, run "B<cl-asuser cl-isidle --SIGCONT>".

=item B<--sigcont> +

Send the currently running daemon a SIGCONT.

=item B<--restart> +

restart the daemon.

=item B<--recent> +

dump recent info to the log.

=item B<--nosuspend> +

do not try to suspend.

=item B<--nohibernate> +

do not try to hibernate.

=item B<--nohybrid> +

do not try to suspend-hybrid.

=item B<--nohalt> +

do not try to halt.

=item B<--quitidletime> I<$seconds> +

If all logged in users must have been idle for I<$seconds>, or infinite if B<0>, then exit but do not perform any command, such as halting, but do send email.
(default: 3600)

=item B<--except> I<$times> +

Do B<NOT> poll during specfied times.
Text values include B<never>, B<officehours>, B<always>.
Numeric values are a pair of integer hours seperated by a dash (I<11-15>) of a pair or colon separted pairs of numbers (I<11:15-15:00>).
Times wrap, so I<12-01> means that it will only poll between I<01:00> and I<12:00>.
(default: never)

=item B<--settings> +

Show the settings.

=item B<--running> +

Show info about the current running instance.

=item B<--min_suspend> I<$seconds>+

the minimum number of seconds that a suspend may last.
If it takes less than this time, it is assumed to have failed, so will try the next method, hibernate.
(default: 2)

=item B<--min_hibernate> I<$seconds>+

the minimum number of seconds that a hibernate may last.
If it takes less than this time, it is assumed to have failed, so will try the next method, shutdown.
(default: 60)

=item B<--pad> I<$ignore>+

The key and value are completely ignored.
It allows the command line to be enlarged so that the it can be over-written with current status information, available using "B<ps>" etc.

=item B<--testnow> +

test it now, ignoring all active users.
e.g. "B<cl-asuser cl-isidle --testnow --halt>" will cause the machine to test
the shutdown mechanism (NB: do not resume immediately, as it might think that the suspend failed. Was 30s or so).

=item B<--requirexidle> +

Require X to be "idle", even if there are no logged in users.

=item B<--timroutcmd> I<$timeoutcmd>+

Use I<timeoutcmd> as a command to timeout commands such as xprintidle

=item B<--now>

Do not loop at all, just test the current status.
Equivalent of B<--count 0 --sleep 0>

=item B<-1|--one>

Just do a single set of B<--count> loops.
If the machine is not idle at any point, exit(1).

=item B<--ignore> I<$user>

Ignore any instances of user I<$user> when testing to see whether a machine is in use.
(default: ^root)

=item B<--command> I<$command>

When the machine is idle, run the command I<$command>.

=item B<--resumecmd> I<$command>

When the machine resumed, run the command I<$command>.

=item B<--count> I<$loops>

There must be I<$loops> tests showing the machine to be idle before exiting.
(default: 5)

=item B<--idletime> I<$seconds>

Any logged in users must have been idle for I<$seconds>, or infinite if B<0>
(default: 3600 if the X idle time can reliably be detected, otherwise 0)

=item B<--sleep> I<$seconds>

Sleep I<$seconds> B<before> each test for the system being idle.
(default: 60)

=item B<--loadavg> I<$sum>

The sum of the three load avarages must be less than I<$sum> for the machine to be idle.
(default: 0.6)

=item B<--logfile> I<$file>

Log instances to $file
(default: /var/log/isidle.log)

=back

=head1 DESCRIPTION

Wait until the machine is "idle" for B<--count> times,
perform any requested tasks
(e.g. B<--command>, B<--email>, B<--cron>, B<--halt>),
and exit with a zero return code.

It sleeps for I<$sleeps> seconds before each poll.

A machine is "idle" if there are no logged in users (other than B<--ignore>)
who have been idle for less than B<--idletime> seconds,
and the sum of the three load averages is less than B<--loadavg>

Only those options marked with a B<+> can be run under B<cl-asuser>

Suitable cron entry is:

45 * * * * root cl-isidle --email isidle+unix-gardening --halt --cron --pad ....................

The defaults (suspend if idle for more than 5 mins) should be suitable for most usage,
but it looks for defaults in
B</etc/default/$name>,
B</etc/default/$name*.conf>,
B</etc/default/$name.$hostname>,
B</etc/sysconfig/$name>,
B</etc/sysconfig/$name*.conf>,
B</etc/sysconfig/$name.$hostname>,
and unless run under B<cl-asuser>,
B<~/.$name>
and
B<~/.$name.$hostname>.
Thus if B<~/.cl-isidle> has the line:
"B<except=officehours>",
it will not poll during office hours.
If B</etc/default/cl-isidle-server.conf> has the line:
"B<except=always>",
it will never poll to check the machine's state, so the server will not be halted.
A condor machine might have file B</etc/default/cl-isidle-condor.conf> as below,
as condor's clock tick is 5mins, so to be sure there isn't any work left in the queue, up the
loop to 10 times, wait 120s between loops, ignoring any instances of the user pb22,

=over 2

 idle_cnt=10
 sleep=120
 ignore=^pb22

=back

Note that later settings override earlier ones.

=cut

$verbose = $ENV{'ISIDLE_VERBOSE'};
$O = $0;
$ver = '';
$ver = $1 if '$Id: cl-isidle.pl,v 1.101 2022/10/17 04:14:23 pb22 Exp $' =~ /Id:\s+\S+\s+(\S+)/;
@origargv = @ARGV;
$SIG__USR1	=  10;	# real interrupt
$SIG__USR2	=  12;	# real interrupt
$SIG__TERM	=  15;	# real interrupt
$SIG__CONT	=  18;	# real interrupt
$SIG__STOP	=  19;	# real interrupt
$RESTART_SUSP	= 'SUSPEND';	# pseudo 'interrupt' number to tell suspend
$RESTART_HIBE	= 'HIBERNATE';	# pseudo 'interrupt' number to tell restart
$GREATER	= 9999999999;
$AWAIT_USER	= int(($GREATER + 999) / 1000);
$SIG{'USR1'} = 'restart';
$SIG{'USR2'} = 'dumprecent';

$timeoutcmd='';

	$nosuspend = 0;
	$nohibernate = 0;
	$nohybrid = 0;
	$nohalt = 0;
%sdm = (	# ShutDown Modes
	0	=> "full shutdown",
	1	=> "no halt",
	2	=> "no hibernate",
	3	=> "no hibernate or halt",
	4	=> "no suspend",
	5	=> "no suspend or halt",
	6	=> "no suspend or hibernate",
	7	=> "no suspend, hibernate or halt",
	8	=> "no hybrid-suspend",
	9	=> "no hybrid-suspend, halt",
	10	=> "no hybrid-suspend or hibernate",
	11	=> "no hybrid-suspend, hibernate or halt",
	12	=> "no hybrid-suspend or suspend",
	13	=> "no hybrid-suspend, suspend or halt",
	14	=> "no hybrid-suspend, suspend or hibernate",
	15	=> "no hybrid-suspend, suspend, hibernate or halt",
);
$hostname = `hostname`;
$hostname = `uname -n` unless $hostname;
chomp($hostname);
$hostname =~ s/\.cl\.cam\.ac\.uk$//;

($name = $0) =~ s=.*/==;
$min_idle = -2;
$who = "";
$la = -1;
$active = -1;
$activeusers = " <not yet checked>";
$foundrunning = "00";
$idle = 0;

$idle_cnt = 5;		# ... for this many loops
$test_gpu_cmd = "/usr/bin/nvidia-smi";
$has_gpu = -e $test_gpu_cmd || 0;
$gpuuse_cmd = "nvidia-smi pmon -c 1";
$xidle_cmd = "xprintidle"; # command to detect X idle time
$xidle = -2;		# last value of &x_idle()
$gpuuse = '<unset>';		# last value of &gpu_idle()
$min_suspend_hybrid_sec = 20;	# if suspend-hybrid takes less, it failed
$min_suspend_sec = 2;	# if suspend takes less, it failed: RT#107591 shows 14s, RT#111996 6s - suspect due to user waking it up
$min_hibernate_sec = 60;# if hibernate took less, it failed
$warns	= "";		# Any outstanding warnings which have not yet been displayed
$display = "-1";	# the X display in use
$displays = "-1";	# the X displays in use
$Xuser	= "<noXuser>";	# the most active X user
$start_time = time;
$rxi="(p)";		# requiredxidle is not yet known.
&setop("starting");

use Getopt::Long;
use Getopt::Long qw(:config require_order);
use Pod::Usage;

$name = "cl-isidle";	# The name of this command for config. locks, etc
$ignore = "^root ";	# ignore pb22 for testing ...
$except	= "never";	# by default, process all the time
$unlessisrunning= "";	# by default, there is no "magic command", which if running, avoids shutdown
$hour	= 60 * 60;	# an hour in seconds
$day	= 24 * $hour;	# a day in seconds
$idletime = $hour;	# all users must be idle for at least this period (s)
$longsleep = 10 * 365 * $day;   # sleep for "ages" - 10 years
$quitidletime = $idletime;# if all idle at least this long, quit with no action such as halt - do send email
$sleep	= 60;		# ... sleeping this long between intervals (s)
$loadavg = 0.6;		# Limit of the sum of the three LAs
$cl_krenew = "(date; cl-krenew --purge --status; cl-krenew -1; cl-krenew --status) >> /var/log/cl-ii-krb 2>&1;"; # do a one off renew of all kerberos tickets and purge any expired ones, so that processes do not hang when trying to suspend
$prepare="${cl_krenew}swapon -L resume; runlevel --set=0"; # prepare to do things
$suspend_hybrid = "pm-suspend-hybrid"; # command to suspend / hibernate
$suspend = "pm-suspend"; # command to suspend
$hibernate = "pm-hibernate"; # command to hibernate
$unhibernate="${cl_krenew}swapoff -L resume";
$anacron = "service anacron start";
$resumef = "/var/run/cl-isidle-resume"; # file of commands to run on resume
$resumed = "test -f $resumef && bash $resumef && rm -f $resumef";
$shutdown = "shutdown -h now"; # command to shut down
$mailcmd = "|mail -s '$name.$hostname%s' %s";	# formal for mail command
$logfile = "/var/log/$name.log";
$n = $cron = $halt = $cmd = $email = $resume_cmd = '';

$in_asuser = $ENV{"IN_ASUSER"} || $ENV{"SUDO_UID"} || '';
$in_asuser = $ENV{"IN_ASUSER"} || '';

$wopts = "-uhf";	# options to pass to "w"
$wmraw	= '?';		# HH:MMm, MM:SS  OR  HH:MM,  MM:SSm ?
$vlockfile	= "/var/run/$name";
$ulockfile	= "/tmp/.LOCK.$name";
$deflockfile = $lockfile = ($< == 0) ? $vlockfile : $ulockfile;

$requirexidle		= 0;	# By default, unset

# Look in /etc/default (e.g. debian)
&read_config("/etc/default/$name");
&read_configs("/etc/default/$name*.conf");
&read_config("/etc/default/$name.$hostname");
# Look in /etc/sysconfig (e.g. Redhat)
&read_configs("/etc/sysconfig/$name*.conf");
&read_config("/etc/sysconfig/$name");
&read_config("/etc/sysconfig/$name.$hostname");
# Look in $HOME
if ($HOME= $ENV{'HOME'}) {
	&read_config("$HOME/.$name") unless $in_asuser;
	&read_config("$HOME/.$name.$hostname") unless $in_asuser;
}


%opts = (
	"verbose|v"	=> \$verbose,
        "help|h"	=> \$help,
	"email:s"	=> \$email,	# send email
	"halt"		=> \$halt,	# halt
	"n"		=> \$n,		# do not actually do anything
	"cron"		=> \$cron,	# running under cron
	"fork"		=> \$fork,	# Do fork
	"foreground"	=> \$foreground,# stay in foreground
	"kill"		=> \$kill,	# kill
	"sigstop"	=> \$sigstop,	# stop
	"sigcont"	=> \$sigcont,	# cont
	"restart"	=> \$restart,	# restart
	"recent"	=> \$recent,	# show recent info
	"nosuspend"	=> \$nosuspend,	# Do not attempt to suspend
	"nohibernate"	=> \$nohibernate,#Do not attempt to hibernate
	"nohybrid"	=> \$nohybrid,	#Do not attempt to hybrid-suspend
	"nohalt"	=> \$nohalt,	# Do not attempt to halt
	"quitidletime:s"=> \$quitidletime,	# quit (without taking any action - do email) if more than this long
	"except:s"	=> \$except,	# times to ignore
	"settings"	=> \$settings,	# show the settings
	"running"	=> \$running,	# show the running instance
	"min_suspend:s"	=> \$min_suspend_sec, # the min time it takes to (fail to) suspend
	"min_hibernate:s"=> \$min_hibernate_sec, # the min time it takes to (fail to) hibernate
	"pad:s"		=> \$pad,	# padding to enlarge the command line
	"testnow"	=> \$testnow,	# test it now, ignoring all active use
	"requirexidle"	=> \$requirexidle,# Require X to be idle, even if no users
	"one|1"		=> \$one,	# run as single shot -- do not wait
	"ignore:s"	=> \$ignore,	# ignore this user
	"command|c:s"	=> \$cmd,	# run this command when idle
	"resumecmd:s"	=> \$resume_cmd,# run this command when resumed
	"now"		=> \$now,	# take an immediate test now
	"count:s"	=> \$idle_cnt,	# number of tests needed
	"loadavg:s"	=> \$loadavg,	# max sum of the three load avarages
	"logfile:s"	=> \$logfile,	# logfile
	"idletime|i:s"	=> \$idletime,	# users must have been idle (s)
	"sleep|s:s"	=> \$sleep,	# seconds between tests
	"timeoutcmd:s"	=> \$timeoutcmd,# command to set a timeout
	"unlessisrunning:s"=> \$unlessisrunning,# do not suspend if this job running
);

%opts = (
	"verbose|v"	=> \$verbose,
        "help|h"	=> \$help,
	"email:s"	=> \$email,	# send email
	"halt"		=> \$halt,	# halt
	"n"		=> \$n,		# do not actually do anything
	"cron"		=> \$cron,	# running under cron
	"fork"		=> \$fork,	# Do fork
	"foreground"	=> \$foreground,# stay in foreground
	"kill"		=> \$kill,	# kill
	"sigstop"	=> \$sigstop,	# stop
	"sigcont"	=> \$sigcont,	# cont
	"restart"	=> \$restart,	# restart
	"recent"	=> \$recent,	# show recent info
	"nosuspend"	=> \$nosuspend,	# Do not attempt to suspend
	"nohibernate"	=> \$nohibernate,#Do not attempt to hibernate
	"nohybrid"	=> \$nohybrid,	#Do not attempt to hybrid-suspend
	"nohalt"	=> \$nohalt,	# Do not attempt to halt
	"quitidletime:s"=> \$quitidletime,	# quit (without taking any action - do email) if more than this long
	"except:s"	=> \$except,	# times to ignore
	"settings"	=> \$settings,	# show the settings
	"running"	=> \$running,	# show the running instance
	"min_suspend:s"	=> \$min_suspend_sec, # the min time it takes to (fail to) suspend
	"min_hibernate:s"=> \$min_hibernate_sec, # the min time it takes to (fail to) hibernate
	"pad:s"		=> \$pad,	# padding to enlarge the command line
	"testnow"	=> \$testnow,	# test it now, ignoring all active use
	"requirexidle"	=> \$requirexidle,# Require X to be idle, even if no users
	#"one|1"	=> \$one,	# run as single shot -- do not wait
	#"ignore:s"	=> \$ignore,	# ignore this user
	#"command|c:s"	=> \$cmd,	# run this command when idle
	#"resumecmd:s"	=> \$resume_cmd,# run this command when resumed
	#"now"		=> \$now,	# take an immediate test now
	#"count:s"	=> \$idle_cnt,	# number of tests needed
	#"loadavg:s"	=> \$loadavg,	# max sum of the three load avarages
	#"logfile:s"	=> \$logfile,	# logfile
	#"idletime|i:s"	=> \$idletime,	# users must have been idle (s)
	#"sleep|s:s"	=> \$sleep,	# seconds between tests
	#"unlessisrunning:s"=> \$unlessisrunning,# do not suspend if this job running
) if $in_asuser;

$ENV{'PATH'} .= ":/sbin:/usr/sbin";

$0= $O, pod2usage() unless GetOptions (%opts);
$0= $O, pod2usage(-verbose => 2) if $help;

$orign = $n;
$rxi = ($requirexidle) ? "(r)" : "";

if ($ENV{'TEST_X'})   { &setop("testing find_Xservers_lock"); &x_idle; print STDERR "# xidle=$xidle\n"; exit(1); }
if ($ENV{'TEST_GPU'}) { &setop("testing GPU"); &gpu_idle; print STDERR "# gpuuse=$has_gpu/'$gpuuse'\n"; exit(1); }
&setop("doing ancillary options");
# Just display the current (interesting) settings
if ($settings) {
	foreach $k (sort keys %opts) {
		$v = $opts{$k};
		next if $k eq "settings" && ! $verbose;
		$v = $$v;
		unless (defined($v) && $v) {
			next unless $verbose || $k eq "idletime|i:s";
			$v = "<undefined>" unless defined $v;
			$v = "''" if $v eq "";
		}
		printf "%-15s : %s\n", $k, $v;
	}
	exit(0);
}

# Just display the current running instance
if ($running) {
	# Can also drop the "u" ...
	foreach $file (($vlockfile,$ulockfile)) {
		if (-e $file) {
			my ($pid) = `cat $file`;
			chomp $pid;
			$pid .= ' @ ' . localtime($stat[8]) if @stat = stat($file);
			print "lockfile $file is $pid\n" 
		}
	}
	open(PS, "ps uaxwww|") || die("$O: open of ps failed: $!\n");
	while (<PS>) {
		next unless /isidle/;
		print STDERR "#w# $_" if $verbose;
		chomp;
		s/\s+$//;
		# 29915 pts/4    R+     0:00 cl-isidle 1s @07:17:00 u=0 la=0.00 i:-2s l=0/5 'doing ancillary options' xi=-2
		# 18058 pts/2    S<+    0:00 cl-isidle 0s @07:59:43 u=0 la=0.00 i:-2s l=0/5 'doing ancillary options' xi=-1

		# pb22 812 0.0 1.0 8948 2808 pts/0 S 07:11 0:00 cl-isidle 0s ...
		#      812                   pts/0 S       0:00 cl-isidle 0s @07:11:16 u=0 la=0.00 i:-12s ...
		my($user, $start, $pmess) = ("", "", "");
		if (s/^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S*isidle\S*)/$2 $7 $8 $10 $11/) {
			$run = "running";
			$user = $1;
			$state = $8;
			$start = $9;
			if ($state eq "T") { $run = "STOPed" }
			elsif ($state =~ /^S/) { }
			else { $run .= " in state '$state'" }
			$pmess = " $run as user $user started $start";
		}
		if (/^\s*(\d+)\s.*isidle\S*\s+(.*)/) {
			my($proc, $txt) = ($1, $2);
			next if $proc == $$ && ! $verbose;
			if ($txt =~ /^(\S+)\s+\@(\S+)\s+u=(.*)\s+la=(\S+)\s+i:(.*)\s+l=(\S+)\s+'(.*)'\s+xi([^=]*)=(\S+)\s*(.*)/) {
				($vt, $vat, $vum, $vla, $vi, $vl, $vs, $vrxi, $vxi, $rest) = ($1, $2, $3, $4,$5,$6, $7, $8, $9, $10);
				$vrxi = ($vrxi eq "(p)") ? " (Pending)" : ($vrxi eq "(r)") ? " (Required)" : ($vrxi eq "()") ? " [DEBUG]" : ($vrxi eq "") ? "" : " [[$vrxi]]";
				print "Process $proc$pmess:\n";
				print "\tlast awake at $vat after running for $vt\n";
				if ($vum eq "0") {
					print "\tno active users\n" unless $vi eq "-2s";
				} elsif ($vum eq "-1") {
					print "\tactive users not yet determined\n";
				} else { print "\tactive users $vum\n"; }
				printf "\tsum of Load Averages is %s\n", ($vla eq "-1.00") ? "not yet determined" : $vla unless $vla eq "0.00" && $vi eq "-2s";
				if ($vi =~ /^(\S+)\s(\S+)\s(\S+)$/) {
					print "\tmost active user is $2 on $3, idle for $1\n";
				} elsif ($vi eq "-2s") {
					print "\tnot yet polled for most active user\n" if $vum ne "-1";
				} elsif ($vi eq "-1s") {
					print "\tno active user\n" unless $vum eq "0";
				} else { print "\tmost active user is $vi\n"; }
				if ($vl =~ /^(\d+)\/(\d+)/) {
					if ($vi eq "-2s" && $vla eq "-1.00") { ; }
					elsif ($1 eq "0") { print "\tnot started $2 loops\n" }
					else { print "\ton loop $1 out of $2\n" }
				} else { print "\tloop $vl\n"; }
				if ($vs =~ /pre sleep (\d+)/) {
					print "\twaiting to start polling at ", "" . localtime($1), "\n";
				} elsif ($vs =~ /^main sleep (\d+)/) {
					print "\tsleeping for ", &times($1), "\n";
				} elsif ($vs =~ /^never: sleep (\d+) from (.*)/) {
					print "\tsleeping for ", &times($1), " from $2 as told to poll 'never'\n";
				} else { print "\tcurrent action is \"$vs\"\n"; }

				$Xuser = "";
				$Xuser = $2 if $vxi =~ s/(\d+):(\S*)/$1/;
				if ($vxi eq "-1") {
					print "\tX server idle time cannot be determined ($vxi)$vrxi\n";
				} elsif ($vxi eq "-2") {
					print "\tX server idle time not yet determined ($vxi)$vrxi\n";
				} elsif ($vxi eq "-3") {
					print "\tX server idle time cannot be determined ($vxi)$vrxi\n";
				} elsif ($vxi eq "-4") {
					print "\tX server idle time cannot be determined ($vxi)$vrxi\n";
				} elsif ($vxi eq "-5") {
					print "\tX server idle time cannot be determined ($vxi)$vrxi\n";
				} elsif ($vxi eq "-6") {
					print "\tX server idle time cannot be determined ($vxi)$vrxi\n";
				} elsif ($vxi eq "-16") {
					print "\tX server idle time cannot be determined ($vxi)$vrxi\n";
				} elsif ($vxi =~ /^-\d+$/) {
					print "\tX server idle time cannot be determined: ($vxi)$vrxi\n";
				} elsif ($vxi eq $AWAIT_USER) {
					print "\tX server awaiting user$vrxi\n";
				} else {
					print "\tX server was idle for [$vxi] ", &times($vxi), "$vrxi $Xuser\n";
				}
				# g=1:'<unset>' 
				print STDERR "# ?parseGPU?: rest='$rest'\n" if $verbose;
				if ($rest =~ s/^g=(\d):'([^']*)' //) {
					my ($has, $info) = ($1, $2);
					print STDERR "# parsed GPU: now rest='$rest', giving <$has> and <$info>\n" if $verbose;
					if (!$has) { printf "\tno $test_gpu_cmd found so assume no GPU (%d:'%s')\n", $has, $info; }
					elsif ($info eq "<unset>") { printf "\tGPU not yet determined (%d:'%s')\n", $has, $info; }
					else { printf "\tGPU=%d GPU process='%s'\n", $has, $info; }
				}
				if ($rest =~ s/^: / /) {
					print  "\tVersion $1\n" if $rest =~ s/ v=(\S+)//;
					printf "\tShutdown Mode: %s\n", $sdm{$1} || $1 if $rest =~ s/ m=(\S+)//;
					printf "\tDisplay :%s\n", ($1 eq "-1") ? "<not yet detected>" : $1 if ($rest =~ s/ d=(\S+)//) && ( $1 ne "-1" ) ;
					print  "\tWarnings: $1\n" if $rest =~ s/ W='([^']*)'//;
					print  "\tunlessisruning: $1\n" if $rest =~ s/ uir=(\S+)//;
					print  "\textra: '$rest'\n" if $rest;
				} else { print "\tunparsed: '$rest'\n" if $rest; }
				next;
			}
			print "Process $proc: $txt\n";
			next;
		}
		print "raw: $_\n";
	}
	exit(0);
}

# Kill the existing server
if ($kill || $sigstop || $sigcont || $restart || $recent) {
	$signal = ($kill) ? $SIG__TERM :
		($sigcont) ? $SIG__CONT :
		($sigstop) ? $SIG__STOP :
		($restart) ? $SIG__USR1 : $SIG__USR2;
	&setop("kill $signal");
	die("$O: no file '$lockfile'\n") unless -e $lockfile;
	if (&lockf($lockfile, 0)) {
		unlink $lockfile || die("$O: failed to unlink $lockfile\n");
		die("$O: managed to lock '$lockfile'\n");
	}
	open(LOCK, $lockfile) || die("$O: failed to open '$lockfile': $!\n");
	@pid = ();
	while (<LOCK>) {
		chomp;
		if (/^\d+$/) { push @pid, $_ }
		else { die("$O: duff pid '$_' in $lockfile\n"); }
	}
	die("$O: no pid in $lockfile\n") unless $#pid >= 0;;
	print STDERR "# $O: run: kill $signal, @pid; rm $lockfile\n" if $verbose;
	unless ($signal == $SIG__USR2 || $signal == $SIG__CONT || $signal == $SIG__STOP) {
		unlink $lockfile || die("$O: failed to unlink $lockfile\n");
		# RACE!
	}
	kill($signal, @pid) || warn("$O: failed to kill @pid: $!\n");
	exit(0);
}

# Check user args are acceptable
if ($fork) {
	die("$O: cannot use both --fork or --foreground\n") if $foreground;
} elsif (!$foreground) {
	$fork = 1 if ! $testnow && ($halt || $cmd || ($except eq "always"));
}
die("$O: email address must be a CRSID, not '$email'\n") if $in_asuser && $email && $email !~ /^[a-zA-Z0-9+-]+$/;

$start = time;
$snaps = "";
($poll_start,$poll_stop) = &start_stop;
print STDERR "# start $poll_start and stop $poll_stop: " . localtime($poll_start) . " - " . localtime($poll_stop) . "\n" if $verbose;

# my($x) = &x_idle('initial');
# print STDERR "## initial x_idle gives: $x idletime = $idletime, start work....\n\n" if  $verbose;

if ($now || $testnow) { # Single shot test
	&setop($now ? "now" : "testnow");
	exit(1) if ! $testnow && ($poll_start < 0 || $poll_start > time);
	$sleep = $idle_cnt = 0; # for the log
	$la = &la;
	&gpu_idle;
	$active = &active unless ($la > $loadavg) || ($gpuuse ne "");
	print STDERR "# display undefined\n" unless defined $display;
	print STDERR "$O: LA $la, users $active$activeusers, xidle $xidle for :$display/$displays, gpu $has_gpu/'$gpuuse'\n" if $verbose;
	exit(1) if ! $testnow && ($active || ($la > $loadavg) || ($gpuuse ne ""));
	$who = ($min_idle < 0) ? "" : " $minu $mind";
	$mins = &times($min_idle);
	$uir = ($unlessisrunning) ? " uir=$unlessisrunning=$foundrunning" : "";
	$snaps .= localtime() . " LA=$la A=$active/$mins$who xi$rxi=$xidle:$Xuser d=$displays$uir g=$has_gpu/'$gpuuse'\n";
} else { # Loop until at least $idle_cnt idle samples
	&setop("locking");
	&lockf($lockfile, 1);
	# If $fork, release the lock, fork, relock, and close TTY
	if ($fork) {
		&setop("forking");
		close(LOCK);
		if (fork) {
			print STDERR "# forked into background\n" if $verbose;
			exit(0) 
		}
		&lockf($lockfile, 2);
		close(STDIN);
		close(STDOUT);
		close(STDERR);
	}
	&dumprecent('Starting');
	&setop("never: sleep $longsleep from " . localtime);
	sleep ($longsleep) if $poll_start < 0;
	&setop("pre sleep " . $poll_start);
	sleep ($poll_start - time) if $poll_start > 0 && $poll_start > time;
	while (($idle < $idle_cnt) && &setop("main sleep $sleep") && sleep($sleep)) {
		# Should we stop polling for a while?
		if ($poll_stop > 0 && time >= $poll_stop) {
			($poll_start,$poll_stop) = &start_stop;
			&setop("mid sleep " . $poll_start);
			if ($poll_start > 0 && $poll_start > time) {
				sleep ($poll_start - time);
				$idle = 0 if ($poll_start - time) > $sleep; # start again after a long sleep
			}
		}
		$n = $orign;
		$la = &la;
		&gpu_idle;
		print STDERR "$O: LA $la, users $active$activeusers, xidle $xidle gpuuse '$gpuuse' for :$display/$displays\n" if $verbose;
		if (($la > $loadavg) || ($gpuuse ne "") || (($idle+1) >= $idle_cnt) && ($active = &active))
			{  unlink $lockfile, exit(1) if $one; $snaps = ''; $idle = 0 }
		else { $idle++} 
		$who = ($min_idle < 0) ? "" : " $minu $mind";
		$mins = &times($min_idle);
		$uir = ($unlessisrunning) ? " uir=$unlessisrunning=$foundrunning" : "";
		$snaps .= localtime() . " LA=$la A=$active/$mins$who xi$rxi=$xidle:$Xuser d=$displays$uir g=$has_gpu/'$gpuuse'\n";
	}
}
&setop("finishing");
$period = time - $start;
&dumprecent('Finishing' . (($cmd) ? "" : " run '$cmd'"));
&halt if $halt;
close(LOCK);
unlink $lockfile if $lockfile eq $deflockfile;
&exec($cmd) if $cmd;
exit (0);

sub dumprecent {
	my($sig) = @_;
	my($why) = (! $sig) ? "dumprecent" : ($sig eq "Starting" || $sig =~ "^Finishing.*") ? $sig :  ($sig eq "USR2") ? "$sig dumprecent" : "dumprecent: $sig";
	$SIG{'USR2'} = 'dumprecent';	# in case called as an interrupt ...
	# endode what type of "halt"
	my ($h) = ($halt) ? $halt | ($nosuspend << 1) | ($nohibernate << 2) | ($nohalt << 3) | ($nohybrid << 4): $halt;
	my ($lla) = (defined $la) ? $la : '<unset>';
	my ($lactive) = (defined $active) ? $active : '<unset>';
	my ($lperiod) = (defined $period) ? $period : '<unset>';
	my $uir = ($unlessisrunning) ? " uir=$unlessisrunning=$foundrunning" : "";
	$onelinelog = "LA=$lla U=$lactive I=$idle_cnt la=$loadavg S=$sleep i=$idletime/$quitidletime c=$cron n=$orign/$n H=$h e=$email c='$cmd' t=$lperiod ia=$in_asuser xi$rxi=$xidle:$Xuser d=$displays$uir g=$has_gpu/'$gpuuse'\n";
	&log($why, $onelinelog, $snaps);
	&cron($why, $onelinelog . $snaps) if $cron;
	&email($email, $why, $onelinelog . $snaps) if $email;
}

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

# Lock a files (to ensure just one instance)

sub lockf {
	my($lockfile, $want) = @_;
	&setop("locking");
	$f_setlk1 = 8; # SunOS 4.1.3
	$f_setlk2 = 6; # SunOS 5.4
	$f_setlk = $f_setlk1;
	$f_wrlck = 2; # Well ?

	$f_setlk = $f_setlk2;
	$f_wrlck = 1;
	$dst = ($want == 2) ? ">$lockfile" : ">>$lockfile";
	unless (open(LOCK, $dst)) {
		die("$O: Can't open $dst: $!");
	}
	print STDERR "# opened $lockfile\n" if $verbose;

	$lockstruct = pack("sslli", $f_wrlck, 0, 0, 0, 0);
	unless (fcntl(LOCK, $f_setlk, $lockstruct)) {
		unless (($! eq "Invalid argument") &&
	     	fcntl(LOCK, ($f_setlk1+$f_setlk2)-$f_setlk, $lockstruct)) {
			print STDERR "# $lockfile already locked\n" if $verbose;
			return 0 unless $want;
			exit(0) if $cron;
        		die("$O: $lockfile already locked ($!) - abandoning\n");
		}
	}
	print STDERR "# locked $lockfile\n" if $verbose;
	&setop("could lock");
	return 1 unless $want;
	seek(LOCK, 0, 0) || warn("$O: seek(0,0) failed: $!\n");
	select((select(LOCK), $| = 1)[0]);
	print LOCK "$$\n";
	print STDERR "#$lockfile has $$\n" if $verbose;
	&setop("locked");
}
#==============================================================================

sub isrunning {
	return 0 unless  $unlessisrunning;
	$foundrunning="0";
	open(PS, "ps ax |") || return '';
	        while (<PS>) {
		$foundrunning .= "+$1" if $unlessisrunning && /^(\d+).*$unlessisrunning/;
		$foundrunning .= "++" if $unlessisrunning && /$unlessisrunning/;
	}
	close(PS);
	return $foundrunning;
}

# How many active users are there?
sub active {
	return 1 if $unlessisrunning && &isrunning;
	my ($active) = 0;	# Number of active users for "do it"
	my ($qactive) = 0;	# Number of active users for "bored with this, quit"
	my ($xsess) = '';	# X related sessions found
	$activeusers = '';
	$Fuser	= 0;
	$Flogin	= 2;
	$Fidle	= 3;
	$min_idle = -1;		# minimum idle time found so far
	my($fullw) = "";
	# $userc = 0;
	print STDERR "# Run: w $wopts\n" if $verbose;
	open(W, "w $wopts |") || return 1;
	while (<W>) {

# USER     TTY      FROM              LOGIN@   IDLE   JCPU   PCPU WHAT
# pb22     pts/0     			20:20   34:22   0.01s  0.01s -bash
# mo114    ttyp0     			9:58am 37:46m  4:56m   ?     -
#	D/H/M/S
# X.Ys	0/0/0/X
# X:Y	0/0/X/Y
# X:Ym	0/X/Y/0
# Xdays	X/0/0/0
		$time = -1;
		@F = split;

		print STDERR "#W# user/$Fuser='$F[$Fuser]' login/$Flogin='$F[$Flogin]': $_" if $verbose;
		$fullw .= $_;

		# Normal "w" rather than "w -uhf"?
		$Flogin++, $Fidle++ if $F[$Fuser] eq "USER" && $F[$Flogin] eq "FROM";

		# USER     TTY      FROM              LOGIN@   IDLE   JCPU   PCPU WHAT
		next if /user /i;
		#  20:08:43 up  3:53,  2 users,  load average: 0.30, 0.42, 0.53

		# ignore some users
		next if /$ignore/; #  && ! $userc++; # why just ignore the first?

		# Look to see if this line is using the X console
		$xsess .= "|$1" if $F[$Flogin] =~ /^:(\d+)/ || $F[$Fidle] =~ /^\?(xdm)\?$/;

		if (/:\d\d[ap]m\s+(\d+)\.(\d+)([s]*)\s+/) {
			if ($3 eq "s") { $time = $1; }
		}
		elsif (/:\d\d[ap]m\s+(\d+):(\d+)([m ])\s+/) {
			if ($3 eq " ") { $time =  ($1 * 60) + $2; }
			else           { $time = (($1 * 60) + $2) * 60; }
		}
		elsif (/:\d\d[ap]m\s+(\d+)days /) {
			$time = $1 * 24 * 60 * 60;
		}
		# The standard format is DDdays, HH:MMm, MM:SS  or SS.CC
		# Otherwise              DDdays, HH:MM,  MM:SSm or SS.CC
		# jul20    tty7            Thu14    4days  1:08m
		#lsif ($F[$Fidle] =~ /^\d+:\d\d$/ || $F[$Fidle] =~ /^[A-Z][a-z][a-z]\d\d$/) ...
		elsif ($F[$Fidle] =~ /^(\d+)days$/) { $time = $1 * 24 * 60 * 60; }
		elsif ($F[$Fidle] =~ /^(\d+)\.(\d+)([s]*)$/) { $time = $1; }
		elsif ($F[$Fidle] =~ /^(\d+):(\d+)([m]*)$/) {
				$m = $3;
				$time =  ($1 * 60) + $2;
				# print STDERR "### $m/$wmraw / $time / $_\n";
				$wmraw = &guess_wmraw($F[1], $time, $m) if $wmraw eq "?";
				$time *= 60 if $m ne $wmraw;
		}
		# USER  TTY FROM  LOGIN@  IDLE   JCPU   PCPU  WHAT
		# $USER :0  -     Fri12   ?xdm?  1:19m  0.00s /bin/sh /usr/bi
		# Set time to a "large" value - check_X will account for it
		elsif ($F[$Fidle] eq "?xdm?") { $time = $quitidletime +2; }
		else { $warns .= "<$_>"; $fullw .= "!!!\n"; print "# Act: skip '$F[$Fidle]': $_"; $active ++;  $activeusers .= " ERR:$_>"; next; }

		if ($min_idle < 0 || $time < $min_idle) {
			$min_idle = $time;
			$minu = $F[$Fuser];
			$mind = $F[1];
		}
		$active  ++, $activeusers .= " $F[$Fuser]" if ($idletime    == 0 && $idletime != 0 && $xsess ne "") || $time <= $idletime;
		$qactive ++, if $quitidletime == 0 || $time <= $quitidletime;
		print STDERR "#a/qa = $active/$qactive for $time <= $idletime <$F[$Flogin]|$F[$Fidle]> ($min_idle) xsess=$xsess, idletime=$idletime\n" if $verbose;
		$fullw .= "#a/qa = $active/$qactive for $time <= $idletime <$F[$Flogin]|$F[$Fidle]> ($min_idle)$xsess\n";
	}
	close(W);
	$xsess = &psxsess if $xsess eq ''; # compiz may not show up console users
	$xsess =~ s/^\|//;
	print STDERR "#active TTYs     $active / $qactive $n at " . time . " (xsess=$xsess gpuuse=$has_gpu/'$gpuuse')\n" if $verbose;
	my($oa) = $active;

	&gpu_idle("active");
	$active  ++, $activeusers .= " gpu:$has_gpu/'$gpuuse'" if $gpuuse ne '';

	&x_idle("[q]active");
	$active  ++, $activeusers .= " X:$Xuser\@:$display" if &check_X($active,  $min_idle, $idletime, $idletime, $xsess, $Xuser, "C1");

	&emailwarn("active=$active$activeusers xidle=$xidle for :$displays gpuuse=$has_gpu/$gpuuse min=$min_idle from\n$fullw") if ($active > 0) && ($xidle >= $hour) && ($min_idle >= $hour);

	$qactive += &check_X($qactive, $min_idle, $idletime, $quitidletime, $xsess, $Xuser, "C2");
	print STDERR "#active TTYs + X + GPU $active / $qactive$activeusers $n at " . time . "\n" if $verbose;

	($active, $n) = ($qactive, 1) if $active && ! $qactive;
	print STDERR "#active returns  $active / $qactive$activeusers $n at " . time . "\n" if $verbose;

	return $active;
}

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

# look for X session using ps
sub psxsess {
	my ($xsess) = '';
	my ($xinst) = '';
	open(PS, "ps axf |") || return '';
	while (<PS>) {
		# 14564 tty7     Ss+    1:54  \_ /usr/bin/X :0 -auth
		$xinst = $1 if /X\s+:([\d+])\s/;
		# 14781 ?        Sl     1:42          \_ compiz
		if (/\scompiz/ && $xinst ne '') {
			$xsess .= "|$xinst";
			$xinst = '';
		}
	}
	close(PS);
	$xsess =~ s/^\|//;
	print STDERR "# psxsess <$xsess>\n" if $verbose;
	return $xsess;
}

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

# return 1 if the X console is active
sub check_X {
	my ($active, $min_idl, $idletime, $longidletime, $xsess, $Xuser, $callid) = @_;
	$Xuser = "<unset>" unless defined $Xuser;
	print STDERR "#### $active active, X sessions $xsess, times $requirexidle: $min_idl < $idletime | $xidle < $longidletime <$Xuser|$ignore> <call=$callid>\n" if $verbose;
	return 0 if $Xuser =~ /$ignore/; # ignore the user of X
	return 0 if $min_idl < 0 && ! $requirexidle; # $min_idl of -1 means "no minimum yet set", so no users, so idle
	return 1 if $idletime == 0;	# If ignoring idletime, it's active!
	return $xidle < $idletime if $xidle >= 0; # Have X info, so use it
	return 0 if $xsess eq '';	# Looks like no X session
	return $min_idl < $longidletime;	# No X info, so use the longer time
}

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

# What is the current LA (sum of three values)
sub la {
	open(W, "/proc/loadavg") || return 1;
	$_ = <W>;
	chomp;
	close(W);
	if ($verbose) {
		my $res = "<unset>";
		$res = $1 + $2 + $3 if /^\s*(\d+\.\d+)\s+(\d+\.\d+)\s+(\d+\.\d+)/;
		chomp;
		print STDERR "#LA: $_ xi: $xidle for :$displays g: $has_gpu:'$gpuuse' <<$_>>\n"
	}
	return $1 + $2 + $3 if /^\s*(\d+\.\d+)\s+(\d+\.\d+)\s+(\d+\.\d+)/;
	return 0;
}

# send warning email
sub emailwarn {
	return if $lastwarn && ((time - $lastwarn) < $hour);
	return unless $email;
	$lastwarn = time;
	&email($email, "isidle WARNING", @_);
}

# send email about what's being done.
sub email {
	&setop("sending email");
	my ($email, $msg, $details) = @_;
	my ($s); ($s = localtime) =~ s/.*\s(\S*:\S*)\s.*/ $1 $ver/;
	my($cmd) = sprintf($mailcmd, " $msg$s", $email);
	warn("$O: email: $cmd\n") if $verbose || $n;
	if (open(EMAIL, $cmd)) {
		print EMAIL $details;
		close(EMAIL) || warn("$O: close of '$cmd' failed: $!\n");
	}  else { warn("$O: open of '$cmd' failed: $!\n"); }
}

# "halt" a machine - try suspend, hibernate and shutdown.
# "-1" means it forked
# "0" means that the maachine managed to hibernate for a period
# "1" means it tried to halt
sub halt {
	&setop("halting");
	warn("$O: halt: try '$prepare', '$suspend_hybrid', '$suspend', '$hibernate' then '$shutdown'\n"); # if $verbose || $n;
	return 1 if $n;
	# Arrange to allow other things to try to run before halting
	return -1 if fork;

	# Prepare
	$att1 = time;
	$rc = system($prepare);
	$att = time - $att1;
	warn("$O: halt: $prepare took $att and gave $rc / $!\n");

	sleep(3);
	($runlevel = `runlevel`) =~ s/.* //;

	unless ($nosuspend || $nohibernate || $nohybrid) {
		# Try suspend
		$att1 = time;
		$! = '(OK)';
		$! = 0;
		$rc = system($suspend_hybrid);
		$att = time - $att1;
		warn("$O: halt: $suspend_hybrid took $att and gave $rc / $!\n");
		&log("halt: $suspend_hybrid took $att and gave $rc / $!\n");
		system("runlevel --set=$runlevel");
		if ($att >= $min_suspend_hybrid_sec) {
			system("runlevel --set=$runlevel");
			system($unhibernate);
			system($anacron);
			system($resumed);
			system($resume_cmd) if $resume_cmd;
			&restart($RESTART_SUSP);
		}
	}

	unless ($nosuspend) {
		# Try suspend
		$att1 = time;
		$! = '(OK)';
		$! = 0;
		$rc = system($suspend);
		$att = time - $att1;
		warn("$O: halt: $suspend took $att and gave $rc / $!\n");
		&log("halt: $suspend took $att and gave $rc / $!\n");
		system("runlevel --set=$runlevel");
		if ($att >= $min_suspend_sec) {
			system("runlevel --set=$runlevel");
			system($unhibernate);
			system($anacron);
			system($resumed);
			system($resume_cmd) if $resume_cmd;
			&restart($RESTART_SUSP);
		}
	}

	unless ($nohibernate) {
		# Try hibernate
		$att1 = time;
		$! = '(OK)';
		$! = 0;
		$rc = system($hibernate);
		$att = time - $att1;
		warn("$O: halt: $hibernate took $att and gave $rc / $!\n");
		&log("halt: $hibernate took $att and gave $rc / $!\n");
		system($unhibernate);
		if ($att >= $min_hibernate_sec) {
			system("runlevel --set=$runlevel");
			system($anacron);
			system($resumed);
			system($resume_cmd) if $resume_cmd;
			&restart($RESTART_HIBE);
		}
	}

	unless ($nohalt) {
		# Try halt
		system("runlevel --set=$runlevel");
		$! = '(OK)';
		$! = 0;
		$rc = system($shutdown);
		warn("$O: halt: '$shutdown' gave $rc / $!\n");
		&log("halt: '$shutdown' gave $rc / $!\n");
		exit(0);
	}
}

sub cron {
	my ($msg, $details) = @_;
	warn("$O: cron:\n$msg $details");
}

# We have hibernated. Restart self
sub restart {
	$SIG{'USR1'} = 'restart';	# ... or something else?
	my ($signal) = @_;
	&setop("restart $signal");
	my ($why) = (! $signal) ? "SIGNAL" :
		($signal eq $RESTART_SUSP) ? "suspend":
		($signal eq $RESTART_HIBE) ? "hibernate":
		($signal eq 'USR1') ? "USR1":
		"($signal)";
	if (($idle_cnt * $sleep) < 60) {
		&email($email, "Too fast: $why", join(" ", $O, @origargv)) if $email;
		die("$0: $why exit as too fast:  @origargv\n");
	}
	&email($email, $why, join(" ", $O, @origargv)) if $email;
	sleep(2);	# allow parent process to delete the lockfile, etc
	exec($O, @origargv);	
}

# Execute a command (unless -n)
sub exec {
	my($cmd) = @_;
	&setop("exec: $cmd");
	warn("$O: exec: $cmd\n") if $verbose || $n;
	return if $n;
	exec($cmd);
}

# Read the configuration files
sub read_configs {
	my ($regexp) = @_;
	&setop("read: $regexp");
	print STDERR "## read_configs($regexp)\n" if $verbose;
	my(@files) = eval "( <$regexp> )" ;
	foreach $file (@files) { &read_config($file); }
}

# Read a configuration files
sub read_config {
	my ($file) = @_;
	&setop("read: $file");
	print STDERR "## read_config(@_)\n" if $verbose && ($verbose > 1);
	return unless open(CF, $file);
	while (<CF>) {
		chomp;
		next if /^\s*#/;
		next if /^\s*$/;
		if (/^\s*([^=\s]+)\s*=\s*([^\s].*)/) {
			($key, $val) = ($1, $2);
			$val =~ s/\s+$//;
			print "#CF: <$key> = <$val>\n" if $verbose;
			$$key = $val;
		} else {
			print STDERR "#RC $file has unexpected: <$_>\n";
		}
	}
	close(CF);
}

# Append a times message to the log file
sub log {
	return unless open(LOG, ">>$logfile");
	$when = "" . localtime;
	my($string) = "$O $when: " . join(' ', @_);
	chomp($string);
	print LOG "$string\n";
	close(LOG);
}

# Guess whether with or without the "m" is "MM:SS" by
# stating the device and seeing how it compares.
# The standard format is DDdays, HH:MMm, MM:SS  or SS.CC
# Otherwise              DDdays, HH:MM,  MM:SSm or SS.CC
sub guess_wmraw {
	my ($dev, $time, $m) = @_;
	my($otherm) = ($m eq "m") ? "" : "m";
	return '?' unless @stat = stat("/dev/$dev");
	$d1 = time - $stat[8];
	$d2 = (time - $stat[8]) * 60;
	$d3 = int ((time - $stat[8]) / 60);
	print STDERR "## guess_wmraw $dev $time '$m' gives $d1 / $d2 / $d3\n" if $verbose;
	return $m if $time == $d1;
	return $otherm if $time == $d3;
	return $otherm if $time == $d2;
	print STDERR "## guess_wmraw $dev $time '$m' FAILS $d1 / $d2 / $d3\n" if $verbose;
	return "?";
}


# Find when the next period to process starts and when it stops.
# 0 means "now"
# -1 means "never"
sub start_stop {
	return (0,0) unless $except;	# Always process
	return (0,0) if $except eq "never";
	return (-1,-1) if $except eq "always";
	$except = "8-18" if $except eq "officehours";
	if ($except =~ /^(\d+)-(\d+)$/ || $except =~ /^(\d+):(\d+)-(\d+):(\d+)$/) {
		if (defined $3) { ($i_start,$i_stop) = ($1 * 60 + $2, $3 * 60 + $4); }
		else { ($i_start,$i_stop) = ($1 * 60,$2 * 60); }
		warn("$O: invalid start hour '$i_start'\n") if $i_start > 24 * 60;
		warn("$O: invalid stop hour '$i_stop'\n") if $i_stop > 24 * 60;
		@when = localtime;
		$startofday = time - ($when[0] + 60 * ($when[1] + 60 * $when[2]));
		# Three cases: now < start; start < now < stop; stop < start
		$i_start = $i_start * 60 + $startofday;
		$i_stop = $i_stop * 60 + $startofday;
		$i_stop += $day if $i_stop < $i_start;
		print STDERR "# Want (" . localtime($i_start) . " | " . localtime($i_stop) . ")\n" if $verbose;
		return (0, $i_start) if $i_start > time;
		return ($i_stop, $i_start + $day) if $i_stop > time;
		return (0, $i_start + $day);
	}
	warn("$O: unrecognised except time '$except' ignored\n");
	return (0,0) unless $except;	# Always process
}

# Display $secs as <int><unit>
sub times {
	my ($secs) = @_;
	return $secs . "s" if $secs < 60;
	return int($secs/60) . "m" if $secs < 60 * 60;
	return int($secs/(60 * 60)) . "h" if $secs < 60 * 60 * 24;
	return int($secs/(60 * 60 *24)) . "d" if $secs < 60 * 60 * 24 * 7;
	return int($secs/(60 * 60 *24 *7)) . "w";
}


# Record the "current operation" in the command line, as seen by "ps".
sub setop {
	my ($arg) = @_;
	@time = localtime;
	$extra = '';
	$extra .= " v=$ver" if $ver;
	$extra .= " d=$displays" if defined $displays;
	$extra .= " W='$warns'" if $warns;
	$extra .= " m=" . (($nosuspend << 2) | ($nohibernate << 1) | $nohalt) if $nosuspend || $nohibernate || $nohalt;
	$extra .= " uir=$unlessisrunning=$foundrunning" if $unlessisrunning;
	$extra = " :$extra" if $extra;
	$0 = sprintf("%s %s \@%02d:%02d:%02d u=%d la=%4.2f i:%s%s l=%s/%s '%s' xi%s=%s:%s g=%d:'%s'%s", $name, &times(time - $start_time), $time[2], $time[1], $time[0], $active, $la, &times($min_idle), $who, $idle, $idle_cnt, $arg, $rxi, $xidle, $Xuser, $has_gpu, $gpuuse, $extra);
	$warns = "";
	return 1;
}

sub gpuuse_cmd {
	print STDERR "# in gpuuse_cmd using $gpuuse_cmd: $has_gpu\n" if $verbose;
	my $res = `$gpuuse_cmd`;
	chomp($res);
	# Failed to initialize NVML: Driver/library version mismatch
	return $res if $res =~ /Failed to initialize NVML/;
	return $res if $res =~ /Driver\/library version mismatch/;
	# # gpu        pid  type    sm   mem   enc   dec   command
	# # Idx          #   C/G     %     %     %     %   name
	#     0       1001     G     -     -     -     -   Xorg           
	#     0       1001     G     -     -     -     -   gnome-shell
	#     0      17490     C     -     -     -     -   python   
	#     0          -     -     -     -     -     -   -
	#
	# Filter out comments (#), null lines (lots of "-"s if nothing)
	# and X server (etc)
	#
	print STDERR $res if $verbose;
	@res = split(/\n/, $res);
	$res = "";
	foreach $_ (@res) {
	 	s/\s+$//;
		my ($g, $p, $type, $sm, $mem, $enc, $dec, $command) = split;
		$g, $p, $type, $sm, $mem, $enc, $dec, $command if 0;
		print "<<$g, $p, $type, $sm, $mem, $enc, $dec, $command>>\n" if $verbose;
		next if $g =~ "^#";
		next if $type eq "-" && $command eq "-";
		next if $command =~ "^Xorg" && $type =~ /G/;
		next if $command =~ "^gnome-shell" && $type =~ /G/;
		next if $command =~ "^firefox" && $type =~ /G/;
		next if $command =~ "^TeamViewer" && $type =~ /G/;
		next if $command =~ "^xfwm4" && $type =~ /G/;
		# G_atom G_Code  ?
		$res .= " ${type}_$command";
	}
	$res =~ s/^\s*//;
	print STDERR "# gpuuse_cmd return '$res'\n" if $verbose;
	return $res;
}

# run $xidle_cmd for all active displays
# "expensive" so only run when the LA has been "idle" for all the loops.
sub xidle_cmd {
	&find_Xservers;
	print STDERR "# in xidle_cmd using $xidle_cmd: $#display\n" if $verbose;
	my($res, $err) = ('', '');
	my($min_X_idle) = 99999999999;
	$display = -1;
	foreach $d (0 .. $#display) {
		my $cmd = (defined($display_user[$d])) ? "sudo -u \\#$display_user[$d] DISPLAY=:$display[$d].0 $timeoutcmd $xidle_cmd 2>&1": 
				($greeter[$d]) ? "echo $GREATER" :
				(defined($xauthority[$d])) ? "XAUTHORITY=$xauthority[$d] DISPLAY=:$display[$d].0 $timeoutcmd $xidle_cmd 2>&1" :
				"DISPLAY=:$display[$d].0 $timeoutcmd $xidle_cmd 2>&1";
		my($out) .= `$cmd`;
		chomp ($out);
		# print STDERR "# ... <$out> <$res> <$err> ($min_X_idle, $display)\n";
		if ($out =~ /^\d+$/) { $res .= "|$out"; ($min_X_idle, $display) = ($out, $d) if $out < $min_X_idle }
		else { $err .= "|$out" }
		printf STDERR "# #%d %-7s %-7s (%s for %4s) gave: %-21s (%02d) <%s,%s> from <$cmd>\n", $d, $greeter[$d] || "<Unset>", $X_user[$d] || $display_user[$d] || "<Unset>", $xauthority[$d] || "<Unset>", $display[$d], $out, $display, $res,$err if $verbose;
	}

	if ($min_idle < 0 || $min_X_idle < $min_idle) {
		$min_idle = $min_X_idle;
		$minu = $X_user[$display];
		$mind = $display[$display];
	}
	$Xuser = $X_user[$display];
	$display = $display[$display];

	$res =~ s/^\|//;
	$err =~ s/^\|//;
	$displays = $display;
	foreach $d (0 .. $#display) {
		$displays .= "|$display[$d]" unless $display[$d] eq $display;
	}
	return ($res ne "") ?  "$min_X_idle\n$res" : $err || "No results for $#display";
}

sub find_Xservers {
	@display=();
	my $lock = &find_Xservers_lock;
	return $lock if $lock > 0;
	return &find_Xservers_ps;
}
sub find_Xservers_lock {
	print STDERR "# find_Xservers_lock\n" if $verbose;
	my($ndisplays) = 0;
	foreach $file (</tmp/.X*-lock>) {
		my ($n) = $1 if $file =~ m=^/tmp/.X(\d+)-lock$=;
		print STDERR "# Try $n: <$file>\n" if $verbose;
		if (defined($n)) {
			$display[$ndisplays] = $n;
			my (@stat) = stat($file);
			print STDERR "# $n:$stat[4]: <$file>\n" if $verbose;
			$display_user[$ndisplays++] = $stat[4] if $stat[4];
		}
	}
	print STDERR "# find_Xservers_lock: ndisplays=$ndisplays / $#display\n" if $verbose;
	return $ndisplays;
}

sub find_Xservers_ps {
	print STDERR "# find_Xservers_ps\n" if $verbose;
	my($was) = $!;
	chomp(@ps = `ps axuwwwwwf`);
	$! = $was;
	my($ndisplays) = 0;
	my($looking) = 0;
	for my $ps (@ps) {
	    if ($ps =~ /X.*auth/) {
		# /usr/bin/X :0 -auth /var/run/lightdm/root/:0 -nolisten tcp vt7 -novtswitch -background none
	    	print STDERR "# starting     : $ps\n" if $verbose;
		if ($ps =~ s/X(org|) (|.* ):(\d+) .*-auth (\S+)// && -e $4) {
			$xauthority[$ndisplays] = $4;
			$display[$ndisplays] = $3;
			$looking = 1;
			print STDERR "# ps $ndisplays: xauthority=$xauthority[$ndisplays] for display=$display[$ndisplays] start looking for a user ...\n" if $verbose;
			$ndisplays++;
			$X_user[$ndisplays -1] = "<none>";
			$greeter[$ndisplays -1] = 0;
		} elsif ($verbose) {
			 printf STDERR "# ps No match in '%s' -- '%s'\n", $ps, $4 || "<UnSet>";
		}
	    # root      ...  \_ /usr/bin/X :0 -auth /var/run/lightdm/root/:0 -nolisten tcp vt7 -novtswitch -background none
	    # root      ...  \_ lightdm --session-child 16 19
	    # lightdm   ...  |   \_ /bin/sh /usr/lib/lightdm/lightdm-greeter-session /usr/sbin/unity-greeter
	    # lightdm   ...  |       \_ /usr/sbin/unity-greeter
	    } elsif (($looking > 0) && ($ps =~ /unity-greeter/)) {
	    	print STDERR "# found greeter: $ps\n" if $verbose;
	    	$looking = 0;
		$greeter[$ndisplays -1] = 1;
	    } elsif (($looking > 0) && ($ps !~ /lightdm/)) {
	    	if ($ps =~ /$ignore/) {
			print STDERR "# ignore, so $ndisplays / $#display" if $verbose;
			pop @display;
			$ndisplays--;
			print STDERR " -> $ndisplays / $#display\n" if $verbose;
		} else {
			$X_user[$ndisplays -1] = $1 if $ps =~ /^(\S+)/;
		}
	    	print STDERR "# stop  looking: $X_user[$ndisplays -1]: $ps\n" if $verbose;
	    	$looking = 0;
	    } elsif ($looking) {
	    	print STDERR "# still looking: $ps\n" if $verbose;
	    }

	}
	print STDERR "# find_Xservers_ps: ndisplays=$ndisplays / $#display\n" if $verbose;
	return $ndisplays;
}

# set $xidle to the number of seconds which the X console has been idle (or -6)
sub x_idle {
	my($why) = @_;
	$why = '<UNSET>' unless defined $why;
	print STDERR "## in x_idle($why): '$xidle_cmd' x=$xidle\n" if $verbose;

	# get the current value
	my ($res) = &xidle_cmd;
	chomp $res if defined $res;

	# if it looks plausible, return the numbert of seconds (not ms)
	if (defined($res) && (($res =~ /^(\d+)$/) || ($res =~ /^(\d+)\n\d+/))) {
		$xidle = int(($1 + 999) / 1000);
	} else { $xidle = -6; }
}

# Return -1 for "unknown", otherwise the number of seconds which the X console has been idle.
sub gpu_idle {
	my($why) = @_;
	$why = '<UNSET>' unless defined $why;
	print STDERR "## in gpu_idle($why) has_gpu=$has_gpu gpuuse='$gpuuse'\n" if $verbose;
	$gpuuse = "", return if $has_gpu < 1;

	# get the current value
	my ($res) = &gpuuse_cmd;
	chomp $res if defined $res;

	# if it looks plausible, return the numbert of seconds (not ms)
	if (defined($res)) {
		$gpuuse = $res;
	} else { $gpuuse = ""; }
}
