#!/usr/bin/perl # # LPRng, IFHP Accounting Sanatiser # # This script performs all the extra handling of pagecount accounting in # LPRng. It is especially designed to handle unusual conditions, such as # aborted print jobs, printer resets, and detection of printing from a # source, outside of this print queue. # # Accounting records, are kept in a seperate file to the normal LPRng "acct" # file (to avoid truncation by "checkpc"), and not only includes the user and # number of pages printed, as in the old BSD printing accounting, but also the # filename printed, date and time, and the current pagecount of the printer # when the job was completed. Basically an easy to read and parse summary of # the highly verbose LPRng "acct" file. # # Included in this report are extra notes (starting with a "---") which # indicate unusal occurances, such as jobs which terminated abnormally, # requiring special "cleanup", detected HP printer resets (between jobs), and # if someone used the printer "Out of Band" (EG directly and not via LPRng). # # Side Effects... # As a bonus, this script will also clean up those annoying "ifhp" conversion # files that the IFHP filter loves to leave behind. # # WARNING: as the script stands, it will ignore "of" filter "start" and "end" # account actions of the IFHP filter, being designed to deal with individual # files being printed by the user. It will also ignore miss-use of the script # when called using the "as" or "ae" printcap entries. # # INSTALLATION & OPERATION... # # To use this program you only need to either add the line "accounting=" # to the IFHP configuration file, or add this to the printcap file. # For Example... # :ifhp=accounting=/opt/lprng/libexec/acct/lprng_acct_sanatizer # # I myself prefer the former as it leaves the :ifhp= printcap free for # model specifications and special cases. # # You can also further modify this scripts behaviour from the printcap. Here # is a list of printcap entries and there normal defaults that are understood. # Note that they are NOT normal LPRng options, so only this program should be # using them. # # :filter_debug Dump a file of Arguments and Environment # :acct_file=acct_summary A readable summary of print accounting # :acct_pc_file=acct_pagecount Last page count retrieved by ifhp # :acct_jobinfo=acct_jobinfo Info on current job in progress (as a backup) # :acct_quota_start= Secondary quota handler (job start) # :acct_quota_end= Secondary quota handler (job end) # # The last two options above are hooks to allow this script to call secondary # script for quota handling if desired. These programs are called with exactly # the same options that IFHP uses. # # If the last job was incomplete when the next job starts, this script will # finish that job first before proceeding. That is the "acct_quota_end" script # script will ALWAYS be called if the "acct_quota_start" script returned "OK" # (status 0) to proceed. This is regardless of if it was aborted by the user, # the printer was reset, or a power failure occured. # # WARNING: A negative pages used (-b) could be passed to the "acct_quota_end" # program in aborted situations. This indicates that something very weird # happened, such as a HP printer was power cycled or the printer was replace # while the last job was still in progress. It is left up to that script on # how it should fix that situation, commonly just reset it to 0 as unknown. # # In the account summary aborted situations are flaged as such, but the # completed job will be listed with a page count of 0 if count was negitive. # For a typical HP power cycle this would only gain the user at most, 9 pages. # If you find the printer is being reset multiple time, it may be that it was # the user listed next, attempting to gain free printouts. # # See LPRng-HowTo, section 18.5 for details of the HP printer page count # eccentricity. # # With this script you can if you want also turn off or reassign the highly # verbose accounting file, which checkpc would normally truncate. We just # renamed it (See below) # # Full Working Printcap Example... # # # Common to all printers # .common: # :sh:mx=0:send_try=1 # # # Postscript Printer Accounting # .ifhp: # :filter=/opt/lprng/libexec/filters/ifhp # :ifhp=accounting=/opt/lprng/libexec/acct/lprng_acct_sanatizer # :af=acct_raw_lprng # :acct_summery=acct # # # Secondary Accounting for student printers # .acct_student: # :acct_quota_start=/opt/lprng/libexec/acct/pquota_start # :acct_quota_end=/opt/lprng/libexec/acct/pquota_end # # # Student printer using all the above. # lwa|student_lab_A # :server # :tc=.common,.ifhp,.acct_student # :lp=hplwa.student-labs%9100 # # # Anthony Thyssen 22 November 2002 # # Comments welcome. # # ---------------------------------------------------------------------------- use strict; use warnings; use Getopt::Std; use FindBin; my $prog = $FindBin::Script; my $bindir = $FindBin::RealBin; my $cleanup = 1; # clean up the old ifhp temporary files? my $debug = ( $ENV{PRINTCAP_ENTRY} =~ /:filter_debug\b/ ); if ( $debug ) { open( DB, ">$ARGV[0].debug" ); print DB "-------- ARGS ------\n"; print DB map { "$_\n" } ( $0, @ARGV ); print DB "-------- ENV -------\n"; print DB `id`; print DB map { "$_=$ENV{$_}\n" } keys %ENV; print DB "-------- LS ---------\n"; print DB `ls -C`; print DB "---------------------\n"; close DB; } sub JSUCC { 0 }; sub JRETRY { 32 }; # JFAIL actually sub JABORT { 33 }; sub JREMOVE { 34 }; my $acct_stage = shift; # When was this script called # -------- Ignore these Accounting actions ---------- if ( grep {$acct_stage eq $_} qw( jobstart jobend start end ) ) { # Accounting script called from unusable situation -- IGNORE exit JSUCC; } # ------------------ Initialization -------------------- # Decode printcap entry -- from LPRng example scripts my %printcap; if ( $ENV{'PRINTCAP_ENTRY'} ) { map { if( m/^\s*:([^=]+)=(.*)/ ){ $printcap{$1}=$2; } elsif( m/^\s*:([^=]+)\@$/ ){ $printcap{$1}="0"; } elsif( m/^\s*:([^=]+)/ ){ $printcap{$1}="1"; } elsif( m/^\s*([^|]+)/ ){ $printcap{"Printer"}=$1; } } split( "\n", $ENV{'PRINTCAP_ENTRY'}); } # Retract the parameters we use and thier default settings my $acct_file = $printcap{'acct_file'} || "acct_summary"; my $check_point = $printcap{'acct_jobinfo'} || "acct_jobinfo"; my $pagecount = $printcap{'acct_pc_file'} || "acct_pagecount"; my $prog_start = $printcap{'acct_quota_start'} || undef; my $prog_end = $printcap{'acct_quota_end'} || undef; # This is a specific to the authors own print server accounting # Undefine if you don't what the domain removed #my $domain_re = undef; # leave the hostname as is. #my $domain_re = qr/\..*/; # remove the domain from the hostanme my $domain_re = qr/\.gu\.edu\.au$/; # remove this RE from acct hostname # Getopt::Std options... # All switch options except 'c' has an argument. (ifhp does not pass '-c') # WARNING: if a option that was not expected is found, it will be parsed as # a list of single litter switches, which can confuse the module # #my $acct_options = "A:B:C:D:...Z:a:b:c:d:e:f:..."; # including 'c' my $acct_options = join(':', 'A'..'Z', 'a'..'z', ''); # ------------------- Start Accounting ----------------- if ( $acct_stage eq "filestart" ) { # Called from IHFP running as a output filter (":of") # The start of a users job with one or more files to print. # my (@args, %args) = @ARGV; # save the arguments to pass to second handler getopts( $acct_options, \%args ); # decoce arguments to this print job # --- Was the last job completed --- if ( -f $check_point ) { # The check point file exists -- the last job did not complete! # Recover the start info of the last job and complete its accounting # Recover the start arguments of the previous job open(CP, "$check_point" ) or die "Unable to read file \"$check_point\": $!\n"; chomp( @ARGV = ); close CP; my %job; # save the arguments to pass to second handler getopts( $acct_options, \%job ); # arguments from previous job start # Fix up the arguments for the end of the job $job{b} = $args{p} - $job{p}; # number of pages used $job{p} = $args{p}; # what the printer pagecount is now! # Add a summary to account file open(ACCT, ">>$acct_file") or die "Unable to append file \"$acct_file\": $!\n"; # note the special condition in the accounting summary file my $t = $args{t}; substr($t, 10, 1) = ' '; substr($t, 19) = ''; printf ACCT "%s %7d %s\n", $t, $job{p}, "--- Found unfinished Job -- Finalising Job Now"; my $b = $job{b}; # the final page usage if ( $b < 0 ) { # is users page usage negative? if ( $job{p} % 10 == 0 ) { # page count an increment of ten? printf ACCT "%s %7d %3d %s\n", $t, $job{p}, $b, "--- HP printer reset detected!"; } else { printf ACCT "%s %7d %3d %s\n", $t, $job{p}, $b, "--- Printer page counter went backward!"; } $b = 0; # zero the negative page count for this users job (noted above) } # output the final record of the print job - use the jobs start time! $t = $job{t}; substr($t, 10, 1) = ' '; substr($t, 19) = ''; my $h = $job{H}; $h =~ s/$domain_re// if defined $domain_re; printf ACCT "%s %7d %3d %s\@%s %s\n", $t, $job{p}, $b, $job{n}, $h, $job{f}; close ACCT; # replace those two options in the start arguements for secondary accting my( @job ) = map( "-$_$job{$_}", keys %job ); # Call the secondary accounting program to do any else needed if ( $prog_end ) { #print STDERR "Running secondary accounting $prog_end\n"; system($prog_end, "fileend", @job); } # Clean up the ifhp conversion files from last job if ( $cleanup ) { opendir(D,"."); my( @files ) = readdir(D); closedir(D); unlink( grep(/^ifhp......$/, @files) ); } } # --- Out of Band Data Check --- # Compare this page count with the previous pagecount # looking for data printed out of band, or HP printer resets. elsif( -f $pagecount ) { my $pc; if ( open(PC, $pagecount) ) { chomp( $pc = ); close PC; } if ( $pc && $pc =~ /^\d+$/ && $pc != $args{p} ) { # something is fishy, the old pagecount that does not match current value open(ACCT, ">>$acct_file") or die "Unable to append file \"$acct_file\": $!\n"; my $t = $args{t}; substr($t, 10, 1) = ' '; substr($t, 19) = ''; if ( $args{p} < $pc && $args{p} == $pc - $pc % 10 ) { printf ACCT "%s %7d %3d %s\n", $t, $args{p}, $args{p}-$pc, "--- HP printer reset detected!"; } elsif ( $args{p} < $pc ) { printf ACCT "%s %7d %3d %s\n", $t, $args{p}, $args{p}-$pc, "--- Page counter went backward!"; } else { printf ACCT "%s %7d %3d %s\n", $t, $args{p}, $args{p}-$pc, "--- Pages printed Out of Band!"; } close ACCT; } } # --- update the pagecount file --- # Note that this is not needed really at the filestart, but it doesn't hurt. open( PC, ">$pagecount"); print PC $args{p}, "\n"; close PC; # --- checkpoint new job --- # create a new check point file for this job open(CP, ">$check_point" ) or die "Unable to open check point file \"$check_point\": $!\n"; print CP map { "$_\n" } ( @args ); close CP; # --- call secondary accounting --- if ( $prog_start ) { #print STDERR "Running secondary accounting $prog_start\n"; my $return = system($prog_start, $acct_stage, @args ); if ( $return ) { # Secondary accounting dod not return OK! # Clean up and log it in account summery file! print STDERR "Secondary Accounting returned : ", $return>>8, " -- ABORTING\n"; unlink($check_point); # check point not needed any more open(ACCT, ">>$acct_file") or die "Unable to append file \"$acct_file\": $!\n"; my $t = $args{t}; substr($t, 10, 1) = ' '; substr($t, 19) = ''; my $h = $args{H}; $h =~ s/$domain_re// if defined $domain_re; printf ACCT "%s %7d %3d %s\@%s %s\n", $t, $args{p}, 0, $args{n}, $h, $args{f}. " --- DENIED"; close ACCT; #exit JABORT; # return ABORT job exit JREMOVE; # forced job REMOVAL } } exit JSUCC; # return OK to proceed to IFHP filter } # --------------- Finish Accounting -------------- if ( $acct_stage eq "fileend" ) { # FUTURE: check if the last job check point point actually matches this job? unlink($check_point); # check point not needed any more my ( @job, %job ) = @ARGV; # save the arguments to pass to second handler getopts( $acct_options, \%job ); # arguments from previous job start # --- update the pagecount file --- open( PC, ">$pagecount"); print PC $job{p}, "\n"; close PC; # --- add a summary to account file --- open(ACCT, ">>$acct_file") or die "Unable to append file \"$acct_file\": $!\n"; my $t = $job{t}; substr($t, 10, 1) = ' '; substr($t, 19) = ''; my $h = $job{H}; $h =~ s/$domain_re// if defined $domain_re; printf ACCT "%s %7d %3d %s\@%s %s\n", $t, @job{qw( p b n )}, $h, $job{f}; close ACCT; # --- call secondary accounting --- if ( $prog_end ) { #print STDERR "Secondary accounting $prog_end\n"; system($prog_end, "fileend", @job); } # Clean up the ifhp conversion files of this job if ( $cleanup ) { opendir(D,"."); my( @files ) = readdir(D); closedir(D); unlink( grep(/^ifhp......$/, @files) ); } # done exit JSUCC; } # ---------------------------- # We should not reach this point! print STDERR "$prog: Accounting error, unknown acct stage ", "\"$acct_stage\" -- ABORTING\n"; exit JRETRY; # Job Fail -- Retry later