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 time
out

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$OIP>,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