-- STPSubrImpl.Mesa, last edit March 8, 1983 12:37 pm
-- Pilot 6.0/ Mesa 7.0
				
DIRECTORY
  CIFS: TYPE USING[Close, create, GetFC, Open, OpenFile, replace, write],
  ConvertUnsafe: TYPE USING[ToRope],
  CWF: TYPE USING [SWF1, SWF4, WF0, WF1, WF2, WFCR],
  DCSFileTypes: TYPE USING [tLeaderPage],
  Directory: TYPE USING [CreateFile, DeleteFile, Error, Handle, ignore, Lookup, Rename],
  Environment: TYPE USING [bytesPerPage],
  File: TYPE USING [Capability, nullCapability, Permissions, read, SetSize],
  FileStream: TYPE USING [Create, GetLength, SetLeaderPropertiesForCapability],
  FQ: TYPE USING[FileQuery, Result],
  Heap: TYPE USING[systemZone],
  Inline: TYPE USING [BITNOT, LowHalf],
  IO: TYPE USING[card, PutF, string, UserAbort],
  LongString: TYPE USING [EquivalentString],
  Process: TYPE USING [Pause, SecondsToTicks],
  Space: TYPE USING [Create, Delete, Handle, LongPointer, Map, nullHandle, virtualMemory],
  UnsafeSTP: TYPE USING [Connect, Create, CreateRemoteStream, DesiredProperties, Destroy, 
  	Error, ErrorCode, FileInfo, GetFileInfo, Handle, 
		Login, NextFileName, Open, SetDesiredProperties, Type],
  UnsafeSTPOps: TYPE USING [FindFileType, Handle, SetPListItem],
  STPSubr: TYPE USING [RetrieveProcType, StpState],
  Stream: TYPE USING [Delete, EndOfStream, GetBlock, Handle, PutBlock],
  String: TYPE USING [AppendChar],
  Subr: TYPE USING [AbortMyself, Any, CopyString, CursorInWindow, 
  	EndsIn, errorflg, FileError, FreeString, GetCreateDate, GetNameandPassword, 
		LongZone, NewStream, Prefix, Read, SetRemoteFilenameProp, strcpy,
		SubStrCopy, TTYProcs, Write],
  UserCredentialsUnsafe: TYPE USING[GetUserCredentials],
  UserTerminal: TYPE USING [cursor, CursorArray, GetCursorPattern, SetCursorPattern];

STPSubrImpl: PROGRAM
IMPORTS CIFS, ConvertUnsafe, CWF, Directory, File, FileStream, FQ, Heap,
	Inline, IO, LongString, Process, Space, STP: UnsafeSTP,
	STPOps: UnsafeSTPOps, Stream, String, Subr, UserCredentialsUnsafe, UserTerminal
EXPORTS STPSubr
SHARES File = {

-- MDS USAGE !!!
useCIFS: BOOL ← TRUE;
connseq: LONG POINTER TO ConnSeqRecord ← NIL;
maxNumberOfTries: CARDINAL ← 10;	-- # tries to get connected if rejecting
-- endof MDS USAGE !!!

-- connection table
MAXCONNS: CARDINAL = 8;
-- we use the username and password from Profile.userName, userPassword
ConnSeqRecord: TYPE = RECORD[
	size: CARDINAL ← 0,	-- # of connections
	body: SEQUENCE maxsize: CARDINAL OF ConnTable
	];
ConnTable: TYPE = RECORD[
	stphandle: STP.Handle ← NIL,
	host: LONG STRING ← NIL
	];


SetUseOfCIFS: PUBLIC PROC[shouldUseCIFS: BOOL] = {
	useCIFS ← shouldUseCIFS;	-- in a procedure to make sure the module is started
	};
	
EnumerateForRetrieve: PUBLIC PROC[host, filePattern: LONG STRING,
	enumProc: STPSubr.RetrieveProcType, h: Subr.TTYProcs, onlyOne: BOOL] ={
stp: STP.Handle;
remoteStream: Stream.Handle ← NIL;
remoteName: LONG STRING ← NIL;
skipRest, tryAgain: BOOL;
shortFilePattern: STRING ← [100];
desiredProperties: STP.DesiredProperties ← ALL[FALSE];

	Cleanup: PROC = {
	IF remoteName ~= NIL THEN Heap.systemZone.FREE[@remoteName];
	IF remoteStream ~= NIL THEN Stream.Delete[remoteStream];
	remoteStream ← NIL;
	};

	-- nested procedure needed to handle retrys of entire enumerates
	TryIt: PROC = {
	skipRest ← FALSE;
	remoteName ← NIL;
	remoteStream ← STP.CreateRemoteStream[stp, shortFilePattern, read
		! STP.Error => 
			IF HandleSTPError[stp, code, error, h] THEN RETRY
		];
	DO
		ENABLE UNWIND => Cleanup[];
		IF remoteName ~= NIL THEN Heap.systemZone.FREE[@remoteName];
		-- may generate STP.Error!
		remoteName ← STP.NextFileName[remoteStream];
		IF remoteName = NIL THEN EXIT;
		IF skipRest THEN LOOP;
		skipRest ← enumProc[remoteName, stp, remoteStream];
		ENDLOOP;
	Cleanup[];
	};

Subr.strcpy[shortFilePattern, filePattern];
desiredProperties[directory] ← TRUE;
desiredProperties[nameBody] ← TRUE;
desiredProperties[version] ← TRUE;
desiredProperties[createDate] ← TRUE;
desiredProperties[size] ← TRUE;
DO
	tryAgain ← FALSE;
	stp ← Connect[host: host, h: h, onlyOne: onlyOne];
	STP.SetDesiredProperties[stp, desiredProperties];
	TryIt[ ! STP.Error => 
		IF HandleSTPError[stp, code, error, h] THEN {
			tryAgain ← TRUE;
			CONTINUE;
			}
		ELSE IF code = connectionClosed THEN {
			CWF.WF1["Connection to %s timed out.\n"L, host];
			tryAgain ← TRUE;
			CONTINUE;
			};
	     ];
	IF NOT tryAgain THEN EXIT;
	stp ← ForceClosed[stp];
	IF h.in.UserAbort[] THEN SIGNAL Subr.AbortMyself;
	ENDLOOP;
};

-- if not found, raises Subr.FileError, for both remote and local files
-- filename looks like [host]<dir>name
GeneralOpen: PUBLIC PROC[filename: LONG STRING, h: Subr.TTYProcs,
	access: File.Permissions, fileType: STP.Type, -- FileType -- createtime: LONG CARDINAL] 
	RETURNS[sh: Stream.Handle, stphandle: STP.Handle] = {
host: STRING ← [30];
sfn: STRING ← [100];
tryAgain: BOOL;
desiredProperties: STP.DesiredProperties ← ALL[FALSE];

sh ← NIL;
-- local case
IF filename[0] ~= '[ AND filename[0] ~= '< THEN {
	sh ← Subr.NewStream[filename, access];
	RETURN[sh, NIL];
	};

-- remote file name
Subr.strcpy[sfn, filename];
StripHost[host, sfn];
StartSTP[];
DO
	tryAgain ← FALSE;
	stphandle ← Connect[host, h, FALSE];
	desiredProperties[directory] ← TRUE;
	desiredProperties[nameBody] ← TRUE;
	desiredProperties[version] ← TRUE;
	desiredProperties[createDate] ← TRUE;
	desiredProperties[size] ← TRUE;
	-- CWF.WF1["%s ... "L, filename];
	STP.SetDesiredProperties[stphandle, desiredProperties];
	sh ← STP.CreateRemoteStream[stp: stphandle, 
		file: sfn, 
		access: IF access = Subr.Read THEN read ELSE write, 
		fileType: IF access = Subr.Read THEN unknown ELSE 
			(IF fileType = unknown THEN GetFileType[sfn] ELSE fileType),
		creation: LOOPHOLE[createtime]
		! STP.Error => 
		IF code = noSuchFile THEN {
			CWF.WF2["Error - %s: %s.\n\n"L, filename, error];
			ERROR Subr.FileError[notFound];
			} 
		ELSE IF code = connectionClosed THEN {
			CWF.WF1["Connection to %s timed out.\n"L, host];
			tryAgain ← TRUE;
			CONTINUE;
			}
		ELSE IF HandleSTPError[stphandle,code,error,h] THEN RETRY];
	IF NOT tryAgain THEN EXIT;
	stphandle ← ForceClosed[stphandle];
	IF h.in.UserAbort[] THEN SIGNAL Subr.AbortMyself;
	ENDLOOP;
};

-- just like GeneralOpen but the filename can be a pattern, e.g. with * in it
PatternGeneralOpen: PUBLIC PROC[filepattern: STRING, proc: STPSubr.RetrieveProcType,
	h: Subr.TTYProcs] = {
host: STRING ← [30];
sfn: STRING ← [100];

-- local case
IF filepattern[0] ~= '[ AND filepattern[0] ~= '< THEN {
	sh: Stream.Handle;
	sh ← Subr.NewStream[filepattern, Subr.Read
		! Subr.FileError => GOTO err];
	[] ← proc[filepattern, NIL, sh];
	Stream.Delete[sh];
	RETURN;
	};
Subr.strcpy[sfn, filepattern];
StripHost[host, sfn];
EnumerateForRetrieve[host, sfn, proc, h, TRUE
	! STP.Error => 
		IF code = noSuchFile THEN {
			CWF.WF2["Error - %s: %s.\n\n"L, filepattern, error];
			GOTO err;
			}
	];
EXITS
err => Subr.errorflg ← TRUE;
};

-- may raise Subr.FileError
CachedOpen: PUBLIC PROC[host, directory, shortname: LONG STRING, 
	version: CARDINAL, wantcreatetime: LONG CARDINAL, h: Subr.TTYProcs,
	stpState: STPSubr.StpState, wantExplicitVersion, onlyOne, tryDollars: BOOL] 
	RETURNS[sh: Stream.Handle, cap: File.Capability] = {
cap ← CachedRetrieve[host, directory, shortname, version, wantcreatetime, h,
	stpState, wantExplicitVersion, onlyOne, tryDollars];
sh ← NIL;
IF cap ~= File.nullCapability THEN
	sh ← FileStream.Create[[cap.fID, File.read]]
ELSE
	-- this case can only happen if the user said no to the retrieval
	-- in which case the existing file is used
	sh ← Subr.NewStream[shortname, Subr.Read];
RETURN[sh, cap];
};

CachedRetrieve: PUBLIC PROC[host, directory, shortname: LONG STRING, 
	version: CARDINAL, wantcreatetime: LONG CARDINAL, h: Subr.TTYProcs,
	stpState: STPSubr.StpState, wantExplicitVersion, onlyOne, tryDollars: BOOL] 
	RETURNS[cap: File.Capability] = {
localOnly: BOOL ← host = NIL OR host.length = 0;
localcreatetime: LONG CARDINAL ← 0;
fileNotRetrieved: BOOL ← FALSE;
cap ← File.nullCapability;
{
cap ← Directory.Lookup[fileName: shortname, permissions: Directory.ignore
	! Directory.Error => GOTO notonlocal];
localcreatetime ← Subr.GetCreateDate[cap];
IF localcreatetime = wantcreatetime 
OR (wantcreatetime = 0 AND localOnly) THEN RETURN[cap];
EXITS
notonlocal => NULL;
};
-- try with $$ on end
IF tryDollars THEN {
	sDollar: STRING ← [100];
	CWF.SWF1[sDollar, "%s$$"L, shortname];
	cap ← Directory.Lookup[fileName: sDollar, permissions: Directory.ignore
		! Directory.Error => GOTO notonlocaldollar];
	localcreatetime ← Subr.GetCreateDate[cap];
	IF localcreatetime = wantcreatetime THEN RETURN[cap];
	EXITS
	notonlocaldollar => NULL;
	};
-- look on remote server
IF NOT localOnly THEN {
		targetFileName: STRING ← [125];
		remoteVersion: CARDINAL;
		remoteCreateTime: LONG CARDINAL;
		
		OneFile: STPSubr.RetrieveProcType = {
			info: STP.FileInfo ← STP.GetFileInfo[stp];
			localname: STRING ← [100];
			skipRest ← TRUE;
			IF tryDollars AND localcreatetime ~= 0 THEN 
				CWF.SWF1[localname, "%s$$"L, shortname]
			ELSE {
				IF stpState.checkForOverwrite 
				AND localcreatetime ~= 0 
				THEN 
					SELECT CheckForOverwrite[shortname, localcreatetime,
						targetFileName, remoteCreateTime, h] FROM
					no =>  { 
						fileNotRetrieved ← TRUE;
						RETURN;
						};
					substitute => {
						cap ← Directory.Lookup[fileName: shortname, permissions: Directory.ignore
							! Directory.Error => CONTINUE];
						-- the file on the local disk is used
						RETURN;
						};
					yes => NULL;
					all => stpState.checkForOverwrite ← FALSE;
					ENDCASE => ERROR;
				Subr.strcpy[localname, shortname];
				};
			CWF.WF1["Retrieving %s "L, targetFileName];
			IF tryDollars AND localcreatetime ~= 0 THEN
				CWF.WF1["(as %s)"L, localname]
			ELSE IF remoteCreateTime < localcreatetime THEN {
				dollar: STRING ← [100];
				CWF.SWF1[dollar, "%s$$"L, shortname];
				Directory.DeleteFile[dollar ! Directory.Error => CONTINUE];
				Directory.Rename[oldName: shortname, newName: dollar];
				CWF.WF1["(local version renamed to %s)"L, dollar];
				};
			CWF.WF0[" ... "L];
			[cap,] ← WriteStreamToDisk[remoteStream, 
				localname, info.size, h];
			Subr.SetRemoteFilenameProp[cap, targetFileName];
			FileStream.SetLeaderPropertiesForCapability[cap: cap, 
				create: LOOPHOLE[remoteCreateTime]];
			CWF.WF1["%lu bytes.\n"L, @info.size];
			RETURN;
			};
		
	{
	fres: FQ.Result;
	[fres: fres, remoteVersion: remoteVersion, remoteCreateTime: remoteCreateTime]
		← FQ.FileQuery[host, directory, shortname,
				version, wantcreatetime, wantExplicitVersion, h, targetFileName];
	SELECT fres FROM
	foundCorrectVersion => {
		vstring: STRING;
		sfn: STRING ← [100];
		IF h.in.UserAbort[] THEN SIGNAL Subr.AbortMyself;
		IF localcreatetime = remoteCreateTime THEN RETURN;	-- already on disk
		vstring ← IF Subr.Prefix[host, "maxc"L] THEN ";"L ELSE "!"L;
		CWF.SWF4[sfn, "<%s>%s%s%u"L, directory, shortname, 
				vstring, @remoteVersion];
		EnumerateForRetrieve[host, sfn, OneFile, h, onlyOne];
		IF fileNotRetrieved THEN 
			cap ← File.nullCapability;
		};
	foundWrongVersion => ERROR Subr.FileError[wrongVersion];
	notFound => ERROR Subr.FileError[notFound];
	ENDCASE => ERROR;
	}};
};
	
CheckForOverwrite: PROC[localname: LONG STRING, localcreate: LONG CARDINAL,
	remotename: LONG STRING, remotecreate: LONG CARDINAL, h: Subr.TTYProcs] 
	RETURNS[answer: {all, yes, no, substitute}] = {
ch: CHAR;
h.out.PutF["Ok to retrieve %s, dated %t,\n\tand overwrite %s of %t ", 
		IO.string[remotename], IO.card[remotecreate], IO.string[localname], IO.card[localcreate]];
ch ← h.Confirm[h.in, h.out, h.data, "", 'y];
IF ch = 'q THEN 
	SIGNAL Subr.AbortMyself
ELSE IF ch = 'y THEN 
	answer ← yes
ELSE IF ch = 'l THEN 
	answer ← substitute
ELSE IF ch = 'a THEN 
	answer ← all
ELSE 
	answer ← no;
};
	
StartSTP: PROC = {
longzone: UNCOUNTED ZONE;
IF connseq ~= NIL THEN RETURN;
longzone ← Subr.LongZone[];
connseq ← longzone.NEW[ConnSeqRecord[MAXCONNS]];
};

StopSTP: PUBLIC PROC = {
ENABLE UNWIND => connseq ← NIL;
longzone: UNCOUNTED ZONE;
IF connseq = NIL THEN RETURN;
longzone ← Subr.LongZone[];
FOR i: CARDINAL IN [0..connseq.size) DO
	IF connseq[i].stphandle ~= NIL THEN CloseAndFree[i];
	ENDLOOP;
longzone.FREE[@connseq];
};

Connect: PUBLIC PROC[host: LONG STRING, h: Subr.TTYProcs, onlyOne: BOOL] 
	RETURNS[stphandle: STP.Handle] = {
free: CARDINAL;
StartSTP[];
IF onlyOne THEN {
	IF connseq.size > 0 
	AND connseq[0].host ~= NIL
	AND NOT LongString.EquivalentString[host, connseq[0].host] THEN {
		CloseAndFree[0];
		IF connseq.size = 1 THEN connseq.size ← 0;
		};
	};
free ← connseq.size;
{
FOR i: CARDINAL IN [0..connseq.size) DO
	IF connseq[i].host ~= NIL
	AND connseq[i].stphandle ~= NIL 
	AND LongString.EquivalentString[host,connseq[i].host] 
	AND LOOPHOLE[connseq[i].stphandle, STPOps.Handle].remoteStream = NIL THEN {
		free ← i;
		GOTO leave;
		};
	IF connseq[i].host = NIL THEN free ← i;
	ENDLOOP;
IF free = connseq.size THEN {
	IF connseq.size >= connseq.maxsize THEN {
		CWF.WF0["Error - to many conns\n"L];
		RETURN[NIL];
		};
	connseq.size ← connseq.size + 1;
	};
connseq[free] ← [NIL, NIL];	-- in case MakeSTPHandle signals
connseq[free].stphandle ← MakeSTPHandle[host, h];
connseq[free].host ← Subr.CopyString[host];
EXITS
leave => NULL;
};
STP.SetDesiredProperties[connseq[free].stphandle, ALL[FALSE]];
RETURN[connseq[free].stphandle];
};

ForceClosed: PUBLIC PROC[stphandle: STP.Handle] RETURNS[STP.Handle] = {
FOR i: CARDINAL IN [0 .. connseq.size) DO
	IF stphandle = connseq[i].stphandle THEN 
		CloseAndFree[i];
	ENDLOOP;
RETURN[NIL];
};

CloseAndFree: PROC[i: CARDINAL] = {
CWF.WF1["Closing connection to %s ... "L, connseq[i].host];
connseq[i].stphandle ← STP.Destroy[connseq[i].stphandle
			! STP.Error => CONTINUE];
CWF.WF0["closed.\n"L];
Subr.FreeString[connseq[i].host];
connseq[i].host ← NIL;
};


CheckStarted: PROC = {
IF connseq = NIL THEN ERROR;
};

-- the procedures below can be called without using the Connect[] stphandles

NPAGEBUFFER: CARDINAL = 6;

-- may raise CIFS.Error[fileBusy]
WriteStreamToDisk: PUBLIC PROC[remotesh: Stream.Handle, 
	localfilename: LONG STRING, byteLengthHint: LONG CARDINAL, 
	h: Subr.TTYProcs] 
	RETURNS[cap: File.Capability, nbytes: LONG CARDINAL] = {
nxfer, npages: CARDINAL;
stopit, flip: BOOL ← FALSE;
space: Space.Handle ← Space.nullHandle;
localsh: Stream.Handle ← NIL;
buffer: LONG POINTER ← NIL;
ca: UserTerminal.CursorArray ← ALL[0];
cursorX, cursorY: INTEGER;
openFile: CIFS.OpenFile;
ftp: UserTerminal.CursorArray ← [
	177400B, 177400B, 177400B, 177400B,
	177400B, 177400B, 177400B, 177400B,
	000377B, 000377B, 000377B, 000377B,
	000377B, 000377B, 000377B, 000377B];

	Cleanup: PROC = {
	IF space ~= Space.nullHandle THEN Space.Delete[space];
	space ← Space.nullHandle;
	IF localsh ~= NIL THEN Stream.Delete[localsh];
	localsh ← NIL;
	IF flip THEN UserTerminal.SetCursorPattern[ca];
	IF useCIFS AND openFile ~= NIL THEN CIFS.Close[openFile];
	openFile ← NIL;
	};
	
npages ← Inline.LowHalf[byteLengthHint/Environment.bytesPerPage] + 2;
nbytes ← 0;
cap ← File.nullCapability;
IF useCIFS THEN {
	openFile ← CIFS.Open[ConvertUnsafe.ToRope[localfilename],
		CIFS.replace+CIFS.create+CIFS.write];
	cap ← CIFS.GetFC[openFile];
	}
ELSE {
	cap ← Directory.CreateFile[localfilename, DCSFileTypes.tLeaderPage, npages
		! Directory.Error => {
			IF type = fileAlreadyExists THEN {
				cap ← Directory.Lookup[fileName: localfilename, 
					permissions: Directory.ignore];
				IF npages > 2 THEN File.SetSize[cap, npages];
				}
			ELSE ERROR Subr.FileError[notFound];
			CONTINUE
		}];
	};
localsh ← FileStream.Create[[cap.fID, Subr.Write]];
space ← Space.Create[NPAGEBUFFER, Space.virtualMemory];
Space.Map[space];
buffer ← Space.LongPointer[space];
IF Subr.CursorInWindow[h] THEN {
	[cursorX, cursorY] ← UserTerminal.cursor↑;
	ca ← UserTerminal.GetCursorPattern[];
	UserTerminal.SetCursorPattern[ftp];
	flip ← TRUE;
	};
WHILE NOT stopit DO
	ENABLE UNWIND => Cleanup[];
	[bytesTransferred: nxfer] ← Stream.GetBlock[remotesh, 
		[buffer, 0, NPAGEBUFFER*Environment.bytesPerPage]
		! STP.Error => IF code = noSuchFile THEN {
			CWF.WF1["\n\tError - %s not found.\n"L, localfilename];
			EXIT;
			};
		  Stream.EndOfStream =>  {
		  	stopit ← TRUE;
			nxfer ← nextIndex;
			CONTINUE
			}
		];
	IF nxfer = 0 THEN EXIT;
	Stream.PutBlock[localsh, [buffer, 0, nxfer]];
	nbytes ← nbytes + nxfer;
	-- only flips if not moved
	IF flip AND cursorX = UserTerminal.cursor↑.x 
	AND cursorY = UserTerminal.cursor↑.y THEN {
		-- code from Cursor.Invert
		bits: UserTerminal.CursorArray ← UserTerminal.GetCursorPattern[];
    		FOR i: CARDINAL IN [0..16) DO
      			bits[i] ← Inline.BITNOT[bits[i]];
      			ENDLOOP;
    		UserTerminal.SetCursorPattern[bits];
		};
	ENDLOOP;
Cleanup[];
};

Store: PUBLIC PROC[stphandle: STP.Handle, remoteName: LONG STRING, 
	localCap: File.Capability, createDate: LONG CARDINAL, 
	h: Subr.TTYProcs] RETURNS[nbytes: LONG CARDINAL] = {
ft: STP.Type;
remName: STRING ← [100];
localStream, remoteStream: Stream.Handle ← NIL;
len: LONG CARDINAL;
lstr: STRING ← [30];

	Cleanup: PROC = {
	IF localStream ~= NIL THEN Stream.Delete[localStream];
	localStream ← NIL;
	IF remoteStream ~= NIL THEN Stream.Delete[remoteStream];
	remoteStream ← NIL;
	};
	
{
ENABLE UNWIND => Cleanup[];
desiredProperties: STP.DesiredProperties ← ALL[FALSE];
nbytes ← 0;
Subr.strcpy[remName, remoteName];
ft ← GetFileType[remoteName];
localStream ← FileStream.Create[[localCap.fID, Subr.Read]];
-- this will read the entire local file to check for 8-th bit
IF ft = unknown THEN ft ← STPOps.FindFileType[localStream];
desiredProperties[directory] ← TRUE;
desiredProperties[nameBody] ← TRUE;
desiredProperties[version] ← TRUE;
STP.SetDesiredProperties[stphandle, desiredProperties];
remoteStream ← STP.CreateRemoteStream[stp: stphandle, file: remName,
	access: write, fileType: ft, creation: LOOPHOLE[createDate]
	! STP.Error => IF HandleSTPError[stphandle, code,
		error, h] THEN RETRY];
len ← FileStream.GetLength[localStream];
CWF.SWF1[lstr, "%lu"L, @len];
STPOps.SetPListItem[LOOPHOLE[stphandle, STPOps.Handle].plist, "Size"L, lstr];
nbytes ← WriteDiskToStream[localStream, remoteStream, h];
};
Cleanup[];
};
	
WriteDiskToStream: PROC[localsh, remotesh: Stream.Handle,
	h: Subr.TTYProcs] RETURNS[nbytes: LONG CARDINAL] = {
nxfer: CARDINAL;
stopit, flip: BOOL ← FALSE;
space: Space.Handle ← Space.nullHandle;
buffer: LONG POINTER ← NIL;
cursorX, cursorY: INTEGER;
ca: UserTerminal.CursorArray ← ALL[0];
ftp: UserTerminal.CursorArray ← [
	177400B, 177400B, 177400B, 177400B,
	177400B, 177400B, 177400B, 177400B,
	000377B, 000377B, 000377B, 000377B,
	000377B, 000377B, 000377B, 000377B];


	Cleanup: PROC = {
	IF space ~= Space.nullHandle THEN Space.Delete[space];
	space ← Space.nullHandle;
	IF flip THEN UserTerminal.SetCursorPattern[ca];
	};
	
nbytes ← 0;
space ← Space.Create[NPAGEBUFFER, Space.virtualMemory];
Space.Map[space];
buffer ← Space.LongPointer[space];
IF Subr.CursorInWindow[h] THEN {
	[cursorX, cursorY] ← UserTerminal.cursor↑;
	ca ← UserTerminal.GetCursorPattern[];
	UserTerminal.SetCursorPattern[ftp];
	flip ← TRUE;
	};
WHILE NOT stopit DO
	ENABLE UNWIND => Cleanup[];
	[bytesTransferred: nxfer] ← Stream.GetBlock[localsh, 
		[buffer, 0, NPAGEBUFFER*Environment.bytesPerPage]
		! Stream.EndOfStream =>  {
		  	stopit ← TRUE;
			nxfer ← nextIndex;
			CONTINUE
			}
		];
	IF nxfer = 0 THEN EXIT;
	Stream.PutBlock[remotesh, [buffer, 0, nxfer]];
	nbytes ← nbytes + nxfer;
	IF flip AND cursorX = UserTerminal.cursor↑.x 
	AND cursorY = UserTerminal.cursor↑.y THEN {
		-- code from Cursor.Invert
		bits: UserTerminal.CursorArray ← UserTerminal.GetCursorPattern[];
    		FOR i: CARDINAL IN [0..16) DO
      			bits[i] ← Inline.BITNOT[bits[i]];
      			ENDLOOP;
    		UserTerminal.SetCursorPattern[bits];
		};
	ENDLOOP;
Cleanup[];
};

-- takes sfn, strips off the host, puts the host in "host"
-- leaves the remainder in sfn
StripHost: PROC[host, sfn: STRING] = {
IF sfn[0] ~= '[ THEN {
	Subr.strcpy[host, "Ivy"L];
	RETURN;
	};
host.length ← 0;
FOR i: CARDINAL IN [1..sfn.length) DO
	IF sfn[i] = '] THEN {
		FOR j: CARDINAL IN [1 .. i-1] DO
			String.AppendChar[host, sfn[j]];
			ENDLOOP;
		Subr.SubStrCopy[sfn, sfn, i+1];
		RETURN;
		};
	ENDLOOP;
CWF.WF1["Error - %s is not a valid remote file name.\n"L, sfn];
};

NSECPAUSE: CARDINAL = 10;	-- # seconds to wait

SetNumberOfConnectTries: PUBLIC PROC[nTries: CARDINAL] = {
	maxNumberOfTries ← nTries;
	};
	
MakeSTPHandle: PUBLIC PROC[host: LONG STRING, h: Subr.TTYProcs]
	RETURNS[stphandle: STP.Handle] = {
herald: LONG STRING;
shorthost: STRING ← [30];
user: STRING ← [40];
password: STRING ← [40];
ntries: CARDINAL ← 0;

IF host = NIL OR host.length = 0 THEN {
	CWF.WF0["Error: remote host has not been specified.\n"L];
	RETURN[NIL];
	};
stphandle ← STP.Create[];
CWF.WF1["Opening connection to %s.\n"L, host];
Subr.strcpy[shorthost, host];
herald ← STP.Open[stphandle, shorthost
	! STP.Error => IF code = connectionRejected THEN {
		CWF.WF1["Connection rejected by %s.\n"L, host];
		ntries ← ntries + 1;
		IF ntries < maxNumberOfTries THEN {
			CWF.WF0["Will pause and try again.  (Type control-DEL to abort.)\n"L];
			IF h.in.UserAbort[] THEN SIGNAL Subr.AbortMyself;
			Process.Pause[Process.SecondsToTicks[NSECPAUSE]];
			IF h.in.UserAbort[] THEN SIGNAL Subr.AbortMyself;
			RETRY;
			}
		ELSE CWF.WF0["Connection rejected too many times.\n"L];
		}];
CWF.WF1["%s\n"L,herald];
Heap.systemZone.FREE[@herald];
-- userName and userPassword may be NIL at this point
STPLogin[stphandle];
};

STPLogin: PROC[stphandle: STP.Handle] = {
-- STP.Login[stphandle, NameAndPasswordOps.userName, NameAndPasswordOps.userPassword];
n: STRING ← [50];
p: STRING ← [50];
UserCredentialsUnsafe.GetUserCredentials[name: n, password: p];
STP.Login[stphandle, n, p];
};

-- can be called by programs that are not using the connection table in this module
HandleSTPError: PUBLIC PROC[stphandle: STP.Handle, stpError: STP.ErrorCode, 
	message: LONG STRING, h: Subr.TTYProcs] RETURNS[retryit: BOOL] ={
IF      stpError = illegalUserPassword OR
        stpError = illegalUserName OR
        stpError = illegalUserAccount OR
	-- misspelled: credentailsMissing
        stpError = credentailsMissing THEN {
		SetPass[stphandle, h, message];
		RETURN[TRUE];
		}
ELSE IF stpError = illegalConnectName OR
	stpError = illegalConnectPassword OR
	stpError = accessDenied
	THEN {
		SetOtherPass[stphandle, h, message];
		RETURN[TRUE];
		};
RETURN[FALSE];
};

-- can be called by programs that are not using the connection table in this module
SetPass: PROC[stphandle: STP.Handle, h: Subr.TTYProcs, error: LONG STRING] = {
IF error ~= NIL THEN CWF.WF1["Error - %s\n"L,error];
CWF.WF0["Enter "L];
Subr.GetNameandPassword[login, NIL, NIL, h];
CWF.WFCR[];
IF stphandle ~= NIL THEN STPLogin[stphandle];
};

-- can be called by programs that are not using the connection table in this module
SetOtherPass: PROC[stphandle: STP.Handle, h: Subr.TTYProcs, error: LONG STRING] = {
user2: STRING ← [40];
password2: STRING ← [40];
IF error ~= NIL THEN CWF.WF1["Error - %s\n"L,error];
CWF.WF0["Enter Connect "L];
Subr.GetNameandPassword[connect, user2, password2, h];
IF stphandle ~= NIL AND user2.length > 0 THEN 
	STP.Connect[stphandle, user2, 
		IF password2.length = 0 THEN NIL ELSE password2];
-- by convention, if not connect name and no password is supplied
-- then we abort
IF user2.length = 0 AND password2.length = 0 THEN {
	IF h.Confirm[h.in, h.out, h.data,
	  "\nType y to retry with different login name, n to abort ", 'n] = 'n THEN 
		SIGNAL Subr.AbortMyself;
	CWF.WF0["Enter "L];
	Subr.GetNameandPassword[login, NIL, NIL, h];
	CWF.WFCR[];
	STPLogin[stphandle];
	};
};

-- can be called by programs that are not using the connection table in this module
AddUserName: PUBLIC PROC[sto: LONG STRING, spat: STRING, h: Subr.TTYProcs] = {
-- u: STRING;
-- p: STRING ← NameAndPasswordOps.userPassword;
u: STRING ← [50];
p: STRING ← [50];
lengthzero: BOOL;
UserCredentialsUnsafe.GetUserCredentials[name: NIL, password: p];
lengthzero ← p = NIL OR p.length = 0;
DO
	-- u ← NameAndPasswordOps.userName;
	UserCredentialsUnsafe.GetUserCredentials[name: u, password: NIL];
	IF u = NIL
   	OR u.length = 0 
   	OR Subr.Any[u,' ]
   	OR Subr.Prefix[u, "CedarUser"L]
   	OR lengthzero THEN {
		lengthzero ← FALSE;
		CWF.WF0["Enter "L];
		Subr.GetNameandPassword[login, NIL, NIL, h];
		CWF.WFCR[];
		}
	ELSE EXIT;
	ENDLOOP;
IF sto ~= NIL THEN CWF.SWF1[sto,spat,u];
};

GetFileType: PROC[filename: LONG STRING] 
	RETURNS[filetype: STP.Type] = {
name: STRING ← [100];
Subr.strcpy[name, filename];
FOR i: CARDINAL IN [0..name.length) DO
	IF name[i] = '! OR name[i] = '; THEN {
		name.length ← i;
		EXIT;
		};
	ENDLOOP;
filetype ← unknown;
IF Subr.EndsIn[name,".mesa"L] OR Subr.EndsIn[name,".bravo"L]
	OR Subr.EndsIn[name,".config"L] OR Subr.EndsIn[name,".cm"L]
	OR Subr.EndsIn[name, ".mail"L] OR Subr.EndsIn[name, ".df"L] THEN 
	filetype ← text;
IF Subr.EndsIn[name,".bcd"L] OR Subr.EndsIn[name,".press"L]
	OR Subr.EndsIn[name,".run"L] THEN filetype ← binary;
};
}.