#! /bin/bash # # Get and print files from remote queue. # # File: printer_client # Author: Bob Walton # Date: Mon Sep 9 11:46:20 EDT 2019 # # The authors have placed this program in the public # domain; they make no warranty and accept no liability # for this program. # NOTE: This file is not to be executed by the judge, # but is to be sent to whoever needs to print # files in printer queues set up by the judge. # Version 3.2 is the lastest that MAC's will support # due to licensing issue, so we restrict ourselves to # this. In particular, `coproc' is not supported. # # shopt -s compat32 document=" printer_client [-doc|RESOURCE_FILE] Get files queued for printing in a remote queue and print them on a local printer. The RESOURCE_FILE defines parameters of operation. The default is ~/printer_client.rc In normal mode the RESOURCE_FILE defines: printer The printer to use. local_queue The directory in which files to be printed are queued. After printing, their extension is changed to \`.done'. This directory is made if it does not exist. remote_account The remote ssh account running the printer_server program. private_key The file name of the local file containing the private key for remote_account. The authorized_keys file of the remote_account must contain the line command=\"printer_server SERVER-QUEUE\" ... where ... is the public key corresponding to the private_key. This line invokes the printer_server program that works with printer_client to move files to be printed from the remote SERVER-QUEUE directory to the local_queue directory and from there to the printer. The RESOURCE-FILE can also be set up to run printer_ client in test mode. See the end of this document. Printer_client reads and executes commands from its standard input. If no input is received within a set time (default 30 seconds), printer_client exe- cutes the \`get' command which gets and prints any unprinted queue files, and then resumes waiting for another command (or another timeout). Files to be printed are put in the local_queue directory with extension .ps. When a file is printed, its exten- sion is changed from .ps to .done in both the local_ queue and server queue The common commands are: get Get any unprinted files and print them. cntrl-D Files in the server queue with extension .ps are copied to the local_queue direc- tory. Then they are printed and their extension is changed to .done in both the local_queue and server queue direc- tories. list List recent files. Ready (.ps) files and recently printed (recent .done) files in the server queue are listed. time [SECONDS] Reset the time interval between when printer_client is ready to read the next command and when it executes an automatic \`get' command. The interval time is in seconds, with default of 30 seconds. The value 0 means infinity. If [SECONDS] is not given, the current time is printed. ? Print list of commands. help quit Exit exit cntrl-C Other occasionally useful commands are the follow- ing. In all these commands, FILE must have exten- sion .ps or .done, except for the \`remove' command it may also have extension .out. listfiles List all the files in the server queue. File names have date and time and are in chronological order. File extensions indicate status: .out Still being written. .ps Ready to print. .done Printed. get FILE Copy FILE from server into local queue directory. print FILE Ditto but then print FILE and, if its extension is .ps, change its extension to .done in local queue and server queue directories after printing it. done FILE If FILE has the .ps extension, just change the extension to .done in the local queue and server queue director- ies. ready FILE If FILE has the .done extension, just change the extension to .ps in the local queue and server queue director- ies. sum FILE Check that the server and the local directory both have a copy of FILE and both copies have the same MD5 sum. remove FILE Remove the file from local directory and and server. debug [on|off] If the debug switch is on, a trace of communications between printer_client and the server is printed. With no option, the switch is toggled. The \`on' and \`off' options set the switch. Lines sent to the server are prefaced with \`---> ', and lines received from the server are prefaced with \`<--- '. See \`printer_server -doc' for documen- tation describing the traced lines. listdir List server queue directory as per \`ls -ltr'. localdir Ditto for printer_client local queue directory. ? Print command list. help doc Print this document. In test mode the RESOURCE_FILE does NOT define remote_account or private_key but instead defines: server The program to run as server. server_queue The server queue directory. This directory is made if it does not exist. " case "$1" in -doc* ) echo "$document" | less -F -K exit 1 ;; "" ) resource_file=~/printer_client.rc ;; * ) resource_file="$1" ;; esac if [[ ! -r "$resource_file" ]] then echo ERROR: Cannot read "$resource_file" exit 1 fi echo ------------------------------------------------ echo Reading Resource File: "$resource_file" . $resource_file if [[ "$printer" == "" ]] then echo "Printer is Default Printer" else echo "Printer is $printer" fi if [[ "$local_queue" != "" ]] then echo "Local Queue Directory is $local_queue" fi if [[ "$remote_account" != "" ]] then echo "Remote Account is $remote_account" fi if [[ "$private_key" != "" ]] then echo "Private Key File is $private_key" fi if [[ "$server" != "" ]] then echo "Server for Test Mode is $server" fi if [[ "$server_queue" != "" ]] then echo "Server Queue for Test Mode is $server_queue" fi echo ------------------------------------------------ if [[ "$remote_account" != "" ]] then test=0 if [[ "$server" != "" ]] then echo ERROR: Cannot define both Remote Account \ and Server exit 1 fi if [[ "$server_queue" != "" ]] then echo ERROR: Cannot define both Remote Account \ and Server Queue exit 1 fi if [[ ! -r "$private_key" ]] then echo ERROR: Cannot read Private Key File \ "$private_key" exit 1 fi elif [[ "$server" == "" ]] then echo ERROR: Remote Account and Server BOTH undefined exit 1 elif [[ "$server_queue" == "" ]] then echo ERROR: Remote Account and Server Queue BOTH \ undefined exit 1 elif [[ ! -x "$server" ]] then echo ERROR: Server "$server" is not executable exit 1 else test=1 fi if [[ "$local_queue" == "" ]] then echo ERROR: Local Queue undefined exit 1 elif [[ ! -d "$local_queue" ]] then echo Making "$local_queue" if ! mkdir "$local_queue" then echo ERROR: Cannot make "$local_queue" directory exit 1 fi fi if [[ "$server_queue" != "" && ! -d "$server_queue" ]] then echo Making "$server_queue" if ! mkdir "$server_queue" then echo ERROR: Cannot make "$server_queue" \ directory exit 1 fi fi FIFOIN=/tmp/printer_client_$$_fifo_in FIFOOUT=/tmp/printer_client_$$_fifo_out FIFO="$FIFOIN $FIFOOUT" trap "rm -f $FIFO" EXIT trap "" PIPE DEBUGOUT=nop DEBUGIN=nop READ_TIMEOUT=30 # coprocess runs ssh or local printer_server # COPROC_TIMEOUT=30 COPROC_DIED=0 COPROC_PID="" # On the MAC $xx> and {xx}> do not work. However # >$xx, <$xx, `eval exec "$xx>&-"' etc. work. # in=3 # file descriptor used to write input read by # coprocess out=4 # file descriptor used to read output written by # coprocess fileout=5 # file descriptor used to write a file WS='[\ ]' NWS='[^\ ]' function nop() { return 0 } # Call when %%-HPCM_ERROR-%% has been read. Reads from # server until %%-HPCM_DONE-%% read and and prints error # message. Sets COPROC_DIED if coprocess output stops # prematurely. # function get_error() { echo "ERROR:" COPROC_DIED=1 while read -r <&$out -t $COPROC_TIMEOUT do if [[ "$REPLY" == "%%-HPCM_DONE-%%" ]] then $DEBUGIN "$REPLY" COPROC_DIED=0 return fi echo " $REPLY" done } # Call to finish server command that has no output. # Reads until %%-HPCM_DONE-%% read, looking for # %%-HPCM_ERROR-%%. If any argument given, echoes # lines read before %%-HPCM_DONE-%% or %%HPCM-ERROR%%. # Otherwise complains that such lines are in error. # Sets COPROC_DIED if coprocess output stops prema- # turely. Returns 0 normally and 1 if any error. # function finish() { echo="$1" COPROC_DIED=1 superfluous_found=0 while read -r <&$out -t $COPROC_TIMEOUT do if [[ "$REPLY" == "%%-HPCM_DONE-%%" ]] then $DEBUGIN "$REPLY" COPROC_DIED=0 break elif [[ "$REPLY" == "%%-HPCM_ERROR-%%" ]] then $DEBUGIN "$REPLY" get_error return 1 fi if [[ "$echo" == "" && \ $superfluous_found -eq 0 ]] then echo "ERROR:" echo " Superfluous output from server:" superfluous_found=1 fi echo " $REPLY" done return $superfluous_found } # remote COMMAND # # Execute command remotely and return 0 on success and # 1 on error. # function remote() { local command="$1" $DEBUGOUT "$command" if ! echo >&$in "$command" then COPROC_DIED=1 elif finish then return 0 fi return 1 } # Get a list of all files in the server queue. The # list is put in the 'files' array, with the length # of that array in 'nfiles'. Returns 0 on success # and 1 on error. # declare -a files declare -i nfiles function listfiles() { files=() nfiles=0 COPROC_DIED=1 $DEBUGOUT listfiles if ! echo >&$in listfiles then return 1 fi while read -r <&$out -t $COPROC_TIMEOUT do if [[ "$REPLY" == "%%-HPCM_DONE-%%" ]] then $DEBUGIN "$REPLY" COPROC_DIED=0 return 0 elif [[ "$REPLY" == "%%-HPCM_ERROR-%%" ]] then $DEBUGIN "$REPLY" get_error return 1 fi files[$nfiles]="$REPLY" (( nfiles += 1 )) done return 1 } # get_file BASE EXT # # Gets file with given base name and extension. Returns # 0 on success and 1 on error. # function get_file() { local base="$1" local ext="$2" rm -f "$local_queue/$base.out" trap "rm -f $local_queue/$base.out $FIFO" EXIT eval exec "$fileout>$local_queue/$base.out" COPROC_DIED=1 $DEBUGOUT get "$base$ext" if ! echo >&$in get "$base$ext" then return 1 fi while read -r <&$out -t $COPROC_TIMEOUT do if [[ "$REPLY" == "%%-HPCM_DONE-%%" ]] then $DEBUGIN "$REPLY" COPROC_DIED=0 break elif [[ "$REPLY" == "%%-HPCM_ERROR-%%" ]] then $DEBUGIN "$REPLY" eval exec "$fileout>&-" get_error rm -f "$local_queue/$base.out" trap "rm -f $FIFO" EXIT return 1 fi echo "$REPLY" >&$fileout done eval exec "$fileout>&-" trap "rm -f $FIFO" EXIT if (( $COPROC_DIED == 0 )) then mv -f "$local_queue/$base.out" \ "$local_queue/$base$ext" >& /dev/null return 0 else rm -f "$local_queue/$base.out" return 1 fi } # print_file BASE EXT # # Prints file with given base name and extension. Gets # file first, and if extension is .ps sets file status # to .done locally and in server after printing. # Returns 0 on success and 1 on error. # function print_file { local base="$1" local ext="$2" if [[ ! -r "$local_queue/$base$ext" ]] then get_file "$base" "$ext" if [[ $? != 0 ]] then return 1 fi fi if [[ "$printer" == "" ]] then echo " lpr" "$local_queue/$base$ext" lpr "$local_queue/$base$ext" else echo " lpr" -P"$printer" \ "$local_queue/$base$ext" lpr -P"$printer" "$local_queue/$base$ext" fi if [[ "$ext" == ".ps" ]] then if ! remote "done $base$ext" then return 1 fi mv -f "$local_queue/$base.ps" \ "$local_queue/$base.done" >& /dev/null fi return 0 } # Executes 'get' command. Returns 0 on success and 1 on # error. # function get_all() { if ! listfiles then return 1 fi for file in "${files[@]}" do if [[ "$file" =~ ^(.*)(\.${NWS}*)$ ]] then base="${BASH_REMATCH[1]}" ext="${BASH_REMATCH[2]}" if [[ "$ext" != ".ps" ]] then continue fi else echo "ERROR: badly formed file name" \ "returned by server: $file" continue fi print_file $base .ps if (( $COPROC_DIED == 1 )) then return 1 fi done return 0 } # Executes 'list' command. Returns 0 on success and 1 # on error. # function list() { if ! listfiles then return 1 fi local -a goodfiles=() local -i ngoodfiles=0 local -i first=-1 for file in "${files[@]}" do if [[ "$file" =~ ^(.*)(\.${NWS}*)$ ]] then base="${BASH_REMATCH[1]}" ext="${BASH_REMATCH[2]}" if [[ "$ext" == ".ps" ]] then if (( $first == -1 )) then first=$ngoodfiles fi elif [[ "$ext" != ".done" ]] then continue fi else continue fi goodfiles[$ngoodfiles]="$file" (( ngoodfiles += 1 )) done if (( $first <= 10 )) then first=0 else (( first -= 10 )) fi while (( $first < $ngoodfiles )) do echo " ${goodfiles[$first]}" (( first += 1 )) done return 0 } # get_sum BASE EXT # # Gets MD5 sum of remote file and returns it in SUM. # Returns 0 on success and 1 on error. # declare SUM= function get_sum() { local base="$1" local ext="$2" local nsum=0 COPROC_DIED=1 $DEBUGOUT sum "$base$ext" if ! echo >&$in sum "$base$ext" then return 1 fi while read -r <&$out -t $COPROC_TIMEOUT do $DEBUGIN "$REPLY" if [[ "$REPLY" == "%%-HPCM_DONE-%%" ]] then COPROC_DIED=0 if (( $nsum == 0 )) then echo "ERROR: no sum returned by server" return 1 else return 0 fi elif [[ "$REPLY" == "%%-HPCM_ERROR-%%" ]] then get_error return 1 elif (( $nsum == 0 )) then SUM="$REPLY" elif (( $nsum == 1 )) then echo "ERROR: Superfluous line from server:" echo " $SUM" fi (( nsum += 1 )) if (( $nsum > 1 )) then echo " $REPLY" fi done return 1 } # Outer loop: creates or recreates coprocess that runs # remote or local printer_server. # while [[ x = x ]] do if [[ "$COPROC_PID" != "" ]] then kill -9 $COPROC_PID >&/dev/null wait $COPROC_PID >&/dev/null echo ERROR: coprocess died: restarting eval exec "$in>&-" "$out<&-" fi rm -f $FIFOIN $FIFOOUT if ! mkfifo $FIFOIN $FIFOOUT then echo ERROR mkfifo failed exit 1 fi if (( $test == 1 )) then "$server" "$server_queue" \ <$FIFOIN >$FIFOOUT & else ssh -T -i "$private_key" "$remote_account" \ <$FIFOIN >$FIFOOUT & fi COPROC_PID=$! eval exec "$in>$FIFOIN" "$out<$FIFOOUT" COPROC_DIED=0 # Inner loop: read and execute commands. # while (( $COPROC_DIED == 0 )) do if [[ $READ_TIMEOUT != 0 ]] then read -r -t $READ_TIMEOUT else read -r fi # # On the MAC we cannot tell the difference # between a timeout and a control-D, so we # treat them the same. # STATUS=$? if (( $STATUS != 0 )) then echo get "[at `date +%T`]" get_all if (( $COPROC_DIED == 1 )) then break else continue fi fi case "$REPLY" in doc) echo "$document" | less -F -K ;; \? | help ) echo "get Get and print ready" \ files echo "control-D Ditto" echo "list List ready and" \ recently printed files echo "time [TIMEOUT] Print or reset" \ timeout echo "" echo "quit Exit" echo "exit Exit" echo "control-C Exit" echo "" echo "get FILE Get file" echo "sum FILE Check file MD5 sums" echo "done FILE Mark file done" \ '(printed)' echo "ready FILE Mark file ready to" \ print echo "print FILE Print file" echo "remove FILE Remove file" echo "debug [on|off] Set or toggle debug" \ output echo "" echo "listdir ls -ltr server queue" echo "localdir ls -ltr local queue" echo "" echo "help Print this help info" echo "? Print this help info" echo "doc Print full" \ documentation ;; quit | exit ) exit 0 ;; listfiles) listfiles for file in "${files[@]}" do echo " $file" done ;; time* ) if [[ "$REPLY" =~ ^time${WS}*$ ]] then echo " time interval is" \ "$READ_TIMEOUT seconds" elif [[ "$REPLY" =~ \ ^time${WS}+([0-9]+)${WS}*$ ]] then READ_TIMEOUT=${BASH_REMATCH[1]} else echo "ERROR: badly formed time" \ "interval in $REPLY" fi ;; get) get_all ;; list) list ;; listdir) $DEBUGOUT listdir if ! echo >&$in listdir then COPROC_DIED=1 else finish echo fi ;; localdir) ls -ltr "$local_queue" ;; get* | sum* | print* | done* | ready* \ | remove* ) if [[ "$REPLY" =~ \ ^(${NWS}+)${WS}+(.*${NWS})${WS}*$ ]] then op="${BASH_REMATCH[1]}" file="${BASH_REMATCH[2]}" if [[ "$file" =~ ^.*/.*$ ]] then echo "ERROR: $file contains a" \ "slash (/)" continue elif [[ "$file" =~ ^\. ]] then echo "ERROR: $file begins with a" \ "dot (.)" continue elif [[ "$file" =~ ^(.*)(\.${NWS}*)$ ]] then base="${BASH_REMATCH[1]}" ext="${BASH_REMATCH[2]}" if [[ $op == "remove" ]] then if [[ "$ext" =~ \ ^(.out|.ps|.done)$ ]] then $DEBUGOUT remove \ "$base$ext" if ! echo >&$in remove \ "$base$ext" then COPROC_DIED=1 elif finish then rm -f \ "$local_queue/$base$ext" fi else echo "ERROR: $file" \ "extension is not" \ ".out, .ps, or .done" fi continue elif [[ "$ext" != ".done" \ && "$ext" != ".ps" ]] then echo "ERROR: $file extension" \ "is not .ps or .done" continue fi # Now $ext is either .ps or .done case $op in get) get_file "$base" "$ext" if [[ $? == 0 ]] then ls -l \ "$local_queue/$base$ext" fi ;; sum) if [[ -r \ "$local_queue/$base$ext" ]] then get_sum "$base" "$ext" if [[ $? != 0 ]] then continue fi locsum=`md5sum \ "$local_queue/$base$ext"` [[ "$locsum" =~ \ ^(${NWS}*)${WS} ]] locsum="${BASH_REMATCH[1]}" if [[ "$locsum" == \ "$SUM" ]] then echo " Local and" \ Server \ "$file" MD5 Sums \ Are Equal else echo " Local $file" \ MD5 \ Sum == "$locsum" echo " !=" echo " Server" \ "$file" \ MD5 Sum == "$SUM" fi else echo " Local" \ "$local_queue/$file" \ "Not Found" \ "(Unreadable)" fi ;; print ) print_file "$base" "$ext" ;; done ) if remote "done $base$ext" then mv -f \ "$local_queue/$base.ps" \ "$local_queue/$base.done" \ >& /dev/null fi ;; ready ) if remote "ready $base$ext" then mv -f \ "$local_queue/$base.done" \ "$local_queue/$base.ps" \ >& /dev/null fi ;; * ) echo "ERROR: unknown command" \ "in $REPLY" ;; esac else echo "ERROR: $file has no extension" fi else echo "ERROR: badly formed command or" \ "file name in $REPLY" fi ;; debug*) if [[ "$REPLY" =~ \ ^debug${WS}+(.*${NWS})${WS}*$ ]] then toggle="${BASH_REMATCH[1]}" elif [[ "$DEBUGOUT" == "nop" ]] then toggle=on else toggle=off fi case "$toggle" in on) DEBUGOUT="echo --->" DEBUGIN="echo <---" ;; off) DEBUGOUT=nop DEBUGIN=nop ;; *) echo ERROR: cannot understand $toggle ;; esac ;; *) echo "ERROR: Could not understand: $REPLY" echo " The \`help' command prints" \ command list. ;; esac done done exit 0