#!/usr/bin/env perl # # chronosquirrel # This is a command-line interface for task-based time tracking. # This script will manage entries in a kronlog file. # # Functionality: # - start timing a task # - stop timing a task # - switch from timing one task to timing another task # - summarize today's tasks # - summarize this week's tasks # - show current timing status # - list valid tasks # - list task aliases # - list krons (in various formats) # - configuration data stored in .chronosquirrelrc # - active display of current timing status # - select day/week to display timing info # - select date range and task to display timing info # - health checks on config and kronlog files # - tasks may be active or inactive # - krons may be inserted, deleted, or modified # - kron logs may be rolled # - user-specified annotations may be added # # usage: # chronosquirrel [-... | -help | -Version] [taskname] # # timelocal() is the magic routine that converts a time array into # an epoch time. # # Revision History # 1. Basic functionality. # 1.0 Initial revision. 180329 # 1.1 Added selection of day or week to display. 180411 # Turned off automatic display of unused tasks. # Added periodic automatic display of status. # Added us-location in config file. # 1.2 Added selection of date or date range for -listkrons. 180413 # Centralized stripping of comments and blanks from kronlog. # 1.3 Added health checks for the config and kronlog files. 180422 # Modified stop lines in the kronlog to use HH:MM:SS # for time-diff field, rather than difference in epoch times. # 1.4 Added active/inactive status for tasks. 180424 # # 2. Editting capabilities. # 2.0 Added -insertkron. 180522 # 2.1 Added -deletekron. 180531 # Added licensing info. # 2.2 Added -changekron. 180705 # 2.3 Fixed bug in status display. 180718 # 2.4 Check for krons crossing midnight. 180825 # Prevent krons from starting after one with a later date. # 2.5 Moved most of the pod into a separate User's Guide. 180829 # 2.6 Added -today recognition to -list. 180909 # Added -weekly recognition to -list. # Added -day as a synonym for -today and -daily. # 2.6.1 Moved all kronlog appends into new appendkron(). 180927 # 2.7 Added -roll for log rolling. 181114 # 2.8 Added -annotate. 181211 # 2.9 Added -lastweek. 190914 # # # ChronoSquirrel.app, a GUI version of this program for Mac OS X, # was written in 2008. For a number of reasons, it has evolved into # this command-line version. The concepts and some implementation # details are very similar between the two, but the interface and # other details are different. # # Copyright 2018 Wayne Morrison # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # use strict; use Time::Local; use Getopt::Long qw(:config no_ignore_case_always); # # Version information. # my $NAME = "chronosquirrel"; my $VERS = "$NAME version: 2.9"; ############################################################################ # # Options fields. # my %opts = (); # Options. # # Command line arguments. # my @opts = ( 'aliases', # List task aliases. 'endkron', # Close the current kron. 'listkrons', # List krons in kron log. 'tasks', # List tasks. 'watch|watcher|clock', # Periodically display status. 'insertkron', # Insert a kron into kronlog. 'deletekron', # Delete a kron from kronlog. 'changekron', # Change a kron in kronlog. 'today|daily|day', # List krons from today. 'week|weekly', # List krons from this week. 'lastweek', # List krons from last week. 'select=s', # Selection criterion. 'raw', # Raw-mode output. 'rare', # Rare-mode output. 'cooked', # Cooked-mode output. 'all', # Display all tasks. 'seconds=s', # Use seconds in time display. 'config=s', # Configuration file. 'kronlog=s', # Kronlog file. 'roll=s', # Roll the kron log. 'annotate', # Annotate the kron log. 'health', # Validate config and kronlog. 'fixit', # Fix kronlog problems. 'verbose', # Give verbose output. 'help', # Give a help message. 'Version', # Display the program version. ); my $verbose = 0; # Verbose flag. my $aliasflag = 0; # -aliases flag. my $allflag = 0; # -all flag. my $fixit = 0; # -fixit flag. my $endflag = 0; # -endkron flag. my $listflag = 0; # -listkrons flag. my $rollmethod = ''; # -roll argument. my $annoflag = 0; # -annotate argument. my $tasksflag = 0; # -tasks flag. my $watchflag = 0; # -watch flag. my $changeflag = 0; # -changekron flag. my $deleteflag = 0; # -deletekron flag. my $insertflag = 0; # -insertkron flag. my $todayflag = 0; # -today flag. my $weekflag = 0; # -week flag. my $lastweekflag = 0; # -lastweek flag. my $select = undef; # -select argument. # # Output-processing modes. The actual mode is settable via command-line # options. # my $COOKED = 1; # Cooked and nicely processed. my $RARE = 2; # Partially processed. my $RAW = 3; # Unprocessed. my $mode = $COOKED; # Output mode. # # Flag for using or not using the seconds in a time display. # Values for this flag: # # -1 Let the program decide what to do. # 0 Never show seconds. # 1 Always show seconds. # my $SECS_DEF = -1; my $SECS_OFF = 0; my $SECS_ON = 1; my %secsvals = ( 'def' => $SECS_DEF, 'off' => $SECS_OFF, 'on' => $SECS_ON, ); my $secsflag = $SECS_DEF; my $usloc = 1; # Flag for in/out of USA. my $debug = 0; # Debug flag. ############################################################################ my $CONFIG = "~/.chronosquirrelrc"; # Config file for chronosquirrel. my $configfile = $CONFIG; # Path to config file. my $KRONLOG = "~/.kronlog"; # Logfile for recording krons. my $kronlogfile = $KRONLOG; # Path to kronlog file. my $task = ''; # Name of task to record. ############################################################################ # # Task information. # # # Dictionaries of recognized tasks and their attributes. # The key is an alias for the actual taskname that will be recorded. # my %tasknames = (); # Names of defined tasks. my %billables = (); # Billable status of tasks. my %ordernums = (); # Ordering numbers of tasks. my %aliases = (); # Task-alias list. my %actives = (); # Active-task list. # # Tasknames, sorted by billable, ordering number, and then alphabetically. # my @sortedtasks = (); # # List of billable/nonbillable values. A task's value in %billables is # the task's index into this list. # my @billable = ( 'nonbillable', 'billable', ); # # Reserved task names. # my $OFFCLOCK = "Off Clock"; # # Flags for tasks' active status. # my $ACTIVE = 1; my $INACTIVE = 0; ############################################################################ # # Constants for parts of a time array. # my $SEC = 0; my $MIN = 1; my $HR = 2; my $DD = 3; my $MM = 4; my $YY = 5; my $WDAY = 6; my $YDAY = 7; ############################################################################ # # State table for kron entries. This is merely a fast way to flip between # the two states. # my %nextstate = ( 'start' => 'end', 'end' => 'start', ); my @months = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec ); my @days = qw( Sun Mon Tue Wed Thu Fri Sat ); my $prevday; # Previous entry's day number. my $prevmon; # Previous entry's month number. my $prevyear; # Previous entry's year number. my $DAYSECS = 86400; # Number of seconds in a day. This rarely changes. my $maxtasklen = -1; # Longest taskname length. ############################################################################ main(); exit(0); #----------------------------------------------------------------------------- # Routine: main() # sub main { $| = 1; # # Munch on the options and arguments. # optsandargs(); # # If -aliases was given, print a list of the tasks and their # aliases, and then exit. # if($aliasflag) { showaliases(); exit(0); } # # If -endkron was given, close the active kron block. # if($endflag) { closekron(1); exit(0); } # # If -tasks was given, print a list of the tasks and exit. # if($tasksflag) { showtasks(); exit(0); } # # If -insertkron was given, we'll insert a new kron in the kron log. # if($insertflag) { insertkron(); exit(0); } # # If -deletekron was given, we'll delete a kron from the kron log. # if($deleteflag) { deletekron(); exit(0); } # # If -changekron was given, we'll change a kron in the kron log. # if($changeflag) { changekron(); exit(0); } # # If -listkrons was given, we'll display all the krons in # the kron log. # if($listflag) { listkrons(); exit(0); } # # Roll the log file. # if($rollmethod ne '') { roller(); exit(0); } # # If -today or -week was given, we'll display all the krons in # the kron log that were generated today. Or this week. # if($todayflag || $weekflag) { summary(); exit(0); } # # Annotate the log file. # if($annoflag) { annotate(); exit(0); } # # Periodically display the current status. # if($watchflag) { watcher(); exit(0); } # # If no taskname was given, we'll give the current state. # if($task eq '') { kronstat(1); exit(0); } # # Add an entry to the kron log. # addkron(); } #---------------------------------------------------------------------- # Routine: optsandargs() # # Purpose: Parse the command line for options and arguments. # sub optsandargs { # # Parse the options. # GetOptions(\%opts,@opts) || usage(); # # Check for some immediate-action options. # usage() if(defined($opts{'help'})); version() if(defined($opts{'Version'})); $verbose = $opts{'verbose'}; $aliasflag = $opts{'aliases'}; $allflag = $opts{'all'}; $endflag = $opts{'endkron'}; $fixit = $opts{'fixit'}; $insertflag = $opts{'insertkron'}; $deleteflag = $opts{'deletekron'}; $changeflag = $opts{'changekron'}; $listflag = $opts{'listkrons'}; $rollmethod = $opts{'roll'}; $annoflag = $opts{'annotate'}; $tasksflag = $opts{'tasks'}; $watchflag = $opts{'watch'}; $todayflag = $opts{'today'}; $weekflag = $opts{'week'}; $lastweekflag = $opts{'lastweek'}; $select = $opts{'select'} if(defined($opts{'select'})); # # Ensure that one one of -today and -week was given. # if(($todayflag + $weekflag) > 1) { print STDERR "-today and -week are mutually exclusive\n"; exit(2); } # # Get the config file to use, and then expand it to its path. # $configfile = defined($opts{'config'}) ? $opts{'config'} : $CONFIG; $configfile = glob($configfile); # # Read the config file. # readconfig($configfile); # # Build the sort list for our defined tasks. # @sortedtasks = tasksort(); # # Get the kronlog file to use, and then expand it to its path. # If it isn't defined, it's either the default or a kronlog set # in the config file. # if(defined($opts{'kronlog'})) { $kronlogfile = $opts{'kronlog'}; newkronlog() if(! -e $kronlogfile); } else { $kronlogfile = glob($kronlogfile); } # # Run validation checks. # if(defined($opts{'health'}) || (defined($opts{'fixit'}))) { checkup(); exit(0); } # # Check for the -seconds and -noseconds options. Only one may # be used, and we'll set $secsflag accordingly. # if(defined($opts{'seconds'})) { $secsflag = $opts{'seconds'}; if(! defined($secsvals{lc($secsflag)})) { print STDERR "bad value given for -seconds \"$secsflag\"\n"; exit(4); } $secsflag = $secsvals{lc($secsflag)}; } # # If -lastweek was given, build a -select argument for seven days ago. # The -lastweek and -select arguments cannot be used together. # if($lastweekflag) { my $curtime; # Current time, in seconds. my $lwksecs; # Seven days ago, in seconds. my @tempus; # Time atoms. if($select != undef) { print STDERR "-select and -lastweek may not be used together\n"; exit(4); } # # Get the seconds count. # $curtime = time(); $lwksecs = $curtime - ((24 * 60 * 60) * 7); # # Get the time atoms. # @tempus = localtime($lwksecs); # # Build the time string. # $select = sprintf("%d/%d/%d", ($tempus[4] + 1), $tempus[3], ($tempus[5] - 100)); } # # Set up the output mode. # $mode = $RAW if(defined($opts{'raw'})); $mode = $RARE if(defined($opts{'rare'})); $mode = $COOKED if(defined($opts{'cooked'})); # # Check for a valid taskname on the command line. # We won't check this for a few commands that don't need a taskname. # if((@ARGV > 0) && ($annoflag != 1)) { my $tname; # Task name or alias. my $valid = 0; # Valid-task flag. $tname = $ARGV[0]; # # Tasknames cannot contain "Off Clock". # if($tname =~ /$OFFCLOCK/i) { print STDERR "user tasks cannot contain \"$OFFCLOCK\"\n"; exit(1); } # # Search the tasknames list for a (caseless) match to that # specified on the command line. # foreach my $tn (keys(%tasknames)) { # # If we find a match, we'll make sure the proper # case is used. # if($tname =~ /^$tn$/i) { $task = $tn; $valid = 1; last; } } # # Ensure the task from the command line is a valid alias name. # if(($task = cvtalias($tname)) ne '') { $valid = 1; } # # Give an error and exit if an unrecognized task was given. # if(! $valid) { print "invalid task: \"$tname\"\n"; exit(1); } } # # Calculate the length of the longest taskname. # foreach my $tn (sort(keys(%tasknames))) { my $tnlen; next if($actives{$tn} == $INACTIVE); $tnlen = length($tn); if($tnlen > $maxtasklen) { $maxtasklen = $tnlen; } } } #---------------------------------------------------------------------- # Routine: readconfig() # # Purpose: Read the config file. This must be done in the midst # of handling options and arguments, not before or after. # # The following config commands are handled: # - task define a task # - alias define a set of taskname aliases # - log set the kronlog file # - seconds choose how to display seconds # - us-location location of user # sub readconfig { my $cfile = shift; # Config file to read. my @lines; # Lines in config file. my %lctasks = (); # Lowercase tasknames. my $errs = 0; # Error count. # # Ensure that the config file exists. # if(! -e $cfile) { print STDERR "chronosquirrel config file \"$cfile\" does not exist\n"; exit(30) } # # Ensure that the config file is readable. # if(! -r $cfile) { print STDERR "chronosquirrel config file \"$cfile\" is not readable\n"; exit(31) } # # Ensure that the config file is not empty. # if(-z $cfile) { print STDERR "chronosquirrel config file \"$cfile\" is empty\n"; exit(33) } # # Read the configuration file. # open(CONF, "< $cfile"); @lines = ; close(CONF); # # Reset our task data. # %tasknames = (); %billables = (); %ordernums = (); %aliases = (); # # Handle each entry in the config file. # foreach my $entry (@lines) { my $cmd; # Command from entry. my $args; # Arguments to command. my $taskname; # Entry's taskname. # # Lop off leading and trailing whitespace. # $entry =~ s/^\s*//g; $entry =~ s/\s*$//g; # # Skip comments and empty lines. # next if(($entry =~ /^\#/) || ($entry eq '')); # # Get the command from the line. # $entry =~ /^(\S+)\s*(.*)$/; $cmd = $1; $args = $2; if($cmd eq 'task') { my $taskname; # Name of new task. my $lctn; # Lowercase taskname. my $active; # Active flag. my $billable; # Billable flag. my $ordernum; # Ordering number. # # Break the task line into its elements. # if(($args =~ /^"(.+)"\s+(\S+)\s+(\S+)\s+(\S+)$/) == 0) { print STDERR "$cfile: invalid config line:\n\t$entry\n"; $errs++; next; } # # Pull the pieces into their own variables. # $taskname = $1; $billable = lc($2); $active = lc($3); $ordernum = $4; # # Ensure the taskname only has valid characters. # if(validtaskname($taskname) == 0) { print STDERR "$cfile: task \"$taskname\" contains invalid characters\n"; $errs++; next; } # # Ensure the taskname hasn't been defined already -- # regardless of case. # $lctn = lc($taskname); # Lowercase taskname. if($lctasks{$lctn}) { print STDERR "$cfile: task \"$taskname\" has already been defined (as $lctasks{$lctn})\n"; $errs++; next; } $lctasks{$lctn} = $taskname; # # Ensure the billable value is valid. # if($billable !~ /^billable|nonbillable$/i) { print STDERR "$cfile: invalid billable value \"$billable\" for task \"$taskname\"\n"; $errs++; next; } # # Ensure the billable value is valid. # if($active !~ /^active|inactive$/i) { print STDERR "$cfile: invalid active value \"$active\" for task \"$taskname\"\n"; $errs++; next; } # # Ensure the ordering number value is valid. # if(($ordernum =~ /[^0-9]/) == 1) { print STDERR "$cfile: order number \"$ordernum\" for task \"$taskname\" contains non-numeric characters\n"; $errs++; next; } # # Save the new task info. # $tasknames{$taskname} = 1; $billables{$taskname} = 0; $billables{$taskname} = 1 if($billable eq 'billable'); $actives{$taskname} = $INACTIVE; $actives{$taskname} = $ACTIVE if($active eq 'active'); $ordernums{$taskname} = $ordernum; # print "configuration command \"task\" not yet implemented\n"; } elsif($cmd eq 'alias') { my $taskname; # Name of new task. my @alnames = (); # Aliases list. my $lctn; # Lowercase taskname. my $billable; # Billable flag. my $ordernum; # Ordering number. # # Break the task line into its elements. # if(($args =~ /^"(.+?)"(.+)$/) == 0) { print STDERR "$cfile: invalid config line:\n\t$entry\n"; $errs++; next; } # # Pull the pieces into their own variables. # $taskname = $1; $args = $2; $args =~ s/^\s+//; # # Ensure the taskname only has valid characters. # if(validtaskname($taskname) == 0) { print STDERR "$cfile: task \"$taskname\" contains invalid characters\n"; $errs++; next; } # # Divide the alias list up into its pieces. # @alnames = split /,/, $args; # # Go through the aliases and validate the aliases # before adding them to the alias list. # for my $alias (@alnames) { my $task; # Alias' task. # # Get the alias name from this list element. # Leading whitespace and surrounding double- # quotes are stripped off. # $alias =~ /^\s*"(.*)"/; $alias = $1; # # Ensure the alias only has valid characters. # if(validtaskname($alias) == 0) { print STDERR "$cfile: alias \"$alias\" contains invalid characters\n"; $errs++; next; } # # Ensure the alias is not already registered. # if(($task=cvtalias($alias)) ne '') { print STDERR "$cfile: alias \"$alias\" is already defined for task \"$aliases{$a}\"\n"; $errs++; next; } # # Add this alias to the table. # $aliases{$alias} = $taskname; } } elsif($cmd eq 'log') { if(($args =~ /^"(.+)"$/) == 0) { print STDERR "unknown log line: \"$args\"\n"; $errs++; next; } $kronlogfile = $1; # # Create the kronlog file if it doesn't exist already. # newkronlog() if(! -e $kronlogfile); } elsif($cmd eq 'seconds') { if($args !~ /^(def|off|on)$/i) { print STDERR "unknown seconds flag: \"$args\"\n"; $errs++; next; } # # Set the use-seconds flag according to user's wish. # $secsflag = $secsvals{$args}; } elsif($cmd eq 'us-location') { # # Set the us-location flag from the config value. # if($args =~ /^(true|yes|1)$/i) { $usloc = 1; } elsif($args =~ /^(false|no|0)$/i) { $usloc = 0; } else { print STDERR "unknown us-location value: \"$args\"\n"; $errs++; next; } } else { print STDERR "unknown configuration command: \"$cmd\"\n"; $errs++; next; } } # # Stop if we hit any errors. # exit(31) if($errs); } ############################################################################# # # Routines in support of a command or option. # # showaliases() Display the defined task aliases. # showtasks() Display the defined tasks. # kronstat() Provide the current status. # listkrons() Give a listing of the krons in the kronlog. # summary() # summarize() Summarize a kron list for weekly/daily display. # addkron() Add a kron entry to the kronlog. # closekron() Close an open kron entry. # #---------------------------------------------------------------------- # Routine: showaliases() # # Purpose: Display the list of valid tasks and their aliases. # sub showaliases { print "tasks:\n"; foreach my $tn (sort(keys(%tasknames))) { my $active = ''; # Task-active string. if($actives{$tn} == $INACTIVE) { $active = "\t\tinactive"; } foreach my $alias (sort(keys(%aliases))) { next if($aliases{$alias} ne $tn); printf("\t%-*s\t%s%s\n", $maxtasklen, $tn, $alias, $active); } } exit(0); } #---------------------------------------------------------------------- # Routine: showtasks() # # Purpose: Display the list of valid tasks, along with their associated # attributes. The tasks are sorted alphabetically, rather than # the way they are for the various displays. # # The output mode determines how the billable column is shown: # mode billable/nonbillable # raw 1/0 # rare billable/nonbillable # cooked yes/no # sub showtasks { if($mode eq $COOKED) { if($verbose) { printf("%-*s\tBillable\tActive\t\tOrdering Number\n", $maxtasklen, 'Task'); } foreach my $tn (sort(keys(%tasknames))) { my $astr; # Active string. my $bstr; # Billable string. $astr = "no"; $astr = "yes" if($actives{$tn} == $ACTIVE); $bstr = "no"; $bstr = "yes" if($billables{$tn}); printf("%-*s\t%s\t\t%s\t\t%d\n", $maxtasklen, $tn, $bstr, $astr, $ordernums{$tn}); } } elsif($mode eq $RARE) { if($verbose) { printf("%-*s\tBillable\t\tActive\t\tOrdering Number\n", $maxtasklen, 'Task'); } foreach my $tn (sort(keys(%tasknames))) { my $astr; # Active string. my $bstr; # Billable string. $astr = "inactive"; $astr = "active " if($actives{$tn} == $ACTIVE); $bstr = "nonbillable"; $bstr = "billable" if($billables{$tn}); printf("%-*s\t%s\t\t%s\t%d\n", $maxtasklen, $tn, $bstr, $astr, $ordernums{$tn}); } } else { if($verbose) { printf("%-*s\tBillable\tActive\t\tOrdering Number\n", $maxtasklen, 'Task'); } foreach my $tn (sort(keys(%tasknames))) { if($verbose) { printf("%-*s\t%d\t\t%d\t\t%d\n", $maxtasklen, $tn, $billables{$tn}, $actives{$tn}, $ordernums{$tn}); } else { printf("%-*s\t%d\t%d\t%d\t%d\n", $maxtasklen, $tn, $billables{$tn}, $actives{$tn}, $ordernums{$tn}); } } } exit(0); } #---------------------------------------------------------------------- # Routine: kronstat() # # Purpose: Give current kron status. If there's an active kron, # the time displayed will be the amount of time since the # task was started. If there isn't an active kron, then # the time spent on the most recent task will be shown. # Cases and example output are given below. # # Normal situations are: # # open kron block before curtime is at end of file: # : # dev: 5:12:40 # # closed kron block before curtime is at end of file: # Off Clock: # Off Clock: 5:12:40 # # Kron from the future in the kronlog: # # closed kron block before curtime and open kron block after curtime: # Off Clock: () # Off Clock: 5:12:40 (1:20:10) # # closed kron block before curtime and closed kron block after curtime: # Off Clock: () # Off Clock: 5:12:40 (1:20:10) # # open kron block before curtime and open kron block after curtime: # : () # dev: 5:12:40 (1:20:10) # # open kron block before curtime and closed kron block after curtime: # : # dev: 5:12:40 (1:20:10) # # closed kron block before curtime and "end" kron line after curtime: # Off Clock: () # Off Clock: 5:12:40 (1:20:10) # sub kronstat { my $outflag = shift; # Print-output flag. my @lines = (); # Lines from kronlog. my $curtime; # Current time. my $lastind; # Index of last lines. my $lastkron = ''; # Last kron logged. my $nextkron = -1; # Index of chronologically next kron. my $prevtime = -1; # Last task's epoch time. my $prevtask = ''; # Last task's taskname. my $prevstate = ''; # Last task's start/end. my $prevelapsed = ''; # Last task's elapses time. my $nexttime = -1; # Next task's epoch time. my $nexttask = ''; # Next task's taskname. my $nextstate = ''; # Next task's start/end. my $elapsed = -1; # Elapsed time. my $msg; # Status message buffer. my $vmsg = ''; # Extra message for -verbose. # # Get the contents of the kron log and save the current time. # @lines = getkrons(0); $curtime = time(); # # Get the last kron block from the file and parse out its fields. # for($lastind = (@lines - 1); $lastind >= 0; $lastind--) { # # Get the line and skip comment lines.. # $lastkron = @lines[$lastind]; chomp($lastkron); next if(($lastkron =~ /^\s*#/) || ($lastkron =~ /^\s*$/)); # # Pick up the pieces of the line. # $lastkron =~ /^(\d+)\t"(.*)"\t(start|end)/; $prevtime = $1; $prevtask = $2; $prevstate = $3; # # If we've found the immediately previous-to-now entry, # we'll save some info for the status. # if(int($prevtime) <= $curtime) { if($prevstate eq 'start') { $elapsed = $curtime - int($prevtime); } elsif($prevstate eq 'end') { $elapsed = -1; # # Pick up the pieces of the line. # $lastkron =~ /^\d+\t".*"\tend\t(.*)/; $prevelapsed = $1; } else { print STDERR "invalid task state - \"$prevstate\"\n"; exit(51); } last; } else { # # This isn't the entry we're looking for, so we'll # save the info and go to the next. # $nexttime = $prevtime; $nexttask = $prevtask; $nextstate = $prevstate; $nextkron = $lastind; } } # # Buid additional message for -verbose. # $vmsg = "; last task was \"$prevtask\" for $prevelapsed" if($verbose); # # The basic, normal responses are handled here. # These are separated off here for the sake of speed. # if($nextkron == -1) { my $elapsedtime = timecolon($curtime - int($prevtime), 1); # # This is the normal on-clock response. # if($elapsed > -1) { $msg = "$prevtask: $elapsedtime"; } elsif(($elapsed == -1) && ($prevtask eq '')) { # # This is the off-clock response if there are no # entries in the kronlog. # $msg = "$OFFCLOCK: no entries" . $vmsg; } else { # # This is the normal off-clock response. # $msg = "$OFFCLOCK: $elapsedtime" . $vmsg; } print "$msg\n" if($outflag); return($msg); } # # The unnatural responses are handled below. These are when a kron # block has been inserted after the current time. This shouldn't # happen often, but the -insertkron and -changekron options make it # much more likely to happen. # my $upcoming; # Time until next kron. $elapsed = timecolon($curtime - int($prevtime), 1); $upcoming = timecolon(int($nexttime) - $curtime, 1); if($nextstate eq 'start') { # # This handles when the current time is between: # - open old kron and open future kron, or # - open old kron and closed future kron # if($prevstate eq 'start') { $msg = "$prevtask: $elapsed"; $msg = "$prevtask: $elapsed ($upcoming)"; } else { # # This handles when the current time is between: # - closed old kron and open future kron, or # - closed old kron and closed future kron # $vmsg = "; last task was \"$prevtask\" for $elapsed" if($verbose); $msg = "$OFFCLOCK 1"; $msg = "$OFFCLOCK: $elapsed ($upcoming)" . $vmsg; } } else { # # This is an extreme corner case. This will happen only if # there's a future kron "end" line without a "start" line. # It's unlikely to happen, but we'll do this Just In Case. # $msg = "$prevtask: $elapsed ($upcoming)"; } # # Give the messages. # print "$msg\n" if($outflag); return($msg); } #---------------------------------------------------------------------- # Routine: listkrons() # # Purpose: List the krons in the kron log. # # If the -select option was given, the option argument contains # the selection criteria that will restrict the output of the # command. The full definition of the criteria is: # # date1-date2@taskname # # If all these fields are given, all the tasks with the given # taskname that were recorded between date1 and date2 are shown. # # The presentation depends on the output mode: # raw mode - almost no processing; # start-of-day comments are added # rare mode - timestamps are converted to human-readable # format; # blank lines inserted between different days # cooked mode - start/end lines condensed to single lines; # timestamps are converted to human-readable # format; # end/start time differences converted to # hh:mm format # sub listkrons { my @lines = (); # Lines from kron log. my $elapsed; # Elapsed time. my $selstart = ''; # Epoch time for start of -select runs. my $maxlen = -1; # Length of longest taskname. # # Get the contents of the kron log. # @lines = getkrons(0); # # If -today was given and a selector wasn't, we'll build # a selector for the current day only. # if($todayflag) { if($select ne undef) { print STDERR "-today cannot be used with -select\n"; exit(22); } my @tempus = localtime(time); # Time atoms. $select = sprintf("%d/%d/%d", ($tempus[4] + 1), $tempus[3], ($tempus[5] - 100)); } # # If -week was given and a selector wasn't, we'll build # a selector for the current week only. # if($weekflag) { my @tempus; # Elements of time. my $midnight; # Midnight of today. my $sunday_last; # Midnight of previous Sunday. my @sunday_epoch; # Epoch of midnight of previous Sunday. if($select ne undef) { print STDERR "-week cannot be used with -select\n"; exit(22); } # # Get the time elements of the current time and move it # back to midnight. # @tempus = localtime(time); $tempus[0] = 0; $tempus[1] = 0; $tempus[2] = 0; # # Get the epoch time of today's midnight. # $midnight = timelocal($tempus[0], $tempus[1], $tempus[2], $tempus[3], $tempus[4], $tempus[5]); # # Adjust the epoch time back to midnight of the previous # start-of-week. # (The previous Sunday is assumed to be the start of the week.) # This is easy to do by subtracting the number of seconds in # a day from each day between now and the previous Sunday. # $sunday_last = $midnight - ($tempus[6] * $DAYSECS); @sunday_epoch = localtime($sunday_last); $select = sprintf("%d/%d/%d-%d/%d/%d", ($sunday_epoch[4] + 1), $sunday_epoch[3], ($sunday_epoch[5] - 100), ($tempus[4] + 1), $tempus[3], ($tempus[5] - 100)); } # # If we should only display a certain date (range) or task, # we'll modify the list of lines to only encompass the selection. # if($select ne undef) { my @newlist = (); # New list of lines. my @selectors; # Things to select upon. my $seldates = ''; # Selected date(s). my $seltask = ''; # Selected task. my $midnight = 0; # First epoch time to save. my $lastepoch; # Last epoch time to save. # # Get the pieces of the selector string. # @selectors = split '@', $select; # # Get and validate the selected date (or date range.) # $seldates = $selectors[0]; if($seldates ne '') { my @startend; # Selected start/end dates. # # Divide the selected date into its pieces. If it # has a dash, it's a range and must have two dates. # If it doesn't have a dash, it's just a single date. # @startend = split '-', $seldates; if((@startend == 1) && ($seldates =~ /\-/)) { print STDERR "missing date in data range: \"$seldates\"\n"; exit(23); } # # Convert the first date in the selected date into # its epoch time. # $midnight = selectday($startend[0]); # # Ensure the initial date was valid. # if($midnight <= 0) { print "invalid selection date: \"$startend[0]\"\n"; exit(24); } # # If a date range was given, then we'll figure out the # epoch time for the final date. We'll also ensure the # final date was valid. # If it isn't a range, we'll calculate the final epoch # time based on the initial date. # if(@startend > 1) { $lastepoch = selectday($startend[1]); if($lastepoch <= 0) { print "invalid selection date: \"$startend[1]\"\n"; exit(25); } $lastepoch += $DAYSECS - 1; } else { $lastepoch = $midnight + $DAYSECS - 1; } # # Ensure the initial date is actually before the # second date. # if($midnight > $lastepoch) { print STDERR "first date in range ($startend[0]) follows second date ($startend[1])\n"; exit(26); } elsif($midnight == $lastepoch) { print STDERR "first date in range ($startend[0]) is same as second date ($startend[1])\n"; exit(27); } $selstart = $midnight; } # # Get and validate the selected task. # if(@selectors > 1) { $seltask = cvtalias($selectors[1]); if($seltask eq '') { print STDERR "invalid taskname: \"$selectors[1]\"\n"; exit(28); } } # # Get rid of all the kronlog entries that are outside the # selected date range and that don't have the selected task # name. # for my $ln (@lines) { my $keep = 1; # Keep-entry flag. # # Parse the pieces of the entry. # $ln =~ /^(\d+)\t"(.*)"\t/; # # If there's a selected date, we'll skip anything # before midnight of the selected date. # if(($midnight > 0) && ($1 < $midnight)) { next; } # # If there's a selected date, we'll skip anything # after the last selected date. # if(($midnight > 0) && ($1 > $lastepoch)) { next; } # # Don't save this entry if it's not for the selected # task. # If there's a selected task. # if(($seltask ne '') && ($seltask ne $2)) { $keep = 0; } # # Copy this entry to the new kron list if it # fits the selection criteria. # if($keep) { push @newlist, $ln; } } # # Ensure that something matched the selection criteria. # if(@newlist == 0) { print "no entries matching selection criteria\n"; exit(0); } # # Move the pared-down kron list to the list we'll be using. # @lines = @newlist; } # # Get the length of the longest taskname in the file. # foreach my $entry (@lines) { my $taskname; # Entry's taskname. my $namelen; # Length of entry's taskname. $entry =~ /^\d+\t"(.*)"\t/; $taskname = $1; $namelen = length($taskname); if($namelen > $maxlen) { $maxlen = $namelen; } } # # If we've showing a selected subset of the kronlog, we'll display # the date we're looking at. # if($selstart != 0) { my @atoms; # Pieces of select-start time. my $mm; # Month string. my $yy; # Year number. @atoms = localtime($selstart); $mm = $months[$atoms[4]]; $yy = $atoms[5] + 1900; printf("Time Records from %s %d, %d\n", $mm, $atoms[3], $yy); } # # If we're in cooked mode, we'll collapse the entries to a single # line each. # if($mode == $COOKED) { my $stchronos; # Start entry's timestamp. my $stchronostr; # Start entry's HR timestamp. my $sttaskname; # Start entry's taskname my $maxlen = -1; # Maximum taskname length. # # Figure out the length of the longest taskname. # foreach my $entry (@lines) { my $tname; $entry =~ /^\d+\t(".*")\tstart|end/; $tname = $1; $maxlen = length($tname) if(length($tname) > $maxlen); } # # Go through the krons and collapse the matching start/end # lines into single lines. # foreach my $entry (@lines) { my $chronos; # Entry's timestamp. my $chronostr; # Human-readable timestamp. my $taskname; # Entry's taskname. my $state; # Entry's state. my $tdiff; # Entry's time diff. my $tdiffstr; # Human-readable time diff. # # Parse the pieces of the entry. # $entry =~ /^(\d+)\t(".*")\t(start|end)(\t(.+))?/; $chronos = $1; $taskname = $2; $state = $3; $tdiff = $5; # # If this is the start line, save the starting # timestamp. # if($state eq "start") { $stchronos = $chronos; $stchronostr = outtime($stchronos); $sttaskname = $taskname; next; } # # Get the time strings, based on output mode. # $chronostr = outtime($chronos); $stchronostr = outtime($stchronos); # # Figure out what the elapsed-time entry looks like. # The old version had it as a count of seconds, which # was the difference between the end time and start # time. This difference was then converted into a # hh:mm:ss form. # The new version of this field is already the # hh:mm:ss form. # if($tdiff =~ /:/) { $tdiffstr = $tdiff; } else { $tdiffstr = timecolon(int($tdiff), 0); } printf("$stchronostr %-*s $chronostr $tdiffstr\n", $maxlen, $taskname); # # Reset the start entry's timestamp. # $stchronos = ''; } # # If we have an active kron, build an entry for it. # if($stchronos ne '') { my $tdiff; # Entry's time diff. my $tdiffstr; # Human-readable time diff. my $dummytstmp; # Dummy timestamp. $tdiff = time - $stchronos; $tdiffstr = timecolon($tdiff, 1); $dummytstmp = $stchronostr; $dummytstmp =~ s/./ /g; printf("$stchronostr %-*s %-*s $tdiffstr\n", $maxlen, $sttaskname, length($stchronostr), ' '); } return; } # # Get the last entry from the file and parse out its fields. # my $someshown = 0; # Output-given flag. (rare mode) foreach my $entry (@lines) { my @chronos; # Elements of entry's time. my $chronos; # Entry's epoch time. my $taskname; # Entry's taskname. my $state; # Entry's start/end. my $tdiff = ''; # Entry's end time difference. my $chronostr; # Text epoch time. # # Parse the pieces of the entry. # $entry =~ /^(\d+)\t(".*")\t(start|end)/; $chronos = $1; $taskname = $2; $state = $3; # # Break the timestamp into its pieces. # @chronos = localtime($chronos); # # If this is an 'end' entry, get the time difference. # if($state eq 'end') { $entry =~ /^\d+\t".*"\tend\t(.+)/; $tdiff = $1; # # Figure out what the elapsed-time entry looks like. # The old version had it as a count of seconds, which # was the difference between the end time and start # time. This difference was then converted into a # hh:mm:ss form. # The new version of this field is already the # hh:mm:ss form. # if($tdiff !~ /:/) { $tdiff = timecolon(int($tdiff), 0); } $prevday = $chronos[3]; } else { if($prevday != $chronos[3]) { my $prevmon = $chronos[4]; my $prevyear = 1900 + $chronos[5]; if($mode == $RAW) { # print "##################################### $months[$prevmon] $prevday, $prevyear\n"; print "#" x 55 . " $days[$chronos[6]] $months[$prevmon] $chronos[3], $prevyear\n"; } else { print "\n" if($someshown); } $prevday = $chronos[3]; } } # # Get the time string, based on output mode. # $chronostr = outtime($chronos); printf("$chronostr\t%-*s\t$state\t$tdiff\n", $maxlen, $taskname); $someshown = 1; } } #---------------------------------------------------------------------- # Routine: summary() # # Purpose: Summarize the krons in the kron log into either a daily # or weekly list. # sub summary { my @lines = (); # Lines from kron log. my $elapsed; # Elapsed time. my $maxlen = -1; # Length of longest taskname. # # Get the contents of the kron log. # @lines = getkrons(0); # # If -today was set, we'll get rid of all kron entries prior to # midnight of the current day. # # This would run a bit faster if we started from the end of the # kron list and worked backwards, rather than starting from the # top and working forwards. We'll fix this RSN. # if($todayflag) { my @tempus; # Elements of time. my $midnight; # Midnight of current day. my $lastind = -1; # Index of last entry before today. my $lastepoch = -1; # Epoch of selected date's end. # # If the user specified a specific date, we'll get the # epoch times for that date's first and last seconds. # If not, we'll get the epoch time for the current day's # midnight. # if($select) { $midnight = selectday($select); $lastepoch = $midnight + $DAYSECS - 1; if($midnight <= 0) { print "invalid date selection: \"$select\"\n"; exit(25); } } else { # # Get current time. # @tempus = localtime(time); # # Convert current time to midnight of current day. # $tempus[0] = 0; $tempus[1] = 0; $tempus[2] = 0; # # Get the epoch time of midnight. # $midnight = timelocal($tempus[0],$tempus[1],$tempus[2],$tempus[3],$tempus[4],$tempus[5]); } # # Search this kron list for the first entry whose time is on # or after midnight of the start of the current day. If we # find one, we'll save the previous index and stop searching. # for(my $ind = 0; $ind < @lines; $ind++) { my $entry; # Kron entry. my $chronos; # Entry's epoch time. $entry = $lines[$ind]; # # Parse the pieces of the entry. # $entry =~ /^(\d+)\t".*"\t/; $chronos = $1; # # If this entry's time is after or on midnight of the # current day, we'll save the previous index and stop. # if($chronos >= $midnight) { $lastind = $ind; last; } } # # If there were no entries yet today, we'll stop here. # if($lastind == -1) { my $msgtail; # Tail-end of message. $msgtail = "today"; $msgtail = $select if($select != undef); print "no entries for $msgtail\n"; exit(0); } # # Lop off the front part of the kron list. # splice(@lines, 0, $lastind); # # If a specific date was selected to report on, we'll find # the first entry of the next day. With the previously # found start and this end, we've got the set of kron # entries that'll be displayed # if($lastepoch != -1) { $lastind = -1; for(my $ind = 0; $ind < @lines; $ind++) { my $entry; # Kron entry. my $chronos; # Entry's epoch time. $entry = $lines[$ind]; # # Parse the pieces of the entry. # $entry =~ /^(\d+)\t".*"\t/; $chronos = $1; # # If this entry's time is after or on midnight # of the next day, save the index and stop. # if($chronos >= $lastepoch) { $lastind = $ind; last; } } # # If we found a following day's entry, we'll drop # everything from there to the end of the kron list. # if($lastind != -1) { # # Lop off the end part of the kron list. # splice(@lines, $lastind); } } # # Ensure that there will be kron blocks to display. # $lastind = -1; foreach my $entry (@lines) { # # If this entry has an epoch time and a task name, # we'll set a flag and stop looking. # if(($entry =~ /^(\d+)\t".*"\t/) > 0) { $lastind = 1; last; } } if($lastind == -1) { print "no entries for $select\n"; exit(0); } } elsif($weekflag) { my $now; # Current time. my @tempus; # Elements of time. my $midnight; # Midnight of today. my $sunday_last; # Midnight of previous Sunday. my $daynum; # Today's day number (0 - 6). my $lastind = -1; # Index of last entry before # # If the user selected a specific date, we'll get the # epoch times for that date's week's first and last seconds. # If not, we'll get the epoch time for the current day's # midnight. # if($select) { $midnight = selectday($select); @tempus = localtime($midnight); if($midnight <= 0) { print "invalid date selection: \"$select\"\n"; exit(26); } } else { # # Get the time elements of the current time. # @tempus = localtime(time); # # Move back that time to today's midnight. # $tempus[0] = 0; $tempus[1] = 0; $tempus[2] = 0; # # Get the epoch time of today's midnight. # $midnight = timelocal($tempus[0], $tempus[1], $tempus[2], $tempus[3], $tempus[4], $tempus[5]); } # # Adjust the epoch time back to midnight of the previous # start-of-week. # (The previous Sunday is assumed to be the start of the week.) # This is easy to do by subtracting the number of seconds in # a day from each day between now and the previous Sunday. # $sunday_last = $midnight - ($tempus[6] * $DAYSECS); # # Search this kron list for the first entry whose time is on # or after midnight of the start of the current week. If we # find one, we'll save the previous index and stop searching. # for(my $ind = 0; $ind < @lines; $ind++) { my $entry; # Kron entry. my $chronos; # Entry's epoch time. $entry = $lines[$ind]; # # Parse the pieces of the entry. # $entry =~ /^(\d+)\t".*"\t/; $chronos = $1; # # If this entry's time is after or on midnight of # the previous start-of-week, we'll save the previous # index and stop. # if($chronos >= $sunday_last) { $lastind = $ind; last; } } # # If there were no entries yet this week, we'll stop here. # if($lastind == -1) { print "no entries for this week\n"; exit(0); } # # Lop off the front part of the kron list. # splice(@lines, 0, $lastind); # # If we've selected a specific week to show, lop off the # entries after the selected week. # if($select) { my $lastepoch; # Epoch of selected date's end. $lastepoch = $sunday_last + ($DAYSECS * 7) - 1; # # Search this kron list for the last entry whose time # is within the selected week. If we find one, we'll # save the previous index and stop searching. # for(my $ind = 0; $ind < @lines; $ind++) { my $entry; # Kron entry. my $chronos; # Entry's epoch time. $entry = $lines[$ind]; # # Parse the pieces of the entry. # $entry =~ /^(\d+)\t".*"\t/; $chronos = $1; # # If this entry's time is after the selected # week, we've found our fencepost. # if($chronos >= $lastepoch) { $lastind = $ind; last; } } # # Stop here if there were no entries yet this week. # if($lastind == -1) { print "no entries for this week\n"; exit(0); } # # Lop off the last part of the kron list. # splice(@lines, $lastind); } # # Ensure that there will be kron blocks to display. # $lastind = -1; foreach my $entry (@lines) { # # If this entry has an epoch time and a task name, # we'll set a flag and stop looking. # if(($entry =~ /^(\d+)\t".*"\t/) > 0) { $lastind = 1; last; } } if($lastind == -1) { print "no entries for $select\n"; exit(0); } } else { print "summaries given for -week and -today only\n"; exit(11); } # # Give a summarized version of the selected records. # summarize(@lines); } #---------------------------------------------------------------------- # Routine: summarize() # # Purpose: Summarize a list of krons for weekly or daily display. # # The list may be the whole or partial contents of a kron log # file. The most common use will be for the current day or # week. However, it all depends on how summary() is called. # sub summarize { my @krons = @_; # Krons to display. my %totals = (); # Totals of task times. my @totals = (); # Totals of task times. my %tasktotals = (); # Weekly task totals. my @daytotals = (); # Daily totals. my @dbtotals = (); # Daily billable totals. my $billtotal = 0; # Total of billable task times. my $alltotal = 0; # Total of all task times. my $chronos; # Entry's epoch time. my @chronos; # Elements of entry's epochtime. my $taskname; # Entry's taskname. my $state; # Entry's start/end. my $tdiff = ''; # Entry's end time difference. my $active = ''; # Active task (for -today.) my $starttime; # Start time for kron block. my $chronostr; # Text epoch time. my $day; # Day number of start time. my $btotstr = "Billable"; # Billable total title string. my $ntotstr = "Nonbillable"; # Nonbillable total title string my $atotstr = "Total"; # Total title string. my %inuse = (); # Lists tasks in use. # # Reset maximum taskname length if it's shorter than # "Nonbillable". # if(length($ntotstr) > $maxtasklen) { $maxtasklen = length($ntotstr); } # # Go through this krons list and add each kron block's time total # to the overall total for the timeframe involved. # foreach my $entry (@krons) { my $day; # Day-of-week number. # # Parse the pieces of the entry. # $entry =~ /^(\d+)\t"(.*)"\t(start|end)(\t(.+))?/; $chronos = $1; $taskname = $2; $state = $3; $tdiff = $5; $inuse{$taskname} = 1; # # If the time difference is in the new (hh:mm:ss) format, # we'll convert it to seconds. # if($tdiff =~ /:/) { $tdiff = colontime($tdiff); } else { $tdiff = int($5); } # # If this is a start entry, we'll save the start time. # If this is an end entry, we'll calculate the kron block's # time and add it in to task's totals. # if($state eq 'start') { $starttime = $chronos; # print "---> ", scalar(localtime($chronos)), "\t $taskname\n"; } elsif($state eq 'end') { my $timechk; # Time diff for checking. # # Ensure the entry's time-difference field is correct. # However, we'll just give a warning if it's wrong. # $timechk = $chronos - $starttime; if($timechk != $tdiff) { print "chronosquirrel: entry at $chronos has incorrect time difference field\n"; } # # Get the day index for this entry. # @chronos = localtime($starttime); $day = $chronos[6]; # # Record the time for this task on this day. # # print "\t\tadding $tdiff to $taskname\n"; $tasktotals{$taskname} += $tdiff; $daytotals[$day] += $tdiff; $dbtotals[$day] += $tdiff if($billables{$taskname}); $totals[$day]{$taskname} += $tdiff; # # Reset $starttime to indicate we've hit the end # of the kron block. # $starttime = 0; } else { print "chronosquirrel: kronlog contains invalid state \"$state\"\n"; exit(7); } } # # If we're in the middle of an active kron block, then we'll add # the time from the current time to the start of the block. # if($starttime > 0) { # # Get the day's index. # @chronos = localtime($starttime); $day = $chronos[6]; # # Calculate how long this task has been running. # $tdiff = time() - $starttime; # # Add the task's time to All The Places! # $tasktotals{$taskname} += $tdiff; $daytotals[$day] += $tdiff; $dbtotals[$day] += $tdiff if($billables{$taskname}); $totals[$day]{$taskname} += $tdiff; # # Save the active taskname for -today output. # $active = $taskname; } # # Save the day index number from the most recently read kron entry. # # XXX This might be a problem if we have an empty kronlog, but # when will that ever happen? # $day = $chronos[6]; # # If we've showing a selected subset of the kronlog, we'll display # the date we're looking at. # if($select ne '') { my @atoms; # Pieces of select-start time. my $mm; # Month string. my $yy; # Year number. my $selkron; # Selected day's kron. $selkron = selectday($select); @atoms = localtime($selkron); $mm = $months[$atoms[4]]; $yy = $atoms[5] + 1900; if($weekflag) { printf("Time Records from the Week of %s %d, %d\n", $mm, $atoms[3], $yy); } else { printf("Time Records from %s %d, %d\n", $mm, $atoms[3], $yy); } } # # Do a weekly summary if -week was given. # Do a daily summary if it wasn't. # if($weekflag) { printf("%-*s\t Sun\t Mon\t Tue\t Wed\t Thu\t Fri\t Sat\tTotals\n", $maxtasklen, "Task"); # # %tasktotals gives the rightmost totals column. # @daytotals gives the daily totals row. # @dbtotals gives the daily billable totals row. # The daily nonbillable totals row is derived from # @daytotals and @dbtotals. # foreach my $task (@sortedtasks) { my $tot = 0; # Task total. # # Skip this task if it hasn't been used during the # week in question. Unless -all was given, in which # case we'll show a big nothing for that task. # if(($inuse{$task} == 0) && ($allflag == 0)) { next; } printf("%-*s", $maxtasklen, $task); for(my $dind = 0; $dind < 7; $dind++) { printf("\t%s", timecolon($totals[$dind]{$task},0)); $tot += $totals[$dind]{$task}; } printf("\t%s\n", timecolon($tot,0)); } print "\n"; # # Print the week's billable totals. # printf("%-*s", $maxtasklen, "Billable"); my $tot = 0; # Overall total. my $tval; # Total value. for(my $dind = 0; $dind < 7; $dind++) { $tval = $dbtotals[$dind]; printf("\t%s", timecolon($tval,0)); $tot += $tval; } printf("\t%s\n", timecolon($tot,0)); # # Print the week's nonbillable totals. # printf("%-*s", $maxtasklen, "Nonbillable"); $tot = 0; for(my $dind = 0; $dind < 7; $dind++) { $tval = $daytotals[$dind] - $dbtotals[$dind]; printf("\t%s", timecolon($tval), 0); $tot += $tval; } printf("\t%s\n", timecolon($tot,0)); # # Print the week's total totals. # printf("%-*s", $maxtasklen, "Total"); $tot = 0; for(my $dind = 0; $dind < 7; $dind++) { $tval = $daytotals[$dind]; printf("\t%s", timecolon($tval,0)); $tot += $tval; } printf("\t%s\n", timecolon($tot,0)); } else { my $nnbtotal; # Nonbillable total. my $first = 1; # Flag for first nonbillable. # # Print the total for each billable task in this kron list. # foreach my $task (@sortedtasks) { # # Skip this task if it hasn't been used during the # week in question. Unless -all was given, in which # case we'll show a big nothing for that task. # if(($inuse{$task} == 0) && ($allflag == 0)) { next; } if($billables{$task} == 1) { $billtotal += $totals[$day]{$task}; } else { # # Print a dividing line between the billables # and the nonbillables. # if($first) { print "\n"; $first = 0; } $nnbtotal += $totals[$day]{$task}; } printf("%-*s: %s", $maxtasklen, $task, timecolon($totals[$day]{$task},1)); print "\tactive" if($task eq $active); print "\n"; } # # Get the overall total. # $alltotal = $billtotal + $nnbtotal; # # Print the day's totals. # print "\n"; printf("%-*s: %s\n", $maxtasklen, $btotstr, timecolon($billtotal,1)) if($billtotal > 0); printf("%-*s: %s\n", $maxtasklen, $ntotstr, timecolon($nnbtotal,1)) if($nnbtotal > 0); printf("%-*s: %s\n", $maxtasklen, $atotstr, timecolon($alltotal,1)) if($alltotal > 0); } } #---------------------------------------------------------------------- # Routine: addkron() # # Purpose: Add an entry to the kron log. If the kron block crosses the # midnight boundary, then the block will be split into a set # of blocks that are (at most) a day long. # sub addkron { my $chronos; # Epoch time. my @lines = (); # Lines from kron log. my $nokrons = 1; # No-krons-in-log boolean. my $lastkron = ''; # Last kron logged. my $newkron = ''; # New kron to log. my $prevtime; # Last task's epoch time. my $prevtask; # Last task's taskname. my $prevstate; # Last task's start/end. my $newstate; # New task's start/end. my @outkrons = (); # List of new krons. my $newkronstr; # String of new krons # # Ensure no extraneous arguments were given. # if(@ARGV > 1) { print STDERR "extraneous arguments follow task name\n"; exit(3); } # # Ensure the task is active. # if($actives{$task} == $INACTIVE) { print STDERR "task \"$task\" is inactive and cannot be used\n"; exit(3); } # # Get the time for the entry. # $chronos = time; # # Get the contents of the kron log. # @lines = getkrons(1); # # Add the first kron to the empty/kronless kronlog. # if(@lines == 0) { newkronlog(); $newkron = "$chronos\t\"$task\"\tstart"; appendkron("$newkron\n"); return; } # # Get the last entry from the file and parse out its fields. # # XXX - Code assumes that last line isn't a comment or blank line. # $lastkron = @lines[-1]; chomp($lastkron); $lastkron =~ /^(\d+)\t"(.*)"\t(start|end)/; $prevtime = $1; $prevtask = $2; $prevstate = $3; # # Ensure that we aren't trying to add an entry after an entry that # should follow the new entry. # if($prevtime > $chronos) { print STDERR "unable to start timer; timestamp in last log entry is later than current time\n"; return; } # # If a previous state wasn't found, we'll assume there are no # earlier krons. Setting the previous state to 'end' will make # the new kron entry have a 'start' state. # if($prevstate eq '') { $prevstate = 'end'; } # # Get the start/end state of the new entry. # $newstate = $nextstate{$prevstate}; # # If the new kron's state should be an end state, we'll # close the previous entry and start a new entry with the # new taskname. # if($newstate eq 'end') { # # Close the active kron block and split on any midnights # within the kron block. # @outkrons = midnightsplit($prevtask,$prevtime,$chronos,1); # # Bump the time. # $chronos += 1; # # And move the state to start a new timing block. # $newstate = 'start'; } # # Build the new kron and put it at the end of the list of # krons to add. This will *always* be a start record. # push @outkrons, "$chronos\t\"$task\"\tstart"; # # If this is closing an open entry, we'll add the time difference # to the end of the entry. # if($newstate eq 'end') { my $tdiff; # Time difference. print "88 -------> \n\n\t\t\tTHIS SHOULD NEVER HAPPEN!!!\n\n"; $tdiff = $chronos - $prevtime; $newkron .= "\t$tdiff"; } # # Give some verbose info. # if($verbose) { print "last entry: $lastkron\n"; print "new entry: $outkrons[-1]\n"; } # # Convert the new-krons list to a new-krons string. # $newkronstr = join("", @outkrons); # # Append the new kron list to the kron log. # appendkron("$newkronstr\n"); } #---------------------------------------------------------------------- # Routine: closekron() # # Purpose: Close the active kron block. If the kron block crosses the # midnight boundary, then the block will be split into a set # of blocks that are (at most) a day long. # sub closekron { my $closed = shift; # Closed flag. my $chronos; # Epoch time. my @lines = (); # Lines from kron log. my $lastkron = ''; # Last kron logged. my $prevtime; # Last task's epoch time. my $prevtask = ''; # Last task's taskname. my $prevstate; # Last task's start/end. my @outkrons = (); # List of new krons. my $newkronstr; # String of new krons # # Get the time for the entry. # $chronos = time; # # Get the contents of the kron log. # @lines = getkrons(0); # # Get the last entry from the file and parse out its fields. # # XXX - Code assumes that last line isn't a comment or blank line. # for(my $ind = (@lines - 1); $ind >= 0 ; $ind--) { $lastkron = $lines[$ind]; chomp($lastkron); $lastkron =~ /^(\d+)\t"(.*)"\t(start|end)/; $prevtime = $1; $prevtask = $2; $prevstate = $3; last; } # # Check for a couple error conditions. # if($prevtask eq '') { print "no task found in kron log\n"; exit(4); } if($prevstate ne 'start') { print "no active task\n"; exit(5); } # # Close the active kron block and split on any midnights # within the kron block. # @outkrons = midnightsplit($prevtask,$prevtime,$chronos,$closed); # # Convert the new-krons list to a new-krons string. # $newkronstr = join("", @outkrons); # # Append the new kron list to the kron log. # appendkron("$newkronstr"); } #---------------------------------------------------------------------- # Routine: roller() # # Purpose: Roll the log file. The contents of the current kronlog are # moved, in whole or in part, to a rolled kronlog file and a # new kronlog file is created. If only part of the current # kronlog is moved to the new roll file, the remainder is left # in the current kronlog. # # We roll at midnight, making rollover-point calculation much # easier. It also makes log rollover much easier to explain # understand. The only exception to this is when the user # requests that rollover happen at the current time. # # We'll figure out the rollpoint, given the rollover method. # The supported methods of rolling are: # - year # - month # - week # - day # - current time # - specific MM/DD/YY date # - specific MM/DD date # # With the rollpoint calculated, we look in the kronlog file # for the last entry recorded before the rollpoint. One of # the following courses of action takes place: # # - If all entries are after the rollpoint, nothing # happens. # # - If the last entry in the kronlog is the same # as the rollpoint, then the whole kronlog is # moved into a rolled kronlog file. A new empty # kronlog will be created. # # - If the rollpoint falls between two entries, then # the contents of the kronlog to the preceding # entry are moved into a rolled kronlog file. The # remaining kronlog contents become the start of # the new kronlog file. # # <<< what happens for open krons? >>> # sub roller { my $kronos; # The rollover point in epoch time. my $rollpoint = -1; # Rollpoint epoch time. my $rollkron = -1; # Kron list index of rollpoint. my $final = -1; # Last actual entry in kron list. my @krons = (); # Kron log. my @oldkrons = (); # Kron log pre-rollpoint. my @newkrons = (); # Kron log post-rollpoint. my $savetime; # Time of last kron entry. my $savetask; # Task in last kron entry. my $savecmd; # Command in last kron entry. my $closed = 0; # Kron-closed toggle. my $newklog; # Name of rolled kron log. my $newkron = ''; # New kron string. # # Get the epoch time for the rollover point. # $kronos = findrollpoint(); for(my $ind = 0; $ind < 2; $ind++) { # # Get the contents of the kron log. # @krons = getkrons(1); # # Find the indices for the krons we'll insert things after # and before. # for(my $ind = (@krons - 1); $ind >= 0; $ind--) { my $epoch; # Epoch time of each kron. # # Skip blank lines and comments. # next if($krons[$ind] =~ /^\s*\#/); # # Save the time, task, and state of the next kron. # $krons[$ind] =~ /^(\d+)\t"(.+)"\t(start|end)/; $savetime = $1; $savetask = $2; $savecmd = $3; # # Save entry's index if it's the last in the kronlog. # if($final == -1) { $final = $ind; } # # Save entry's index if it's the last in the kronlog. # if($savecmd eq 'start') { $closed = 0; } else { $closed = 1; } # # Drop out if entry's time is less than the rollpoint. # if($savetime < $kronos) { $rollpoint = $ind; last; } } # # Drop out of the loop if the rollpoint's kron is closed. # if($savecmd eq 'end') { last; } # # If the rollpoint is after the saved time, we'll close the # active kron and maybe add a new start kron. # if($kronos >= $savetime) { closekron($closed); if($newkron eq '') { $newkron = "$kronos\t\"$savetask\"\tstart\n"; } } } # # If we didn't find a kron whose time is earlier than the rollover # point, we'll tell the user and do nothing further. # if($rollpoint == -1) { print "no rollover is possible since all entries in $kronlogfile are after\n"; printf("specified time - %s\n", scalar(localtime($kronos))); exit(0); } # # Adjust the rollpoint back to the new starting point. # $rollpoint++; # # Get the set of krons for the rolled kronlog. # @oldkrons = @krons; splice @oldkrons, $rollpoint; # # Add a header to the list of old krons. # unshift @oldkrons, "#\n"; unshift @oldkrons, sprintf("# Log rolled on %s; roll method: $rollmethod\n", scalar(localtime(time))); unshift @oldkrons, "#\n"; # # Get the set of krons for the current kronlog. # @newkrons = @krons; splice @newkrons, 0, $rollpoint; # # Get the name for the new roll file. # $newklog = findrollname(@krons); # # Write the rolled entries into the rolled log file. # if(open(ROLLKLOG, "> $newklog") == 0) { print STDERR "unable to create roll file \"$newklog\"\n"; exit(92); } print ROLLKLOG @oldkrons; close(ROLLKLOG); # # Add a header and write the set of unrolled krons to the kron log. # unshift @newkrons, "#\n"; unshift @newkrons, "# Proceed at your own risk.\n"; unshift @newkrons, "# Time log created and maintained by chronosquirrel -- do not edit!\n"; unshift @newkrons, "#\n"; writekrons(@newkrons); print "rolling kronlog: $kronlogfile\n"; print "rolled kronlog: $newklog\n"; print "roll method: $rollmethod\n"; exit(0); } #---------------------------------------------------------------------- # Routine: findrollpoint() # # Purpose: This routine finds the epoch time for the log split when a # log rollover will occur. The kron log will be searched for # the most recent change in year, month, week, or day. Also, # a specific date can be specified. # # In the fullness of time "now" will also be implemented as # a time for rollover. # sub findrollpoint { my $kronos; # The rollover point in epoch time. my $curtime; # Current time. my @atoms = (); # Time atoms. my $yy; # Year value. my $mm; # Month value. my $dd; # Day value. # # Get the current time and blast it into its time atoms. # $curtime = time(); @atoms = localtime($curtime); # # Convert the roll method to lowercase. This can be handled in the # individual regexp's below, but doing it once is (probably) faster. # $rollmethod = lc($rollmethod); # # Adjust the time atoms according to the requested method of rolling. # if($rollmethod =~ /^(year|yea|ye|yy|y)$/) { # # Set the date values to January 1 of the current year. # The year does not need to be set! # $atoms[4] = 0; $atoms[3] = 1; # # Set the time values to midnight. # $atoms[0] = 0; $atoms[1] = 0; $atoms[2] = 0; } elsif($rollmethod =~ /^(month|mont|mon|mo|mm|m)$/) { # # Set the date values to January 1 of the current year. # The year and month do not need to be set! # $atoms[3] = 1; # # Set the time values to midnight. # $atoms[0] = 0; $atoms[1] = 0; $atoms[2] = 0; } elsif($rollmethod =~ /^(week|wee|we|ww|w)$/) { # # Readjust the current time to be on the starting day of the # current week, regardless of what month or year the day is in. # (This assumes that Sunday starts off the week.) # We'll then recalculate the various time atoms for that date. # $curtime -= ($atoms[6] * $DAYSECS); @atoms = localtime($curtime); # # Set the time values to midnight. # $atoms[0] = 0; $atoms[1] = 0; $atoms[2] = 0; } elsif($rollmethod =~ /^(day|da|dd|d)$/) { # # Set the time values to midnight. # $atoms[0] = 0; $atoms[1] = 0; $atoms[2] = 0; } elsif($rollmethod =~ /^(now|no|n)$/) { # # Nothing to be done for this roll method. # print STDERR "roll method \"now\" is not yet implemented\n"; exit(99); } elsif($rollmethod =~ /^((\d+)\/(\d+)\/(\d+))$/) { # # Make sure the specified date is in the correct format. # if($rollmethod !~ /^((\d{1,2})\/(\d{1,2})\/(\d{2}|20\d{2}))$/) { print STDERR "invalid date \"$rollmethod\" for rolling kronlog\n"; exit(9); } # # Set the date values to January 1 of the current year. # $atoms[4] = int($2) - 1; $atoms[3] = int($3); $atoms[5] = int($4); # # Adjust the year value to fit the expected value. # If it's a four-digit value (2000 - 2099), then we'll # subtract 1900 from it. # If it's a two-digit value (00 - 99), then we'll add 100. # if($atoms[5] >= 2000) { $atoms[5] -= 1900; } else { $atoms[5] += 100; } # # Set the time values to midnight. # $atoms[0] = 0; $atoms[1] = 0; $atoms[2] = 0; } elsif($rollmethod =~ /^((\d+)\/(\d+))$/) { # # Make sure the specified date is in the correct format. # if($rollmethod !~ /^((\d{1,2})\/(\d{1,2}))$/) { print STDERR "invalid date \"$rollmethod\" for rolling kronlog\n"; exit(9); } # # Save the specified month and day. # $mm = int($2) - 1; $dd = int($3); # # Adjust the year value to fit the expected value. # If the month number is less than this month, # use this current year. # If the month number is greater than this month, # use the previous year. # If the month number is the same as this month and # the day number is greater than the current day, # use the previous year. # If the month number is the same as this month and # the day number is not greater than the current day, # use the current year. # # if($atoms[4] == $mm) { if($atoms[5] < $dd) { $atoms[5] -= 1; } } elsif($atoms[4] < $mm) { $atoms[5] -= 1; } # # Set the date values to January 1 of the current year. # The year does not need to be set! # $atoms[4] = $mm; $atoms[3] = $dd; # # Set the time values to midnight. # $atoms[0] = 0; $atoms[1] = 0; $atoms[2] = 0; } else { print STDERR "-roll option must have one of the following arguments: year, month, week, day, now, or a date (mm/dd or mm/dd/yy)\n"; exit(63); } # # Get the epoch time for the rollover point. # $kronos = timelocal(@atoms); return($kronos); } #---------------------------------------------------------------------- # Routine: findrollname() # # Purpose: Build a name for the rolled kronlog. The name will look # like this: # -YYMMDD.roll # is the kron log filename currently # in use. # YYMMDD is the year, month, and day of the # first entry in the kron log. # # If there's already a file with that name, then a two-digit # numeric suffix will be appended: # -YYMMDD-NN.roll # sub findrollname { my @krons = @_; # Krons in the kronlog. my $newname; # The new kronlog name. my $curtime; # Current time. # # Search kron list for the first kron in the file. The timestamp # of that entry will be used in the roll file's name. # foreach my $kron (@krons) { my @atoms = (); # Time atoms. my $epoch; # Epoch time of each kron. # # Skip blank lines and comments. # next if($kron =~ /^\s*\#/); # # Get the epoch time from the first kron. # $kron =~ /^(\d+)\t"(.+)"\t(start|end)/; $epoch = $1; # # Skip this entry if it doesn't match the expected kron format. # next if($epoch eq ''); # # Get the time atoms from the kron's timestamp. # @atoms = localtime($epoch); # # Build the filename for the rolled kron log. # $newname = sprintf("%s-%02d%02d%02d", $kronlogfile, ($atoms[5] - 100), ($atoms[4] + 1), $atoms[3]); last; } # # Ensure we found a valid kron. # if($newname eq '') { print "$kronlogfile does not appear to contain any kronlog data\n"; exit(57); } # # If a file already exists at this name, we'll append a counter # to the end. # if(-e "$newname.roll") { # # We'll look at up to 100 possible file names for any # particular date. # for(my $ind = 0; $ind < 100; $ind++) { my $cname; # New name with counter. # # Build the name and check its existence. # $cname = sprintf("$newname-%02d", $ind); if(! -e "$cname.roll") { $newname = $cname; last; } } } # # Append the roll suffix to the name. # $newname .= ".roll"; return($newname); } #---------------------------------------------------------------------- # Routine: watcher() # # Purpose: Provides an active display of the status. This loops until # killed, displaying the status as if chronosquirrel were # called without any arguments. The status is displayed once # every second. # # In order to keep as fresh a view of things as possible, the # config file will be re-read periodically. If the seconds # flag (provided by command-line argument, config file, or the # program default) is "def" or "on", then the config file is # re-read every ten seconds. If the seconds flag is "off", # then the config file is re-read every 60 seconds. # sub watcher { my $chronos; # Epoch time. my $itercnt = 0; # No-krons-in-log boolean. while(42) { my $taskstat; # Task status. my $outlen = -1; # Output length. my $modder; # Modulus amount. # # Get the current status. # $taskstat = kronstat(0); chomp($taskstat); # # Hoping that using a max length will overwrite everything # and blank out all previous output. # if(length($taskstat) > $outlen) { $outlen = length($taskstat); } printf("%-*s\r", $outlen, $taskstat); # # Decide how often we're going to re-read the config file. # This depends on what we're doing with displaying seconds. # If we're using the default display or are always displaying # seconds, then we'll reconfig every ten seconds. If we # aren't displaying seconds at all, we'll reconfig once a # minute. # $modder = 60; $modder = 5 if(($secsflag == $SECS_DEF) || ($secsflag == $SECS_ON)); # # Re-read the config file, if it's time. # $itercnt++; if(($itercnt % $modder) == 0) { readconfig($configfile); $itercnt = 0; } sleep(1); } } ############################################################################# # # Routines for editting. # # insertkron() Insert a kron entry to the kronlog. # deletekron() Delete a kron entry from the kronlog. # changekron() Change a kron entry in the kronlog. # argtimes() Convert a list of arguments into a time range. # times2krons() Translate a time range into a set of krons. # midnightsplit() Convert kron into kron list, split on midnight. # insertrange() Insert a range of krons into the kron list. # deletekronblock() Delete a kron block from the kronlog. # #---------------------------------------------------------------------- # Routine: insertkron() # # Purpose: Insert a kron entry (or set of entries) into the kronlog. # # The arguments are examined to determine when the new kron block # starts, when it stops, how long it lasts, and the task it's for. # # After these are determined, a set of new kron entries is built # to cover the requested time span. chronosquirrel likes for # kron blocks to stay within the bounds of a single day, so if # any number of midnights are crossed, the kron block is split # so required. # # The current set of krons are searched to determine the right # place to insert the new krons. Once that's found, some checks # are run to ensure things will all fit together properly. # Finally, the new kron block(s) is inserted in the correct place # and the modified kron log is saved to disk. # # TODO: # - handle "last" # - handle "first" # sub insertkron { my $taskname = ''; # Name of new task. my $date1 = ''; # First date in args. my $date2 = ''; # Second date in args. my $time1 = ''; # First time in args. my $time2 = ''; # Second time in args. my $elapsed = ''; # Elapsed time. my @newkrons = (); # New krons to insert. my @krons = (); # Final set of krons. # # Figure out what's what in the arguments. # ($taskname, $date1, $time1, $date2, $time2, $elapsed) = argtimes(@ARGV); # # We've gone through all the arguments, so it's time to put # everything together. # @newkrons = times2krons($taskname, $date1, $time1, $date2, $time2, $elapsed); # # Insert a range of krons into the kron list. # @krons = insertrange(\@newkrons, undef); # # Write the final set of krons to the kron log. # writekrons(@krons); } #---------------------------------------------------------------------- # Routine: deletekron() # # Purpose: Delete a kron entry from the kronlog. # The start and end entries that contain the target time are # found in the kronlog. This can be an open kron block, in # which case only the start entry is found. These entries # are then deleted from the list of krons, and the list is # saved to disk. # # TODO: # - handle "any" # sub deletekron { my $taskname = ''; # Name of new task. my $date1 = ''; # First date in args. my $time1 = ''; # First time in args. my $targettime; # Target time to delete. # # Figure out what's what in the arguments. # ($taskname, $date1, $time1) = argtimes(@ARGV); if($debug) { print "\n"; print "taskname - <$taskname>\n"; print "date1 - <$date1>\n"; print "time1 - <$time1>\n"; print "\n"; } # # Check the arguments and convert the time to an epoch. # # THIS DOES NOT HANDLE THE "any" TIME VALUE. # $targettime = times2krons($taskname, $date1, $time1, '', '', ''); if($debug) { print "targettime - <$targettime>\n"; print "\n"; } # # Delete a range of krons from the kron list. # deletekronblock($taskname, $targettime); } #---------------------------------------------------------------------- # Routine: changekron() # # Purpose: Change a kron entry in the kronlog. # The start and end entries that contain the target time are # found in the kronlog. This can be an open kron block, in # which case only the start entry is found. These entries # are then deleted from the list of krons, and the list is # saved to disk. # # -changekron task date time newtask newdate1 newtime1 # elapsed | newend # # first three identify kron to change # remainder are new kron's pieces # maybe find-n-delete old, then insert new # sub changekron { my $cktask = ''; # Current taskname name of kron. my $ckdate = ''; # Current kron date in args. my $cktime = ''; # Current kron time in args. my $targettime; # Target time to change. my $taskname = ''; # Name of new task. my $date1 = ''; # First date in args. my $date2 = ''; # Second date in args. my $time1 = ''; # First time in args. my $time2 = ''; # Second time in args. my $elapsed = ''; # Elapsed time. my @krons = (); # Kron log. my $startind = -1; # Index to kron block's start. my $endind = -1; # Index to kron block's end. my @args; # Partially handled arguments. my $arg; # An argument from list. my @atoms; # Pieces of time. my @newkrons = (); # New krons to insert. my $equalsused = 0; # Equals sign in args list. my $equalsmax = 0; # Max equals signs allowed. # # Get the arguments that are identifying the kron to be changed. # ($cktask, $ckdate, $cktime, @args) = argtimes(@ARGV); # # Check the arguments and convert the time to an epoch. # $targettime = times2krons($cktask, $ckdate, $cktime, '', '', ''); # # Get the contents of the kron log. # @krons = getkrons(1); # # Find the kron block that the specified time is referring to. # ($startind, $endind) = getkronblock($targettime, @krons); ############################################## # # Get the taskname, start date/time, end date/time, and elapsed # time from the specified kron block. # # # Get the task and time info from the block's start line. # $krons[$startind] =~ /^(\d+)\t"(.*)"\tstart\n$/; $time1 = $1; $taskname = $2; # # Ensure the task from the command line is a valid alias name. # $cktask = cvtalias($cktask); if($cktask ne $taskname) { print "selecting taskname in arguments ($cktask) does not match taskname in kronlog ($taskname)\n"; exit(77); } @atoms = localtime(int($time1)); $date1 = sprintf("%d/%d/%d", ($atoms[4] + 1), $atoms[3], ($atoms[5] - 100)); $time1 = sprintf("%02d:%02d:%02d", $atoms[2], $atoms[1], $atoms[0]); # # Get the time and elapsed info from the block's end line. # if($endind != -1) { $krons[$endind] =~ /^(\d+)\t".*"\tend\t(.+)\n$/; $time2 = $1; $elapsed = $2; @atoms = localtime(int($time2)); $date2 = sprintf("%d/%d/%d", ($atoms[4] + 1), $atoms[3], ($atoms[5] - 100)); $time2 = sprintf("%02d:%02d:%02d", $atoms[2], $atoms[1], $atoms[0]); } ############################################## # # Replace the taskname, start date/time, end date/time, and elapsed # time with values from the argument list. Unless the arg value is # an equals sign, in which case we'll let the original value stand. # # # Ensure that there's something to change the kron block to. # if(@args == 0) { print STDERR "-changekron is missing new values\n"; exit(70); } # # Check the arguments for a taskname. We'll also ensure that # the name is a valid taskname or task alias. # if(($arg = shift @args) ne undef) { if($arg ne '=') { if(($taskname=cvtalias($arg)) eq '') { print STDERR "\"$arg\" is not a valid task name or alias\n"; exit(73); } } else { $equalsused++; } # # If there aren't any more arguments, we're just changing # the taskname -- so we'll reset the end date/time. # if(@args == 0) { $date2 = ''; $time2 = ''; } } # # Check the arguments for an initial date. # if(($arg = shift @args) ne undef) { if($arg ne '=') { $date1 = $arg; } else { $equalsused++; } } # # Check the arguments for an initial time. # if(($arg = shift @args) ne undef) { if($arg ne '=') { $time1 = $arg; } else { $equalsused++; } } # # If there are no arguments left in the argument list, then we'll # simulate that four equal-sign arguments were given. # If there's only one argument left in the argument list, then we're # only going to get the elapsed time. # If there are more than one argument, we'll assume they're the # end date and time. # if(@args == 0) { # # We're only changing the taskname, and no other change # fields were given. We'll pretend that four equal-sign # arguments were given. # $equalsused = 4; } elsif(@args == 1) { # # If there's only one argument left in the argument list, # then we're only going to get the elapsed time. # $arg = shift @args; if($arg ne '=') { $elapsed = $arg; } else { $equalsused++; } # # Reset the pre-set end date and end-time arguments. # $date2 = ''; $time2 = ''; $equalsmax = 4; } elsif(@args > 1) { # # Check the remaining arguments for an end date and end time. # if(($arg = shift @args) ne undef) { if($arg ne '=') { $date2 = $arg; } else { $equalsused++; } } # # Check the arguments for an end time. # if(($arg = shift @args) ne undef) { if($arg ne '=') { $time2 = $arg; } else { $equalsused++; } } # # Reset the elapsed-time argument. # $elapsed = ''; $equalsmax = 5; } # # Ensure that not all the "changed to" arguments are equals signs. # if($equalsused == $equalsmax) { print STDERR "-changekron cannot use all equal-signs for change data\n"; exit(71); } ############################################## # # Build the new kron block from the user's arguments and replace # the selected kron block with the new one. # # # Build a set of krons from the "change to" arguments. # $changeflag = 0; @newkrons = times2krons($taskname, $date1, $time1, $date2, $time2, $elapsed); # # Remove the old kron block from the set of krons. # Do as two separate operations because we don't know # what comments may be lurking in the hearts of krons. # splice(@krons,$endind, 1); splice(@krons,$startind, 1); # # Insert a range of krons into the kron list. # @krons = insertrange(\@newkrons, \@krons); # # Write the final set of krons to the kron log. # writekrons(@krons); } #---------------------------------------------------------------------- # Routine: annotate() # # Purpose: Add a user-specified annotation (comment) to the kronlog. # # If the first argument is an equals sign, then the annotation # will be added in exactly the place the specified time requires. # # If the first argument is a date, then the annotation will be # added immediately before the kron that is precedes the given # time. # # Format: # -annotate [xpst] date time message # sub annotate { my @args = @ARGV; # Command-line args. my $nextarg; # Next argument. my $date1 = ''; # Date in args. my $time1 = ''; # Time in args. my @atoms = (); # Pieces of an argument. my $exact = 0; # Exact-timestamp flag. my $pretty = 0; # Pretty-output flag. my $tstmp = 0; # Add-timestamp flag. my $separator = ''; # Add-separator flag. my $usrepoch; # Epoch of user's timestamp. my $annotation = ''; # Annotation string. my @krons = (); # Kron log. my $ind; # Index into kron list. my $lastind = -1; # Index to previous kron in list. # # Get the current time. # @atoms = localtime(time); #-------------------------------------------------- # # Argument 0 -- options # This is either a set of options or a date. # If it's a date, we'll move on to the next block. # # If it's an option, we'll set the appropriate flag # and remove it from the argument list. # if($args[0] !~ /^\d/) { if($args[0] =~ /x/) { $exact = 1; $args[0] =~ s/x//g; } if($args[0] =~ /p/) { $pretty = 1; $args[0] =~ s/p//g; } if($args[0] =~ /s/) { $separator = "#" . "-" x 79 . "\n"; $args[0] =~ s/s//g; } if($args[0] =~ /t/) { $tstmp = 1; $args[0] =~ s/t//g; } if($args[0] ne '') { print STDERR "invalid -anno options: \"$args[0]\"\n"; exit(52); } shift @args; } #-------------------------------------------------- # # Argument 1 -- date # This may be in either "MM/DD/YY" or "MM/DD" format. # $nextarg = shift @args; if(($nextarg =~ /^\d{1,2}\/\d{1,2}\/\d{1,2}$/) || ($nextarg =~ /^\d{1,2}\/\d{1,2}$/)) { my @datoms; # Date atoms. $date1 = $nextarg; @datoms = split /\//, $date1; # # Set the date fields in the timestamp @atoms. # $atoms[4] = int(@datoms[0]) - 1; $atoms[3] = int($datoms[1]); $atoms[5] = (int($datoms[2]) + 100) if(@datoms == 3); } else { print STDERR "invalid date arguments for -annotate\n"; exit(45); } #-------------------------------------------------- # # Argument 2 -- time # This may be in either "HH:MM:SS" or "HH:MM" format. # $nextarg = shift @args; if(($nextarg =~ /^\d{1,2}\:\d{1,2}\:\d{1,2}$/) || ($nextarg =~ /^\d{1,2}\:\d{1,2}$/)) { my @tatoms; # Date atoms. $time1 = $nextarg; @tatoms = split /\:/, $time1; # # Set the time fields in the timestamp @atoms. # $atoms[2] = int(@tatoms[0]); $atoms[1] = int($tatoms[1]); $atoms[0] = (@tatoms == 3) ? int($tatoms[2]) : 0; } else { print STDERR "invalid time arguments for -annotate\n"; exit(45); } #-------------------------------------------------- # # Find the place to add the annotation in the kron log. # # # Get the epoch time to search for. # $usrepoch = timelocal(@atoms); # # Get the krons list. # @krons = getkrons(1); if($exact) { # # Find the index for the location to insert the user's # annotation. # for($ind = (@krons - 1); $ind >= 0; $ind--) { my $epoch; # Epoch time of each kron. my $cmd; # Command of each kron. next if($krons[$ind] =~ /^\s*\#/); # # Get the epoch time for this kron. # $krons[$ind] =~ /^(\d*)\s/; $krons[$ind] =~ /^(\d+)\t"(.*)"\t(start|end)/; $epoch = $1; $cmd = $3; # # Drop out if this kron's time is before the user's # epoch. # if($epoch < $usrepoch) { $ind = $lastind; last; } # # Save the index of this kron. # $lastind = $ind; } } else { my $lastcmd; # Command of last kron. # # Find the index for the location to insert the user's # annotation. # for($ind = (@krons - 1); $ind >= 0; $ind--) { my $epoch; # Epoch time from kron. my $cmd; # Command from kron. # # Skip non-kron lines. # next if($krons[$ind] =~ /^\s*\#/); # # Get the epoch time for this kron. # $krons[$ind] =~ /^(\d+)\t"(.*)"\t(start|end)/; $epoch = $1; $cmd = $3; # # Drop out if the current kron is the end of the # previous (earlier) kron. We'll also bump the # index back to the next (later) kron. # if(($epoch < $usrepoch) && ($cmd eq 'end')) { $ind = $lastind; last; } # # If this kron matches the specified time and is # also an end kron, we'll move directly to the # previous line in the file. # if(($epoch == $usrepoch) && ($cmd eq 'end')) { next; } # # Save the index of this kron. # $lastind = $ind; $lastcmd = $cmd; } } # # If we couldn't find a matching epoch time, we'll add the annotation # at the beginning of the file. # $ind = $lastind if($ind == -1); #-------------------------------------------------- # # Build the annotation, using the user input and options. # # # Join up the bits to get the annotation. # $annotation = join ' ', @args; # # Add a timestamp, if the 't' option for -annotate was given. # if($tstmp) { my $tstr; # Time string. $tstr = sprintf("%d/%02d/%d %d:%02d:%02d", ($atoms[$MM] + 1), $atoms[$DD], ($atoms[$YY] - 100), $atoms[$HR], $atoms[$MIN], $atoms[$SEC]); $annotation = "$tstr $annotation"; } #-------------------------------------------------- # # Add the annotation (and optional prettiness) to the internal # kron list, then save it to the log file. # # # Add the annotation into the kron log. # if($lastind == -1) { push @krons, "# $annotation\n"; } else { # # Make it pretty, if the 'p' option for -annotate was given. # if($pretty) { splice @krons, $ind, 0, "#\n"; } splice @krons, $ind, 0, "# $annotation\n"; # # Make it pretty again. # if($pretty) { splice @krons, $ind, 0, "#\n"; } # # Add a separator, if the 's' option for -annotate was given. # if($separator ne '') { splice @krons, $ind, 0, $separator; } } # # Write the final set of krons to the kron log. # writekrons(@krons); } #---------------------------------------------------------------------- # Routine: argtimes() # # Purpose: Figure out what the various editting arguments are. # This is used by -insertkron, -deletekron, and -changekron. # # The commands below are the possible ones we recognize. # The various types of arguments are also described below. # # The insert command lines look like: # # -insert task now 2:30 # # -insert task 5/20/18 10:00 2:30 # -insert task 5/20/18 now 2:30 # -insert task 5/20/18 last 2:30 # # -insert task 5/20/18 10:00-12:30 # -insert task 5/20/18 last-12:30 # -insert task 5/20/18 now-12:30 # # -insert task 5/20/18 5/23/18 # # -insert task 5/20/18 10:00 5/23/18 12:00 # -insert task 5/20/18 now 5/23/18 12:00 # -insert task 5/20/18 last 5/23/18 12:00 # # The delete command lines look like: # # -deletekron taskname date time # -deletekron taskname date all # # The change command lines look like: # # -changekron taskname date time # # is the same as the arguments # to -insertkron. # # Possible argument values: # # arg1: task name (always) # # arg2: date # "now" # "last" # # arg3: time # time range # date # "now" # "last" # "all" delete only # # arg4: elapsed arg3 is time, "now", or "last"; # no arg5 # date arg3 is time, "now", or "last" # # arg5: time arg4 is date # "first" arg4 is date # # arg4 and arg5 are insert only. # sub argtimes { my @args = @_; # Command-line args. my $taskname = ''; # Name of new task. my $date1 = ''; # First date in args. my $date2 = ''; # Second date in args. my $time1 = ''; # First time in args. my $time2 = ''; # Second time in args. my $elapsed = ''; # Elapsed time. my $nextarg; # Next argument. my @atoms = (); # Pieces of an argument. # # We need at least three arguments for any -insertkron operation. # We need exactly three arguments for any -deletekron operation. # if($insertflag && (scalar(@args) < 3)) { print "taskname, start datetime, and {elapsed time | end datetime} are required to insert a new kron\n"; exit(72); } elsif($deleteflag && (scalar(@args) != 3)) { print "taskname, date, and time are required to delete a kron\n"; exit(73); } elsif($changeflag && (scalar(@args) < 4)) { print "taskname, date, and time, and change data are required to modify a kron\n"; exit(74); } #-------------------------------------------------- # # Argument 1 -- task name # # # Get the required task name argument. We'll also ensure it's # valid and active. # $taskname = shift @args; # # If the taskname isn't an actual task name, we'll check to see # if it's a task alias. If so, we'll quietly change the new kron's # alias name to be the associated task name. # if(!defined($tasknames{$taskname})) { # # Check if the taskname is an alias in the alias list. # if(! defined($aliases{$taskname})) { print STDERR "unknown task or alias \"$taskname\"\n"; exit(95); } # # Translate the alias to its taskname. # $taskname = $aliases{$taskname}; } if($actives{$taskname} == $INACTIVE) { print STDERR "inactive task \"$taskname\"\n"; exit(96); } #-------------------------------------------------- # # Argument 2 -- date, "now", or "last" # $nextarg = shift @args; $nextarg = lc($nextarg); if(($nextarg =~ /^\d{1,2}\/\d{1,2}\/\d{1,2}$/) || ($nextarg =~ /^\d{1,2}\/\d{1,2}$/)) { # # If the second argument is a date, we'll use it # as the start date. # $date1 = $nextarg; } elsif($nextarg eq "now") { # # If the second argument is now, we'll use the current # time and date. # $date1 will be set to the current date. # The colonified version of the current time will be put # at the beginning of the argument list and handled next. # @atoms = localtime(time); $date1 = sprintf("%d/%d/%d", ($atoms[4] + 1), $atoms[3], ($atoms[5] - 100)); $nextarg = sprintf("%02d:%02d:%02d", $atoms[2], $atoms[1], $atoms[0]); unshift @args, $nextarg; } elsif($nextarg eq "last") { # # If the second argument is "last", we'll use the time # right after the current date's last recorded time. # # # nothing to do here just now... # # # need to figure out what to do if last kron is open # } else { print STDERR "unknown date field \"$nextarg\"\n"; exit(71); } #-------------------------------------------------- # # Argument 3 -- time, time range, date, "now", or "last" # $nextarg = shift @args; $nextarg = lc($nextarg); # # If time1 is a range, we'll split it into time1 and time2. # date1 and time2 are put on the argument list to be the end kron. # if($nextarg =~ /\-/) { if($changeflag) { print "time ranges should not be given when changing a kron\n"; exit(71); } @atoms = split /-/, $nextarg; if(@atoms > 2) { print STDERR "invalid time range \"$nextarg\"\n"; exit(90); } # # There should be no more arguments if we've got a range. # if(@args > 0) { print STDERR "extraneous arguments on command line\n"; exit(91); } # # Handle the first time in the range, and set up the # rest of the time range in the arguments. # $nextarg = $atoms[0]; @args = ($date1, $atoms[1]); } # # Check the first time in the range for an actual time, a date, # or one of the "special" times. # if(($nextarg =~ /^\d{1,2}:\d{1,2}:\d{1,2}$/) || ($nextarg =~ /^\d{1,2}:\d{1,2}$/)) { # # time1 is just a time, not a date or a special time. # # XXX probably can delete this line # @atoms = split ':', $nextarg; $time1 = $nextarg; } elsif($nextarg eq "last") { # # time1 is right after the date's last recorded time. # $time1 = $nextarg; } elsif($nextarg eq "now") { # # time1 is "now", so we'll get the time. # # XXX probably can delete this line # @atoms = localtime(time); $time1 = $nextarg; } elsif(($nextarg =~ /^\d{1,2}\/\d{1,2}\/\d{1,2}$/) || ($nextarg =~ /^\d{1,2}\/\d{1,2}$/)) { # # If the third argument isn't a time, we'll assume it's a # date and that we've got a date range. # $date2 = $nextarg; } else { print STDERR "unknown time or date \"$nextarg\"\n"; exit(70); } # # If we're deleting a kron, then we'll return here. We'll also # ensure that the only arguments we were given were a taskname, # a date, and a time. # if($deleteflag) { if($date2 ne '') { print "end dates should not be given when deleting a kron\n"; exit(71); } if(scalar(@args) > 0) { print "time ranges should not be given when deleting a kron\n"; exit(71); } return($taskname, $date1, $time1); } # # If we're changing a kron, then we'll return here. We'll also # ensure that the only arguments we were given were a taskname, # a date, and a time. # if($changeflag) { if($date2 ne '') { print "end dates should not be given when changing a kron\n"; exit(71); } return($taskname, $date1, $time1, @args); } #-------------------------------------------------- # # Argument 4 -- elapsed time or date # $nextarg = shift @args; # print "arg4: <$nextarg>\n"; if(($nextarg =~ /^\d{1,2}:\d{1,2}:\d{1,2}$/) || ($nextarg =~ /^\d{1,2}:\d{1,2}$/)) { # # Argument 4 is the elapsed time. # Calculate the number of seconds required. # # XXX probably can delete these lines # @atoms = split ':', $nextarg; # $elapsed = ($atoms[0] * 3600) + ($atoms[1] * 60) + $atoms[2]; $elapsed = $nextarg; } elsif(($nextarg =~ /^\d{1,2}\/\d{1,2}\/\d{1,2}$/) || ($nextarg =~ /^\d{1,2}\/\d{1,2}$/)) { # # If the fourth argument isn't a time, it better be the # end date. # $date2 = $nextarg; } else { if($date2 eq '') { print STDERR "missing elapsed-time/end-datetime argument\n"; exit(93); } } #-------------------------------------------------- # # Argument 5 -- time or "first" # $nextarg = shift @args; $nextarg = lc($nextarg); if(($nextarg =~ /^\d{1,2}:\d{1,2}:\d{1,2}$/) || ($nextarg =~ /^\d{1,2}:\d{1,2}$/)) { # # Argument 5 is a time. # if($date2 eq '') { # XXX Will this ever happen? Can condition be deleted? # XXX probably can delete these lines # @atoms = split ':', $nextarg; # $elapsed = ($atoms[0] * 3600) + ($atoms[1] * 60) + $atoms[2]; $elapsed = $nextarg; } else { $time2 = $nextarg; } } elsif($nextarg eq 'first') { $date2 = $nextarg; } return($taskname, $date1, $time1, $date2, $time2, $elapsed); } #---------------------------------------------------------------------- # Routine: times2krons() # # Purpose: Build a set of new krons from the argument list. # # In most cases, a list of krons will be returned. However, # in early processing of changing or deleting a kron entry, # we'll return an epoch time. # sub times2krons { my $taskname = shift; # Name of new task. my $date1 = shift; # First date in args. my $time1 = shift; # First time in args. my $date2 = shift; # Second date in args. my $time2 = shift; # Second time in args. my $elapsed = shift; # Elapsed time. my $starttime = 0; # Start-time epoch. my $endtime = 0; # End-time epoch. my $endstr = ''; # End-kron string. my $tdiff; # End/start difference. my @atoms = (); # Pieces of an argument. my @tempus = (); # Pieces of time date. my @kronos = (); # End time fields. my @newkrons = (); # New krons to insert. # # Get the current time. # @tempus = localtime(time); ############################################################# # # Get the pieces of the user's first date argument. # # # Ensure we've got something for the first date. # if($date1 eq '') { print STDERR "missing date\n"; exit(73); } # # Split the date into its pieces. # @atoms = split '/', $date1; # # Put the month and day into the time structure. # $tempus[4] = $atoms[0] - 1; $tempus[3] = $atoms[1]; # # If the date has a year, we'll add that in as well. This assumes the # year is after 1999. Some might think this is an unreasonable # assumption. I don't care. # if(@atoms == 3) { $tempus[5] = $atoms[2] + 100; } elsif(@atoms != 2) { print STDERR "woof 1 invalid date - \"$date1\"\n"; exit(74); } ############################################################# # # Get the pieces of the user's first time argument. # # If time1 is 'now', then we can leave the time fields alone, # as they were picked up in the localtime() call above. # # # Ensure we've got something for the first time. # if($time1 eq '') { $time1 = 'last'; } if($time1 eq 'last') { print "time \"last\" is not yet implemented\n"; exit(0); } # # Handle an actual time, rather than one of the special times. # if(($time1 ne 'now') && ($time1 ne 'last')) { # # Split the time into its pieces. # @atoms = split ':', $time1; if((@atoms != 2) && (@atoms != 3)) { print STDERR "invalid time - \"$time1\"\n"; exit(75); } # # Put the hours, minutes, and seconds into the time structure. # $tempus[2] = $atoms[0]; $tempus[1] = $atoms[1]; $tempus[0] = $atoms[2]; } # # Convert the start date and start time into an epoch time and # build the start line. # $starttime = timelocal(@tempus); push @newkrons, "$starttime\t\"$taskname\"\tstart\n"; # # If we're changing or deleting a kron, then we've got all we need. # if($changeflag || $deleteflag) { return($starttime); } # # If there's no end-times given, we'll return the start line # that we've put together. # if(($date2 eq '') && ($time2 eq '') && ($elapsed eq '')) { # print "\n\n--------> no end-times in sight; returning new kron list\t($changeflag/$deleteflag)\n\n"; return(@newkrons); } ############################################################# # # Figure out how to construct the end time. # If the elapsed time was given, then we'll add that to the start # time to get the end time. # If the elapsed time wasn't given, then we'll calculate the end # time from the end date and end time. # if($elapsed ne '') { # # Split the elapsed time into its pieces. # @atoms = split ':', $elapsed; if((@atoms != 2) && (@atoms != 3)) { print STDERR "invalid elapsed time - \"$elapsed\"\n"; exit(76); } # # Calculate the elapsed time and end time. # $elapsed = (int($atoms[0]) * 3600) + (int($atoms[1]) * 60) + int($atoms[2]); $endtime = $starttime + $elapsed; } else { # # Get a copy of the time value and then set the various fields. # @kronos = @tempus; # # Split the second date into its pieces. # @atoms = split '/', $date2; # printf("date2 atoms : %02d %02d %02d\n", $kronos[4], $kronos[3], $kronos[5]); # # Put the month and day into the time structure. # $kronos[4] = $atoms[0] - 1; $kronos[3] = $atoms[1]; # # If the date has a year, we'll add that in as well. This # assumes the year is after 1999. Some might think this is # an unreasonable assumption. I don't care. # if(@atoms == 3) { $kronos[5] = $atoms[2] + 100; } elsif(@atoms != 2) { print STDERR "woof 2 invalid date - \"$date2\"\n"; exit(77); } # # Get the time fields. # @atoms = split ':', $time2; # printf("time2 atoms : %02d %02d %02d\n", $kronos[2], $kronos[1], $kronos[0]); # # Ensure we got one of the proper types of times. # if((@atoms != 2) && (@atoms != 3)) { if($time2 eq '') { print STDERR "missing time value\n"; } else { print STDERR "invalid time - \"$time2\"\n"; } exit(78); } # # Put the hours, minutes, and seconds into the time structure. # $kronos[2] = $atoms[0]; $kronos[1] = $atoms[1]; $kronos[0] = $atoms[2]; # # Convert the end date and end time into an epoch time. # $endtime = timelocal(@kronos); } # # Ensure that the end time is not before the start time. # if($endtime < $starttime) { print STDERR "error: end time is before start time\n"; exit(80); } # # Ensure that the end time is not the same as the start time. # if($endtime == $starttime) { print STDERR "error: start time is the same as the end time\n"; exit(81); } # # Calculate the different between the start time and end time, # and convert it to a colon time. # $tdiff = $endtime - $starttime; $tdiff = timecolon($tdiff, 1); # # Build the end line. # $endstr = sprintf("$endtime\t\"$taskname\"\tend\t$tdiff\n"); ############################################################# # # Check for midnight crossings. # push @newkrons, midnightsplit($taskname, $starttime, $endtime, 1); if($debug) { print "new krons:\n"; foreach my $k (@newkrons) { print "$k"; } print "done\n"; print "new krons w/ time:\n"; foreach my $k (@newkrons) { my $t; my $r; $k =~ /^(\d+)(.*)$/; $t = $1; $r = $2; $t = localtime($t); print "$t: $r\n"; } print "done\n"; } # # Return our list of new krons, all set for adding to the kronlog. # return(@newkrons); } #---------------------------------------------------------------------- # Routine: midnightsplit() # # Purpose: This routine takes a start and end time, with a taskname, and # builds a list of krons that don't cross a midnight boundary. # The list of krons is returned. # # If the two times are both in the same day, then a single # start/end pair of entries is returned. # # If the two times are in different days, then enough start/end # pairs are created to cover the full timespan. This is done # if the times are for consecutive days or if there are multiple # days separating them. # sub midnightsplit { my $taskname = shift; # Current task. my $starttime = shift; # Start time of this task. my $endtime = shift; # End time of this task. my $closed = shift; # Closed flag. my @tempus = (); # Pieces of time date. my @kronos = (); # End time fields. my @newkrons = (); # New krons to insert. my $endstr = ''; # End-kron string. my $tdiff; # End/start difference. # # Get the time atoms for the start time and the end time. # @tempus = localtime($starttime); @kronos = localtime($endtime); # # If these two times are on the same day, the times don't cross one # or more midnights. In this case, we'll add the end time to the # list of kron entries and return. # if(($tempus[3] == $kronos[3]) && ($tempus[4] == $kronos[4]) && ($tempus[5] == $kronos[5])) { $tdiff = $endtime - $starttime; $tdiff = timecolon($tdiff, 1); $endstr = sprintf("$endtime\t\"$taskname\"\tend\t$tdiff\n"); push @newkrons, $endstr; return(@newkrons); } # # This block crosses midnight, so we'll split it into a set of # smaller blocks. # while(42) { my $epoch; # Epoch time. my $kronstr; # Kron entry line. # # Get the epoch time of this day's end-of-day. # $tempus[0] = 59; $tempus[1] = 59; $tempus[2] = 23; # # If it's past the end time, we'll pull it back to the end. # $epoch = timelocal(@tempus); if($epoch > $endtime) { $epoch = $endtime; } # # Get the difference in seconds between the two times # in this kron block. # $tdiff = $epoch - $starttime; $tdiff = timecolon($tdiff, 1); # # Build the end entry and add it to our list of new krons # iff the kron should be closed. # if($closed) { $kronstr = sprintf("$epoch\t\"$taskname\"\tend\t$tdiff\n"); push @newkrons, $kronstr; } # # If we've hit the end of the new krons, we'll hop out. # last if($epoch == $endtime); # # If we're here, $epoch is an end-of-day epoch time. We'll # increment it to get midnight of the next day. # $epoch++; # # Build a new start kron and add it to our list of new krons. # $kronstr = sprintf("$epoch\t\"$taskname\"\tstart\n"); push @newkrons, $kronstr; # # Get the time bits for the next day's start. # @tempus = localtime($epoch); $starttime = $epoch; } # # Return our list of new krons, all set for adding to the kronlog. # return(@newkrons); } #---------------------------------------------------------------------- # Routine: insertrange() # # Purpose: Insert a range of krons into the kron list. # The kron list will either be read from disk or will be # provided by the caller. # The resulting list will be returned to the caller, but # not written out to disk. # # This starts by finding the proper location at which the # new krons should be inserted. # Then there are error checks to ensure that: # - the new krons aren't inserted inside an existing # start/end block # - the new krons aren't interleaved with existing # krons # - the first and last new krons don't match exactly # some existing krons # - the new krons don't wrap an existing kron block # sub insertrange { my $newkrons = shift; # Ref to list of new krons. my $krons = shift; # Ref to list of old krons. my @newkrons; # List of new krons. my @krons; # List of new krons. my $firstkron; # First new kron. my $lastkron; # Last new kron. my $firstepoch; # First new kron's time. my $lastepoch; # Last new kron's time. my @krons = (); # Lines from kron log. my $ultkron; # Last current kron. my $ultepoch = -1; # Last current kron's time. my $ultind = -1; # Index for last current kron. my $prevind = -1; # Index to insert after. my $nextind = -1; # Index to insert before. my $krn1; # Epoch from previous kron. my $cmd1; # Command from previous kron. my $krn2; # Epoch from next kron. my $cmd2; # Command from next kron. my $nkrn1; # Epoch from first new kron. my $nkrn2; # Epoch from last new kron. # # Get the list of new krons. # @newkrons = @$newkrons; # # Set up the local krons list. # If the $krons parameter was not defined, we'll read the kron log. # If the $krons parameter was defined, we'll use that list. # if($krons eq undef) { @krons = getkrons(1); } else { @krons = @$krons; } # # Get the first and last krons to be inserted. # $firstkron = $newkrons[0]; $lastkron = $newkrons[-1]; chomp $firstkron; chomp $lastkron; # # Dig out the epoch times from the first and last krons. # $firstkron =~ /^(\d*)\t/; $firstepoch = $1; $lastkron =~ /^(\d*)\t/; $lastepoch = $1; # # Find the indices for the krons we'll insert things after and before. # for(my $ind = (@krons - 1); $ind >= 0; $ind--) { my $epoch; # Epoch time of each kron. next if($krons[$ind] =~ /^\s*\#/); # # Save info about the last existing kron in the kronlog. # if($ultepoch == -1) { $ultkron = $krons[$ind]; $ultind = $ind; $ultkron =~ /^(\d+)\t/; $ultepoch = $1; } # # Get the epoch time for this kron. # $krons[$ind] =~ /^(\d*)\t/; $epoch = $1; # # Save this kron index as the previous-entry index. # $prevind = $ind; # # Drop out if this existing kron is before the first new kron. # last if($epoch < $firstepoch); # # Save this kron index as the next-entry index. # $nextind = $ind; } # # If the previous-kron and next-kron indices are the same, then # we've gone to the beginning of the kronlog. We'll force the # previous-kron index to 0, in order to look for the first non- # comment line. # if($prevind == $nextind) { $prevind = 0; } # # If the previous-kron and ultimate-kron indices are the same, then # we've the new krons must be added to the end of the kronlog. # if($prevind == $ultind) { # # Ensure we aren't trying to add new entries into an # open kron block. # $krons[$prevind] =~ /^\d+\t".*"\t(start|end)/; if($1 eq 'start') { print STDERR "cannot insert entries into an open kron block\n"; exit(98); } # # Add the new kron entries to the end of the kronlog. # # $newkrons = join("\n", @newkrons) . "\n"; $newkrons = join("", @newkrons); push @krons, $newkrons; # # Return the set of krons to the caller. # return(@krons); } # # If we're at the beginning of the file and the first line is # a comment, we'll insert at the end of the comment header. # if(($prevind == 0) && ($krons[$prevind] =~ /^\s*#/)) { for(my $nextind = 1; $nextind < @krons; $nextind++) { last if($krons[$nextind] !~ /^\s*\#/); $prevind = $nextind; } } # # Get the epochs and commands from the previous and next krons. # $krons[$prevind] =~ /^(\d+)\t".*"\t(start|end)/; $krn1 = $1; $cmd1 = $2; $krons[$nextind] =~ /^(\d+)\t".*"\t(start|end)/; $krn2 = $1; $cmd2 = $2; # # Ensure that none of the new krons have an epoch time that # exactly matches one of the existing times. # for(my $ind = 0; $ind < @newkrons; $ind++) { my $nkrn; # New kron's epoch. $newkrons[$ind] =~ /^(\d+)\t".*"\t(start|end)/; $nkrn = $1; if(($nkrn == $krn1) || ($nkrn == $krn2)) { print STDERR "an entry in the new kron block exactly matches an existing kronlog entry\n"; exit(99); } } # # Ensure the new krons aren't going in the middle of a kron block. # if(($cmd1 eq 'start') && ($cmd2 eq 'end')) { print STDERR "cannot insert entries inside a kron block\n"; exit(99); } # # Get the epochs from the first and last new krons. # $newkrons[0] =~ /^(\d+)\t".*"\t(start|end)/; $nkrn1 = $1; $newkrons[-1] =~ /^(\d+)\t".*"\t(start|end)/; $nkrn2 = $1; # # Check if the new kron block contains an existing kron entry. # if(($nkrn1 <= $krn1) && ($nkrn2 >= $krn2)) { print STDERR "new kron block contains an existing kronlog entry\n"; exit(99); } # # Check if the new kron block crosses an existing kron entry. # if(($nkrn1 < $krn1) && ($nkrn2 >= $krn1) || ($nkrn1 < $krn2) && ($nkrn2 >= $krn2)) { print STDERR "new kron block crosses into an existing kronlog entry\n"; exit(99); } # # Build a string of the new krons and squeeze it into the existing # list of krons. # $newkrons = join("", @newkrons); splice(@krons, $nextind, 0, $newkrons); # # Return the set of krons to the caller. # return(@krons); } #---------------------------------------------------------------------- # Routine: deletekronblock() # # Purpose: Delete a kron block from the kron list. # We'll find the start and end entries that contain the target # time. This can be an open kron block at the end of the file, # in which case only the start entry is found. These entries # are then deleted from the list of krons, and the list is # saved to disk. # # The task name specified by the user must match the name given # in the kron entries. Strictly speaking, this isn't necessary, # but it might help ensure that the correct kron block will be # deleted. # sub deletekronblock { my $taskname = shift; # Task in kron block to delete. my $targettime = shift; # Time in kron block to delete. my @krons = (); # Lines from kron log. my $newkrons; # String of new krons. my $startind = -1; # Index of block's start entry. my $startkron; # Epoch from start kron. my $starttask; # Taskname from start kron. my $endind = -1; # Index of block's end entry. my $endkron; # Epoch from end kron. my $endtask; # Taskname end next kron. # # Get the contents of the kron log. # @krons = getkrons(1); # # Find the indices for the krons we'll insert things after and before. # for(my $ind = (@krons - 1); $ind >= 0; $ind--) { my $epoch; # Epoch time of each kron. my $krontask; # Taskname of each kron. my $cmd; # Command from each kron. next if($krons[$ind] =~ /^\s*\#/); # # Get the epoch time and taskname for this kron. # $krons[$ind] =~ /^(\d+)\t"(.+)"\t(start|end)/; $epoch = $1; $krontask = $2; $cmd = $3; # # Get the info for the start and end entries, depending on # where we are in the scan. # # Compare the user's target time to the time on this kron # entry. By looking at a number of entries, we'll find # the relevant kron block's start entry and end entry. # if($targettime > $epoch) { # # The requested time is later than this entry's epoch # field. # If it's a start line, we've got the start and end # time for the block to be deleted. # If it's an end line, then the requested time is not # in a kron block. This is a user error. # if($cmd eq 'end') { print STDERR "Given time is not in a kron block; cannot delete.\n"; exit(80); } $startind = $ind; $startkron = $epoch; $starttask = $krontask; last; } elsif($targettime == $epoch) { # # The requested time matches this entry's epoch field. # If it's a start line, we've got the start and end # time for the block to be deleted. # If it's an end line, we'll save the info and look at # the next line. # if($cmd eq 'start') { $startind = $ind; $startkron = $epoch; $starttask = $krontask; last; } else { $endind = $ind; $endkron = $epoch; $endtask = $krontask; } } else { # # This might be the end line for the relevant kron # block. It might not be. We'll save the info in # case the next line we look at is the start line. # $endind = $ind; $endkron = $epoch; $endtask = $krontask; } } # # Ensure that the user's specified time does not come before the # earliest kron block. (In this file, that is.) # if(($startind == -1) || ($starttask eq '')) { print STDERR "Given time is before any recorded kron block; can't delete imaginary krons.\n"; print STDERR "Yet.\n" if($verbose); exit(81); } # # Ensure the kron entries have the proper task name. # if($starttask ne $taskname) { print STDERR "Kron start taskname ($starttask) does not match\nuser-specified taskname ($taskname).\n"; exit(82); } if(($endind != -1) && ($endtask ne $taskname)) { print STDERR "Kron end taskname ($endtask) does not match\nuser-specified taskname ($taskname).\n"; exit(83); } # # Delete the selected kron entry/entries. # if($endind == -1) { # # If we're deleting an open (start-line only) kron block, all # we need to do is delete start line. # splice(@krons, $startind, 1); } else { # # If we're deleting a closed kron block, we've got to delete # the end kron entry and then delete the start kron entry. # The ordering of these is important. # splice(@krons, $endind, 1); splice(@krons, $startind, 1); } # # Write the set of krons to the kron log. # writekrons(@krons); } ############################################################################# # # Utility routines. # # baklogfile() Backup the kron log file. # checkup() Run validation checks. # checktasks() Check task definitions for errors. # endkron() Close an active kron block # getkronblock() Return indices of kron block containing epoch. # getkrons() Read a kronlog file. # newkronlog() Create a new kronlog file. # outtime() Format epoch time for human reading. # selectday() Get epoch time of a selected day at midnight. # timecolon() Format time difference for human reading. # tasksort() Sort tasks in our special order. # validtaskname() Ensure taskname has valid characters. # writekrons() Replace contents of kron log file. # version() Give version message and exit. # usage() Give usage message and exit. # #---------------------------------------------------------------------- # Routine: baklogfile() # # Purpose: Back up the kronlog file by moving it in its entirety to a # new filename. We'll build a new filename and then rename # the kronlog to the backup file. # # THIS DOES NOT COPY THE KRONLOG FILE! # # We'll try to back up to a file named by appending ".bak" to # the kronlog filename. If a file already exists with that # name, we'll try some others by appending a series of two-digit # numbers (00-99) to the default filename. # sub baklogfile { my $bakname; # Name of backup file. my $rc; # Return code from rename. # # Build the default backup filename. # $bakname = $kronlogfile . ".bak"; # # If the default backup file exists, we'll try some others by # appending a two-digit number to the default. # if(-e $bakname) { my $ind; # Loop index. # # Build a digitized backup filename (from 00 to 99), and # we'll use the first that doesn't exist. # for($ind = 0; $ind < 100; $ind++) { my $numname; # Numbered backup file. $numname = sprintf("$bakname.%02d", $ind); # # If this filename isn't in use, it'll be our # backup file. # if(! -e $numname) { $bakname = $numname; last; } } # # If all the backup filenames are in use, we'll complain # and exit. # if($ind == 100) { print STDERR "unable to back up $kronlogfile; too many logfile backups\n"; exit(73); } } # # If all the backup filenames are in use, we'll complain and exit. # if($verbose) { print "backing up $kronlogfile to $bakname\n"; } $rc = rename $kronlogfile, $bakname; if($rc == 0) { print STDERR "unable to backup $kronlogfile to $bakname\n"; exit(74); } } #---------------------------------------------------------------------- # Routine: checkup() # # Purpose: Run validation checks to ensure things are hunky dory. # The configuration file and the kronlog must be checked. # # The kronlog tests are divided into two groups. The first # group tests for problems that cannot be fixed automatically. # If any of these tests fail, the check-up will stop after # all the tests have run. # If all of these tests pass, then the second group of tests # will be run, and they will try to fix errors. # # Due to how things are coded, we don't need to check the # config file now because it was checked when it was read # at the start. # # chronosquirrel kronlog test group 1: # - kronlog exists (by getkrons()) # - kronlog is readable (by getkrons()) # - kronlog is writable (by getkrons()) # - 1.1: entries have bad format # - 1.2: undefined taskname used # - 1.3: stop time <= start time # - 1.4: missing lines (consecutive starts or stops) # # chronosquirrel kronlog test group 2: # - 2.1: alias used instead of taskname # - 2.2: convert old tdiff -> new tdiff # - 2.3: kron block spans midnight (split into two) # - 2.4: tdiff does not match (stop-start) # # chronosquirrel kronlog test group 3: # - 3.1: all times are unique and monotonically ascending # sub checkup { my @krons; # List of kron entries. my @badkrons; # List of errant kron entries. my $errs = 0; # Error count. my $fixes = 0; # Count of kronlog fixes. my $starttime; # Start of a kron block. # # Get the contents of the kronlog, without comments and blank lines. # @krons = getkrons(0); #################################################################### # # Phase 1: These checks will not fix problems in the kronlog. # # # Test 1.1: Ensure that the kron entries fit the proper format. # print "Test 1.1: Checking kronlog entries for valid format\n"; for(my $ind = 0; $ind < @krons; $ind++) { my $kron; # Kron to examine. chomp $krons[$ind]; $kron = $krons[$ind]; if(($kron =~ /^(\d+)\t(".*")\t(start|end)(\t(.+))?/) == 0) { print "\tinvalid format: $kron\n"; $krons[$ind] = ''; $errs++; } } ################################################ # # Test 1.2: Ensure that only defined tasknames are used. # my %badtasks = (); # Invalid tasknames. my %undeftasks = (); # Undefined tasknames. my %lctasks = (); # Lowercase tasknames. print "Test 1.2: Ensuring only defined tasknames are used\n"; # # Get a set of valid tasknames that are all lowercase. # for my $tn (keys(%tasknames)) { $lctasks{lc($tn)} = 1; } # # Look through the krons for any invalid tasknames. # for(my $ind = 0; $ind < @krons; $ind++) { my $kron = $krons[$ind]; # Kron to examine. my $taskname; # Entry's taskname. # # Skip entries that were found to be in error. # next if($kron eq ''); # # Get the entry's taskname and remove its double-quotes. # $kron =~ /^(\d+)\t(".*")\t(start|end)(\t(.+))?/; $taskname = $2; $taskname =~ s/"//g; # # If this taskname isn't defined, increment that name's # count in the hash of bad tasks. # if(! defined($lctasks{lc($taskname)})) { $undeftasks{$taskname}++; $errs++; } # # If this taskname isn't defined, increment that name's # count in the hash of bad tasks. # if(validtaskname($taskname) == 0) { $badtasks{$taskname}++; $errs++; } } # # Report any undefined tasknames being used. # for my $tn (sort(keys(%undeftasks))) { print "\tundefined taskname: $tn\t\tuse count: $undeftasks{$tn}\n"; } # # Report any invalid tasknames being used. # for my $tn (sort(keys(%badtasks))) { print "\tbad taskname: $tn\t\tuse count: $badtasks{$tn}\n"; } ################################################ # # Test 1.3: Kron timestamps should be strictly ascending. # This test ensures that this is so. # my $prevtstmp = -1; # Previous timestamp. print "Test 1.3: Checking timestamps for ascending temporal order\n"; for(my $ind = 0; $ind < @krons; $ind++) { my $kron = $krons[$ind]; # Kron to examine. my $chronos; # Entry's timestamp. # # Skip entries that were found to be in error. # next if($kron eq ''); # # Get the entry's timestamp. # $kron =~ /^(\d+)\t(".*")\t(start|end)(\t(.+))?/; $chronos = $1; # # Skip the first kron, but save its timestamp. # if($ind == 0) { $prevtstmp = $chronos; next; } # # Check this kron's timestamp against the previous one. # if($chronos < $prevtstmp) { print "\ttimestamp $chronos precedes previous entry's timestamp $prevtstmp\n"; $krons[$ind] = ''; $errs++; } elsif($chronos == $prevtstmp) { print "\ttimestamp $chronos matches previous entry's timestamp $prevtstmp\n"; $krons[$ind] = ''; $errs++; } else { $prevtstmp = $chronos; } } ################################################ # # Test 1.4: Check for incomplete kron blocks. This is indicated by # consecutive starts or stops, or start/end pairs that # have different task names. # print "Test 1.4: Checking for incomplete kron blocks\n"; my $prevtask = ''; # Previous taskname. for(my $ind = 0; $ind < @krons; $ind += 2) { my $kron1 = $krons[$ind]; # Start kron to examine. my $kron2 = $krons[$ind + 1]; # End kron to examine. my $tstmp1; # 1st entry's timestamp. my $tstmp2; # 2nd entry's timestamp. my $task1; # 1st entry's task. my $task2; # 2nd entry's task. my $cmd1; # 1st entry's command. my $cmd2; # 2nd entry's command. # # Skip entry pairs that were found to be in error. # next if(($kron1 eq '') || ($kron2 eq '')); # # Get the entries' command fields. # $kron1 =~ /^(\d+)\t(".*")\t(start|end)(\t(.+))?/; $tstmp1 = $1; $task1 = $2; $cmd1 = $3; $kron2 =~ /^(\d+)\t(".*")\t(start|end)(\t(.+))?/; $tstmp2 = $1; $task2 = $2; $cmd2 = $3; # # If the first entry is a start, the second entry is an end, # and they both have matching tasknames, then these entries # are assumed to be okay # if(($cmd1 eq 'start') && ($cmd2 eq 'end') && ($task1 eq $task2)) { next; } # # It's an error if both entries are starts. # if(($cmd1 eq 'start') && ($cmd2 eq 'start')) { print "\tkron pair for timestamps $tstmp1/$tstmp2 are both \"start\"\n"; $krons[$ind] = ''; $krons[$ind + 1] = ''; $errs++; next; } # # It's an error if both entries are ends. # if(($cmd1 eq 'end') && ($cmd2 eq 'end')) { print "\tkron pair for timestamps $tstmp1/$tstmp2 are both \"end\"\n"; $krons[$ind] = ''; $krons[$ind + 1] = ''; $errs++; next; } # # It's an error if the entries have different tasknames. # if($task1 ne $task2) { print "\tkron pair for timestamps $tstmp1/$tstmp2 have mismatched tasknames\n"; $krons[$ind] = ''; $krons[$ind + 1] = ''; $errs++; next; } } # # If we hit any errors, we cannot proceed to the additional tests. # if($errs) { print "checkup: errors in first phase of tests; cannot continue\n"; exit(0); } #################################################################### # # Phase 2: These checks fix problems in the kronlog. # # # Get a fresh copy of the kronlog, this time with the comments and # blank lines. # @krons = getkrons(1); ################################################ # # Test 2.1: If a task alias was used instead of the taskname, we'll # replace the alias with the name. # print "Test 2.1: Changing alias use to tasknames\n"; for(my $ind = 0; $ind < @krons; $ind++) { my $kron = $krons[$ind]; # Kron to examine. my $chron; # Entry's timestamp. my $taskname; # Entry's taskname. my $remainder; # Rest of entry. # # Skip comment and blank lines. # next if(($kron =~ /^\s*\#/) || ($kron =~ /^\s*$/)); # # Get the entry's fields and strip off the double-quotes # from around the taskname. # $kron =~ /^(\d+)\t(".*")\t((start|end)(\t(.+))?)/; $chron = $1; $taskname = $2; $remainder = $3; $taskname =~ s/"//g; # # If this taskname is really an alias, change the alias # to its related taskname. # if(defined($aliases{$taskname})) { chomp $kron; $kron = "$chron\t\"$aliases{$taskname}\"\t$remainder\n"; print "\twarning: alias \"$taskname\" used rather than taskname \"$aliases{$taskname}\"\n"; # # Save the fixed entry back into the kron list. # $krons[$ind] = $kron; $fixes++; } } ################################################ # # Test 2.2: This check looks for entries with the old elapsed-time # format (epoch time) and converts it to the new elapsed- # time format (HH:MM:SS). # print "Test 2.2: Updating old elapsed-time format to new format\n"; for(my $ind = 0; $ind < @krons; $ind++) { my $kron = $krons[$ind]; # Kron to examine. my $chronos; # Entry's timestamp. my $taskname; # Entry's taskname. my $cmd; # Entry's command. my $tdiff; # Time difference. # # Skip entries that were found to be in error. # next if(($kron =~ /^\s*\#/) || ($kron =~ /^\s*$/)); # # Get the entry's fields. # $kron =~ /^(\d+)\t(".*")\t(start|end)(\t(.+))?/; $chronos = $1; $taskname = $2; $cmd = $3; $tdiff = $5; if($cmd eq 'end') { if($tdiff !~ /\d:\d/) { my $tsecs; # Time in seconds. $tsecs = $tdiff; $tdiff = timecolon($tsecs,1); $kron = "$chronos\t$taskname\t$cmd\t$tdiff\n"; printf("\twarning: difference %5d used instead of elapsed time $tdiff\n", $tsecs); # # Save the fixed entry back into the kron list. # $krons[$ind] = $kron; $fixes++; } } } ################################################ # # Test 2.3: Look for kron blocks that span a midnight, and split # them in two at midnight. The new second block will # be checked to ensure that it isn't spanning yet another # midnight; if so, it too will be split. # my $origend = 0; print "Test 2.3: Fixing kron blocks that span midnight\n"; for(my $ind = 0; $ind < @krons; $ind++) { my $kron = $krons[$ind]; # Kron to examine. my $chronos; # Entry's timestamp. my $taskname; # Entry's taskname. my $cmd; # Entry's command. my $tdiff; # Time difference. my @starter; # Start record times. my @ender; # End record times. my $elapsed; # Elapsed time in new kronblock. # # Skip entries that were found to be in error. # next if(($kron =~ /^\s*\#/) || ($kron =~ /^\s*$/)); # # Get the entry's timestamp. # $kron =~ /^(\d+)\t(".*")\t(start|end)(\t(.+))?/; $chronos = $1; $taskname = $2; $cmd = $3; $tdiff = $5; # # Save timestamp for start lines and go on to next line. # if($cmd eq 'start') { $starttime = $chronos; next; } # # Divide the epoch times of the kron block into their pieces. # @starter = localtime($starttime); @ender = localtime($chronos); if($starter[3] != $ender[3]) { my @dayend; # End of first day. my $border; # Border time for time split. my $newdiff; # Time diff for new kron. my $tdclock; # Clock-form of time diff. my $kron1; # New end kron for insertion. my $kron2; # New start kron for insertion. # # Only announce the kron split once per kron block, # no matter how many krons it actually is split into. # if($chronos > $origend) { $origend = $chronos; print "\twarning: kron block crosses midnight at start $starttime\n"; } # # Get the epoch time that's the last second, # end-of-day, time of the first(ish) day. # @dayend = @starter; $dayend[0] = 59; $dayend[1] = 59; $dayend[2] = 23; $border = timelocal(@dayend); # # Get the time difference between the end of the # day and the start time. # $newdiff = $border - $starttime; $tdclock = timecolon($newdiff, 1); # # Build the end-of-day kron. # $kron1 = "$border\t$taskname\tend\t$tdclock\n"; # # Build the start-of-next-day kron. # $border++; $kron2 = "$border\t$taskname\tstart\n"; # # Adjust the time in the original kron block's end kron. # $elapsed = $chronos - $border; $elapsed = timecolon($elapsed, 1); $krons[$ind] = "$chronos\t$taskname\tend\t$elapsed\n"; # # Plop the two new krons into the kron list. # splice(@krons, $ind, 0, ($kron1, $kron2)); # # Move the start-of-day time to the starting # position. # $starttime = $border; @starter = localtime($border); $fixes++; } } ################################################ # # Test 2.4: Ensuring that the epoch time difference # (end time - start time) is the same as the elapsed # time stored in the end entry. If the two times are # different, the end entry's epoch time will be fixed # to match the elapsed time. # print "Test 2.4: Fixing elapsed time/calculated time mismatches\n"; for(my $ind = 0; $ind < @krons; $ind++) { my $kron = $krons[$ind]; # Kron to examine. my $chronos; # Entry's timestamp. my $taskname; # Entry's taskname. my $cmd; # Entry's command. my $tdiff; # Recorded time difference. my $actual; # Actual time difference. # # Skip entries that were found to be in error. # next if(($kron =~ /^\s*\#/) || ($kron =~ /^\s*$/)); # # Get the entry's timestamp. # $kron =~ /^(\d+)\t(".*")\t(start|end)(\t(.+))?/; $chronos = $1; $taskname = $2; $cmd = $3; $tdiff = $5; # # Save timestamp for start lines and go on to next line. # if($cmd eq 'start') { $starttime = $chronos; next; } # # Get the time string for the *actual* difference between # the start and end times. # $actual = $chronos - $starttime; $actual = timecolon($actual, 1); # # If the calculated time difference (end time - start time) # doesn't match the recorded time difference, we'll adjust # the end time to match the recorded time. # if($actual ne $tdiff) { my $newend; # New end epoch time. print "\twarning: time difference for $starttime/$chronos block ($actual)\n"; print "\t does not match elapsed time ($tdiff);\n"; # print "\t changing time difference to match elapsed time\n"; $newend = $starttime + colontime($tdiff); $krons[$ind] = "$newend\t$taskname\tend\t$tdiff\n"; $fixes++; } } #################################################################### # # Phase 3: Final checks after all other fixes completed. # ################################################ # # Test 3.1: Ensure all times are unique and monotonically ascending. # This was done in test 1.3, but things might have changed # in phase two so a re-test is required. # my $lastchron = -1; # Time on last kron. my %equaltimes = (); # Equal epoch-time counts. my %switchtimes = (); # Switched epoch-time counts. for(my $ind = 0; $ind < @krons; $ind++) { my $kron = $krons[$ind]; # Kron to examine. my $chronos; # Entry's timestamp. # # Skip entries that were found to be in error. # next if(($kron =~ /^\s*\#/) || ($kron =~ /^\s*$/)); # # Get the entry's timestamp. # $kron =~ /^(\d+)\t(".*")\t(start|end)(\t(.+))?/; $chronos = $1; # # Check for misordered times and duplicated times. # if($chronos == $lastchron) { $equaltimes{$chronos}++; } elsif($chronos < $lastchron) { $switchtimes{$chronos}++; } } # # Report on duplicated times. # foreach my $chron (sort(keys(%equaltimes))) { print "\twarning: time $chron used multiple times\n"; } # # Report on misordered times. # foreach my $chron (sort(keys(%switchtimes))) { print "\twarning: time $chron used after an earlier time\n"; } #################################################################### # # All phases complete! Backup and save iff any changes were made. # # # Celebrate if we found no errors. # if(($errs == 0) && ($fixes == 0)) { print "no problems found in log $kronlogfile\n"; exit(0); } # # There were changes made to the kronlog contents. The user # didn't tell us to fix things, so we'll ask now. # if(! $fixit) { while(42) { my $answer; # User's decision to fix. print "\n\n"; print "fixes made, should they be installed? (y/n): "; $answer = ; # # Look for a negative response. # if($answer =~ /^n$/i) { last; } # # Look for a positive response. # if($answer =~ /^y$/i) { $fixit = 1; last; } print "unrecognized response\n"; } } # # If the user wants us to use the fixed version, we'll do several # things to get it all in place: # - save the modified kron entries into a temporary log file # - move the current log file to a backup file # - install the temporary log file as the current log file # if($fixit) { my $tmplog; # Name of temporary log. my $bakname; # Name of back-up file. my $rc; # Return code. print "\n"; print "saving fixes to $kronlogfile\n"; # # Build the temporary name for kronlog. # $tmplog = $kronlogfile; $tmplog .= ".tmp"; # # Create the temporary file for writing, zapping anything # that's there. # if(open(TMP, "> $tmplog") == 0) { print STDERR "unable to open $tmplog for writing\n"; exit(72); } # # Write the modified kron to the temporary file. # print TMP @krons; close(TMP); # # Backup the current kronlog. # baklogfile(); # # Move the temporary kronlog into place as the new kronlog. # # # And finally, we'll move the fixed logfile into place. # $rc = rename $tmplog, $kronlogfile; if($rc == 0) { print STDERR "unable to install $tmplog in place as $kronlogfile\n"; exit(75); } print "\n"; } exit($errs); } #---------------------------------------------------------------------- # Routine: checktasks() # # Purpose: Validate the user's tasks to ensure that there isn't a # problem with them. The following problems are checked: # # - taskname cannot contain "Off Clock" # - billable values are 0 or 1 # - task aliases cannot contain "Off Clock" # - task aliases cannot reference "Off Clock" # sub checktasks { my $errs = 0; # Error count. # # Check the task info for validity. # foreach my $tn (sort(keys(%tasknames))) { # # Tasknames cannot contain "Off Clock". # if($tn =~ /$OFFCLOCK/i) { print STDERR "user tasknames cannot contain \"$OFFCLOCK\"\n"; $errs++; } # # Billable values must be 0 or 1. # if(($billables{$tn} != 0) && ($billables{$tn} != 1)) { print STDERR "user task \"$tn\" has invalid value \"$billables{$tn}\"\n"; $errs++; } } # # Check the task aliases hash for validity. # foreach my $alias (sort(keys(%aliases))) { # # Task aliases cannot contain "Off Clock". # if($alias =~ /$OFFCLOCK/i) { print STDERR "task aliases ($alias) cannot contain \"$OFFCLOCK\"\n"; $errs++; } # # Task aliases references cannot contain "Off Clock". # if($aliases{$alias} =~ /$OFFCLOCK/i) { print STDERR "task aliases ($alias) cannot reference \"$OFFCLOCK\"\n"; $errs++; } } # # Exit if we've hit any errors. # if($errs) { exit(2); } } #---------------------------------------------------------------------- # Routine: cvtalias() # # Purpose: Convert a potential alias to its taskname. The checks # are caseless. The taskname is returned if it's found; # an empty string is returned if it isn't. # # cvtalias will first check the tasklist to see if there # is a (caseless) match there. If so, the version from # the tasklist is returned. # # Return Values: If the specified name is a valid task name or an # alias, then the associated taskname is returned. # # If the name isn't a valid name or alias, then an # empty string is returned. # sub cvtalias { my $tname = shift; # Taskname to look up. # # Force the search target to lowercase. # $tname = lc($tname); # # Check the list of tasknames for the caller's task. # foreach my $tn (sort(keys(%tasknames))) { if(lc($tn) eq $tname) { return($tn); } } # # Now check the keys of the alias dict for the caller's task. # foreach my $tn (keys(%aliases)) { if(lc($tn) eq $tname) { return($aliases{$tn}); } } # # Invalid name/alias, so return an empty string. # return(''); } #---------------------------------------------------------------------- # Routine: endkron() # # Purpose: Close a kron block by writing an end-kron entry to the kron # log. # # If the just-closed kron's taskname matches the next kron's # taskname, we're only supposed to close that task and do # nothing else. # sub endkron { my $chronos = shift; # Time to use. my $taskname = shift; # Kron's taskname. my $prevtime = shift; # Kron's start time. my $newtask = shift; # Next kron's taskname. my $tdiff; # Time difference. # # Calculate the end kron's time and time-difference values. # $chronos--; $tdiff = $chronos - $prevtime; $tdiff = timecolon($tdiff, 1); # # Add the new kron to the end of the kron log. # appendkron("$chronos\t\"$taskname\"\tend\t$tdiff\n"); # # If the just-closed kron's taskname matches the next kron's taskname, # we're only supposed to close that task and do nothing else. # exit(0) if($newtask eq $taskname); } #---------------------------------------------------------------------- # Routine: getkronblock() # # Purpose: Return the indices of a kron block that contains a given # epoch time. # # We'll find the start and end entries that contain the target # time. This can be an open kron block at the end of the file, # in which case only the start entry is found. The indices of # these entries are then returned to the caller. # sub getkronblock { my $targettime = shift; # Time in kron block to delete. my @krons = @_; # Lines from kron log. my $startind = -1; # Index of block's start entry. my $startkron; # Epoch from start kron. my $endind = -1; # Index of block's end entry. my $endkron; # Epoch from end kron. # # Find the indices for the krons we'll insert things after and before. # for(my $ind = (@krons - 1); $ind >= 0; $ind--) { my $epoch; # Epoch time of each kron. my $krontask; # Taskname of each kron. my $cmd; # Command from each kron. next if($krons[$ind] =~ /^\s*\#/); # # Get the epoch time and command for this kron. # $krons[$ind] =~ /^(\d+)\t".+"\t(start|end)/; $epoch = $1; $cmd = $2; # # Get the info for the start and end entries, depending on # where we are in the scan. # # Compare the user's target time to the time on this kron # entry. By looking at a number of entries, we'll find # the relevant kron block's start entry and end entry. # if($targettime > $epoch) { # # The requested time is later than this entry's epoch # field. # If it's a start line, we've got the start and end # time for the block to be deleted. # If it's an end line, then the requested time is not # in a kron block. This is a user error. # if($cmd eq 'end') { if($changeflag) { print STDERR "Given time is not in a kron block; cannot change kron.\n"; } elsif($deleteflag) { print STDERR "Given time is not in a kron block; cannot delete kron.\n"; } exit(80); } $startind = $ind; $startkron = $epoch; last; } elsif($targettime == $epoch) { # # The requested time matches this entry's epoch field. # If it's a start line, we've got the start and end # time for the block to be deleted. # If it's an end line, we'll save the info and look at # the next line. # if($cmd eq 'start') { $startind = $ind; $startkron = $epoch; last; } else { $endind = $ind; $endkron = $epoch; } } else { # # This might be the end line for the relevant kron # block. It might not be. We'll save the info in # case the next line we look at is the start line. # $endind = $ind; $endkron = $epoch; } } return($startind, $endind); } #---------------------------------------------------------------------- # Routine: getkrons() # # Purpose: Get the contents of the kronlog. We'll strip out all the # comment lines and blank lines, unless the caller says they # want them. # sub getkrons { my $asis = shift; # Don't-process flag. my @lines = (); # Lines from kronlog. my @stripped = (); # Stripped kronlogs. if($debug) { print "reading kronlog \"$kronlogfile\"\n"; } # # Ensure the kronlog exists and is readable and writable. # if(! -e $kronlogfile) { print STDERR "kronlog \"$kronlogfile\" does not exist\n"; exit(50); } if((! -r $kronlogfile) || (! -w $kronlogfile)) { print STDERR "kronlog \"$kronlogfile\" must be readable and writable\n"; exit(51); } open(KLOG, "< $kronlogfile"); @lines = ; close(KLOG); # # If the caller wants the file exactly as it is, give it to 'em. # if($asis) { return(@lines); } # # Strip the comment lines and blank lines out of the file. # foreach my $ln (@lines) { # # Skip comment and blank lines. # next if(($ln =~ /^\s*\#/) || ($ln =~ /^\s*$/)); push @stripped, $ln; } return(@stripped); } #---------------------------------------------------------------------- # Routine: newkronlog() # # Purpose: Create a new kronlog. An existing file will not be overwritten. # sub newkronlog { if(-e kronlogfile) { print STDERR "file \"$kronlogfile\" already exists; not creating new kronlog\n"; exit(34); } if(open(KLOG, ">", $kronlogfile) == 0) { print STDERR "unable to create new kronlog file \"$kronlogfile\"\n"; exit(35); } print KLOG "# # Time log created and maintained by chronosquirrel -- do not edit! # Proceed at your own risk. # "; close(KLOG); } #---------------------------------------------------------------------- # Routine: outtime() # # Purpose: Build a time value for display, based on the output mode. # cooked - convert to nice text string # rare - convert to nice text string # raw - original Epoch time # sub outtime { my $chronos = shift; # Time to convert. my $chronostr; # Text epoch time. if($mode == $COOKED) { $chronostr = localtime($chronos); } elsif($mode == $RARE) { $chronostr = localtime($chronos); } elsif($mode == $RAW) { $chronostr = $chronos; } else { print "chronosquirrel: unknown output mode \"$mode\"\n"; exit(9); } return($chronostr); } #---------------------------------------------------------------------- # Routine: selectday() # # Purpose: Get the epoch time of a user-selected date. The caller's # date string can be in one of these forms: MM/DD, MM/DD/YY, # DD/MM, or DD/MM/YY. The last two are assumed to be for # non-USA users; this is determined by the value of the $usloc # flag. # # The Epoch Time of the requested date string is returned. # sub selectday { my $timestr = shift; # Time string to convert. my @timebits; # Pieces of the time string. my @now; # Time elements of current time. my $epoch; # Converted time's epoch time. # # Get the pieces of the current time. # @now = localtime(time); if(($timestr !~ /^\d{1,2}\/\d{1,2}$/) && ($timestr !~ /^\d{1,2}\/\d{1,2}\/\d{2,4}$/)) { return(0); } # # Divide the caller's time string into its pieces. # @timebits = split /\//, $timestr; # # Set time to midnight. # $now[0] = 0; $now[1] = 0; $now[2] = 0; # # Divide the caller's time string into its pieces. The ordering of # the month and day will depend on whether this is for someone in # the US or outside of the US. # if($usloc) { $now[3] = $timebits[1]; $now[4] = $timebits[0] - 1; } else { $now[3] = $timebits[0]; $now[4] = $timebits[1] - 1; } # # If the caller's time string includes a year, we'll set the year # field. Otherwise, we'll assume it's part of the current year. # if(@timebits == 3) { my $yy = $timebits[2]; if($yy =~/^\d\d$/) { $now[5] = (2000 + $yy) - 1900; } else { $now[5] = $yy - 1900; } } # # Convert the modified time elements into an epoch time value. # $epoch = timelocal(@now); return($epoch); } #---------------------------------------------------------------------- # Routine: timecolon() # # Purpose: Convert a seconds count into an "hh:mm" or "hh:mm:ss" # time format string. # sub timecolon { my $nsecs = shift; # Seconds count to convert. my $secflag = shift; # Flag for including seconds. my $hours; # Hours in seconds count. my $nohours; # Non-hours in seconds count. my $mins; # Minutes in seconds count. my $secs; # Seconds in seconds count. my $digitime; # hh:mm:ss time string. # # If zero seconds were given and we aren't being verbose, we'll # return a bunch of spaces. This is primarily used in weekly output. # if(($nsecs == 0) && ! $verbose) { return(" "); } # # If the user wants control of the display of seconds, then we'll # set the local seconds flag to what the user wants. # if($secsflag != $SECS_DEF) { $secflag = $secsflag; } # # Calculate the number of hours in this seconds count. # $hours = int($nsecs / 3600); # # Calculate the number of minutes and seconds in this seconds count. # $nohours = int($nsecs % 3600); $mins = int($nohours / 60); $secs = int($nohours % 60); if($secflag) { $digitime = sprintf("%d:%02d:%02d", $hours, $mins, $secs); } else { $digitime = sprintf("%2d:%02d", $hours, $mins); } return($digitime); } #---------------------------------------------------------------------- # Routine: colontime() # # Purpose: Convert a "hh:mm" or "hh:mm:ss" time format string into a # seconds count. # sub colontime { my $timestr = shift; # Time string to convert. my @pieces; # Pieces of time string. my $total = 0; # Total seconds in time string. # # Break the time string into its pieces. # @pieces = split /:/, $timestr; # # If this time string has the seconds element, we'll add it in. # if(@pieces == 3) { $total = $pieces[2]; } # # Add the hours in. # $total += $pieces[0] * 3600; # # And now the minutes. # $total += $pieces[1] * 60; return($total); } #---------------------------------------------------------------------- # Routine: tasksort() # # Purpose: Sort the tasknames with this ordering: # # - billables vs. nonbillables # - ordering number # - alphabetically # # The sorted list is returned to the caller. # sub tasksort { my @billtasks = (); # Billable tasks. my @nonbtasks = (); # Nonbillable tasks. # # Divide the tasks into billable and nonbillable lists. # foreach my $tn (sort(keys(%tasknames))) { if($billables{$tn}) { push @billtasks, $tn; } else { push @nonbtasks, $tn; } } # # Sort the two lists by ordering number. This also slips in an # alphabetic sort along the way. # @billtasks = sort { $ordernums{$a} <=> $ordernums{$b} } @billtasks; @nonbtasks = sort { $ordernums{$a} <=> $ordernums{$b} } @nonbtasks; # # Combine the two arrays of tasknames. # push @billtasks, @nonbtasks; # # Returned the sort list of tasknames. # return(@billtasks); } #---------------------------------------------------------------------- # Routine: validtaskname() # # Purpose: Check the validity of the characters in a taskname. # A true value is returned to indicate validity. # False indicates invalidity. # For now, a taskname is considered valid if it has # only the following characters: # # - letters (any case) # - numbers # - spaces # - dashes # - single quote # sub validtaskname { my $task = shift; # Taskname to check. $task =~ s/[a-z0-9 \-']//gi; return(1) if(length($task) == 0); return(0); } #---------------------------------------------------------------------- # Routine: writekrons() # # Purpose: Replace the contents of the kronlog fie with the given # list of krons. Before writing the new kronlog file, the # current kronlog file will be backed up. # sub writekrons { my @krons = @_; # New contents of kronlog file. # # Backup the current kronlog. # baklogfile(); # # Write the set of krons to the kron log. # open(KLOG, "> $kronlogfile"); print KLOG @krons; close(KLOG); } #---------------------------------------------------------------------- # Routine: appendkron() # # Purpose: Append a string to the end of the kronlog file. # sub appendkron { my $kronstr = shift; # String to append. # # Append the given string to the kron log. # open(KLOG, ">> $kronlogfile"); print KLOG "$kronstr"; close(KLOG); } #---------------------------------------------------------------------- # Routine: version() # # Purpose: Print the version number(s) and exit. # sub version { print STDERR "$VERS\n"; exit(0); } #---------------------------------------------------------------------- # Routine: usage() # # Purpose: Give usage message and exit. # sub usage { print STDERR "usage: chronosquirrel [options]\n"; print STDERR "\n"; print STDERR "\twhere [options] are:\n"; print STDERR "\t\t-aliases print a list of tasks and their aliases\n"; print STDERR "\t\t-endkron close the current kron\n"; print STDERR "\t\t-listkrons print krons in kron log\n"; print STDERR "\t\t-tasks print a list of recognized tasks\n"; print STDERR "\n"; print STDERR "\t\t-insertkron insert new kron in kron log\n"; print STDERR "\t\t-deletekron delete kron from kron log\n"; print STDERR "\n"; print STDERR "\t\t-day print krons generated today\n"; print STDERR "\t\t-daily print krons generated today\n"; print STDERR "\t\t-today print krons generated today\n"; print STDERR "\t\t-week print krons generated this week\n"; print STDERR "\t\t-weekly print krons generated this week\n"; print STDERR "\t\t-select select day/week to display\n"; print STDERR "\n"; print STDERR "\t\t-all print info for all tasks\n"; print STDERR "\n"; print STDERR "\t\t-raw set raw output mode\n"; print STDERR "\t\t-rare set rare output mode\n"; print STDERR "\t\t-cooked set cooked output mode\n"; print STDERR "\n"; print STDERR "\t\t-seconds def|on|off set seconds-display flag\n"; print STDERR "\n"; print STDERR "\t\t-config file select alternate configuration file\n"; print STDERR "\t\t-kronlog file select alternate kronlog file\n"; print STDERR "\t\t-roll method roll the kronlog file\n"; print STDERR "\t\t-annotate annotate the kronlog file\n"; print STDERR "\t\t-health validate configuration and kronlog files\n"; print STDERR "\t\t-fixit automatically fix health issues\n"; print STDERR "\n"; print STDERR "\t\t-help\n"; print STDERR "\t\t-verbose\n"; print STDERR "\t\t-Version\n"; exit(0); } 1; ############################################################################## =pod =head1 NAME B - task-based time tracking =head1 SYNOPSIS chronosquirrel [options] [taskname] =head1 DESCRIPTION B is a time-tracking program, that is based on tracking the clock-time spent on particular tasks. It manages Is, which are records of time blocks used for a particular task. Task start time and task end time are recorded, so that current tasks, as well as tasks from times past, may be tracked and reported. Since B was originally written to manage time charged to clients, it separates the concept of billable and nonbillable time. B manages a set of I stored in the B file. I blocks may be started or closed, and they are assigned to particular tasks. Current (open) and previous (closed) I may be displayed. B was designed to have a minimal impact on the system. There are only three files needed: B itself, the B file (a regular file used to store time records), and a configuration file. No DBMS, no database files, and no nonstandard Perl modules are required. This man page primarily describes the options required to use B. A very complete, detailed description of how to use B is provided in the Chronosquirrel User's Guide, available at B. =head1 GENERAL USE At a high-level, the arguments to B define the actions that may be taken. All of B's functioning is self-contained; there are no other programs required to use it. Almost all the operations are invoked by running B with the appropriate option. However, if B is run without an option then the action take will depend on if a I was given on the command line. If I is specified, then one of the following will be done: =over 4 =item * If a time block is inactive, a new time block will be started. An inactive state exists if the final entry in the log file has an I state. =item * If a time block is active, the current time block will be ended and a new time block will be started. An active state exists if the final entry in the log file has a I state. =back If an option and a I are not specified on the command line, then a status entry will be displayed. =head1 OPTIONS B takes the following options: =over 4 =item I<-aliases> This option lists the aliases of the defined tasks. The alias-to-task mappings are given, with mappings for each alias. =item I<-all> This option is used in conjunction with I<-daily> or I<-week> and forces display of all defined tasks. By default, these options only display timing data on tasks that have been used during the day or week, depending on option chosen. =item I<-annotate [options] date time message> Add a user-specified annotation to the kronlog. This is a new comment added to the B file. If the first argument is not a date, then the annotation will be added in exactly the place the specified time requires. The following options are recognized: - x use the exact date, without rounding up to the nearest kron - p "prettify" the annotation with some blank comment lines - s add a separator line before the annotation - t prefix the annotation with its timestamp If the first argument is a date, then the annotation will be added immediately before the kron that precedes the given time. The I argument can be in either the MM/DD or MM/DD/YY format. The I