-- RemoteDeleteAllImpl.Mesa, last edit February 9, 1983 6:13 pm
-- Pilot 6.0/ Mesa 7.0
	
-- Usage: RemoteDeleteAll directory list of dfs (nesting allowed)
	
DIRECTORY
	  BTreeDefs: TYPE USING [BTreeHandle, Call, CreateAndInitializeBTree, Desc,
	  	EnumerateFrom, Insert, ReleaseBTree, TestKeys],
	  BTreeSupportExtraDefs: TYPE USING [CloseFile, OpenFile],
	  CWF: TYPE USING [SetWriteProcedure, SWF1, SWF2, SWF4],
	  DateAndTimeUnsafe: TYPE USING [Parse, Unintelligible],
	  DFSubr: TYPE USING [AllocateDFSeq, DFSeq, FlattenDF, FreeDFSeq, StripLongName],
	  File: TYPE USING [Capability, Create, Delete, nullCapability],
	  FileIO: TYPE USING[Open],
	  Inline: TYPE USING [BITOR, BITXOR, BytePair, LowHalf],
	  IO: TYPE USING[card, Close, CreateDribbleStream, GetChar, Handle, 
	  	PutChar, PutF, PutFR, PutRope, string, UserAbort],
	  LongString: TYPE USING [AppendString, EqualString, EquivalentString, StringToDecimal],
	  Process: TYPE USING [Detach],
	  Rope: TYPE USING[ROPE, Text],
	  RopeInline: TYPE USING[InlineFlatten],
	  UnsafeSTP: TYPE USING [CompletionProcType, Delete, DesiredProperties, 
	  	Enumerate, Error, FileInfo, GetFileInfo,
	  	Handle, NoteFileProcType, SetDesiredProperties],
	  STPSubr: TYPE USING [Connect, HandleSTPError, StopSTP],
	  String: TYPE USING [AppendChar, AppendString, LowerCase],
	  Subr: TYPE USING [AbortMyself, Any, CopyString, debugflg, EndsIn, errorflg, 
	  	FreeHugeZone, HugeZone, MakeTTYProcs, PagesUsedInHugeZone, PrintGreeting, 
	  	strcpy, SubrInit,	SubrStop, SubStrCopy, TTYProcs],
	  Time: TYPE USING [Current],
	  TypeScript: TYPE USING[TS, Create],
	  UECP: TYPE USING[Argv, Parse],
	  UserExec: TYPE USING[AcquireResource, AskUser, CommandProc, RegisterCommand, 
	  	ReleaseResource, UserAbort],
	  ViewerEvents: TYPE USING[EventProc, EventRegistration, RegisterEventProc,
  	UnRegisterEventProc],
     ViewerIO: TYPE USING[CreateViewerStreams],
	  Volume: TYPE USING [systemID];
	
RemoteDeleteAllImpl: PROGRAM
IMPORTS BTreeDefs, BTreeSupportExtraDefs, CWF, DateAndTimeUnsafe,
		DFSubr, File, FileIO, Inline, IO, LongString, 
		Process, RopeInline, STP: UnsafeSTP, STPSubr, String, Subr, Time, TypeScript, UECP,
		UserExec, ViewerEvents, ViewerIO, Volume = {
	
-- max number of files in all DF file we want to ignore
MAXDELETEFILE: CARDINAL = 9000;	-- should be 12000
	
	
-- MDS usage!
globalhost: STRING ← [30];
globalremotepattern: STRING ← [100];
typeScript: TypeScript.TS;
in, out, logFile: IO.Handle;
destroyEventRegistration: ViewerEvents.EventRegistration;
-- endof MDS

possibleDelete: BOOL = TRUE;
initialBackingPages: CARDINAL = 10;

-- this is the procedure called by the Simple Executive

Main: UserExec.CommandProc = TRUSTED {
	ENABLE UNWIND => [] ← UserExec.ReleaseResource[$RemoteDeleteAll];
	p: PROCESS;
	dfseq: DFSubr.DFSeq ← NIL;
	flat: Rope.Text;
	tok: STRING ← [100];
	time, starttime: LONG CARDINAL;
	remnam: STRING ← [100];
	stemp: STRING ← [100];
	last: STRING ← [100];
	host: STRING ← [100];
	remotepattern: STRING ← [100];
	npages: CARDINAL;
	parm: CARDINAL;
	argv: UECP.Argv ← UECP.Parse[event.commandLine];
	wh: Subr.TTYProcs;
	
	Cleanup: PROC = {
		DFSubr.FreeDFSeq[@dfseq];
		STPSubr.StopSTP[];
		Subr.SubrStop[];
		};
	
	[] ← UserExec.AcquireResource[$RemoteDeleteAll, "RemoteDeleteAll", exec];
	starttime ← Time.Current[];
	wh ← Subr.MakeTTYProcs[in, out, typeScript, MyConfirm];
	Subr.errorflg ← Subr.debugflg ← FALSE;
	Subr.PrintGreeting["RemoteDeleteAll"L];
	{
		ENABLE {
		STP.Error => {
			lcode: LONG CARDINAL ← LOOPHOLE[code, CARDINAL];
			out.PutF["FTP Error. "];
			IF error ~= NIL THEN 
				out.PutF["message: %s, code %d in Stp.Mesa\n", IO.string[error],
					IO.card[lcode]];
			Subr.errorflg ← TRUE;
			GOTO leave;
			};
		Subr.AbortMyself => {
			out.PutF["RemoteDeleteAll Aborted.\n"];
			GOTO leave;
			};
		UNWIND => Cleanup[];
		};
	
	IF argv.argc = 1 THEN GOTO usage;
	Subr.SubrInit[256];
	dfseq ← DFSubr.AllocateDFSeq[maxEntries: MAXDELETEFILE, zoneType: huge];
	IF UserExec.UserAbort[exec] THEN SIGNAL Subr.AbortMyself;
	flat ← RopeInline.InlineFlatten[argv[1]];
	Subr.strcpy[remnam, LOOPHOLE[flat]];
	time ← Time.Current[];
	parm ← 2;
	WHILE parm < argv.argc DO
		flat ← RopeInline.InlineFlatten[argv[parm]];
		Subr.strcpy[tok, LOOPHOLE[flat]];
		IF NOT Subr.Any[tok, '.] THEN 
			LongString.AppendString[tok, ".DF"L];
		out.PutF["\nReading %s:\n", IO.string[tok]];
		DFSubr.FlattenDF[dfseq: dfseq, dffilename: tok, h: wh,
			checkForOverwrite: FALSE, printStatus: TRUE];
		parm ← parm + 1;
		ENDLOOP;
	time ← Time.Current[] - time;
	out.PutF["Time to read in all DF files: %r.\n", IO.card[time]];
	[] ← DFSubr.StripLongName[remnam, host, stemp, last];
	CWF.SWF2[remotepattern, "<%s>%s"L, stemp, last];
	IF NOT Subr.EndsIn[remotepattern, "*"L] THEN 
		String.AppendChar[remotepattern, '*];
	IF NOT Subr.Any[remotepattern, '!] THEN
		String.AppendString[remotepattern, "!*"L];
	npages ← Subr.PagesUsedInHugeZone[dfseq.dfzone];
	out.PutF["%d pages used in Huge Zone, %d entries in flattened DF files.\n", 
		IO.card[npages], IO.card[dfseq.size]];
	Subr.strcpy[globalhost, host];
	Subr.strcpy[globalremotepattern, remotepattern];
	
	p ← FORK RunDeleteAll[dfseq, starttime, wh];
	Process.Detach[p];
	dfseq ← NIL;
	EXITS
	usage => {
		out.PutF["Usage: RemoteDeleteAll directory dffile(s).\n"];
		};
	leave => NULL;
	};
	[] ← UserExec.ReleaseResource[$RemoteDeleteAll];
	};
	
IFSBytesPerPage: CARDINAL = 2048;
	
-- forked as a separate process
RunDeleteAll: PROC[dfseq: DFSubr.DFSeq, starttime: LONG CARDINAL, wh: Subr.TTYProcs] = {
	stphandle: STP.Handle ← NIL;
	ndelete, nskipdf, nskipno: CARDINAL ← 0;
	connuser: STRING ← [40];
	connpass: STRING ← [40];
	deleteBackingFile: File.Capability ← File.nullCapability;
	deleteBTree: BTreeDefs.BTreeHandle;	-- no init value allowed
	haveDeleteBTree: BOOL ← FALSE;
	elapt: LONG CARDINAL ← starttime;
	nskippages, ndeletepages: LONG CARDINAL ← 0;
	fullspeed: BOOL ← FALSE;
	lastDirectory: STRING ← [100];
	NameList: TYPE = LONG POINTER TO NameListRecord;
	NameListRecord: TYPE = RECORD[
		host: LONG STRING,
		directory: LONG STRING,
		version: CARDINAL,
		createtime: LONG CARDINAL,
		list: NameList
		];
	shortnamearray: LONG POINTER TO ShortRec ← NIL;
	ShortRec: TYPE = ARRAY [0 .. 8000] OF RECORD[
		shortname: LONG STRING,
		list: NameList
		] ← ALL[[NIL, NIL]];
	hugezone: UNCOUNTED ZONE ← Subr.HugeZone[];
	indigoHost: STRING ← "Indigo"L;
	ivyHost: STRING ← "Ivy"L;
	lastD: LONG STRING ← NIL;
	nLook, nScan, nInsert: CARDINAL ← 0;
	deleteFile, keepFile: IO.Handle;
	
		ComputeHost: PROC[o: LONG STRING] RETURNS[n: LONG STRING] = {
		RETURN[IF LongString.EquivalentString[o, indigoHost] THEN indigoHost
		       ELSE IF LongString.EquivalentString[o, ivyHost] THEN ivyHost
		       ELSE Subr.CopyString[o, hugezone]];
		};
		
		ComputeDirectory: PROC[o: LONG STRING] RETURNS[n: LONG STRING] = {
		IF lastD ~= NIL 
		AND LongString.EquivalentString[o, lastD] THEN n ← lastD
		ELSE n ← lastD ← Subr.CopyString[o, hugezone];
		};
		
		AddName: PROC[host, directory, shortname: LONG STRING, version: CARDINAL,
			createtime: LONG CARDINAL] = {
		l: NameList ← NIL;
		element: CARDINAL ← HashedLookup[shortname];
		IF shortnamearray[element].shortname = NIL THEN {-- not in table
			shortnamearray[element] ← [shortname: Subr.CopyString[shortname, hugezone],
				list: NIL];
			nInsert ← nInsert + 1;
			};
		l ← hugezone.NEW[NameListRecord];
		l↑ ← [host: ComputeHost[host], directory: ComputeDirectory[directory],
			version: version, createtime: createtime, 
			list: shortnamearray[element].list];
		shortnamearray[element].list ← l;
		};
		
		-- returns element with shortname = NIL if can't find
		-- (this will be the first NIL encountered)
		-- returns element ~= NIL if found
		HashedLookup: PROC[shortname: LONG STRING] RETURNS[element: CARDINAL] = {
		hv: CARDINAL ← Hash[shortname, LENGTH[shortnamearray↑]]; -- hv IN [0 .. LENGTH[])
		nLook ← nLook + 1;
		FOR i: CARDINAL IN [hv .. LENGTH[shortnamearray↑]) DO
			nScan ← nScan + 1;
			IF shortnamearray[i].shortname = NIL THEN RETURN[i];
			IF LongString.EquivalentString[shortnamearray[i].shortname, shortname] THEN 
				RETURN[i];
			ENDLOOP;
		FOR i: CARDINAL IN [0 .. hv) DO
			nScan ← nScan + 1;
			IF shortnamearray[i].shortname = NIL THEN RETURN[i];
			IF LongString.EquivalentString[shortnamearray[i].shortname, shortname] THEN 
				RETURN[i];
			ENDLOOP;
		ERROR;	-- full
		};
		
		BuildBTree: PROC = {
		nFiles: CARDINAL ← 0;
		time: LONG CARDINAL ← Time.Current[];
		desiredProperties: STP.DesiredProperties ← ALL[FALSE];

			-- NOTE: Desired properties have been set
			EnumProc: STP.NoteFileProcType = {
			pages: CARDINAL;
			info: STP.FileInfo;
			key: STRING ← [100];
			value: STRING ← [100];
			continue ← yes;
			info ← STP.GetFileInfo[stphandle];
			IF file.length > 2 AND Subr.EndsIn[file, ">!1"L] THEN
				RETURN;	-- skip these questionable cases
			nFiles ← nFiles + 1;
			pages ← Inline.LowHalf[info.size/IFSBytesPerPage + 2];
			IF Match[globalhost, info] 
			-- OR LongString.EquivalentString[info.body, "Dir.Dir"L] 
			THEN {
				IF NOT LongString.EquivalentString[lastDirectory, info.directory] THEN {
					keepFile.PutChar['\n];	-- extra space
					Subr.strcpy[lastDirectory, info.directory];
					};
				out.PutChar['+];
				IF file.length > 60 THEN keepFile.PutF["%s    %s  %4d\n", IO.string[file], 
						IO.string[info.create], IO.card[pages]]
				ELSE keepFile.PutF["%-60s    %s  %4d\n", IO.string[file], 
						IO.string[info.create], IO.card[pages]];
				nskippages ← nskippages + pages;
				nskipdf ← nskipdf + 1;
				}
			ELSE {-- insert in really delete bTree
				author: STRING;
				CWF.SWF1[key, "%07d"L, @nFiles];
				-- value has a trailing "M" if file was stored by Morris (now obsolete)
				author ← ""L;
				CWF.SWF4[value, "%s*000%s*000%d%s"L, file, info.create, @pages, author];
				ndeletepages ← ndeletepages + pages;
				ndelete ← ndelete + 1;
				BTreeDefs.Insert[deleteBTree, MakeBTreeDesc[key],
					MakeBTreeDesc[value]];
				};
			IF in.UserAbort[] THEN continue ← no;
			};
			
		desiredProperties[directory] ← TRUE;
		desiredProperties[nameBody] ← TRUE;
		desiredProperties[version] ← TRUE;
		desiredProperties[createDate] ← TRUE;
		desiredProperties[size] ← TRUE;
		STP.SetDesiredProperties[stphandle, desiredProperties];
		STP.Enumerate[stphandle, globalremotepattern, EnumProc
		   ! STP.Error => 
			IF code = noSuchFile THEN {
				out.PutF["%s not found.\n", IO.string[globalremotepattern]];
				CONTINUE
				}
			ELSE IF STPSubr.HandleSTPError[stphandle, code, 
				error, wh] THEN RETRY
			];
		time ← Time.Current[] - time;
		out.PutF["\nEnumeration complete, %d files, elapsed time %r.\n", 
			IO.card[nFiles], IO.card[time]];
		out.PutF["Will offer to delete %d files.\n", IO.card[ndelete]];
		out.PutF["This will free %d pages, and will leave %d pages alone.\n", 
			IO.card[ndeletepages], IO.card[nskippages]];
		ndelete ← 0;
		};
		
		PrintListOfDeletes: BTreeDefs.Call = {
		value: LONG STRING ← LOOPHOLE[BASE[v]];
		longfile: STRING ← [100];
		stringcreate: STRING ← [100];
		stringpages: STRING ← [100];
		directory: STRING ← [100];
		create: LONG CARDINAL;
		pages: CARDINAL;
		morris: BOOL ← FALSE;
		tonywest: BOOL ← FALSE;
	
		Subr.strcpy[longfile, value];
		FOR i: CARDINAL IN [0 .. value.length) DO
			IF value[i] = 0C THEN {
				longfile.length ← i;
				EXIT;
				};
			ENDLOOP;
		Subr.SubStrCopy[stringcreate, value, longfile.length + 1];
		FOR i: CARDINAL IN [0 .. stringcreate.length) DO
			IF stringcreate[i] = 0C THEN {
				stringcreate.length ← i;
				EXIT;
				};
			ENDLOOP;
		create ← DateAndTimeUnsafe.Parse[stringcreate
			! DateAndTimeUnsafe.Unintelligible => {
				out.PutF["Error - invalid date\n"];
				create ← 0;
				CONTINUE;
				}].dt;
		Subr.SubStrCopy[stringpages, value, 
			longfile.length + stringcreate.length + 2];
		IF Subr.EndsIn[stringpages, "M"L] THEN {
			stringpages.length ← stringpages.length - 1;
			morris ← TRUE;
			};
		IF Subr.EndsIn[stringpages, "T"L] THEN {
			stringpages.length ← stringpages.length - 1;
			tonywest ← TRUE;
			};
		pages ← LongString.StringToDecimal[stringpages];
		Subr.strcpy[directory, longfile];
		FOR i: CARDINAL DECREASING IN [0 .. longfile.length) DO
			IF longfile[i] = '> THEN {
				directory.length ← i;
				EXIT;
				};
			ENDLOOP;
		IF NOT LongString.EquivalentString[lastDirectory, directory] THEN {
			deleteFile.PutChar['\n];	-- extra space
			Subr.strcpy[lastDirectory, directory];
			};
		IF morris THEN deleteFile.PutF["Stored by Morris.PA:\n"]
		ELSE IF tonywest THEN deleteFile.PutF["Stored by TonyWest.PA:\n"];
		IF longfile.length > 60 THEN 
			deleteFile.PutF["%s   %s  %4d\n", IO.string[longfile], IO.string[stringcreate],
				IO.card[pages]]
		ELSE 
			deleteFile.PutF["%-60s   %s  %4d\n", IO.string[longfile], IO.string[stringcreate],
				IO.card[pages]];
		IF in.UserAbort[] THEN RETURN[FALSE, FALSE];
		RETURN[TRUE, FALSE];
		};
		
		ReallyDeleteFiles: BTreeDefs.Call = {
		ch: CHAR;
		value: LONG STRING ← LOOPHOLE[BASE[v]];
		longfile: STRING ← [100];
		stringcreate: STRING ← [100];
		mustConnect: BOOL ← FALSE;
		r: Rope.ROPE;
		
			DelComplete: STP.CompletionProcType = {
			-- it turns out we don't get the right signal
			-- so we have to raise it ourselves
			-- the error is STP.Error[accessDenied], but the string
			-- is IFS-specific
			-- can't raise STP.Error in here, unwind will kill connection
			-- so we set a flag
			out.PutF["%s\n", IO.string[fileOrError]];
			IF what = error 
			AND LongString.EqualString[fileOrError, "File is protected - access denied."L]
			THEN
				mustConnect ← TRUE;
			};
		
		Subr.strcpy[longfile, value];
		FOR i: CARDINAL IN [0 .. value.length) DO
			IF value[i] = 0C THEN {
				longfile.length ← i;
				EXIT;
				};
			ENDLOOP;
		Subr.SubStrCopy[stringcreate, value, longfile.length + 1];
		FOR i: CARDINAL IN [0 .. stringcreate.length) DO
			IF stringcreate[i] = 0C THEN {
				stringcreate.length ← i;
				EXIT;
				};
			ENDLOOP;
		r ← IO.PutFR["Delete %s of %s ", IO.string[longfile], IO.string[stringcreate]];
		IF fullspeed THEN {
			out.PutRope[r];
			ch ← 'y;
			}
		ELSE ch ← wh.Confirm[wh.in, wh.out, wh.data, r, 'n];
		IF ch = 'q THEN {
			nskipno ← nskipno + 1;
			RETURN[FALSE, FALSE];
			};
		IF ch = 'a THEN {
			out.PutF["All\nDo you really want to delete without confirmation (Type ↑ to confirm) "];
			IF wh.in.GetChar[] = '↑ THEN {
				ch ← 'y;
				fullspeed ← TRUE;
				};
			}; -- continues on to delete this file
		IF ch = 'y THEN {
			ndelete ← ndelete + 1;
			stphandle ← STPSubr.Connect[host: globalhost, h: wh, 
				onlyOne: TRUE];
			DO
				mustConnect ← FALSE;
				STP.Delete[stp: stphandle, name: longfile, confirm: NIL,
					complete: DelComplete
					! STP.Error => 
					IF code = noSuchFile THEN {
						out.PutF["%s not found.\n", IO.string[longfile]];
						CONTINUE
						}
					ELSE IF STPSubr.HandleSTPError[stphandle, code, 
						error, wh] THEN RETRY
					];
				IF NOT mustConnect THEN EXIT;
				[] ← STPSubr.HandleSTPError[stphandle, accessDenied, 
					NIL, wh];
				ENDLOOP;
			}
		ELSE {
			nskipno ← nskipno + 1;
			};
		RETURN[NOT in.UserAbort[], FALSE];
		};
	
		Match: PROC[host: LONG STRING, info: STP.FileInfo] RETURNS[dontDelete: BOOL] = {
		element: CARDINAL;
		remdate: LONG CARDINAL ← 0;
		l: NameList;
		element ← HashedLookup[info.body];
		IF shortnamearray[element].shortname = NIL THEN RETURN[FALSE];	-- not found
		l ← shortnamearray[element].list;
		WHILE l ~= NIL DO
			IF LongString.EquivalentString[l.directory, info.directory]
			AND LongString.EquivalentString[l.host, host] THEN {
				IF l.createtime > 0 THEN {
					IF remdate = 0 THEN
						remdate ← DateAndTimeUnsafe.Parse[info.create
								! DateAndTimeUnsafe.Unintelligible => CONTINUE
								].dt;
					IF remdate = l.createtime THEN RETURN[TRUE];
					};
				IF l.version > 0 THEN {
					IF info.version ~= NIL AND info.version.length > 0 THEN {
						vers: CARDINAL ← LongString.StringToDecimal[info.version];
						IF vers = l.version THEN RETURN[TRUE];
						};
					};
				IF l.createtime = 0 AND l.version = 0 THEN   --  >, ~=
					RETURN[TRUE];
				};
			l ← l.list;
			ENDLOOP;
		RETURN[FALSE];
		};
		
		Cleanup: PROC = {
		IF keepFile ~= NIL THEN keepFile.Close[];
		keepFile ← NIL;
		IF deleteFile ~= NIL THEN deleteFile.Close[];
		deleteFile ← NIL;
		IF haveDeleteBTree THEN BTreeSupportExtraDefs.CloseFile[
			BTreeDefs.ReleaseBTree[deleteBTree]];
		haveDeleteBTree ← FALSE;
		IF deleteBackingFile ~= File.nullCapability THEN 
			File.Delete[deleteBackingFile];
		deleteBackingFile ← File.nullCapability;
		DFSubr.FreeDFSeq[@dfseq];
		STPSubr.StopSTP[];
		Subr.SubrStop[];
		};
			
	{
	ENABLE {
		UNWIND => Cleanup[];
		STP.Error => {
			lcode: LONG CARDINAL ← LOOPHOLE[code, CARDINAL];
			out.PutF["FTP Error. "];
			IF error ~= NIL THEN 
				out.PutF["message: %s, code %d in Stp.Mesa\n", 
					IO.string[error], IO.card[lcode]];
			Subr.errorflg ← TRUE;
			GOTO out;
			};
		ABORTED, Subr.AbortMyself => {
			out.PutF["RemoteDeleteAll Aborted.\n"];
			GOTO out;
			};
		};
	shortnamearray ← hugezone.NEW[ShortRec ← ALL[[NIL, NIL]]];
	out.PutF["Filling in hash table ... "];
	FOR i: CARDINAL IN [0 .. dfseq.size) DO
		AddName[dfseq[i].host, dfseq[i].directory, dfseq[i].shortname,
			dfseq[i].version, dfseq[i].createtime];
		ENDLOOP;
	out.PutF["done.\n"];
	-- debugging
	out.PutF["%d insertions, %d scans in %d looks.\n", IO.card[nInsert], IO.card[nScan], IO.card[nLook]];
	deleteBackingFile ← File.Create[Volume.systemID, initialBackingPages, [12345]];
	deleteBTree ← BTreeDefs.CreateAndInitializeBTree[
	    	fileH: BTreeSupportExtraDefs.OpenFile[deleteBackingFile],
	    	initializeFile: TRUE, isFirstGreaterOrEqual: IsFirstGEQ,
	    	areTheyEqual: AreTheyEQ];
	haveDeleteBTree ← TRUE;
	out.PutF["Enumerating [%s]%s ... \n", IO.string[globalhost], IO.string[globalremotepattern]];
	stphandle ← STPSubr.Connect[host: globalhost, h: wh, onlyOne: TRUE];
	out.PutF["Writing list of files that will NOT be deleted on 'RemoteDeleteAll.KeepFiles$'\n"];
	keepFile ← FileIO.Open["RemoteDeleteAll.KeepFiles$", overwrite];
	keepFile.PutF["\n\nThese files will not be deleted.\n\n"];
	keepFile.PutF["        FileName                                                 CreateTime         IFSPages\n"];
	BuildBTree[];
	keepFile.PutF["\n\n----------------------\n"];
	keepFile.Close[];
	keepFile ← NIL;
	out.PutF["List of files that will NOT be deleted written on 'RemoteDeleteAll.KeepFiles$'\n"];
	STPSubr.StopSTP[];
	hugezone ← Subr.FreeHugeZone[hugezone];
	-- be careful: do not use hugezone data structures after this
	shortnamearray ← NIL;
	dfseq ← NIL;
	IF in.UserAbort[] THEN SIGNAL Subr.AbortMyself;
	--
	out.PutF["Writing list of files that will be deleted on 'RemoteDeleteAll.DeleteFiles$'\n"];
	deleteFile ← FileIO.Open["RemoteDeleteAll.DeleteFiles$", overwrite];
	deleteFile.PutF["\nThese files will be deleted.\n\n"];
	deleteFile.PutF["        FileName                                                 CreateTime         IFSPages\n"];
	BTreeDefs.EnumerateFrom[deleteBTree, MakeBTreeDesc[""L], PrintListOfDeletes];
	deleteFile.PutF["\n\n----------------------\n"];
	deleteFile.Close[];
	deleteFile ← NIL;
	IF in.UserAbort[] THEN SIGNAL Subr.AbortMyself;
	--
	out.PutF["Type print RemoteDeleteAll.KeepFiles$ RemoteDeleteAll.DeleteFiles$ to print the files.\n"];
	IF possibleDelete THEN {
		out.PutF["\n\n\n(For each file, type y to delete, q to quit, \n\ta to delete subsequent files w/o confirmation and any other char to not delete.)\n"];
		out.PutF["\nEnumerating [%s]%s ... \n", IO.string[globalhost], IO.string[globalremotepattern]];
		BTreeDefs.EnumerateFrom[deleteBTree, MakeBTreeDesc[""L], 
			ReallyDeleteFiles];
		};
	out.PutF["\n%d files matched, %d files deleted, %d files skipped.\n",
		IO.card[nskipdf], IO.card[ndelete], IO.card[nskipno]];
	EXITS
	out => NULL;
	};
	Cleanup[];
	elapt ← Time.Current[] - elapt;
	out.PutF["\nTotal elapsed time for RemoteDeleteAll %r.",IO.card[elapt]];
	IF Subr.errorflg THEN 
		out.PutF["\tErrors logged.\n"];
	out.PutChar['\n];
	PrintSeparator[];
	};
	
Hash: PROC[shortname: LONG STRING, modulo: CARDINAL] RETURNS[hv: CARDINAL] = {
	h, i: CARDINAL ← 0;
	v: Inline.BytePair;
	IF shortname.length = 0 THEN ERROR;
	DO
		v.low ← LOOPHOLE[String.LowerCase[shortname[i]], CARDINAL];
		i ← i + 1;
		v.high ← IF i >= shortname.length THEN 0 
			ELSE LOOPHOLE[String.LowerCase[shortname[i]], CARDINAL];
		i ← i + 1;
		h ← Inline.BITXOR[h, v];
		IF i >= shortname.length THEN EXIT;
		ENDLOOP;
	hv ← h MOD modulo;
	-- debugging CWF.WF1["%d\n"L, @hv];
	};
		
MakeBTreeDesc: PROC [s: STRING] RETURNS [d: BTreeDefs.Desc] = INLINE
	  {RETURN[DESCRIPTOR[LOOPHOLE[s, POINTER], (s.length + 1)/2 + 2]]};
	
IsFirstGEQ: BTreeDefs.TestKeys =
	  BEGIN
	  aS: LONG STRING = LOOPHOLE[BASE[a]];
	  bS: LONG STRING = LOOPHOLE[BASE[b]];
	  FOR i: CARDINAL IN [0..MIN[aS.length, bS.length]) DO
	    aC: CHAR = Inline.BITOR[aS[i], 40B];
	    bC: CHAR = Inline.BITOR[bS[i], 40B];
	    SELECT aC FROM
	      > bC => RETURN [TRUE];
	      < bC => RETURN [FALSE];
	      ENDCASE;
	    ENDLOOP;
	  RETURN [aS.length >= bS.length]
	  END;
	
AreTheyEQ: BTreeDefs.TestKeys =
	  BEGIN
	  aS: LONG STRING = LOOPHOLE[BASE[a]];
	  bS: LONG STRING = LOOPHOLE[BASE[b]];
	  IF aS.length ~= bS.length THEN RETURN [FALSE];
	  FOR i: CARDINAL IN [0..aS.length) DO
	    IF Inline.BITOR[aS[i], 40B] ~= Inline.BITOR[bS[i], 40B] THEN RETURN [FALSE];
	    ENDLOOP;
	  RETURN [TRUE]
	  END;
	
Init: PROC = {
	typeScript ← TypeScript.Create[info: [name: "RemoteDeleteAll Window", iconic: FALSE]];
	[in: in, out: out, file: logFile] ← SetUpLogStreams[typeScript];
	[] ← CWF.SetWriteProcedure[TTYProc];
	UserExec.RegisterCommand["RemoteDeleteAll.~", Main];
	destroyEventRegistration ← ViewerEvents.RegisterEventProc[MyDestroy, destroy];
   };
	
NullTTYProc: PROC[ch: CHAR] = {};	-- prints nothing

-- cannot print anything in this, monitor is locked
MyDestroy: ViewerEvents.EventProc = TRUSTED
	{
	IF event ~= destroy OR viewer ~= typeScript THEN RETURN;
	[] ← CWF.SetWriteProcedure[NullTTYProc];	-- turn off printing
	Subr.SubrStop[];
	logFile.Close[];
	ViewerEvents.UnRegisterEventProc[destroyEventRegistration, destroy];
	};
	   
SetUpLogStreams: PROC[ts: TypeScript.TS] RETURNS[in, out, file: IO.Handle] =
	   {
	   file ← FileIO.Open["RemoteDeleteAll.Log", overwrite];
	   [in, out] ← ViewerIO.CreateViewerStreams[name: "RemoteDeleteAll Window", viewer: ts,
	   				editedStream: FALSE];
	   out ← IO.CreateDribbleStream[out, file];
	   };
	
MyConfirm: SAFE PROC[in, out: IO.Handle, data: REF ANY, msg: Rope.ROPE, dch: CHAR]
	RETURNS[CHAR] = CHECKED {
	value: ATOM;
	value ← UserExec.AskUser[msg: msg, viewer: NARROW[data], 
		keyList: LIST[$Yes, $No, $All, $Local, $Quit]];  -- order is important
	SELECT value FROM
	$All => RETURN['a];
	$Local => RETURN['l];
	$No => RETURN['n];
	$Quit => RETURN['q];
	$Yes => RETURN['y];
	ENDCASE => ERROR;
	};

TTYProc: PROC[ch: CHAR] = {
	out.PutChar[ch];
	};
	
PrintSeparator: PROC = {
	out.PutF["===============================\n"];
	};
	
Init[];
}.