-- FileOp.mesa
-- edited by Schroeder, March 16, 1981 6:04 PM
-- edited by Brotz, February 13, 1981 11:44 AM
DIRECTORY
crD: FROM "CoreDefs",
csD: FROM "CoreStreamDefs",
dsD: FROM "DisplayDefs",
exD: FROM "ExceptionDefs",
FTPDefs,
gsD: FROM "GlobalStorageDefs",
InlineDefs,
intCommon: FROM "IntCommon",
inD: FROM "InteractorDefs",
LaurelSendDefs,
opD: FROM "OperationsDefs",
ovD: FROM "OverviewDefs",
SendDefs,
Storage,
String,
Stream,
TimeDefs,
TimeExtraDefs;
FileOp: PROGRAM
IMPORTS crD, csD, dsD, exD, FTPDefs, gsD, InlineDefs, intC: intCommon,
LaurelSendDefs, SendDefs, Storage, String, TimeDefs, TimeExtraDefs
EXPORTS opD
SHARES opD =
PUBLIC BEGIN
ftpInitialized: BOOLEAN ← FALSE;
expandProtocol: LaurelSendDefs.ProtocolType = LaurelSendDefs.GetSendProtocol[];
-- utility procedures
NameType: TYPE = {remote, dl, local, bad};
ParseName: PROCEDURE [name: STRING]
RETURNS [t: NameType, part1, part2: STRING] =
BEGIN
start: [0..1];
i, upArrowIndex: CARDINAL;
upArrowSeen: BOOLEAN ← FALSE;
part1 ← NIL;
part2 ← NIL;
IF name = NIL OR name.length = 0 THEN GOTO bad;
start ← IF name[0] = ’@ THEN 1 ELSE 0;
FOR i IN [start .. name.length) DO
SELECT name[i] FROM
’↑ => {upArrowSeen ← TRUE; upArrowIndex ← i};
’* => GOTO bad;
ENDCASE;
ENDLOOP;
SELECT TRUE FROM
name[start] = ’[ => --remote--
BEGIN
t ← remote;
[part1, i] ← MakeDelimitedString[’[, ’], start, name];
IF part1 = NIL THEN GOTO bad;
[part2, ] ← MakeDelimitedString[’], 0C, i, name];
IF part2 = NIL THEN GOTO bad;
END;
upArrowSeen => --dl--
BEGIN
t ← dl;
IF upArrowIndex+1 < name.length --chars after ↑ must be registry--
THEN
BEGIN
[part1, ] ← MakeDelimitedString[’., 0C, upArrowIndex+1, name];
IF part1 = NIL THEN GOTO bad;
[part2, ] ← MakeDelimitedString[0C, 0C, start, name];
END
ELSE
BEGIN
part1 ← MakeHeapString [intC.user.registry];
[part2, ] ← MakeDelimitedString[0C, 0C, start, name, part1.length+1];
String.AppendChar[part2, ’.];
String.AppendString[part2, part1];
END;
END;
ENDCASE => --local--
BEGIN
t ← local;
[part1, ] ← MakeDelimitedString[0C, 0C, start, name];
END;
EXITS bad => t ← bad;
END; --of ParseName--
MakeDelimitedString: PRIVATE PROCEDURE [startChar, endChar: CHARACTER,
start: CARDINAL, source: STRING, space: CARDINAL ← 0]
RETURNS [target: STRING, next: CARDINAL] =
-- If source[start] = startChar and endChar appears later in source, allocates a
-- string for target and places in it all the intervening characters from
-- source. Next is index of endChar in source. (startChar = NIL means target
-- unconditionally begins with source[start]; endChar = NIL means target
-- unconditionally ends with last character of source.) IF result would be of
-- length 0 or startChar or endChar are not found then returns target = NIL.
-- When a string is returned it will have space extra character slots at the end.
BEGIN
index, length: CARDINAL;
target ← NIL;
IF source.length <= start THEN RETURN;
IF startChar # 0C THEN
{IF source[start] # startChar THEN RETURN; start ← start + 1};
IF endChar # 0C THEN
FOR next IN [start .. source.length) DO
IF source[next] = endChar THEN EXIT;
REPEAT
FINISHED => RETURN;
ENDLOOP
ELSE next ← source.length;
length ← next - start;
IF length = 0 THEN RETURN;
target ← Storage.String[length+space];
FOR index IN [0 .. length) DO
target[index] ← source[index+start];
ENDLOOP;
target.length ← length;
END; -- of MakeDelimitedString --
MakeHeapString: PRIVATE PROCEDURE [input1: STRING,
input2: STRING ← NIL, input3: STRING ← NIL, input4: STRING ← NIL]
RETURNS [output: STRING] =
-- Concatenate string bodies into a heap string
BEGIN
IF input1 = NIL THEN output ← NIL
ELSE BEGIN
length: CARDINAL ← input1.length;
IF input2 # NIL THEN length ← length + input2.length;
IF input3 # NIL THEN length ← length + input3.length;
IF input4 # NIL THEN length ← length + input4.length;
output ← Storage.String[length];
String.AppendString[output, input1];
IF input2 # NIL THEN String.AppendString[output, input2];
IF input3 # NIL THEN String.AppendString[output, input3];
IF input4 # NIL THEN String.AppendString[output, input4];
END;
END; -- of MakeHeapString --
FreeErrorString: PUBLIC PROCEDURE [string: STRING] =
{Storage.FreeString[string]};
MakeLocalFileErrorString: PRIVATE PROCEDURE [e: ovD.ErrorCode]
RETURNS [STRING] =
{ RETURN [ MakeHeapString [ "Local file "L,
SELECT e FROM
ovD.illegalFilename => "name illegal."L,
ovD.fileNotFound => "name not found."L,
ovD.fileInUse => "already in use."L,
ovD.diskFull => "would overfill disk."L,
ovD.fileTooBig => "would be too big."L,
ENDCASE => "disk error."L] ] };
MakeCantConnectMessage: PROCEDURE[h, fs: STRING]
RETURNS [ovD.ErrorCode, STRING] =
{RETURN[ovD.cantConnect,
MakeHeapString["Can’t connect to """L, h, """: "L, fs]]};
ReportBadName: PROCEDURE RETURNS [ovD.ErrorCode, STRING] =
{RETURN[ovD.illegalFilename, MakeHeapString["Illegal name given."L]]};
MakeFTPError: PROCEDURE [h, m, e: STRING] RETURNS [s: STRING]=
BEGIN
IF e#NIL THEN Storage.FreeString[e];
s ← MakeHeapString ["FTP error from """L, h, """: "L, m];
END;
InitializeFilePrimitives: PRIVATE PROCEDURE [fp: FTPDefs.FilePrimitives] =
BEGIN
fp.CreateFileSystem ← MyCreateFileSystem;
fp.DestroyFileSystem ← MyDestroyFileSystem;
fp.CloseFile ← MyCloseFile;
END;
SetUpFTP: PROCEDURE =
{IF NOT ftpInitialized THEN {FTPDefs.FTPInitialize[]; ftpInitialized ← TRUE}};
TryNextCredentials: PROCEDURE[f: FTPDefs.FTPUser, try: CARDINAL]
RETURNS [nextTry: CARDINAL] =
--first try is #1, returns 0 when client should give up
BEGIN
pr, heapName: BOOLEAN ← TRUE;
n, p: STRING;
guestString: STRING = "Guest"L;
nextTry ← try + 1;
SELECT try FROM
1 => {n ← MakeHeapString[intC.user.name, "."L, intC.user.registry];
p ← intC.user.password};
2 => {n ← intC.user.name; p ← intC.user.password; heapName ← FALSE};
3 => {n ← p ← guestString; heapName ← FALSE};
4 => {n ← intC.user.name; p ← intC.user.password; pr ← heapName ← FALSE};
ENDCASE => {nextTry ← 0; RETURN};
FTPDefs.FTPSetCredentials[f, IF pr THEN primary ELSE secondary, n, p];
IF heapName THEN Storage.FreeString[n];
END; --of TryNextCredentials--
-- procedures and declarations of my ftp file system
MyCreateFileSystem: PRIVATE PROCEDURE [bufferSize: CARDINAL]
RETURNS [fileSystem: FTPDefs.FileSystem] =
{RETURN[NIL]};
MyDestroyFileSystem: PRIVATE PROCEDURE
[fileSystem: FTPDefs.FileSystem] = {};
MyCloseFile: PRIVATE PROCEDURE [fileSystem: FTPDefs.FileSystem,
fileHandle: FTPDefs.FileHandle, aborted: BOOLEAN] = {};
Expand: PROCEDURE [sourceName: STRING,
AcceptBlock: PROC [POINTER, CARDINAL] RETURNS [ovD.ErrorCode],
GetReady: PROC [LONG CARDINAL, TimeDefs.PackedTime] ← NIL ]
RETURNS [ovD.ErrorCode, STRING] =
BEGIN
RetrieveLocalFile: PROCEDURE [fileName: STRING]
RETURNS [code: ovD.ErrorCode, errorString: STRING] =
-- retrieves from a local file
BEGIN
BlockAcceptor: PROCEDURE [b: Stream.Block] =
BEGIN
IF b.startIndex#0 AND b.startIndex#b.stopIndexPlusOne THEN exD.SysBug[];
code ← AcceptBlock[LOOPHOLE[InlineDefs.LowHalf[b.blockPointer],POINTER],
InlineDefs.LowHalf[b.stopIndexPlusOne-b.startIndex] ];
SwingPendulum[];
IF code#ovD.ok THEN ERROR csD.Error[ovD.cancelCode];
END;
sH: csD.StreamHandle ← NIL;
fH: crD.UFileHandle ← NIL;
length: LONG CARDINAL;
errorString ← NIL;
BEGIN ENABLE csD.Error =>
BEGIN
IF reason#ovD.cancelCode
THEN errorString ← MakeLocalFileErrorString[code←reason];
GOTO destroy;
END;
[code, fH] ← crD.OpenFile[intC.user, fileName, read];
IF code#ovD.ok THEN ERROR csD.Error[code];
sH ← csD.Open[fH, byte, read, 2];
length ← csD.GetLength[sH];
IF GetReady#NIL THEN
BEGIN
created: TimeDefs.PackedTime;
[ , , created, code] ← crD.GetUFileTimes[fH];
IF code#ovD.ok THEN ERROR csD.Error[code];
GetReady[length, created];
END;
csD.ReadStream[sH, length, BlockAcceptor];
EXITS destroy => NULL;
END; --of enable--
IF sH#NIL THEN csD.Destroy[sH];
IF fH#NIL THEN [] ← crD.CloseFile[fH];
END; --RetrieveLocalFile--
RetrieveRemoteFile: PROCEDURE [dlExpansion: BOOLEAN,
hostName, firstName: STRING]
RETURNS [errorCode: ovD.ErrorCode, errorString: STRING] =
BEGIN
MyOpenFile: PRIVATE PROCEDURE [fileSystem: FTPDefs.FileSystem,
file: STRING, mode: FTPDefs.Mode, fileTypePlease: BOOLEAN,
info: FTPDefs.FileInfo]
RETURNS [fileHandle: FTPDefs.FileHandle, fileType: FTPDefs.FileType] =
BEGIN
IF GetReady#NIL THEN
BEGIN
IF info=NIL
THEN GetReady[0, TimeDefs.DefaultTime]
ELSE GetReady[info.byteCount,
TimeExtraDefs.PackedTimeFromString[info.creationDate]];
END;
RETURN[NIL, unknown];
END; --MyOpenFile--
MyWriteFile: PROCEDURE [fileSystem: FTPDefs.FileSystem,
fileHandle: FTPDefs.FileHandle,
receiveBlock: PROC [UNSPECIFIED, POINTER, CARDINAL]
RETURNS [CARDINAL], receiveBlockData: UNSPECIFIED] =
BEGIN
bytes: CARDINAL;
bP: gsD.MemoryPagePtr ← gsD.GetMemoryPages[2];
DO ENABLE UNWIND => gsD.ReturnMemoryPages[2, bP];
bytes ← receiveBlock[receiveBlockData, bP, 512];
IF (errorCode←AcceptBlock[bP, bytes])#ovD.ok
THEN ERROR FTPDefs.FTPError[noRoomForFile, NIL];
SwingPendulum[];
IF bytes=0 THEN EXIT;
ENDLOOP;
gsD.ReturnMemoryPages[2, bP];
END; -- of MyWriteFile --
-- variables for RetrieveRemoteFile
ftpUser: FTPDefs.FTPUser;
myFilePrimitivesObject: FTPDefs.FilePrimitivesObject;
myFilePrimitives: FTPDefs.FilePrimitives = @myFilePrimitivesObject;
-- code for RetrieveRemoteFile
errorString ← NIL;
SetUpFTP[];
InitializeFilePrimitives[myFilePrimitives];
myFilePrimitives.OpenFile ← MyOpenFile;
myFilePrimitives.WriteFile ← MyWriteFile;
ftpUser ← FTPDefs.FTPCreateUser
[myFilePrimitives, FTPDefs.PupCommunicationPrimitives[]];
BEGIN -- block for EXITS
try: CARDINAL ← 1;
FTPDefs.FTPOpenConnection[ftpUser, hostName, files, NIL
! FTPDefs.FTPError =>
BEGIN
[errorCode, errorString] ← MakeCantConnectMessage[hostName, message];
GOTO stop;
END];
DO
IF NOT dlExpansion THEN try ← TryNextCredentials[ftpUser, try];
IF try=0 THEN {errorCode ← ovD.ftpError; GOTO stop};
[] ← FTPDefs.FTPRetrieveFile[ftpUser, NIL, firstName, unknown
! FTPDefs.FTPError =>
BEGIN
SELECT ftpError FROM
noRoomForFile => GOTO freeOldErrorString; --errorCode set--
noSuchPrimaryUser, incorrectPrimaryPassword,
noSuchSecondaryUser, incorrectSecondaryPassword,
requestedAccessDenied =>
BEGIN
IF try=2 THEN --save first credentials error message--
errorString ← MakeFTPError[hostName, message, errorString];
LOOP;
END;
ENDCASE;
errorString ← MakeFTPError[hostName, message, errorString];
errorCode ← ovD.ftpError;
EXIT
END];
errorCode ← ovD.ok;
GOTO freeOldErrorString;
ENDLOOP;
EXITS
freeOldErrorString =>
IF errorString#NIL
THEN {Storage.FreeString[errorString]; errorString←NIL};
stop => NULL;
END; -- block for EXITS
FTPDefs.FTPDestroyUser[ftpUser];
END; -- of RetrieveRemoteFile --
GVExpand: PROCEDURE [dlName: STRING]
RETURNS [ec: ovD.ErrorCode, es: STRING] =
BEGIN
Work: PROCEDURE[n: STRING] =
BEGIN
IF workCalled
THEN PutString[", "L]
ELSE
BEGIN
IF GetReady#NIL THEN GetReady[0, TimeDefs.DefaultTime];
workCalled ← TRUE
END;
PutString[n];
END; -- of Work--
PutString: PROCEDURE [s: STRING] =
BEGIN
IF AcceptBlock[@s.text, s.length]#ovD.ok
THEN {reason ← canceled; ERROR SendDefs.ExpandFailed};
SwingPendulum[];
END; --of PutString--
workCalled: BOOLEAN ← FALSE;
reason: {canceled, failed, noGroup, noMatch, noRServers} ← failed;
BEGIN --for exits--
SELECT SendDefs.Expand[dlName, Work ! SendDefs.ExpandFailed =>
IF workCalled THEN GOTO error ELSE RETRY] FROM
ok => {ec ← ovD.ok; es ← NIL};
notFound => {reason ← noMatch; GOTO error};
individual => {reason ← noGroup; GOTO error};
allDown => {reason ← noRServers; GOTO error};
ENDCASE => exD.SysBug[];
PutString[""L ! SendDefs.ExpandFailed => GOTO error];
EXITS
error => BEGIN
es ← MakeHeapString["GV error: "L,
SELECT reason FROM
canceled => NIL,
failed => "Communication problem."L,
noMatch, noGroup => "DL name not found."L,
noRServers => "No server for that registry responded."L,
ENDCASE => ERROR];
ec ← SELECT reason FROM
canceled => ovD.cancelCode,
failed => ovD.ftpError,
noGroup, noMatch => ovD.fileNotFound,
noRServers => ovD.cantConnect,
ENDCASE => ERROR;
END;
END;
END;
-- code for Expand --
erc: ovD.ErrorCode;
firstPart, secondPart, ers: STRING;
t: NameType;
oldShape: dsD.CursorShape;
[t, firstPart, secondPart] ← ParseName[sourceName];
[oldShape, , ] ← dsD.GetCursor[];
SELECT t FROM
local => [erc, ers] ← RetrieveLocalFile [firstPart];
remote => [erc, ers] ← RetrieveRemoteFile[FALSE, firstPart, secondPart];
dl => IF expandProtocol = mtp
THEN [erc, ers] ← RetrieveRemoteFile[TRUE, firstPart, secondPart]
ELSE [erc, ers] ← GVExpand[secondPart];
ENDCASE => [erc, ers] ← ReportBadName[];
IF firstPart # NIL THEN Storage.FreeString[firstPart];
IF secondPart # NIL THEN Storage.FreeString[secondPart];
dsD.ChangeCursor[oldShape];
RETURN [erc, ers];
END; -- of Expand --
Stuff: PROCEDURE [
targetName: STRING,
GetBlock: PROC RETURNS [POINTER, CARDINAL, ovD.ErrorCode],
OverwriteOK: PROC RETURNS [BOOLEAN],
createTime: TimeDefs.PackedTime ← TimeDefs.DefaultTime,
callerFileType: FTPDefs.FileType ← binary]
RETURNS [ovD.ErrorCode, STRING] =
BEGIN
StoreLocalFile: PROCEDURE [fileName: STRING]
RETURNS [code: ovD.ErrorCode, errorString: STRING] =
-- stores to a local file
BEGIN
sH: csD.StreamHandle ← NIL;
fH: crD.UFileHandle ← NIL;
bP: POINTER;
bC: CARDINAL;
newFile: BOOLEAN ← FALSE;
errorString ← NIL;
BEGIN
ENABLE csD.Error =>
BEGIN
errorString ← MakeLocalFileErrorString[code ← reason];
GOTO delete;
END;
[code, fH] ← crD.OpenFile[intC.user, fileName, read];
SELECT code FROM
ovD.ok =>
BEGIN
p, b: CARDINAL;
[ , p, b] ← crD.UFileLength[fH];
newFile ← p = 0 AND b = 0;
END;
ovD.fileNotFound => newFile ← TRUE;
ENDCASE => ERROR csD.Error[code];
[] ← crD.CloseFile[fH];
fH ← NIL;
IF newFile OR OverwriteOK[] THEN
BEGIN
[code, fH] ← crD.OpenFile[intC.user, fileName, update];
IF code#ovD.ok THEN ERROR csD.Error[code];
sH ← csD.Open[fH, byte, overwrite, 2];
DO
[bP, bC, code] ← GetBlock[];
IF code#ovD.ok THEN GOTO delete;
csD.WriteBlock[sH, bP, 0, bC];
SwingPendulum[];
IF bC=0 THEN EXIT;
ENDLOOP;
IF createTime#TimeDefs.DefaultTime THEN
BEGIN
code ← crD.SetUFileTimes[uFH: fH, create: createTime];
IF code#ovD.ok THEN ERROR csD.Error[code];
END;
csD.Checkpoint[sH];
GOTO close;
END;
code ← ovD.cancelCode; -- client canceled the file overwrite--
EXITS
delete => IF fH#NIL THEN [] ← crD.DeleteFile[fH];
close => IF fH#NIL THEN [] ← crD.CloseFile[fH];
END;
IF sH#NIL THEN csD.Destroy[sH];
END; --StoreLocalFile--
StoreRemoteFile: PROCEDURE[host, fileName: STRING]
RETURNS [errorCode: ovD.ErrorCode, errorString: STRING] =
BEGIN
MyOpenFile: PRIVATE PROCEDURE [fileSystem: FTPDefs.FileSystem,
file: STRING, mode: FTPDefs.Mode, fileTypePlease: BOOLEAN,
info: FTPDefs.FileInfo]
RETURNS [fileHandle: FTPDefs.FileHandle, fileType: FTPDefs.FileType] =
BEGIN
IF createTime#TimeDefs.DefaultTime
AND info#NIL AND info.creationDate#NIL
THEN TimeDefs.AppendDayTime
[info.creationDate, TimeDefs.UnpackDT[createTime]
! TimeDefs.InvalidTime => CONTINUE];
RETURN[NIL, callerFileType];
END; --MyOpenFile--
MyReadFile: PROCEDURE [fileSystem: FTPDefs.FileSystem,
fileHandle: FTPDefs.FileHandle,
sendBlock: PROC [UNSPECIFIED, POINTER, CARDINAL],
sendBlockData: UNSPECIFIED] =
-- to be called by FTPStoreFile
BEGIN
bP: POINTER;
bC: CARDINAL;
DO
[bP, bC, errorCode] ← GetBlock[];
IF errorCode#ovD.ok THEN ERROR FTPDefs.FTPError[fileDataError, NIL];
sendBlock[sendBlockData, bP, bC];
SwingPendulum[];
IF bC=0 THEN EXIT;
ENDLOOP;
END; --MyReadFile--
-- variables for StoreRemoteFile
ftpUser: FTPDefs.FTPUser ← NIL;
myFilePrimitivesObject: FTPDefs.FilePrimitivesObject;
myFilePrimitives: FTPDefs.FilePrimitives = @myFilePrimitivesObject;
-- code for StoreRemoteFile
errorString ← NIL;
SetUpFTP[];
InitializeFilePrimitives[myFilePrimitives];
myFilePrimitives.OpenFile ← MyOpenFile;
myFilePrimitives.ReadFile ← MyReadFile;
ftpUser ← FTPDefs.FTPCreateUser
[myFilePrimitives, FTPDefs.PupCommunicationPrimitives[]];
BEGIN -- block for EXITS
try: CARDINAL ← 1;
FTPDefs.FTPOpenConnection[ftpUser, host, files, NIL
! FTPDefs.FTPError =>
BEGIN
[errorCode, errorString] ← MakeCantConnectMessage[host, message];
GOTO stop;
END];
DO
try ← TryNextCredentials[ftpUser, try];
IF try=0 THEN {errorCode ← ovD.ftpError; GOTO stop};
[] ← FTPDefs.FTPStoreFile[ftpUser, NIL, fileName, callerFileType
! FTPDefs.FTPError =>
BEGIN
SELECT ftpError FROM
fileDataError => GOTO deleteOldErrorString; --errorCode already set--
noSuchPrimaryUser, incorrectPrimaryPassword,
noSuchSecondaryUser, incorrectSecondaryPassword,
requestedAccessDenied =>
BEGIN
IF try = 2 THEN
errorString ← MakeFTPError[host, message, errorString];
LOOP
END;
ENDCASE;
errorString ← MakeFTPError[host, message, errorString];
errorCode ← ovD.ftpError;
EXIT
END];
errorCode ← ovD.ok;
GOTO deleteOldErrorString;
ENDLOOP;
EXITS
deleteOldErrorString =>
IF errorString#NIL
THEN {Storage.FreeString[errorString]; errorString←NIL};
stop => NULL;
END; -- block for EXITS
IF ftpUser # NIL THEN FTPDefs.FTPDestroyUser[ftpUser];
END; -- of StoreRemoteFile--
--code for Stuff
erc: ovD.ErrorCode;
firstPart, secondPart, ers: STRING;
t: NameType;
oldShape: dsD.CursorShape;
[t, firstPart, secondPart] ← ParseName[targetName];
[oldShape, , ] ← dsD.GetCursor[];
SELECT t FROM
local => [erc, ers] ← StoreLocalFile [firstPart];
remote => [erc, ers] ← StoreRemoteFile[firstPart, secondPart];
ENDCASE => [erc, ers] ← ReportBadName[];
IF firstPart # NIL THEN Storage.FreeString[firstPart];
IF secondPart # NIL THEN Storage.FreeString[secondPart];
dsD.ChangeCursor[oldShape];
RETURN [erc, ers];
END; -- of Stuff --
pendulumState: {left, right} ← right;
swingTime: CARDINAL ← inD.realTimeClock↑;
SwingPendulum: PROCEDURE =
BEGIN
leftPendulum: dsD.CursorBitMap =
[177740B, 147140B, 150540B, 160340B, 162340B, 165340B, 150540B, 147140B,
142140B, 142140B, 144140B, 144140B, 154140B, 154140B, 140140B, 177740B];
rightPendulum: dsD.CursorBitMap =
[177740B, 147140B, 150540B, 160340B, 162340B, 165340B, 150540B, 147140B,
142140B, 142140B, 141140B, 141140B, 141540B, 141540B, 140140B, 177740B];
IF inD.realTimeClock↑ - swingTime < 4 THEN RETURN;
swingTime ← inD.realTimeClock↑;
IF pendulumState = right THEN
BEGIN
pendulumState ← left;
dsD.cursorBM↑ ← leftPendulum;
END
ELSE BEGIN
pendulumState ← right;
dsD.cursorBM↑ ← rightPendulum;
END;
END; -- of SwingPendulum --
END. -- of RetrieveOp --