previous next top contents index framed top this page unframed
Of course, nothing in MAINSAIL prevents you from writing such things
without using STREAMS.
You could write a program using operating-system-specific calls
(using the Foreign Language Interface) and
then port the program to each platform you need to run it on.
However, in STREAMS, XIDAK has already ported a set of general-purpose
facilities to many different platforms.
These general-purpose facilities can be used to write many different
types of applications using a single, consistent model.
A server is similar in many ways to a MAINSAIL MODULE.
Both a server and a MODULE have
well-defined interfaces that provide
specific functions.
The MAINSAIL Remote Procedure Call (RPC) mechanism takes advantage
of the similarity between servers and MODULEs:
it lets you write a server that is just a MAINSAIL MODULE.
Writing and starting a server in MAINSAIL entails these steps:
MAINSAIL automatically deals with client connections,
using the appropriate communications protocols.
Example 2–1. Intmod Containing Interface for ECHOMOD
Example 2–2. ECHOMOD: A MODULE That Provides a Trivial Service
Example 2–3. ECHOTST, a MODULE That Tests ECHOMOD
Examples 2–5, 2–6, and 2–7 show the
changed MODULEs.
The changed parts are underlined.
The MODULE names have also been changed so that you cannot confuse
them with the MODULEs of the previous section,
but this is not required when you write your own server.
Example 2–5. Changes to ECHOHDR for RPC
Example 2–6. Changes to ECHOMOD for RPC
Example 2–7. Changes to ECHOTST for RPC
2.1. What Can I Do Using STREAMS?
STREAMS can be used to write the following types of applications
(among others) in a portable way:
2.2. How to Write Network Servers
Network servers (or “traditional” servers) are separate, independent
processes that provide services to other processes (the client
processes).
The server process often runs on a different machine from the client
process.
*rpcsrv service>serviceName yourModuleName<eol>
2.2.1. A Normal MAINSAIL MODULE That Provides a Simple Service
Examples 2–1 and 2–2 show the header and implementation for
a MODULE ECHOMOD that provides a trivial service
(converting a STRING to upper case).
This is a normal MAINSAIL MODULE that can be tested by using the
MODULE of Example 2–3;
a sample run is shown in Example 2–4.
BEGIN "echoHdr"
$DIRECTIVE "NOOUTPUT";
CLASS echoCls (
STRING PROCEDURE echoUpperCase (STRING s);
);
MODULE(echoCls) echoMod;
SAVEON;
END "echoHdr"
BEGIN "echoMod"
RESTOREFROM "echoHdr";
STRING PROCEDURE echoUpperCase (STRING s);
RETURN(cvu(s));
END "echoMod"
BEGIN "echoTst"
RESTOREFROM "echoHdr";
INITIAL PROCEDURE;
BEGIN
POINTER(echoCls) p;
p := new(echoMod);
write(logFile,
p.echoUpperCase
($sGet("String to convert to upper case: ")),
eol);
dispose(p);
END;
END "echoTst"
2.2.2. Changing a MODULE to Be an RPC Server
The following changes are needed to a MODULE that provides a service
and a MODULE that calls it in order to make them into an RPC server
and an RPC client:
BEGIN "echoHdrRpc"
$DIRECTIVE "NOOUTPUT";
RESTOREFROM "rpcHdr";
CLASS($remoteModuleCls) echoCls (
STRING PROCEDURE echoUpperCase (STRING s);
);
MODULE(echoCls) echoModRpc;
SAVEON;
END "echoHdrRpc"
BEGIN "echoModRpc"
RESTOREFROM "echoHdrRpc";
STRING PROCEDURE echoUpperCase (STRING s);
RETURN(cvu(s));
$remoteModuleDefaults
END "echoModRpc"
BEGIN "echoTstRpc"
RESTOREFROM "echoHdrRpc";
INITIAL PROCEDURE;
BEGIN
POINTER(echoCls) p;
$initializeStreams;
p := $newRemoteModule("service>echosrv","echoModRpc");
write(logFile,
p.echoUpperCase
($sGet("String to convert to upper case: ")),
eol);
dispose(p);
END;
END "echoTstRpc"
Example 2–8. Compiling with the RPC Subcommand
MAINSAIL (R) Compiler Copyright (c) 1984-1998 by XIDAK, Inc., Point Arena, California, USA. compile (? for help): echomodrpc.msl,<eol> > rpc<eol> > <eol> Opening intmod for $SYS from ... echomodrpc.msl 1 Opening intmod for ECHOHDRRPC from ... Opening intmod for RPCHDR from ... Opening intmod for $SYS from ... Client for ECHOMODRPC stored on echomodrpccli.msl Server for ECHOMODRPC stored on echomodrpcsrv.msl Intmod for ECHOMODRPC not stored |
The compilation produces two MODULEs with names derived from the
name of the server MODULE:
one for the server (ending in srv) and one for the client
(ending in cli).
It is possible (but not required) that the server and client may
run on two different types of machines.
In Examples 2–9 and 2–10 it is assumed that
the server runs on SPARC UNIX and the client runs
on RISC System/6000 UNIX (abbreviated USPA and UIRS,
respectively; the abbreviations used in file names for
these operating systems are usp and uir).
Example 2–9. Compiling the Server MODULEs
Example 2–10. Compiling the Client MODULEs
To simplify this example, we will make a private copy
of the services table and a private copy of the unregistered
server directory (which avoids having to register the service with
the operating system).
This means you will be the only user who can access the server
you will set up (unless other users also elect to use your private
services table and unregistered server directory).
To make a server that could be accessed by everyone, you would
modify the site-wide services table and register the
service with the operating system as described in
Appendix A.
To set up your own services table describing the service
echosrv running on the host serverHost and to be accessed from
the host clientHost,
create a file (call it xidakservices) containing the following:
This example assumes that TCP is the appropriate protocol for these
two hosts;
substitute a different protocol if appropriate to your system
(if a system-wide services table is already set up, you
may be able to find the appropriate protocols in it;
try opening the file with the logical name:
from MAINSAIL).
Put the xidakservices file you have created on a directory that is
accessible from both client and server nodes.
For purposes of the example, assume the full file name is
/myNode/xidakservices.
Create a directory (say, servicesDirectory; the name does not
matter) that is accessible from both client and server nodes.
This is the unregistered server directory.
For purposes of the example, assume the full file name is
/myNode/servicesDirectory.
Finally, in the system parameter file (e.g.,
v1620.prm) on
both the directory where the server
is to run and the directory where the client is to run,
add the following lines to the $MAINEX group:
This assures that you access your own services table
and unregistered server directory instead of the system-wide ones,
if system-wide ones exist.
After finishing with this example, be sure to remove the above lines
from your v1620.prm file
so that you can once
again access system-wide servers.
Your server is now running.
Then, while connected to the node where the client is to run,
run the test client:
If you like, verify that more than one client process can communicate
with the server at a time by running the test simultaneously in
another MAINSAIL process.
In this example, there is no polite way to kill the server;
you will have to abort the server process in a system-dependent
manner, e.g., with CTRL-C.
Finally, when you are finished testing your server,
do not forget to remove the lines you added to
your v1620.prm file; otherwise, you will not
be able to access system-wide servers.
It is often useful to invoke a legacy application from another program.
You can do this by creating a child process that communicates through
a pseudoterminal stream with its parent.
The STREAMS MODULE PROCESS
is used to create such a child process.
Example 2–11 shows a MODULE that invokes the UNIX C compiler
(which is normally named cc).
This program passes some arguments to cc,
then waits for cc to finish execution.
It writes any output from cc during execution to logFile.
A sample execution of the program of Example 2–11 is shown in
Example 2–12.
Example 2–11. Invoking the C Compiler from a Program
Example 2–12. Execution of the Program of Example 2–11
As an example of a program where parallel processing
could be useful, consider Conway's Life.
Life is a popular cellular automaton
that has been written about in many places.
Two excellent references are
Martin Gardner's Wheels, Life, and Other Mathematical Amusements
(New York: W.H. Freeman and Company, 1983)
and William Poundstone's The Recursive Universe
(New York: William Morrow and Company, 1985).
In case you are unfamiliar with the rules, here they are:
It is easy to write a program that executes and displays the
rules above,
and many such programs have been written.
It is also easy to see how to decompose the problem so
as to use parallel processing:
the board can be divided into regions,
and each subprocess can work on a single region.
Examples 2–14, 2–15, 2–16, and 2–17 show
a program that divides the work at each step in this way.
The basic architecture of the program is as shown in
Figure 2–13.
The parent process starts a number child processes (which are RPC
servers) on different hosts.
It then communicates with them through RPC calls.
When the parent is finished, it terminates the child processes.
Figure 2–13. The Main Program Starts Multiple
Parallel Processes on Different Hosts and Communicates
with Them through RPC
In order to run the program of
Examples 2–14, 2–15, 2–16, and 2–17,
you must compile LIFEHDR, LIFE, and LIFECOMOD
for the machine where
the main program is to run.
You can also compile SUBLIFEMOD for the same machine if you
want to use the ! option (meaning run SUBLIFEMOD in the same
process instead of remotely).
You must also compile SUBLIFEMOD with the RPC compiler
(as in Section 2.2) and
the result client MODULE (SUBLIFEMODCLI)
for the machine where the
main program is to run.
You must then compile LIFEHDR, SUBLIFEMOD,
and the server MODULE
produced by the RPC compilation (SUBLIFEMODSRV)
on the login directory
on each machine where a child process is to run.
Finally, you must make sure that the gensrv server process is
running on each machine where a child process is to run.
If your site uses STREAMS extensively, it may already be running.
However, if you attempt to run LIFE and get an error about a
refused connection, you can follow the directions in
Section A.5 to start gensrv.
Example 2–14. LIFEHDR, Life Program Intmod
Example 2–15. LIFE, Life Main Program
Example 2–16. LIFECOMOD, MODULE for Dealing with a Single
Client Coroutine
Example 2–17. SUBLIFEMOD, Server MODULE to Which Computation
Is “Subcontracted”
The only kind of device for which STREAMS currently provides such
low-level control is terminals.
Normal line-oriented terminal I/O automatically provides such
features as line editing and echo (which are actually under the control
of the operating system).
STREAMS provides the ability to control such characteristics directly.
For example, Example 2–18 shows a program that
reads in a password without echoing it.
Example 2–18. A Program That Reads a Password without
Echoing It
2.2.4. Compiling the Server and Client MODULEs
You need to compile two MODULEs to run in the server
and two MODULEs
to run in the client.
The MODULEs that run in the server are the server MODULE itself
and the srv MODULE produced by the RPC compilation of the
previous section.
The MODULEs that run in the client are the client MODULE itself
and the cli MODULE produced by the RPC compilation.
MAINSAIL (R) Compiler
Copyright (c) 1984-1998 by XIDAK, Inc., Point Arena,
California, USA.
compile (? for help): echomodrpc.msl,<eol>
> target uspa<eol>
> <eol>
Opening intmod for $SYS from ...
echomodrpc.msl 1
Opening intmod for ECHOHDRRPC from ...
Opening intmod for RPCHDR from ...
Objmod for ECHOMODRPC stored on echomodrpc-usp.obj
Intmod for ECHOMODRPC not stored
compile (? for help): echomodrpcsrv.msl<eol>
Opening intmod for $SYS from ...
echomodrpcsrv.msl 1
Opening intmod for RPCHDR from ...
Objmod for ECHOMODRPCSRV stored on echomodrpcsrv-usp.obj
Intmod for ECHOMODRPCSRV not stored
MAINSAIL (R) Compiler
Copyright (c) 1984-1998 by XIDAK, Inc., Point Arena,
California, USA.
compile (? for help): echotstrpc.msl,<eol>
> target uirs<eol>
>
Opening intmod for $SYS from ...
echotstrpc.msl 1
Opening intmod for ECHOHDRRPC from ...
Opening intmod for RPCHDR from ...
Objmod for ECHOTSTRPC stored on echotstrpc-uir.obj
Intmod for ECHOTSTRPC not stored
compile (? for help): echomodrpccli.msl<eol>
Opening intmod for $SYS from ...
echomodrpccli.msl 1
Opening intmod for RPCHDR from ...
Objmod for ECHOMODRPCCLI stored on echomodrpccli-uir.obj
Intmod for ECHOMODRPCCLI not stored
2.2.5. Adding an Entry for Your Service to the Services
Table and Setting Up the Unregistered Server
Directory
These two steps are the
most difficult in creating a server, since they normally
involve modifying
resources (the services table and the operating system's
service registry) that are shared with other users.
This may require coordination with your system manager.
Fortunately, these steps normally need to be done only once for
each server name that is used.
Once a server name is entered in the services table and
registered, it stays that way unless someone explicitly removes it.
XIDAKSERVICES 1
DEFAULTPROTOCOL tcp
SERVICE serverHost:echosrv SUPPORTS echomodrpc
(xidak services)
ENTER (xidak services) /myNode/xidakservices
GLOBALSYMBOL servicesDirectory /myNode/servicesDirectory
2.2.6. Starting the Server
While connected to the node where the server is to run,
run MAINSAIL and invoke RPCSRV:
*rpcsrv service>echosrv echomodrpc<eol>
service>echosrv = ECHOMODRPC
*echotstrpc<eol>
String to convert to upper case: This is a string.<eol>
THIS IS A STRING.
2.3. How to Write Embedded Legacy Applications
For the purposes of this section, a legacy application is one
whose only available interface is through its terminal interaction.
The UNIX C compiler is such an application.
BEGIN "cc"
RESTOREFROM "strHdr";
STRING PROCEDURE invokeCc (STRING args);
BEGIN
STRING line,s;
POINTER($stream) st;
s := "";
$openStream(st,"process>cc " & args);
# Create the child process and open a stream to its
# pseudoterminal
WHILE $success($readStream(st,line,errorOK)) DO
# as long as reading from the child's pseudoterminal
# does not result in the end-of-stream indicator...
write(s,line,eol);
# ... append the result to the output
$closeStream(st);
# close the stream (since the child process has died)
RETURN(s);
# Return cc's output to the caller
END;
INITIAL PROCEDURE;
BEGIN
$initializeStreams;
write(logFile,invokeCc($sGet("cc arguments: ")));
END;
END "cc"
*cc<eol>
cc arguments: -o prog prog.c<eol>
*
2.4. How to Write Parallel Processing Applications
A program that uses parallel processing divides a problem into
several parts (subprocesses)
that can be farmed out to several different processors.
The main program sends the data for each part to the appropriate
remote processor,
the remote processor computes a result,
and finally the remote processor transmits the result back to the
main program.
The main program may have to integrate the results from the
various subprocesses
or periodically synchronize the activities of the subprocesses.

BEGIN "lifeHdr"
$DIRECTIVE "NOOUTPUT";
RESTOREFROM "rpcHdr";
MODULE life (
INTEGER ARRAY(1 TO *,1 TO *) lifeBoard;
# The INTEGER is a character code in the range '0'
# to '9', representing how many turns this cell
# has been filled ('9' if more than 9 turns), or
# ' ', meaning empty
INTEGER maxX,maxY; # upper bounds of lifeBoard
INTEGER numStepsAtATime;
# Number of Life steps to perform between displays
);
CLASS lifeCoCls (
# One data section of this MODULE per coroutine.
# Each coroutine talks to a remote instance of
# subLifeMod.
INTEGER lowX,highX,lowY,highY;
# Bounds of lifeBoard for which this MODULE is
# responsible
STRING hostName;
# Name of host on which to run my subLifeMod
# server, or "!" if should run locally.
PROCEDURE initMe;
PROCEDURE deInit;
PROCEDURE doLifeCo;
PROCEDURE copyResultsToLifeBoard;
);
MODULE(lifeCoCls) lifeCoMod;
CLASS($remoteModuleCls) subLifeCls (
PROCEDURE runLife
(MODIFIES INTEGER ARRAY(*,*) ary;
INTEGER numSteps);
);
MODULE(subLifeCls) subLifeMod;
SAVEON;
END "lifeHdr"
BEGIN "life" # Conway's life, distributed
RESTOREFROM "lifeHdr";
POINTER($ranCls) ranPtr;
LONG INTEGER stepNum; # Current step number in game
PROCEDURE initializeLifeBoard;
BEGIN
INTEGER x,y;
maxX := $iGet("Width of Life board: ");
maxY := $iGet("Height of Life board: ");
new(lifeBoard,1,maxX,1,maxY);
# Randomly fill in the board. Approximately 1/3 of
# spaces (arbitrary) are initially filled.
FOR x := 1 UPTO maxX DO FOR y := 1 UPTO maxY DO
lifeBoard[x,y] :=
IF NOT ranPtr.$rand MOD 3L THEN '0' EL ' ';
END;
PROCEDURE displayLifeBoard;
# A more sophisticated display would be nice.
BEGIN
INTEGER x,y;
write(logFile,eol & "Life at step #",stepNum,
":" & eol & eol);
FOR y := maxY DOWNTO 1 DOB
FOR x := 1 UPTO maxX DO
cWrite(logFile,lifeBoard[x,y]);
write(logFile,eol) END;
write(logFile,eol);
END;
INITIAL PROCEDURE;
BEGIN
INTEGER i,numHosts,stripWidth;
STRING s,hostNames;
POINTER(lifeCoCls) p;
POINTER(lifeCoCls) ARRAY (1 TO *) lifeCoAry;
$initializeStreams;
ranPtr := new($ranMod);
ranPtr.$initRand($date,$time);
initializeLifeBoard;
numStepsAtATime := $iGet
("Number of Life steps to perform between displays: ");
numHosts := 0; hostNames := "";
WHILE s := $sGet
("Next host ('!' to run locally, <eol> to stop): ")
DOB write(hostNames,s,eol); numHosts .+ 1 END;
# Divide lifeBoard into vertical strips (other divisions
# are possible, but this one is easy)
IF NOT stripWidth := maxX DIV numHosts THEN
errMsg("More hosts than the board is wide!","",fatal);
new(lifeCoAry,1,numHosts);
FOR i := 1 UPTO numHosts DOB
lifeCoAry[i] := p := new(lifeCoMod);
p.lowX := 1 + stripWidth * (i - 1);
p.highX := IF i = numHosts THEN maxX
EL stripWidth * i;
p.lowY := 1; p.highY := maxY;
read(hostNames,p.hostName);
p.initMe END;
displayLifeBoard;
DOB FOR i := 1 UPTO numHosts DO
$queueCoroutine($createCoroutine(lifeCoAry[i],
"doLifeCo"));
$waitForDescendants;
FOR i := 1 UPTO numHosts DO
lifeCoAry[i].copyResultsToLifeBoard;
stepNum .+ cvli(numStepsAtATime);
displayLifeBoard END
UNTIL $sGet
("<eol> to continue, anything else to stop: ");
FOR i := 1 UPTO numHosts DOB
lifeCoAry[i].deInit; dispose(lifeCoAry[i]) END;
dispose(lifeBoard); dispose(ranPtr);
END;
END "life"
BEGIN "lifeCoMod"
RESTOREFROM "lifeHdr";
# Each instance of this MODULE runs in its own coroutine
# when in the "doLifeCo" interface PROCEDURE. The
# coroutine's job is to talk with a single remote
# subLifeMod instance long enough to do a single call to
# the runLife PROCEDURE.
POINTER($stream) chCtl,chTty;
POINTER(subLifeCls) subLifePtr;
INTEGER ARRAY(*,*) subLifeBoard;
# My own copy of my area of the board
PROCEDURE initMe;
# Note: The child server can be properly started only if
# the host has a bootstrap named $MS/mainsa and the
# remote MODULE subLifeMod has been compiled for the
# host and is sitting on the login directory on that
# host.
BEGIN
IF hostName = "!" THEN # do locally (within this process)
subLifePtr := new(subLifeMod)
EB $openStream(chTty,chCtl,
"process(" & hostName & ")>"
& "$MS/mainsa rpcsrv - subLifeMod");
subLifePtr := $newRemoteModule(chCtl,"subLifeMod");
END;
new(subLifeBoard,
# Allow extra space because edges become inaccurate
# as computation proceeds
(lowX - numStepsAtATime) MAX 1,
(highX + numStepsAtATime) MIN maxX,
(lowY - numStepsAtATime) MAX 1,
(highY + numStepsAtATime) MIN maxY);
END;
PROCEDURE deInit;
BEGIN
dispose(subLifePtr);
IF chCtl THENB
$closeStream(chCtl);
$closeStream(chTty); # This kills the child process
END;
dispose(subLifeBoard);
END;
PROCEDURE reader;
BEGIN
STRING s;
WHILE $success($readStream(chTty,s,errorOK)) DO
$writeStream($tty,s,$line);
# Can get here only if child dies unexpectedly
errMsg("Unexpected error reading from child on host",
hostName,fatal);
END;
PROCEDURE doLifeCo;
BEGIN
INTEGER x,y;
POINTER($coroutine) readerCo;
IF chCtl THEN
# Handle child process's TTY output, in case of errors
$queueCoroutine(readerCo :=
$createCoroutine(thisDataSection,"reader"));
FOR x := subLifeBoard.lb1 UPTO subLifeBoard.ub1 DO
FOR y := subLifeBoard.lb2 UPTO subLifeBoard.ub2 DO
subLifeBoard[x,y] := lifeBoard[x,y];
subLifePtr.runLife(subLifeBoard,numStepsAtATime);
IF chCtl THEN $killCoroutine(readerCo);
$reschedule(delete);
END;
PROCEDURE copyResultsToLifeBoard;
# This is called when all the children have finished
# to update the master board.
BEGIN
INTEGER x,y;
FOR x := lowX UPTO highX DO
FOR y := lowY UPTO highY DO
lifeBoard[x,y] := subLifeBoard[x,y];
END;
END "lifeCoMod"
BEGIN "subLifeMod"
RESTOREFROM "lifeHdr";
# This MODULE is the child (which is also the server, in
# this case).
INTEGER ARRAY(*,*) newAry;
INTEGER PROCEDURE numNeighbors (INTEGER ARRAY(*,*) ary;
INTEGER x,y);
BEGIN
INTEGER xx,yy,sum;
sum := 0;
FOR xx := x - 1 MAX ary.lb1 UPTO x + 1 MIN ary.ub1 DO
FOR yy := y - 1 MAX ary.lb2 UPTO y + 1 MIN ary.ub2 DO
IF NOT (xx = x AND yy = y) AND ary[xx,yy] NEQ ' '
THEN sum .+ 1;
RETURN(sum);
END;
PROCEDURE runLife
(MODIFIES INTEGER ARRAY(*,*) ary;
INTEGER numSteps);
BEGIN
INTEGER i,x,y;
INTEGER ARRAY(*,*) swapAry;
IF newAry AND
(newAry.lb1 NEQ ary.lb1 OR newAry.ub1 NEQ ary.ub1
OR newAry.lb2 NEQ ary.lb2 OR newAry.ub2 NEQ ary.ub2)
THEN dispose(newAry);
IF NOT newAry THEN
new(newAry,ary.lb1,ary.ub1,ary.lb2,ary.ub2);
FOR i := 1 UPTO numSteps DOB
FOR x := ary.lb1 UPTO ary.ub1 DO
FOR y := ary.lb2 UPTO ary.ub2 DO
CASE numNeighbors(ary,x,y) OFB
# If a filled cell has two or three filled
# neighbors, it survives.
# If an empty cell has three filled
# neighbors, it is filled (born).
# Otherwise, the cell becomes empty.
[2] IF '0' LEQ ary[x,y] LEQ '8' THEN
newAry[x,y] := ary[x,y] + 1
# Mark it has having been around
# one turn longer
EL newAry[x,y] := ary[x,y];
[3] IF ary[x,y] = ' ' THEN
newAry[x,y] := '0'
EF '0' LEQ ary[x,y] LEQ '8' THEN
newAry[x,y] := ary[x,y] + 1
EL newAry[x,y] := '9';
[ ] newAry[x,y] := ' ';
END;
swapAry := ary; ary := newAry; newAry := swapAry END;
END;
$remoteModuleDefaults
END "subLifeMod"
2.5. How to Write Low-Level Device Control Applications
If you open a stream to a particular device, you can use STREAMS
PROCEDUREs (some of which may be device-specific) to control that
device.
BEGIN "dontEchoPassword"
RESTOREFROM "strHdr";
IFC $charSet = $ascii THENC
DEFINE bs = 8, del = 127;
ELSEC
MESSAGE "Unknown character set","error";
ENDC
INITIAL PROCEDURE;
BEGIN
INTEGER ch;
STRING s,ss;
$initializeStreams;
$writeStream($tty,"Password: "); ss := "";
DOB "outer"
$readStream($tty,s);
# Because $line bit is not specified, no echoing
# occurs; read returns as soon as a character is
# available
WHILE ch := cRead(s) GEQ 0 DOB
# If you want to allow backspace or delete to
# erase (invisible) previously typed character,
# you have to do it explicitly here (operating
# system does not provide line editing in this
# mode).
IF ch = $optionalFirstEol OR ch = last(eol) THEN
DONE "outer";
IF ch = bs OR ch = del THEN rcRead(ss)
EL cWrite(ss,ch) END END "outer";
write(logFile,eol & "The password you typed was ",ss,eol);
END;
END "dontEchoPassword"
MAINSAIL STREAMS User's Guide, Chapter 2