objnam OPRJMR.LIT ; Created 2-Feb-88, Last modified 4-Feb-88 ; by Irv Bromberg, Medic/OS Consultants, Toronto, CANADA radix 10 vedit=4 vminor=2 vmajor=1 vsub=0 asmmsg "Hash total of OPRJMR.LIT version 1.2(4) = 642-107-376-000" if eq,1 Syntax: OPRJMR ptrnam{=trmnam},ptrnam{=trmnam},... OPRJMR stands for OPERATOR JOB Message Redirector. It is designed to re- direct OPERATOR JOB messages to a specified terminal, a feature which is of use where terminals are moved from job to job using facilities such as JOBSCN, SCAN, MULTI, FLiP, VTAM, or simple manual use of the ATTACH command. It ensures that the appropriate terminals continue to receive printer spooler and task manager OPERATOR JOB messages even though they may be detached at any time or attached to a job other than the one they were attached to at system bootup time. I suggested over a year ago to Alpha Micro that the OPERATOR=JOBNAM syntax in printer and task manager .INI files should also allow a new syntax in the form OPERATOR=TRM:TRMNAM so that one could guarantee that a specific terminal will get the messages even when it changes jobs or is detached. However, since Alpha Micro has not yet moved to adopt and implement this suggestion I was forced to develop OPRJMR which serves the same purpose without having to modify the operating system software. This capability is essential for all the installations I am directly responsible for. Does anybody know how Alpha Micro deals with this problem in installations using MULTI? In the OPRJMR command line syntax any number of printers and terminals may be specified. You may continue the list of terminals in the definition onto subsequent command lines for as many lines as necessary, by terminating each line except the last with a comma (OPRJMR prompts with "*" for each additional command line). Where a terminal name is not specified it is assumed to be the same as the printer name (only of use with AUX.DVR or equivalent where it is the CRT's auxiliary port that supports the printer). The "printer" name "TASK" is reserved for specification of a target terminal for task manager operator job messages. The terminal specified for the "TASK" printer (which must NOT actually exist as a defined printer) is the one that will receive ALL task manager operator job messages that are sent to OPRJMR (since the task manager specifies nothing about which background task originated the message OPRJMR cannot otherwise determine the appropriate destination from more than one choice). When no TASK terminal has been specified all received task manager messages are ignored. Printer spooler messages for other than the defined printers are redirected to the TASK terminal (if defined) but the first character of the message (normally a ";") will be changed to "!" as a sign that the message has been redirected to the TASK terminal as a default. OPRJMR is re-entrant, re-usable, normally is run logged out, requires only a very small job partition (8 bytes is sufficient when OPRJMR.LIT is preloaded into SYSTEM memory). May be killed using the KILL command or by hitting control-C on the user's terminal (when invoked on a real terminal, that is). Works with LPTSPL as well as TSKSPL. OPRJMR uses a 2-second timeout for output of the message to the target terminal. When the terminal is busy (DTR line held low or output suspended by XOFF control code = ^S) then the attempt to output the message is abandoned but OPRJMR continues running and normal message processing as required. Bootup .INI file changes: It is convenient to install OPRJMR.LIT in the SYS: account but it may be anywhere provided that it is correctly referred to in the SYSTEM command that loads it. OPRJMR does not HAVE to be in SYSTEM memory, it is simply most efficient to put it there because it obviates the need for any more that a minimal amount of memory in the user's partition. Define an OPRJMR pseudo terminal with a large enough input buffer to hold at least one, but preferably two pending spooler/task manager messages -- TRMDEF OPRJMR,PSEUDO,NULL,80,100,2 The type-ahead input buffer size (100 in the example above) should be larger than the input line buffer (80 in the example above) so that it can hold more than one pending message while OPRJMR is processing a previous message. Although the example shows PSEUDO.IDV and NULL.TDV in fact after invoking OPRJMR you will see by using the TRMDEF command at monitor level that the OPRJMR terminal has the OPRJMR.IDV and OPRJMR.TDV and when the OPRJMR program is killed the original .IDV and .TDV (PSEUDO and NULL in the case of the example given) will be restored. Increment your JOBS command by one and add OPRJMR to a JOBALC command. Add the command SYSTEM OPRJMR near the end of the SYSTEM loading sequence (prior to the last SYSTEM command). This loads the program into system memory so that the OPRJMR job itself only needs 4 bytes for a memory partition (otherwise it would have to search the disk to load OPRJMR.LIT, and that would require a few KB of memory of which less than 800 bytes are actually required by the program). Before the printer spooler and task manager are initialized set up the OPRJMR job -- ATTACH OPRJMR,OPRJMR KILL OPRJMR FORCE OPRJMR MEMORY 4 ; that's right, only 4 bytes! OPRJMR EPSON=EAST,QUME=MADDY,OFFICE,TASK=CYNTH WAIT OPRJMR The above example defines the "EPSON" printer's messages to be re-directed to the "EAST" terminal, the "QUME" printer's messages to be re-directed to terminal "MADDY", the "OFFICE" auxiliary printer's messages to be sent to the "OFFICE" terminal, and all task manager messages to be sent to the "CYNTH" terminal. After the OPRJMR.LIT program is executed the TRMDEF command will show that the terminal driver of the OPRJMR terminal has changed to "OPRJMR", but this is restored to the original terminal driver when (if) this program is killed. No output buffer is required since no output actually takes place to the OPRJMR terminal (the driver cancels the output and redirects it as input to OPRJMR itself). Each printer.INI and background task.INI file where you wish the OPRJMR program to handle and re-direct the messages should include the command OPERATOR=OPRJMR instead of the job name that would normally be put there. OPRJMR does not interfere with messages sent in the normal way to normal (stable, permanently attached) jobs. Limitations: OPRJMR does its level best to figure out messages are supposed to go. It is rather difficult to know where to send blank lines. OPRJMR sets a flag when it gets a blank line and when it figures out where the next message is to go it preceeds that message with a single blank line and then clears the blank line flag. Thus several blank lines in a row will be converted to a single blank line which is not sent until the next time a non-blank line is received. This is unlikely to be a problem, but where a task manager .CTL file wants to send multi-line messages with several blank lines, some of the blank lines will be ignored and one of the trailing blank lines might be redirected elsewhere later. Actually OPRJMR checks only for carriage return in the first position, so by sending a single space or tab for a $OPR task manager message any number of "blank" lines can be made to appear on the target terminal. Troubleshooting: Try using a normal job and real terminal and invoke OPRJMR from monitor level with the command syntax you are attempting to use. Observe any error messages output as a guide to troubleshooting. You cannot use a PSEUDO.IDV for the OPRJMR trmdef because that will hang the TSKMGR in an IO wait state. Where messages are getting truncated, characters are lost, or directed to the wrong terminal it is likely that the type-ahead input buffer is too small. It is a good idea not to use a ";" at the beginning of task manager $OPR messages, otherwise OPRJMR may get confused into thinking that it is a printer spooler message. Programming note: The ADDW and SUBW instructions that affect address registers normally operate on all 32 bits in the Motorola 68000 so it is not a bug that I have used them to save space wherever immediate operands are being added or subtracted on address registers. I suggested long ago to Alpha Micro, in a written letter to Bob Currier, that the M68 assembler should automatically optimize this at assembly time but are they listening? This program illustrates the following interesting techniques: - command line parsing with locator error messages - command line continuation technique - TCB searching to find matching terminal name - TDV and IDV switching and restoring (all self-contained, stand-alone) - output to any target TCB with timeout - use of user stack for variable-length workspace - symbolic register references (I swear by it!) - RADIX 10 usage (ditto) - Extensive comments (these) between conditional assembly directives that never are true (if eq,1...endc). Avoids need for ";" at beginning of each line of comments, allows paragraph wrapping conveniently. Watch out for the word "if" and the word "endc" at the beginning of lines because that will confuse the assembler! endc search SYS search SYSSYM search TRM extern $CMDER JCB=A0 Rad50=A1 ErrMsg=A1 BufPtr=A1 Buffer=A2 IDV=A3 TDV=A4 TCB=A5 Atemp=A6 SavBuf=D0 Char=D1 PTRNAM=D2 Size=D2 Count=D3 Timeout=D4 Flags=D4 CRLF.flag=31 ; use high bit as CRLF flag Length=D5 TRMNAM=D5 Dtemp=D6 NULL=0 BELL=7 LF=10 CR=13 phdr -2,0,PH$REE!PH$REU ; re-entrant & re-usable, can run ; logged out (normally not assigned ; enough memory to log in anyways!) clr Count ; pre-clear clr Flags ; ditto Process:; handles input command line, Buffer=A2 points at next char byp ; skip whitespace lin ; end of line? jeq EndDefs ; yes, end of definitions More: mov Buffer,SavBuf ; in case terminal not found call PackIt lea ErrMsg,NoName ; get set up in case name missing mov Dtemp,PTRNAM beq Syntax ; 0=name missing, syntax error mov PTRNAM,TRMNAM ; default to terminal = printer name byp ; skip possible whitespace ; at this point we may have "," "=" or line terminator movb @Buffer,Char ; get next character cmpb Char,#', ; end of this definition? beq SchTRM ; yes, go find the terminal lin ; end of line? beq SchTRM ; yes, ditto lea ErrMsg,BadPunct ; get set up in case not "=" cmpb Char,#'= ; equal sign? bne Syntax incw Buffer ; bypass "=" byp ; and any trailing whitespace lin ; end of line? beq SchTRM ; yes, default for AUX printer cmpb @Buffer,#', ; comma? beq SchTRM ; yes, default for AUX printer mov Buffer,SavBuf ; in case terminal not found call PackIt mov Dtemp,TRMNAM ; if TRMNAM=0 then take PTRNAM as bne SchTRM ; default mov PTRNAM,TRMNAM SchTRM: ; search Terminal Definitions Chain for TRMNAM & get TCB mov TRMDFC,Atemp ; index head of trmdef chain NextTrm:mov Atemp,Dtemp ; test if end of terminals defined beq NotFound ; 0=end of list, error message cmp TRMNAM,4(Atemp) ; check for matching terminal name beq 10$ ; matches, return TCB mov @Atemp,Atemp ; index the next terminal in chain br NextTrm 10$: addw #8,Atemp ; bypass link and name to TCB address ; Note that we use the user stack for storing our defined printers list ; no need to use job partition impure space, there's plenty of space in ; the allocated stack area. That another reason why we can get away with ; only a 10 byte partition if this program is pre-loaded into system memory. push Atemp ; add to the table TCB ptr push PTRNAM ; and the RAD50 packed printer name incw Count ; and increment the totals counter byp ; skip trailing whitespace lin ; end of line? beq EndDefs ; yes, all done, check count lea ErrMsg,Comma cmpb @Buffer,#', ; comma here? bne Syntax incw Buffer ; bypass comma byp ; skip possible whitespace lin ; end of line? jne More ; no, go process more type <*> ; prompt for next line kbd Abort ; wait for next line input jmp Process Syntax: call $CMDER ; output errmsg and locator exit ; let EXIT clean up stack NotFound:mov SavBuf,Buffer ; set up for proper errmsg locator lea ErrMsg,BadTRM br Syntax EndDefs:lea ErrMsg,Nothing ; if Count=0 abort with errmsg decw Count ; pre-decr for later DBF bmi Syntax Switch: jobidx JCB ; get user's JCB mov JOBTRM(JCB),TCB ; get user's TCB mov T.TDV(TCB),TDV ; save original .TDV mov T.IDV(TCB),IDV ; save original .IDV orw #T$ILC,T.STS(TCB) ; allow lowercase input lea Atemp,JMRIDV ; switch to OPRJMR.IDV mov Atemp,T.IDV(TCB) lea Atemp,JMRTDV ; switch to OPRJMR.TDV mov Atemp,T.TDV(TCB) Wait: kbd Abort ; wait for input line ; Cannot use LIN monitor call to check for blank line here because ; then it would also think that printer spooler messages which all ; start with ";" are blank lines too -- LIN considers ";" to be a ; line terminator. cmpb @Buffer,#CR ; check for empty line bne DoMSG ; not blank, process it bset #CRLF.flag,Flags ; remember we got a blank line br Wait ; and wait for following line DoMSG: ; all printer spooler messages begin with ";" mov #[TAS]_16+[K ],PTRNAM ; default to say it's TASK manager mov Buffer,SavBuf ; save buffer pointer cmpb @Buffer,#'; ; is it a printer spooler message? bne SchPTR ; go search for TASK target ; count the characters for yourself if you're not convinced, the typical ; message from a printer spooler is as follows (including the ";") ;TSKSPL - Please mount form NORMAL on OFFICE ;LPTSPL - Please mount form NORMAL on OFFICE addw #38,Buffer ; point at printer name in input line call PackIt ; convert to RAD50 mov Dtemp,PTRNAM ; and get ready for search SchPTR: ; search stack for specified printer, SP points at last defined ; Count was previously pre-decremented movw Count,Dtemp ; get #items-1 to check on stack mov SP,Atemp 10$: cmp PTRNAM,(Atemp)+ beq GotPTR addw #4,Atemp ; skip TCB lword on stack dbf Dtemp,10$ ; Printer was not found, if first character is ";" change it to "!" and ; try again, this time to send it to the TASK terminal mov SavBuf,Buffer ; restore buffer cmpb @Buffer,#'; ; was it printer message? bne Wait ; no, didn't find "TASK" terminal movb #'!,@Buffer ; cause ptr msg --> TASK terminal br DoMSG ; and go try again GotPTR: mov @Atemp,TCB ; get printer's OPR terminal TCB bclr #CRLF.flag,Flags ; did we get preceeding blank line? beq 10$ ; no save D0,A2 ; save SavBuf,Buffer lea Buffer,NewLine ; send CRLF first mov Buffer,SavBuf bcall toutput ; output CRLF rest D0,A2 ; recall where we were & continue bmi Wait ; abandon if timed out 10$: mov SavBuf,Buffer ; restore buffer pointer bcall toutput ; output the string to target bmi Wait ; abandon if timed out lea Buffer,Beep ; ring terminal's bell too mov Buffer,SavBuf bcall toutput br Wait ; and go back for more work Abort: mov JOBTRM(JCB),TCB ; recall our own TCB ; no need to flush buffer after CTRLC because monitor does it mov TDV,T.TDV(TCB) ; restore original terminal driver mov IDV,T.IDV(TCB) ; restore original interface driver exit ; let EXIT handle stack cleanup PackIt: ; Subroutine called to pack a name into RAD50 and return it in Dtemp ; register. Temporarily uses the stack as a workspace. push ; get workspace on stack for pack mov SP,Rad50 pack pack pop Dtemp ; get packed name & clean up stack rtn toutput: ; sends NULL-terminated string pointed at by Buffer=A2=D0 to the ; terminal whose TCB=A5 is passed. Returns nothing. ; Destroys several registers that don't matter to caller. ; Timeout is fixed at 1 second maximum. ; does nothing when target TDV has TD$NUL attribute ; returns with N-bit set (minus) if timed out ChkTDV: mov T.TDV(TCB),Atemp ; check if terminal has NULL output btst #6,TD.TYP(Atemp) ; by checking TD$NUL bit of TDV attr beq 10$ ; 0=output is not NULL, continue rtn 10$: save D3 movw #20,Timeout ; preset to 2 second timeout ChkLEN: ; first determine the length of the message to be sent clr Length ; pre-clear 10$: tstb (Buffer)+ ; get length of string beq 20$ ; terminate at NULL byte inc Length ; count 'em as we go br 10$ ; loop back for more 20$: tst Length ; was string empty? beq outEND ; yes, ignore it mov SavBuf,Buffer ; restore ptr to start of string AddData:supvr ; into supervisor mode svlok ; disable interrupts mov T.OBS(TCB),Size ; get total output buffer size mov T.OBX(TCB),BufPtr ; get current buffer count sub BufPtr,Size ; calculate space remaining bne 5$ lsts #0 decw Timeout ; out of time? bmi outEND ; yes, abandon output sleep #1000 ; wait 1/10 sec then try again ; no need to test for CTRLC because only 1 second timeout anyways br AddData ; go back and try again 5$: add T.OBF(TCB),BufPtr cmp Length,Size ; will whole string fit? bhi 10$ ; no mov Length,Size ; set total string size for output 10$: sub Size,Length ; decrease size by amount transferred add Size,Count ; count how many we output add Size,T.OBX(TCB) br 30$ ; enter at end of DBF loop 20$: movb (Buffer)+,(BufPtr)+ ; transfer chars into output buffer 30$: dbf Size,20$ lsts #0 ; unlock CPU clr D3 ; clear D3 count for TRMBFQ call trmbfq ; merely to cause TINIT call tst Length ; any more left in this string? bhi AddData ; yep, keep going outEND: rest D3 ; don't change to POP! (CCR affected) rtn ; output all done (or timed out) word [OPR],[JMR] ; OPRJMR.IDV JMRIDV: br ChrOut ; handle output "kick-start" rtn ; no init routine ChrOut: ; Instead of physically "kick-starting" the output simply flush the ; entire output queue until empty. push Char ; save register clr Char ; pre-clear for byte move by TRMOCP NxtChr: trmocp ; get next output character tst Char ; negative=end of output queue bpl NxtChr ; loop until no longer positive ClrOIP: andw #^C,T.STS(TCB) ; clear Output-In-Progress flag pop Char ; restore register rtn word [OPR],[JMR] ; OPRJMR.TDV JMRTDV: word TD$LCL ; terminal attributes - new format, no echo rtn ; No Input routine. br TrapOut ; Special OUTPUT routine. rtn ; No ECHO routine. rtn ; No CRT routine. rtn ; No INIT routine. TrapOut: cmpb Char,#LF ; ignore linefeeds, added by TRMSER for us beq 10$ ; following CRs that we force as input trmicp Char ; re-direct output as input! 10$: clr Char ; cancel character rtn ; TRMSER will ignore character ; error messages output by syntax checker BadTRM: asciz "Terminal does not exist" NoName: asciz "Terminal or Printer name missing here" BadPunct: asciz "Expecting comma, equal sign, or line terminator here" Nothing: asciz "No definitions specified" Comma: asciz "Expecting comma here" NewLine: byte CR,LF,NULL Beep: byte BELL,NULL asmmsg "Now execute the command: .LNKLIT OPRJMR to finish up" even end .