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!
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