-- 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[]; }.