#!/usr/bin/perl -T ######################################################################### # # # Display System Up-Time Records Verbosely # # Copyright (C) 2000-08, John Zaitseff # # # ######################################################################### # Author: John Zaitseff # Date: 2nd June, 2008. # Version: 2.3 # This script file displays the system's up-time from records kept by # ud(1) and uptime(1). It does this in rather verbose English prose. ######################################################################### # Compilation information use POSIX qw(uname floor); use Text::Wrap qw(wrap fill $columns); use Time::Local qw(timelocal); ######################################################################### # Configuration variables $debverfile = "/etc/debian_version"; # Debian version file $lsbverfile = "/etc/lsb-release"; # LSB version file $uptimefile = "/proc/uptime"; # Machine up-time $udrecfile = "/var/lib/misc/uptime.record"; # Previous up-times $columns = 78; # Number of columns in the output $showWMY = 0; # 0 = show only DHMS, 1 = show YMWDHMS # for actual up-times ######################################################################### # Global constants and variables @monthstrs = (January, February, March, April, May, June, July, August, September, October, November, December); %shmonh = (JAN => 0, FEB => 1, MAR => 2, APR => 3, MAY => 4, JUN => 5, JUL => 6, AUG => 7, SEP => 8, OCT => 9, NOV => 10, DEC => 11); our @uptime; # List of up-times. 0 = current our @starttime; # List of start times (UNIX format) our @endtime; # List of end times (UNIX format) our @osname; # List of Operating System names our @osrelease; # List of Operating System releases our $idleprop; # Proportion of idle time as a fraction our $isdebian; # True if running Debian GNU/Linux our $haslsbinfo; # True if LSB information available our $lsbinfo; # Linux Standard Base DISTRIB_ID; our $prevavail; # True if previous up-times available ######################################################################### # Convert a length of time (in seconds) to string (in grammatical English) sub TimeLengthString ($$) { my $time = $_[0]; # Time in seconds my $uptodays = $_[1]; # True to calculate only up to days my ($secs, $mins, $hours, $days, $weeks, $months, $years); my (@ustr, $index); # Calculate sub-times $mins = floor($time / 60); $secs = floor($time - $mins * 60); $hours = floor($mins / 60); $mins = $mins - $hours * 60; $days = floor($hours / 24); $hours = $hours - $days * 24; if ($uptodays) { $weeks = 0; $months = 0; $years = 0; } else { $weeks = floor($days / 7); $days = $days - $weeks * 7; $months = floor($weeks / 4); $weeks = $weeks - $months * 4; $years = floor($months / 12); $months = $months - $years * 12; } # Convert it into English $index = 0; if ($years == 1) { $ustr[$index++] = "1 year"; } elsif ($years != 0) { $ustr[$index++] = "$years years"; } if ($months == 1) { $ustr[$index++] = "1 month"; } elsif ($months != 0) { $ustr[$index++] = "$months months"; } if ($weeks == 1) { $ustr[$index++] = "1 week"; } elsif ($weeks != 0) { $ustr[$index++] = "$weeks weeks"; } if ($days == 1) { $ustr[$index++] = "1 day"; } elsif ($days != 0) { $ustr[$index++] = "$days days"; } if ($hours == 1) { $ustr[$index++] = "1 hour"; } elsif ($hours != 0) { $ustr[$index++] = "$hours hours"; } if ($mins == 1) { $ustr[$index++] = "1 minute"; } elsif ($mins != 0) { $ustr[$index++] = "$mins minutes"; } if ($secs == 1) { $ustr[$index++] = "1 second"; } elsif ($secs != 0) { $ustr[$index++] = "$secs seconds"; } # Place the correct associatives (commas and conjunction) if ($index == 1) { return $ustr[0]; } elsif ($index == 2) { return $ustr[0] . " and " . $ustr[1]; } else { return join(', ', @ustr[0..$index-2]) . " and " . $ustr[$index-1]; } } ######################################################################### # Return an ordinalised string (1st, 2nd, 3rd, 4th, etc) from a cardinal sub Ordinalise ($) { my $num = $_[0]; # Cardinal number greater than zero my $num10 = $num % 10; my $num100 = $num % 100; if (($num10 == 1) && ($num100 != 11)) { return $num . "st"; } elsif (($num10 == 2) && ($num100 != 12)) { return $num . "nd"; } elsif (($num10 == 3) && ($num100 != 13)) { return $num . "rd"; } else { return $num . "th"; } } ######################################################################### # Convert a UNIX date (in seconds) to a string (in grammatical English) # Return the date in the form "7th December, 2000" sub DateString ($) { my $date = $_[0]; # Date/time in seconds since Epoch my ($secs, $mins, $hours, $day, $month, $year, $wday, $yday, $isdst); # Calculate the date and time in the local time zone ($secs, $mins, $hours, $day, $month, $year, $wday, $yday, $isdst) = localtime($date); # Convert it into English. We could have used strftime(3), except # that it is locale-specific. return Ordinalise($day) . " " . $monthstrs[$month] . ", " . ($year+1900); } ######################################################################### # Convert the difference between two UNIX dates to an English string # of the form "from 4th October to 7th December, 2000", "from 4th to # 7th December, 2000" or "on 7th December, 2000". sub DateToDateString ($$) { my $date1 = $_[0]; # Starting date in seconds from Epoch my $date2 = $_[1]; # Ending date in seconds from Epoch my ($secs1, $mins1, $hours1, $day1, $month1, $year1); my ($secs2, $mins2, $hours2, $day2, $month2, $year2); # Calculate dates in local time zone, swapping the dates if $date1 is # later than $date2 if ($date1 > $date2) { ($date1, $date2) = ($date2, $date1); } ($secs1, $mins1, $hours1, $day1, $month1, $year1) = localtime($date1); ($secs2, $mins2, $hours2, $day2, $month2, $year2) = localtime($date2); # Calculate difference between dates using the least number of words if (($year1 == $year2) && ($month1 == $month2) && ($day1 == $day2)) { return "on " . Ordinalise($day1) . " " . $monthstrs[$month1] . ", " . ($year1 + 1900); } elsif (($year1 == $year2) && ($month1 == $month2)) { return "from " . Ordinalise($day1) . " to " . Ordinalise($day2) . " " . $monthstrs[$month1] . ", " . ($year1 + 1900); } elsif ($year1 == $year2) { return "from " . Ordinalise($day1) . " " . $monthstrs[$month1] . " to " . Ordinalise($day2) . " " . $monthstrs[$month2] . ", " . ($year1 + 1900); } else { return "from " . Ordinalise($day1) . " " . $monthstrs[$month1] . ", " . ($year1 + 1900) . " to " . Ordinalise($day2) . " " . $monthstrs[$month2] . ", " . ($year2 + 1900); } } ######################################################################### # Read information from the LSB distribution info file sub ReadLSBInfo() { $isdebian = (-r $debverfile); $haslsbinfo = (-r $lsbverfile); $lsbinfo = ""; if ($haslsbinfo) { open(LSBINFO, $lsbverfile) || die "Could not open $lsbverfile: $!\n"; while () { chomp; /^DISTRIB_ID=(.*)$/ && { $lsbinfo = $1 }; } close(LSBINFO); } } ######################################################################### # Read the current system information (up-time, dates, O/S) sub ReadCurrentInfo () { my ($data, $idletime); # Discover what operating system and version we are running ($osname[0], my $nn, $osrelease[0], my $v, my $m) = uname(); # Read the current system up-time from the relevant dynamic file, # usually /proc/uptime. This file contains two floating point # numbers: the number of seconds that the system has been up and # running, and the number of idle seconds. open(UPTIME, $uptimefile) || die "Could not open $uptimefile: $!\n"; chomp($data = ); ($uptime[0], $idletime) = split(' ', $data); close(UPTIME); # Calculate the starting and ending times $endtime[0] = time; $starttime[0] = $endtime[0] - $uptime[0]; # Calculate the idle time as a fraction $idleprop = $idletime / $uptime[0]; $idleprop = 1.00 if $idleprop > 1.00; $idleprop = 0.00 if $idleprop < 0.00; } ######################################################################### # Process data from ud(1) into usable data (internal to ReadPreviousInfo) sub udProcess ($$) { my $idx = $_[0]; # Index into various arrays (1..3) my $datastr = $_[1]; # Data string from ud(1) record my @data; my $line = $idx * 2 + 1; my ($sec, $min, $hour, $day, $month, $year); my $date; # Split the data string into individual words. Exit if the data # string is null @data = split(' ', $datastr, 9); if (! defined($data[0])) { return; } # Sanity check of the data, just in case... if (($data[0] ne "running") || ($data[3] ne "ended")) { die "Unknown format in $udrecfile on line $line.\n"; } # The data string (read from the ud(1) record file) has the format # "running Linux 2.2.17, ended Thu Dec 7 10:00:00 2000". This # has been split up into @data[0..8]. The comma at the end of # $data[2] needs to be removed. $osname[$idx] = $data[1]; chop($osrelease[$idx] = $data[2]); # The following code assumes ud(1)'s data is in English $month = $shmonh{uc($data[5])}; if (! defined($month)) { die "Could not determine month in $udrecfile on line $line: $data[5]\n"; } $day = $data[6]; ($hour, $min, $sec) = split(':', $data[7], 3); $year = $data[8]; # Some more sanity checking, just in case... And yes, you can have # 60 seconds (remember leap seconds) if (($year < 1900) || ($day < 1) || ($day > 31) || ($hour < 0) || ($hour > 23) || ($min < 0) || ($min > 59) || ($sec < 0) || ($sec > 60)) { die "Illegal time values in $udrecfile on line $line.\n"; } # Convert the date and time values to a UNIX date $date = timelocal($sec, $min, $hour, $day, $month, $year); # Calculate the starting and ending dates $endtime[$idx] = $date; $starttime[$idx] = $date - $uptime[$idx]; } ######################################################################### # Read previous system information from ud(1) sub ReadPreviousInfo () { my ($udver, @data); # Check for the record file written by ud(1) and extract the # information contained therein $prevavail = (-r $udrecfile); if ($prevavail) { open(UDREC, $udrecfile) || die "Could not open $udrecfile: $!\n"; chomp($udver = ); if ($udver !~ /^0\.7\./) { die "Expecting version 0.7.x for ud(1), not $udver.\n"; } # Retrieve the longest, 2nd longest and 3rd longest up-times # Note that ud(1) stores these in sorted order already chomp($uptime[1] = ); chomp($data[1] = ); chomp($uptime[2] = ); chomp($data[2] = ); chomp($uptime[3] = ); chomp($data[3] = ); close(UDREC); # If the longest up-time is zero, no previous up-times are available if ($uptime[1] == 0) { $prevavail = 0; goto rpi_exit; } # Process the data in @data into usable values udProcess(1, $data[1]); udProcess(2, $data[2]); udProcess(3, $data[3]); } rpi_exit: } ######################################################################### # Main program { my $str; ####################################### # Read in the information that we need ReadLSBInfo(); ReadCurrentInfo(); ReadPreviousInfo(); ########################################### # Display the information in English prose $str = "This "; if ($haslsbinfo) { $str .= "$lsbinfo "; } elsif ($isdebian) { $str .= "Debian GNU/$osname[0] "; } $str .= "system (running $osname[0] $osrelease[0]) has been up for "; $str .= "a total of " . TimeLengthString($uptime[0], ! $showWMY) . " "; $str .= DateToDateString($starttime[0], $endtime[0]) . "."; $str .= " During this time, the system has been idle for "; $str .= sprintf("%4.2f", $idleprop * 100) . "% of the time."; if ($prevavail) { if ($uptime[0] >= $uptime[1]) { $str .= " This is the longest time the system has been "; $str .= "running without a reboot."; } else { $str .= " The longest this system has run without a reboot is "; $str .= TimeLengthString($uptime[1], ! $showWMY); if (($osname[0] ne $osname[1]) || ($osrelease[0] ne $osrelease[1])) { $str .= ", running $osname[1] $osrelease[1],"; } $str .= " " . DateToDateString($starttime[1], $endtime[1]); $str .= "."; } } $str .= "\n"; print wrap("", "", $str); # This version does not use the information that is potentially in # records 2 and 3 of @uptime, etc. }