#!/opt/gnu/bin/perl

# phaserif -- A simple filter for Tektronix Phaser printers (and, in
# theory at least, any printer using TCP port 9100 for sending jobs
# and getting pagecounts and UDP port 9101 for status information).

# This is a simple print filter for Tektronix Phaser printers which
# gets pagecounts and, depending upon the existence of the -a filter
# option either opens the file specified by -a and writes accounting
# information in it or sends the information back to LPD.  It was
# designed with LPRng in mind, but should work elsewhere.  It is known
# to work on the Phaser 450 and Phaser 300X models.

# Intial coding by Al Marquardt (almar@uiuc.edu) with modifications by
# Russ Thacher <thacher@brl.uiuc.edu> and John Lane
# <lanejohn@cps.msu.edu>.

use Getopt::Std;
use Socket;
use Sys::Hostname;

pop @ARGV;

# Get all the filter options LPRng knows

# 'v' is not one of the common options -- it is used here for filter
# verbose/debug mode.
getopts('a:b:c:d:e:f:h:i:j:k:l:m:n:p:r:s:t:v:w:x:y:F:P:S:C:H:A:J:L:Q:');

$verbose = $opt_v;

# set default exit status (JFAIL)

$! = 1;

# Set default error messages

$udpsockerr = "ERROR: Cannot establish UDP socket: $!\n";
$udpbinderr = "ERROR: Cannot bind to UDP socket: $!\n";
$udpsenderr = "ERROR: Cannot send UDP status request: $!\n";
$udprecverr = "ERROR: Cannot receive UDP status report: $!\n";
$tcpsockerr = "ERROR: Cannot establish TCP socket: $!\n";
$tcpconnecterr = "ERROR: Cannot connect to TCP socket: $!\n";
$tcpcloseerr = "ERROR: Cannot close TCP socket: $!\n";

# Open the accounting file
if ( defined($opt_a) ) {
  if ( $verbose ) { print STDERR "Using accounting file $opt_a.\n"; }
  open ( ACCT, $opt_a ) || die "Couldn't open accounting file $opt_a.\n";
} else { # This is better ... it should allow for network accounting via LPRng
  if ( $verbose ) { print STDERR "Sending accounting info to LPD.\n"; }
# Taken from CTI-ifhp: ifhp.c
  open ( ACCT, ">&STDIN" ) || die "Couldn't dup STDIN to ACCT: $!.\n";
  if ( fileno(ACCT) == 4 ) {
    close ACCT;  open( ACCT, ">&=3" ) || die "Couldn't open ACCT fd 3: $!.\n";
  } else {
    close ACCT;  open( ACCT, ">&=0" ) || die "Couldn't open ACCT fd 0: $!.\n";
  }
  if ( $verbose ) { 
    print STDERR "ACCT is fd ";
    print STDERR fileno(ACCT);
    print STDERR ".\n";
  }
}

# Sanity check
if ( !defined(ACCT) ) { die "ACCT undefined: $!\n"; }

select ACCT; $| = 1;
select STDOUT;

# Get current time/date

@MONTHS = ( "Jan", "Feb", "Mar", "Apr", "May", "Jun",
           "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" );
($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
$month = $MONTHS[$mon];
if (length($sec)  == 1)  { $sec = "0$sec" }
if (length($min)  == 1)  { $min = "0$min" }
if (length($hour) == 1)  { $hour= "0$hour" }
if (length($mday) == 1)  { $mday= " $mday" }

$datestamp = "$month $mday $hour:$min:$sec";

# Write a 'job begin' line to printer log file
print STDERR "START: job number $opt_j (dfile: $opt_e) for $opt_n\@$opt_h on $opt_P at $datestamp\n";

$tcpport = '9100';
$udpport = '9101';

# Setting up UDP socket info so we can get status reports from phasers...

$myip = gethostbyname(hostname());

$udpproto = getprotobyname('udp');
$myudppaddr = sockaddr_in(0, $myip);

$printip = inet_aton($opt_P);

$printudppaddr = sockaddr_in($udpport, $printip);     

socket(UDPSOCK, PF_INET, SOCK_DGRAM, $udpproto) or die $udpsockerr;
bind(UDPSOCK, $myudppaddr) or die $udpbinderr;

# Setting up TCP socket info so we can send jobs to the printer
# and read pagecounts

$tcpproto = getprotobyname('tcp');
$printtcppaddr = sockaddr_in($tcpport, $printip);
socket(TCPSOCK, PF_INET, SOCK_STREAM, $tcpproto) or die $tcpsockerr;
setsockopt(TCPSOCK, SOL_SOCKET, SO_KEEPALIVE, 0);
setsockopt(TCPSOCK, SOL_SOCKET, SO_LINGER, 0);     

# Before any printing check to be sure printer is idle
# If it's not, check every 5 seconds until it is 

defined(send(UDPSOCK, "\r\n", 0, $printudppaddr)) or die $udpsenderr;
$udpout = "";
($printudppaddr = recv(UDPSOCK, $udpout, 100, 0)) or die $udprecverr;
$realudpout = unpack("a*", $udpout);

# If something is wrong with the printer, I'd like to see exactly what
# it is in the logs, but I don't want to see it unless something is
# really wrong.  It seems common enough to me to have to query 10
# times (on the average) initially to get a pagecount (for the printer
# to warm up or whatever).
$looped = 0;
while ($realudpout ne 'status: idle') {
  defined(send(UDPSOCK, "\r\n", 0, $printudppaddr)) or die $udpsenderr;
  $udpout = "";
  ($printudppaddr = recv(UDPSOCK, $udpout, 100, 0)) or die $udprecverr;
  $realudpout = unpack("a*", $udpout);
  if ( $verbose || ++$looped % 10 == 0 ) { 
    print STDERR "Checking $opt_P for idle status.  Cannot start print job: $realudpout.  Attempt #$looped.\n";
  }
  sleep 5;
}

# Get the initial page count

connect(TCPSOCK, $printtcppaddr) or die $tcpconnecterr;
select TCPSOCK; $| = 1;
select STDOUT;

# The printer may have something going on that prevents it from
# answering ... for example %%[ PrinterError: head temperature low ]%%
# can be returned instead of a pagecount.  Thus, we loop.
$looped = 0;
$havePageCount = 0;
while ( !$havePageCount ) {
  print TCPSOCK "%!\n";
  print TCPSOCK "(%%\[ pagecount: )print statusdict /pagecount get exec ";
  print TCPSOCK "(                )cvs print ";
  print TCPSOCK "( \]%%) = flush\n";
  $tcpout = <TCPSOCK>;
  if ($tcpout =~ /%%\[ pagecount:.*/) {
    @tcparray = split /\s/, $tcpout;
    $pagecount1 = $tcparray[2];
    $havePageCount = 1;
    if ( $verbose ) { print STDERR "Got initial pagecount: $pagecount1\n"; }
  } else { 
    if ( $verbose || ++$looped % 5 == 0 ) { 
      chop $tcpout;
      print STDERR "Failed to get initial pagecount;  Got \"$tcpout\" instead.  Attempt #$looped.\n";
    }
    sleep 3;
  }
}

# Get current time/date (again, this time for the accounting file)

($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
$month = $MONTHS[$mon];
if (length($sec)  == 1)  { $sec = "0$sec" }
if (length($min)  == 1)  { $min = "0$min" }
if (length($hour) == 1)  { $hour= "0$hour" }
if (length($mday) == 1)  { $mday= " $mday" }

$datestamp = "$month $mday $hour:$min:$sec";

# Send the accounting information
if ( $verbose ) { print STDERR "DEBUG: printer return string= $tcpout"; }
print ACCT "start  -p'$pagecount1' -q'$opt_j' -J'$opt_J' -k'$opt_k' -n'$opt_n' -h'$opt_h' -P'$opt_P' -F'o' -t'$datestamp'\n" ||  print STDERR "Could not write initial accounting information: $!\n";

# Start shoving data out to printer

# Set print/transparency by queue name

if ($opt_Q =~ /.*t/) {
  if ( $verbose ) { print STDERR "Setting print/transparency by queue name.\n"; }
  print TCPSOCK "%!\n";
  print TCPSOCK "mark\n";
  print TCPSOCK "{\n";
  print TCPSOCK " 3 dict begin\n";
  print TCPSOCK " /MediaType null def\n";
  print TCPSOCK " /MediaColor (Transparent) def\n";
  print TCPSOCK " currentdict end setpagedevice\n";
  print TCPSOCK "} stopped cleartomark\n";
} 

# Shove the rest of the data file out to the printer
if ( $verbose ) { print STDERR "Sending data to the printer.\n"; }
while ($line = <STDIN>) { print TCPSOCK $line; }

# Listen for 'status:idle' from printer- this signifies that job is done and
# we can ask printer for page count
# we wait until 'status:idle' is received- retrying every 3 seconds

$realudpout = "";
$looped = 0;
while ($realudpout ne 'status: idle') {
  defined(send(UDPSOCK, "\r\n", 0, $printudppaddr)) or die $udpsenderr;
  $udpout = "";
  ($printudppaddr = recv(UDPSOCK, $udpout, 100, 0)) or die $udprecverr;
  $realudpout = unpack("a*", $udpout);
  if ( $verbose || ++$looped % 100 == 0 ) {
    print STDERR "Checking for job end (idle) before getting final pagecount $opt_P: $realudpout. Attempt #$looped.\n";
  }
  sleep 3;
}

# Now we're ready to grab the final page count from the printer

# Sometimes it happens that the printer hasn't yet updated it's
# pagecounter (ie. hasn't really finished printing yet).
sleep 5;
$havePageCount = $looped = 0;
while ( ! $havePageCount ) {
  print TCPSOCK "%!\n";
  print TCPSOCK "(%%\[ pagecount: )print statusdict /pagecount get exec ";
  print TCPSOCK "(                )cvs print ";
  print TCPSOCK "( \]%%) = flush\n";
  $tcpout = <TCPSOCK>;
  if ($tcpout =~ /%%\[ pagecount:.*/) {
    @tcparray = split /\s/, $tcpout;
    $pagecount2 = $tcparray[2];
    $havePageCount = 1;
  } else { 
    if ( $verbose || ++$looped % 5 == 0 ) { 
      print STDERR "Couldn't get final pagecount ... Retrying.\n";
      chop $tcpout; print STDERR "Got \"$tcpout\" instead.\n";
    }
    sleep 3;
  }
}

close TCPSOCK or die $tcpcloseerr;

# Get date/time again

($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
$month = $MONTHS[$mon];
if (length($sec)  == 1)  { $sec = "0$sec" }
if (length($min)  == 1)  { $min = "0$min" }
if (length($hour) == 1)  { $hour= "0$hour" }
if (length($mday) == 1)  { $mday= " $mday" }

$datestamp = "$month $mday $hour:$min:$sec";

# Update accounting file upon close

$pages = $pagecount2 - $pagecount1; 

if ( $verbose ) { 
  print STDERR "Got final pagecount: $pagecount2 ... pages printed for this job: $pages.\n";
  print STDERR "DEBUG: printer return string= $tcpout"; 
}
print ACCT "end  -b'$pages' -p'$pagecount2' -q'$opt_j' -J'$opt_J' -k'$opt_k' -n'$opt_n' -h'$opt_h' -P'$opt_P' -F'o' -t'$datestamp'\n" || print STDERR "Could not write final accounting information: $!\n";

close ACCT;

# Write a 'Job End' line to the printer log
print STDERR "END: job number $opt_j (dfile: $opt_e) for $opt_n\@$opt_h on $opt_P at $datestamp.  Printed $pages pages.\n";
