-- ftpmanimpl.mesa
-- Implementation of the FTP component of the
-- Cedar Interim File System.
-- Mike Schroeder October 6, 1982 10:32 am

DIRECTORY
Ascii: TYPE USING [NUL],
CIFS: TYPE USING [Error, ErrorCode, maxPath],
CIFSFeedback: TYPE,
Convert: TYPE USING [IntFromRope],
ConvertUnsafe: TYPE USING [AppendRope, ToRope],
Date: TYPE USING [StringToPacked],
File: TYPE USING [Capability, LimitPermissions, read],
FileLookup: TYPE USING [LookupFile, Result],
FileStream: TYPE USING [Create, GetLeaderProperties, SetLength, SetLeaderProperties],
FtpMan: TYPE,
Process: TYPE USING [Detach, Seconds, SecondsToTicks],
Rope: TYPE USING [ROPE, Equal, Cat, Length],
Runtime: TYPE USING [BoundsFault],
Stream: TYPE USING [Handle, Delete],
Storage: TYPE USING [FreeString],
STP: TYPE USING [Login, Continue, Confirmation, Delete, FileInfo, Enumerate,
GetFileInfo, NoteFileProcType, ConfirmProcType, Store, Retrieve, SetHost,
Handle, Create, defaultOptions, Open, Error, ConnectionErrors, Connect,
Close, Destroy],
UserCredentials: TYPE USING [SetUserCredentials],
UserCredentialsUnsafe: TYPE USING [GetUserCredentials];

FtpManImpl: MONITOR
IMPORTS CIFS, Convert, ConvertUnsafe, Date, Rope, FileLookup, FileStream,
Stream, STP, Storage, File, Runtime, Process, UserCredentials, UserCredentialsUnsafe
EXPORTS FtpMan, CIFSFeedback = {

-- Exceptional conditions are reported with STP.Error
-- OKAY, you signal handling jocks, you won't believe how clever (er convoluted) this
-- module is until you read the code!!

maxConnections: CARDINAL = 5;
-- only need enough for all used in last TimeOut seconds

TimeOut: Process.Seconds ← 15;
-- keep connections open this long after use

-- Only four words of MDS. DEATH TO THE MDS!!

global: REF GlobalObject ← NIL;

GlobalObject: TYPE = RECORD [
registeredProc: PROC [Rope.ROPE] ← NIL,
cname, cpassword: Rope.ROPENIL,
forASecond: CONDITION ← [Process.SecondsToTicks[1]],
opened, reused: INT ← 0,  -- connection counters
s: SEQUENCE mx: [1..maxConnections+1] OF Connection
];

Connection: TYPE = RECORD [
server: Rope.ROPE,
h: STP.Handle,
touched: BOOLEAN
];

Connect: PUBLIC ENTRY PROCEDURE [name: Rope.ROPE, password: Rope.ROPE] = {
-- Set credentials on all open connections
n: STRING ← [64];
p: STRING ← [32];
-- unlock monitor on unwind
{ENABLE
UNWIND => NULL;
ConvertUnsafe.AppendRope[from: name, to: n
! Runtime.BoundsFault => CONTINUE];
ConvertUnsafe.AppendRope[from: password, to: p
! Runtime.BoundsFault => CONTINUE];
FOR i:CARDINAL IN [1..maxConnections] DO
IF global[i].h#NIL THEN STP.Connect[global[i].h, n, p];
ENDLOOP;
global.cname ← name;
global.cpassword ← password;
}};

Login: PUBLIC ENTRY PROCEDURE [name: Rope.ROPE, password: Rope.ROPE] = {
-- Set credentials on all open connections
UserCredentials.SetUserCredentials[name, password];
};

Delete: PUBLIC PROCEDURE[server: Rope.ROPE, name: Rope.ROPE] = {
-- this hack is because RETURN WITH ERROR can't handle
-- REF's. Sigh.
xcode: CIFS.ErrorCode;
xerror: Rope.ROPE;
xreply: CHARACTER;
DO {ENABLE
STP.Error => {
xcode ← LOOPHOLE[code];
xerror ← ConvertUnsafe.ToRope[error];
IF code IN STP.ConnectionErrors THEN
xerror ← Rope.Cat[server,": ",xerror];
xreply ← reply;
GOTO BailOut;
};
XDelete[server, name];
EXIT;
EXITS
BailOut => {SIGNAL CIFS.Error[xcode, xerror, xreply]; LOOP};
};
ENDLOOP;
};

XDelete: ENTRY PROCEDURE[server: Rope.ROPE, name: Rope.ROPE] = {
n: STRING ← [CIFS.maxPath];
-- find server
{ENABLE UNWIND => Report[];
hi: CARDINAL ← GetConnection[server];
{ENABLE
STP.Error => {
IF code IN STP.ConnectionErrors THEN global[hi].server ← NIL;
};
-- copy the name into the MDS
Report["Deleting ", server, name];
ConvertUnsafe.AppendRope[from: name, to: n
! Runtime.BoundsFault => CONTINUE];
-- Convert name to use ">"
ConvertName[n];
-- Delete file at server.
LoginOnSTPConnection[global[hi].h];
STP.Delete[global[hi].h, n];
Report[];
}}};

GetFileInfo: PUBLIC PROCEDURE[server: Rope.ROPE, name: Rope.ROPE]
RETURNS[create: LONG CARDINAL, count: LONG CARDINAL] = {
-- this hack is because RETURN WITH ERROR can't handle
-- REF's. Sigh.
xcode: CIFS.ErrorCode;
xerror: Rope.ROPE;
xreply: CHARACTER;
result: FileLookup.Result;
DO {
[result: result, create: create, count: count] ← FileLookup.LookupFile[server, name];
SELECT result FROM
ok => RETURN;
noSuchName => {
xcode ← noSuchFile;
xerror ← Rope.Cat[server, ": file does not exist."];
xreply ← Ascii.NUL;
GOTO BailOut};
noResponse => {
xcode ← connectionTimedOut;
xerror ← Rope.Cat[server, "did not respond."];
xreply ← Ascii.NUL;
GOTO BailOut};
noSuchPort => NULL; -- try STP --
ENDCASE => ERROR;
[create, count] ← XGetFileInfo[server, name
! STP.Error => {
xcode ← LOOPHOLE[code];
xerror ← Rope.Cat[server, ": ", ConvertUnsafe.ToRope[error]];
xreply ← reply;
GOTO BailOut
}];
EXIT;
EXITS
BailOut => {SIGNAL CIFS.Error[xcode, xerror, xreply]; LOOP};
};
ENDLOOP;
};

XGetFileInfo: ENTRY PROCEDURE[server: Rope.ROPE, name: Rope.ROPE]
RETURNS[create: LONG CARDINAL, count: LONG CARDINAL] = {
-- Gets create time and byte count of the specified file
-- find connection
{ENABLE UNWIND => Report[];
hi: CARDINAL ← GetConnection[server];
{ENABLE
STP.Error => IF code IN STP.ConnectionErrors THEN global[hi].server ← NIL;
n: STRING ← [CIFS.maxPath];
info: STP.FileInfo;
cp: STP.NoteFileProcType = {
info ← STP.GetFileInfo[global[hi].h];
RETURN[STP.Continue[yes]]
};
Report["Checking ", server, name];
ConvertUnsafe.AppendRope[from: name, to: n
! Runtime.BoundsFault => CONTINUE];
-- convert name to use ">"
ConvertName[n];
LoginOnSTPConnection[global[hi].h];
STP.Enumerate[global[hi].h, n, cp];
Report[];
RETURN[Date.StringToPacked[info.create], info.size];
}}};

Retrieve: PUBLIC PROCEDURE[server: Rope.ROPE, name: Rope.ROPE, fc: File.Capability] = {
-- this hack is because RETURN WITH ERROR can't handle
-- REF's. Sigh.
xcode: CIFS.ErrorCode;
xerror: Rope.ROPE;
xreply: CHARACTER;
DO {ENABLE
STP.Error => {
xcode ← LOOPHOLE[code];
xerror ← Rope.Cat[server, ": ", ConvertUnsafe.ToRope[error]];
xreply ← reply;
GOTO BailOut;
};
XRetrieve[server, name, fc];
EXIT;
EXITS
BailOut => {SIGNAL CIFS.Error[xcode, xerror, xreply]; LOOP};
};
ENDLOOP;
};

XRetrieve: ENTRY PROCEDURE[server: Rope.ROPE, name: Rope.ROPE, fc: File.Capability] = {
-- fc^ ← name^
-- find connection
{ENABLE UNWIND => Report[];
hi: CARDINAL ← GetConnection[server];
{ENABLE
STP.Error => IF code IN STP.ConnectionErrors THEN global[hi].server ← NIL;
info: STP.FileInfo;
n: STRING ← [CIFS.maxPath];
stream: Stream.Handle ← FileStream.Create[fc, STP.defaultOptions];
cp: STP.ConfirmProcType = {RETURN[STP.Confirmation[do], stream]};
Report["Retrieving ", server, name];
ConvertUnsafe.AppendRope[from: name, to: n
! Runtime.BoundsFault => CONTINUE];
-- convert name to use ">"
ConvertName[n];
LoginOnSTPConnection[global[hi].h];
STP.Retrieve[stp: global[hi].h, file: n, confirm: cp];
info ← STP.GetFileInfo[global[hi].h];
FileStream.SetLength[stream, info.size];
FileStream.SetLeaderProperties[sH: stream,
create: Date.StringToPacked[info.create]];
Stream.Delete[stream];
Report[];
}}};

Store: PUBLIC PROCEDURE[server: Rope.ROPE, name: Rope.ROPE, fc: File.Capability]
RETURNS[version: INT] = {
-- this hack is because RETURN WITH ERROR can't handle
-- REF's. Sigh.
xcode: CIFS.ErrorCode;
xerror: Rope.ROPE;
xreply: CHARACTER;
DO {ENABLE
STP.Error => {
xcode ← LOOPHOLE[code];
xerror ← Rope.Cat[server, ": ", ConvertUnsafe.ToRope[error]];
xreply ← reply;
GOTO BailOut;
};
version ← XStore[server, name, fc];
EXIT;
EXITS
BailOut => {SIGNAL CIFS.Error[xcode, xerror, xreply]; LOOP};
};
ENDLOOP;
};

XStore: ENTRY PROCEDURE[server: Rope.ROPE, name: Rope.ROPE, fc: File.Capability]
RETURNS[version: INT] = {
-- name^ ← fc^
-- find connection
{ENABLE UNWIND => Report[];
hi: CARDINAL ← GetConnection[server];
info: STP.FileInfo;
{ENABLE
STP.Error => IF code IN STP.ConnectionErrors THEN global[hi].server ← NIL;
n: STRING ← [CIFS.maxPath];
stream: Stream.Handle ← FileStream.Create[
File.LimitPermissions[fc, File.read],
STP.defaultOptions];
create: LONG CARDINAL;
Report["Storing ", server, name];
[create: create] ← FileStream.GetLeaderProperties[sH: stream];
ConvertUnsafe.AppendRope[from: name, to: n
! Runtime.BoundsFault => CONTINUE];
-- convert name to use ">"
ConvertName[n];
LoginOnSTPConnection[global[hi].h];
STP.Store[stp: global[hi].h, file: n, stream: stream, creation: LOOPHOLE[create]];
Stream.Delete[stream];
info ← STP.GetFileInfo[global[hi].h];
Report[];
RETURN[Convert.IntFromRope[ConvertUnsafe.ToRope[info.version]]];
}}};

Register: PUBLIC ENTRY SAFE PROC [p: PROC [Rope.ROPE]] = TRUSTED
 {global.registeredProc ← p};


-- Internal procedures

Report: PROC [a, s, n: Rope.ROPENIL] = {
IF global.registeredProc=NIL THEN RETURN;
IF a.Length = 0
THEN global.registeredProc[NIL]
ELSE global.registeredProc[Rope.Cat[a, "/", s, "/", n]]
};

ConvertName: PROC [name: STRING] = {
-- Convert "/" to be ">"
foundSlash: BOOLEANFALSE;
i: CARDINAL;
FOR i IN [0..name.length)
DO
IF name[i]='/ THEN {
foundSlash ← TRUE;
name[i] ← '>;
};
ENDLOOP;
-- if we found a slash, there is a subdirectory
IF foundSlash THEN {
FOR i DECREASING IN [0..name.length)
DO
-- add "<" in the beginning
name[i+1] ← name[i];
ENDLOOP;
name.length ← name.length + 1;
name[0] ← '<;
};
};

SetConnectCredentials: PROC [h: STP.Handle] = {
n: STRING ← [64];
p: STRING ← [32];
ConvertUnsafe.AppendRope[from: global.cname, to: n
! Runtime.BoundsFault => CONTINUE];
ConvertUnsafe.AppendRope[from: global.cpassword, to: p
! Runtime.BoundsFault => CONTINUE];
STP.Connect[h, n, p];
};

LoginOnSTPConnection: INTERNAL PROC [h: STP.Handle] = {
 n: STRING = [64];
 p: STRING = [32];
 UserCredentialsUnsafe.GetUserCredentials[n, p];
STP.Login[h, n, p];
 };


GetConnection: INTERNAL PROC[server: Rope.ROPE]
RETURNS [hi: CARDINAL] = {
-- returns the index a connection to server
s: STRING ← [50];
herald: STRING;
noLiveConnection: BOOLEAN;
-- see if we already have one
-- remember the highest numbered unused one
-- note if some connection still alive
DO
noLiveConnection ← TRUE;
hi ← 0;
FOR i:CARDINAL IN [1..maxConnections] DO
IF global[i].server=NIL
THEN hi ← i
ELSE {
IF Rope.Equal[global[i].server, server] THEN {
global.reused ← global.reused + 1;
hi ← i;
global[hi].touched ← TRUE;
RETURN;
};
noLiveConnection ← FALSE
};
ENDLOOP;
IF hi#0 THEN EXIT;
-- all connections active, so wait for one
WAIT global.forASecond;
ENDLOOP;
-- create new stp handle if it doesn't exist
IF global[hi].h = NIL THEN {
global[hi].h ← STP.Create[];
IF global.cname # NIL THEN SetConnectCredentials[global[hi].h];
};
-- convert to Short Stirngs for STP
ConvertUnsafe.AppendRope[from: server, to: s
! Runtime.BoundsFault => CONTINUE];
STP.SetHost[global[hi].h, s];
-- open the connection
herald ← STP.Open[global[hi].h, s];
Storage.FreeString[herald];
global[hi].server ← server;
global[hi].touched ← TRUE;
global.opened ← global.opened + 1;
-- if there was no live connection before, start watcher
IF noLiveConnection THEN Process.Detach[FORK ConnectionWatcher[]];
RETURN;
};

-- connection time out logic

ConnectionWatcher: ENTRY PROC = {
-- time out connections after TimeOut seconds
ForTimeout: CONDITION ← [Process.SecondsToTicks[TimeOut]];
noLiveConnection: BOOLEAN;
DO
-- look at touched bits
noLiveConnection ← TRUE;
FOR i:CARDINAL IN [1..maxConnections] DO
IF global[i].server#NIL THEN
SELECT global[i].touched FROM
TRUE => {
global[i].touched ← FALSE;
noLiveConnection ← FALSE
};
FALSE => {
-- close connection
STP.Close[global[i].h];
global[i].server ← NIL;
-- destroy all but the first handle to be used
IF i # maxConnections THEN {
[] ← STP.Destroy[global[i].h];
global[i].h ← NIL;
};
};
ENDCASE;
ENDLOOP;
-- kill watcher if no more live connections --
IF noLiveConnection THEN EXIT;
-- pause for the timeout
WAIT ForTimeout;
ENDLOOP;
};



Init: PROC = {
global ← NEW [GlobalObject[maxConnections]];
FOR i: CARDINAL IN [1..maxConnections] DO
global[i] ← [NIL, NIL, FALSE];
ENDLOOP
};

-- Start trap initializes
Init[];

}..