-- ReleaseImpl.Mesa, last edit March 16, 1983 6:54 pm
-- Pilot 6.0/ Mesa 7.0

-- Phase	Meaning
-- 1		verify consistency of all DF files:
--				make sure there are no version conflicts and
--				all files exist on remote servers
-- 2		verify sufficiency: run VerifyDF on all DF files
	-- 3		transfer files to new homes, produce new DF files
-- note that only Phase 2 involves looking at the internals of the files


-- Phase 2 and 3 are in Release23Impl.Mesa
-- Phase 4 is in Release4Impl.Mesa

DIRECTORY
  BTreeDefs: TYPE USING[BTreeHandle, CreateAndInitializeBTree, Desc, Insert, KeyNotFound,
  	Lookup,ReleaseBTree, TestKeys],
  BTreeSupportDefs: TYPE USING[FileHandle--, SetLength--],
  BTreeSupportExtraDefs: TYPE USING[CloseFile, OpenFile],
  Buttons: TYPE USING [Button, ButtonProc, Create],
  Containers: TYPE USING [ChildXBound, Container, Create],
  CWF: TYPE USING [SetWriteProcedure, SWF1, SWF2, SWF3, WF0, WF1, WF2, WF3, WF4,
  	WFC, WFCR],
  DateAndTime: TYPE USING [Parse],
  Directory: TYPE USING[Error, Lookup, ignore],
  DFSubr: TYPE USING [AllocateDFSeq, DF, DFSeq, FlattenDF, FreeDFSeq, SortByFileName,
  	StripLongName, TooManyEntries],
  File: TYPE USING [Capability, nullCapability],
  FileIO: TYPE USING[minimumStreamBufferParms, Open],
  FQ: TYPE USING[FileQuery, Result],
  IFSFile: TYPE USING [CantOpen, Error, UnableToLogin],
  Inline: TYPE USING[BITOR],
  IO: TYPE USING[card, Close, CreateDribbleStream, Flush, Handle,
  	Put, PutChar, PutF, ResetUserAbort, rope, SetUserAbort, string, UserAbort, UserAborted],
  Labels: TYPE USING [Create, Label, Set], 
  LeafSubr: TYPE USING [PrintLeafAccessFailure, PrintLeafProblem, StopLeaf],
  LongString: TYPE USING [EquivalentString],
  Menus: TYPE USING [CreateEntry, CreateMenu, InsertMenuEntry, Menu, MenuProc], 
  MessageWindow: TYPE USING[Append, Blink],
  Process: TYPE USING [Detach, Priority, priorityBackground, priorityForeground,
  	priorityNormal, SetPriority],
  ReleaseSupport: TYPE USING [TransferFiles, VerifySufficiency],
  Rope: TYPE USING[ROPE, Text],
  RopeInline: TYPE USING[InlineFlatten],
  UnsafeSTP: TYPE USING [Error],
  STPSubr: TYPE USING [SetNumberOfConnectTries, StopSTP],
  Stream: TYPE USING [Delete, Handle],
  Subr: TYPE USING [AbortMyself, Any, CopyString, debugflg, EndsIn, errorflg, FileError, 
  	FreeHugeZone, FreeString, GetLine, LongZone, MakeTTYProcs, NewFile, NewStream,
  	PagesUsedInHugeZone, Prefix, Read, 
		ReadWrite, strcpy, SubStrCopy, SubrInit, SubrStop, TTYProcs],
  Time: TYPE USING [Current],
  TypeScript: TYPE USING[Create, Destroy, TS],
  UserExec: TYPE USING[AskUser],
  ViewerClasses: TYPE USING [Viewer], 
  ViewerEvents: TYPE USING[EventProc, EventRegistration, RegisterEventProc,
  	UnRegisterEventProc],
  ViewerOps: TYPE USING [PaintViewer, SetMenu, SetOpenHeight], 
  ViewerIO: TYPE USING[CreateViewerStreams],
  ViewerTools: TYPE USING [MakeNewTextViewer, GetContents, SetSelection];

ReleaseImpl: MONITOR
IMPORTS BTreeDefs, BTreeSupportExtraDefs, Buttons, Containers,
	CWF, DateAndTime, DFSubr,
	Directory, FileIO, FQ, IFSFile, Inline, IO, Labels, LeafSubr, LongString, 
	Menus, MessageWindow, Process, ReleaseSupport, RopeInline,  
	STP: UnsafeSTP, STPSubr, Stream, Subr, Time, TypeScript, UserExec, 
	ViewerEvents, ViewerOps, ViewerIO, ViewerTools = {

-- number of entries in all df files
MAXALLFILES: CARDINAL = 6500;

-- number of bytes in an IFS page
-- (all page counts are in IFS pages, not Pilot pages)
bytesPerIFSPage: CARDINAL = 2048;


EPhase: TYPE = {One, Two, Three, OneThree};

-- all global data in a record to help scoping
Global: TYPE = RECORD[
	-- viewers data
	container: Containers.Container ← NIL, 
	typeScript: TypeScript.TS ← NIL,
	tty: Subr.TTYProcs,		-- for typeScript
	in: IO.Handle ← NIL,			-- to the dribble stream
	out: IO.Handle ← NIL,			-- to the dribble stream
	logFile: IO.Handle ← NIL,		-- to the log file
	-- fields
	fileNameButton: Buttons.Button ← NIL,
	fileNameViewer: ViewerClasses.Viewer ← NIL,
	phaseLabel: Labels.Label ← NIL,
	phaseButton: Buttons.Button ← NIL,
	priorityLabel: Labels.Label ← NIL,
	priorityButton: Buttons.Button ← NIL,
	useOldPhase1FileLabel: Labels.Label ← NIL,
	useOldPhase1FileButton: Buttons.Button ← NIL,
	useOldPhase3FileLabel: Labels.Label ← NIL,
	useOldPhase3FileButton: Buttons.Button ← NIL,
	updateBTreeLabel: Labels.Label ← NIL,
	updateBTreeButton: Buttons.Button ← NIL,
	verboseLabel: Labels.Label ← NIL,
	verboseButton: Buttons.Button ← NIL,
	-- program data
	busy: BOOL ← FALSE,
	phase: EPhase ← One,
	priority: Process.Priority ← Process.priorityNormal,
	checkOverwrite: BOOL ← FALSE,
	verbose: REF BOOL ← NIL,
	nProbablePages: LONG CARDINAL ← 0,
	nProbableFiles: CARDINAL ← 0,
	dfseqall: DFSubr.DFSeq ← NIL,
	useOldPhase1FileCache: BOOL ← TRUE,
	useOldPhase3FileCache: BOOL ← TRUE,
	updateBTree: BOOL ← TRUE,
	oldPhase1FileCacheExists: BOOL ← FALSE,
		oldestBcdDate: LONG CARDINAL ← 0,
		-- btree data
	bTreeCap: File.Capability ← File.nullCapability,
	bTreeHandle: BTreeDefs.BTreeHandle ← NULL
	];
	
-- mds usage
g: REF Global;
destroyEventRegistration: ViewerEvents.EventRegistration;
-- end of mds usage

Go: Menus.MenuProc = TRUSTED
	   {
	   p: PROCESS;
	   IF g.busy THEN {
		CWF.WF0["Still busy - try later.\n"L];
		RETURN;
		};
	   p ← FORK ChooseAmong[g.phase];
	   Process.Detach[p];
	   };
	   
	ChooseAmong: ENTRY PROC[phase: EPhase] = {
	ENABLE UNWIND => g.busy ← FALSE;
	topdffilename: STRING ← [100];
	dfFileRef: Rope.Text;
	starttime: LONG CARDINAL ← Time.Current[];
	
	{
	ENABLE {
		UNWIND => TemporaryStop[];
		IFSFile.Error, IFSFile.UnableToLogin => {
			LeafSubr.PrintLeafProblem[reason];
			GOTO leave;
			};
		IFSFile.CantOpen => {
			LeafSubr.PrintLeafAccessFailure[reason];
			GOTO leave;
			};
		ABORTED, Subr.AbortMyself, IO.UserAborted => {
			g.in.ResetUserAbort[];
			CWF.WF0["\nRelease Aborted.\n"L];
			GOTO leave;
			};
		STP.Error => {
			CWF.WF0["FTP Error. "L];
			IF error ~= NIL THEN 
				CWF.WF2["message: %s, code %u in Stp.Mesa\n"L, 
					error, @code];
			Subr.errorflg ← TRUE;
			GOTO leave;
			};
		};
					
	DoPhase: PROC[phase: EPhase] = {
		time: LONG CARDINAL ← Time.Current[];
		Process.SetPriority[g.priority];
		SELECT phase FROM
		One => CWF.WF0["Phase One: Verify consistency of all the DF files:\nStart by reading all DF files.\n"L];
		Two => CWF.WF0["Phase Two: Verify sufficiency by invoking VerifyDF on all the DF files:\n"L];
		Three => CWF.WF0["Phase Three: Transfer remote files and fix up DF files to match.\n"L];
		ENDCASE => ERROR;
		CWF.WF1["Phase started at %lt.\n"L, @time];
		Subr.errorflg ← Subr.debugflg ← FALSE;
		SELECT phase FROM
		One => VerifyConsistency[topdffilename];
		Two => ReleaseSupport.VerifySufficiency[topdffilename, g.tty, g.out,
			g.checkOverwrite];
		Three => ReleaseSupport.TransferFiles[topdffilename, g.dfseqall, g.tty, g.in, g.out, g.logFile,
			g.checkOverwrite, g.useOldPhase3FileCache, g.updateBTree, g.verbose];
		ENDCASE => ERROR;
		TemporaryStop[];
		time ← Time.Current[];
		CWF.WF1["Phase ended at %lt.\n"L, @time];
		};
	
	STPSubr.SetNumberOfConnectTries[64000];	-- infinite number
	g.in.ResetUserAbort[];
	g.busy ← TRUE;
	[] ← CWF.SetWriteProcedure[ToolTTYProc];
	dfFileRef ← RopeInline.InlineFlatten[ViewerTools.GetContents[g.fileNameViewer]];
	IF dfFileRef = NIL THEN {
		CWF.WF0["Error - must specify a df file name.\n"L];
		GOTO leave;
		};
	IF NOT Subr.Any[LOOPHOLE[dfFileRef], '.] THEN
		CWF.SWF1[topdffilename, "%s.DF"L, LOOPHOLE[dfFileRef]]
	ELSE Subr.strcpy[topdffilename, LOOPHOLE[dfFileRef]];
	IF NOT Subr.EndsIn[topdffilename, ".df"L] THEN {
		CWF.WF1["Error - %s must be a DF file name.\n"L, topdffilename];
		GOTO leave;
		};
	IF phase = OneThree THEN {
		DoPhase[One];
		CWF.WFCR[];
		PrintSeparator[];
		DoPhase[Three];
		CWF.WFCR[];
		}
	ELSE DoPhase[phase];
	EXITS
	leave => NULL;
	};
	TemporaryStop[];
	starttime ← Time.Current[] - starttime;
	CWF.WF1["\nTotal elapsed time for ReleaseTool %lr."L,@starttime];
	IF Subr.errorflg THEN CWF.WF0["\tErrors logged."L];
	CWF.WFCR[];
	PrintSeparator[];
	g.busy ← FALSE;
	};
	
-- Phase 1
VerifyConsistency: PROC[topdffilename: LONG STRING] = {
	nconflicts, nmissing, nentries, npages: CARDINAL;
			
	g.out.PutF["Remember: max entries in flat DF files = %d\n", IO.card[MAXALLFILES]];
	g.out.Put[IO.rope["(Do not Destroy this window.  Destroy the upper one only.)\n"]];
	-- make btree only if it will be used
	IF g.useOldPhase1FileCache OR g.updateBTree THEN 
		MakeBTree[];
	g.nProbablePages ← 0;
	g.nProbableFiles ← 0;
	IF g.dfseqall ~= NIL THEN {
		hz: UNCOUNTED ZONE ← g.dfseqall.dfzone;
		-- DANGEROUS: make sure there are no outstanding pointers
		FreeDFSeqAll[];
		hz ← Subr.FreeHugeZone[hz];	-- we know it is a huge zone
		};
	g.dfseqall ← ReadDF[topdffilename];
	IF g.dfseqall.size = 0 THEN {
		FreeDFSeqAll[];
		RETURN;
		};
	npages ← Subr.PagesUsedInHugeZone[g.dfseqall.dfzone];
	CWF.WF1["%u pages used in HugeZone.\n"L, @npages];
	Flush[];
	CWF.WF0["Sorting the flattened DF files.\n"L];
	-- this sorts with shortname highest precedence
	DFSubr.SortByFileName[g.dfseqall, 0, g.dfseqall.size-1, TRUE];
	-- sorting must precede CheckExceptions, CheckForExistence, and VerifySpanningTree
	Flush[];
	VerifySpanningTree[g.dfseqall, topdffilename];
	-- sorting must precede CheckExceptions and CheckForExistence
	Flush[];
	CheckExceptions[g.dfseqall];
	Flush[];
	CheckReleaseAs[g.dfseqall];
	Flush[];
	[nmissing, nentries] ← CheckForExistence[g.dfseqall];
	CWF.WF0["Sorting the flattened DF files by shortname.\n"L];
	-- this step is necessary since phase 3 depends on this being sorted by shortname
	DFSubr.SortByFileName[g.dfseqall, 0, g.dfseqall.size-1, TRUE];
	Flush[];
	CheckBcdDates[g.dfseqall];
	Flush[];
	nconflicts ← CheckConflicts[g.dfseqall];
	SetDFFileCreateTimes[g.dfseqall, topdffilename];
	CWF.WF2["Summary for Phase 1: %u conflicting versions, %u files/versions not found.\n"L,
		@nconflicts, @nmissing];
	CWF.WF4["Total %u entries, %u unique entries\n\t%lu probable pages (2048 byte pgs) in release of %u probable files.\n"L,
		@g.dfseqall.size, @nentries, @g.nProbablePages, @g.nProbableFiles];
	npages ← Subr.PagesUsedInHugeZone[g.dfseqall.dfzone];
	CWF.WF1["%u pages used in HugeZone.\n"L, @npages];
	Flush[];
	};
	
ReadDF: PROC[topdffilename: LONG STRING] 
	RETURNS[dfseq: DFSubr.DFSeq] = {
	time: LONG CARDINAL ← Time.Current[];
	dfseq ← DFSubr.AllocateDFSeq[maxEntries: MAXALLFILES, zoneType: huge];
	IF g.in.UserAbort[] THEN SIGNAL Subr.AbortMyself;
	-- the nested entries are definitely NOT to be forced to be readonly!
	DFSubr.FlattenDF[dfseq: dfseq, dffilename: topdffilename, h: g.tty,
		checkForOverwrite: g.checkOverwrite, setRecorder: TRUE, printStatus: g.verbose↑,
		allowForceReadOnly: FALSE, skipCameFrom: TRUE, tryDollars: FALSE
		! DFSubr.TooManyEntries => {
			CWF.WF0["Error - too many entries for the one data structure that holds every DF entry.\n"L];
			CWF.WF0["Increase its size and try again.\n"L];
			CONTINUE;
			}
		];
	time ← Time.Current[] - time;
	Flush[];
	CWF.WF1["Time to read in all the DF files: %lr.\n"L, @time];
	};
	
-- must be sorted by shortname
VerifySpanningTree: PROC[dfseq: DFSubr.DFSeq, topdffilename: LONG STRING] = {
	df, dfj: DFSubr.DF;
	natsign, nreleaseas: CARDINAL;
	out: IO.Handle ← FileIO.Open["SpanningTree.List$", overwrite];
	out.PutF["Verify spanning tree rule: every DF file is mentioned once and only once with a ReleaseAs clause. (skipping CameFroms)\n"];
	FOR i: CARDINAL IN [0 .. dfseq.size) DO
		dfseq[i].eval ← FALSE;
		ENDLOOP;
	FOR i: CARDINAL IN [0 .. dfseq.size) DO
		j: CARDINAL;
		df ← @dfseq[i];
		IF df.eval OR NOT Subr.EndsIn[df.shortname, ".df"L] 
		OR df.parentCameFrom OR df.cameFrom THEN LOOP;
		natsign ← nreleaseas ← 0;
		-- this is a df we have not seen before (not nested in a cameFrom DF)
		Flush[];
		out.PutF["\nFile %s is referenced by:\n", IO.string[df.shortname]];
		j ← i;
		DO
			IF j >= dfseq.size THEN EXIT;
			dfj ← @dfseq[j];
			IF dfj.shortname.length = df.shortname.length 
			AND LongString.EquivalentString[dfj.shortname, df.shortname] THEN {
				IF dfj.atsign AND NOT dfj.readonly THEN 
					natsign ← natsign + 1;
				IF dfj.releaseDirectory ~= NIL 
				AND NOT dfj.cameFrom
				AND NOT dfj.readonly 
				AND NOT LongString.EquivalentString[dfj.shortname, dfj.recorder] THEN
					nreleaseas ← nreleaseas + 1;
				dfj.eval ← TRUE;
				out.PutF[" %s", IO.string[dfj.recorder]];
				IF dfj.readonly THEN out.PutF[" (readonly)"];
				out.PutF[","];
				}
			ELSE EXIT;
			j ← j + 1;
			ENDLOOP;
		out.PutF["\n"];
		IF natsign = 0 AND NOT LongString.EquivalentString[df.shortname, topdffilename] THEN 
			out.PutF["\tWarning - no Includes for %s anywhere.\n", IO.string[df.shortname]];
		IF nreleaseas ~= 1 THEN
			out.PutF["\tError - there were %d ReleaseAs statements for %s (not counting self references).  There should be exactly one.\n",
				IO.card[nreleaseas], IO.string[df.shortname]];
		ENDLOOP;
	out.Close[];
	};
	
-- invariant: after completion, df.version = 0 means
-- highest version or no such file, otherwise df.version is the correct one
--
-- dfseq must be sorted
CheckForExistence: PROC[dfseq: DFSubr.DFSeq] RETURNS[nmissing, nentries: CARDINAL] = {
	df: DFSubr.DF;
	inCache: BOOL;
	nIfsPages, remoteVersion, i: CARDINAL ← 0;
	remoteCreateTime: LONG CARDINAL ← 0;
	fres: FQ.Result;
	starttime: LONG CARDINAL ← Time.Current[];
	
	-- check for existence
	nmissing ← nentries ← 0;
	-- now verify each and every file is out there
	CWF.WF0["Now make sure all those files are really out there.\n"L];
	IF NOT g.oldPhase1FileCacheExists THEN {
		CWF.WF0["(Running priorityForeground.)\n"L];
		Process.SetPriority[Process.priorityForeground];
		};
	i ← 0;
	WHILE i < dfseq.size DO
		df ← @dfseq[i];
		-- skip if same create or adjacent entries are for ~= or >
		IF i > 0 
		AND ((df.createtime > 0 AND dfseq[i-1].createtime = df.createtime)
		     OR (df.criterion ~= none AND dfseq[i-1].criterion ~= none))
		AND LongString.EquivalentString[dfseq[i-1].shortname, df.shortname]
		AND LongString.EquivalentString[dfseq[i-1].directory, df.directory] 
		AND LongString.EquivalentString[dfseq[i-1].host, df.host] THEN {
			df.version ← dfseq[i-1].version;
			i ← i + 1;	-- skip this one, go to next;
			LOOP;
			};
		nentries ← nentries + 1;
		IF g.in.UserAbort[] THEN SIGNAL Subr.AbortMyself;
		-- Flush[];
		IF g.verbose↑ THEN
			CWF.WF4["Check [%s]<%s>%s in %s ... "L, df.host, df.directory, 
				df.shortname, df.recorder]
		ELSE
			CWF.WFC['&];
		nIfsPages ← 0;
		remoteCreateTime ← 0;
		remoteVersion ← 0;
		inCache ← FALSE;
		fres ← notFound;
		IF g.oldPhase1FileCacheExists
		AND g.useOldPhase1FileCache
		AND df.createtime ~= 0 THEN {
			[inCache, nIfsPages] ← LookupInOldFileCache[df];
			fres ← foundCorrectVersion;	-- highversion and highdate have no value
			};
		IF NOT inCache THEN 
			[fres, nIfsPages, remoteVersion, remoteCreateTime] ← CheckFile[df];
		SELECT fres FROM
		foundCorrectVersion => {
			IF g.verbose↑ THEN
				CWF.WF1["ok. %s\n"L, IF inCache THEN "(in cache)"L ELSE ""L];
			IF df.releaseDirectory ~= NIL AND NOT df.cameFrom THEN {
				g.nProbableFiles ← g.nProbableFiles + 1;
				g.nProbablePages ← g.nProbablePages + nIfsPages;
				};
			IF NOT inCache AND g.updateBTree THEN
				InsertIntoCache[df, nIfsPages];
			};
		foundWrongVersion => {
			-- fixup for phase 3: might as well get the highest ones
			-- note this may screw up consistency checking
			df.createtime ← remoteCreateTime;
			df.version ← remoteVersion;
			};
		notFound => nmissing ← nmissing + 1;
		ENDCASE => ERROR;
		i ← i + 1;
		ENDLOOP;
	IF NOT g.oldPhase1FileCacheExists THEN {
		Process.SetPriority[g.priority];
		CWF.WF0["\n(Priority reset.)\n"L];
		};
	starttime ← Time.Current[] - starttime;
	CWF.WF1["Total time for CheckFiles %lr.\n"L,@starttime];
	};
	
	BVal: TYPE = RECORD[
		version: CARDINAL ← 0,
		nIfsPages: CARDINAL ← 0
		];
	
-- specialPrefix is removed
LookupInOldFileCache: PROC[df: DFSubr.DF] 
 RETURNS[inCache: BOOL, nIfsPages: CARDINAL] = {
	bval: BVal ← [];
	sfn: STRING ← [100];
	len: CARDINAL;
	specialPrefix: STRING ← "[Indigo]<PreCedar>"L;
	file: STRING ← [100];
	inCache ← FALSE;
	nIfsPages ← 0;
	IF df.createtime = 0 
	OR NOT g.oldPhase1FileCacheExists 
	OR NOT g.useOldPhase1FileCache THEN RETURN;
	CWF.SWF3[file, "[%s]<%s>%s"L, df.host, df.directory, df.shortname];
	IF Subr.Prefix[file, specialPrefix] THEN
		Subr.SubStrCopy[file, file, specialPrefix.length];
	CWF.SWF2[sfn, "%lu\000%s"L, @df.createtime, file];
	len ← BTreeDefs.Lookup[g.bTreeHandle, MakeBTreeDesc[sfn],
		DESCRIPTOR[@bval, SIZE[BVal]]];
	IF len = BTreeDefs.KeyNotFound THEN RETURN;
	df.version ← bval.version;
	nIfsPages ← bval.nIfsPages;
	RETURN[TRUE, nIfsPages];
	};
	
-- specialPrefix is removed
InsertIntoCache: PROC[df: DFSubr.DF, nIfsPages: CARDINAL] = {
	bval: BVal ← [version: df.version, nIfsPages: nIfsPages];
	specialPrefix: STRING ← "[Indigo]<PreCedar>"L;
	sfn: STRING ← [100];
	file: STRING ← [100];
	IF df.version = 0 THEN ERROR;
	CWF.SWF3[file, "[%s]<%s>%s"L, df.host, df.directory, df.shortname];
	IF Subr.Prefix[file, specialPrefix] THEN
		Subr.SubStrCopy[file, file, specialPrefix.length];
	CWF.SWF2[sfn, "%lu\000%s"L, @df.createtime, file];
	BTreeDefs.Insert[g.bTreeHandle, MakeBTreeDesc[sfn], DESCRIPTOR[@bval, SIZE[BVal]]];
	};
	
CheckFile: PROC[df: DFSubr.DF] 
	RETURNS[fres: FQ.Result, nIfsPages, remoteVersion: CARDINAL,
		remoteCreateTime: LONG CARDINAL] = {
	remoteByteLength: LONG CARDINAL;
	targetFileName: STRING ← [100];
		
		nIfsPages ← 0;
		[fres: fres, remoteVersion: remoteVersion, remoteCreateTime: remoteCreateTime,
			remoteByteLength: remoteByteLength] 
		     ← FQ.FileQuery[df.host, df.directory, df.shortname, df.version, df.createtime, 
		     			df.criterion = none AND df.createtime = 0, g.tty, targetFileName];
		SELECT fres FROM
		foundCorrectVersion => {
			IF df.createtime > 0 AND df.version > 0 AND df.version ~= remoteVersion THEN {
			    CWF.WF1["\n(Warning %s:  "L, df.shortname];
				CWF.WF4["Create date %lt has version !%u, but %s refers to !%u)\n"L, 
					@remoteCreateTime, @remoteVersion, df.recorder, @df.version];
				};
			df.version ← remoteVersion;
			nIfsPages ← (remoteByteLength/bytesPerIFSPage) + 2;
			};
		foundWrongVersion => {
			g.out.PutF["\nError - %s of %t not found.\n", IO.string[targetFileName], 
				IO.card[df.createtime]];
			};
		notFound => {
			g.out.PutF["\nError - %s not found.\n", IO.string[targetFileName]];
			};
		ENDCASE => ERROR;
		IF df.createtime = 0 AND remoteCreateTime > 0 THEN {
			-- handles cases where there is a naked entry
			df.createtime ← remoteCreateTime;
			df.version ← remoteVersion;
			};
		};
		
CheckConflicts: PROC[dfseq: DFSubr.DFSeq] RETURNS[nconflicts: CARDINAL] = {
	df, dflast: DFSubr.DF;
	nconflicts ← 0;
	CWF.WF0["Check consistency of DF files.\n"L];
	FOR i: CARDINAL IN [1 .. dfseq.size) DO
		dflast ← @dfseq[i-1];
		df ← @dfseq[i];
		IF dflast.createtime ~= df.createtime
		AND dflast.createtime > 0
		AND df.createtime > 0 
		AND LongString.EquivalentString[dflast.shortname, df.shortname]
		THEN {
			Flush[];
			CWF.WF2["%s: Conflict- %s "L,
				dflast.shortname, dflast.recorder];
			CWF.WF3["has an entry for [%s]<%s>%s"L,
				dflast.host, dflast.directory, dflast.shortname];
			CWF.WF4[" dated %lt,\n\tbut %s has an entry for [%s]<%s>"L,
				@dflast.createtime, df.recorder, df.host, df.directory];
			CWF.WF2["%s dated %lt.\n"L, df.shortname, @df.createtime];
			nconflicts ← nconflicts + 1;
			};
		ENDLOOP;
	IF nconflicts > 0 THEN 
		CWF.WF1["Warning - there were %u conflicts.\n"L, @nconflicts]
	ELSE CWF.WF0["Success - there were no conflicts among the files.\n"L];
	};
	
-- for phase 3: make up create times for the DF files we will write out
SetDFFileCreateTimes: PROC[dfseq: DFSubr.DFSeq, topdffilename: LONG STRING] = {
	time: LONG CARDINAL ← Time.Current[];
	df: DFSubr.DF;
	FOR i: CARDINAL IN [0..dfseq.size) DO
		df ← @dfseq[i];
		IF df.atsign
		AND df.releaseDirectory ~= NIL 
		AND NOT df.cameFrom
		AND NOT df.readonly 
		AND Subr.EndsIn[df.shortname, ".df"L] 
		AND (NOT LongString.EquivalentString[df.shortname, df.recorder] 
			OR LongString.EquivalentString[df.shortname, topdffilename]) THEN {
			df.createtime ← time;
			df.version ← 0;
			time ← time + 1;
			FOR j: CARDINAL IN [0 .. dfseq.size) DO
				IF i ~= j 
				AND dfseq[j].shortname.length = df.shortname.length
				AND LongString.EquivalentString[dfseq[j].shortname, df.shortname] THEN {
					IF LongString.EquivalentString[dfseq[j].host, df.host]
					AND LongString.EquivalentString[dfseq[j].directory, df.directory] THEN
						{
						dfseq[j].createtime ← df.createtime;
						dfseq[j].version ← 0;
						}
					ELSE CWF.WF4["Warning: %s refers to a version of [%s]<%s>%s that is not being released.\n"L,
						dfseq[j].recorder, dfseq[j].host, dfseq[j].directory, 
						dfseq[j].shortname];
					};
				ENDLOOP;
			};
		ENDLOOP;
	};
	
	
NEXCEPTIONS: CARDINAL = 100;
	
EXSeq: TYPE = LONG POINTER TO EXSeqRecord;
EXSeqRecord: TYPE = RECORD[
	size: CARDINAL ← 0,
	body: SEQUENCE maxsize: CARDINAL OF RECORD[
		host: LONG STRING ← NIL,	-- "Ivy"
		directory: LONG STRING ← NIL,   -- "APilot>Pilot>Private"
		shortname: LONG STRING ← NIL	-- "FileName.Mesa"
		]
	];
	
ParseExceptionList: PROC[filename: STRING] RETURNS[exseq: EXSeq] = {
	sh: Stream.Handle;
	line: STRING ← [100];
	host: STRING ← [100];
	directory: STRING ← [100];
	shortname: STRING ← [100];
	longzone: UNCOUNTED ZONE ← Subr.LongZone[];
	exseq ← longzone.NEW[EXSeqRecord[NEXCEPTIONS]];
	sh ← Subr.NewStream[filename, Subr.Read
		! Subr.FileError => {
			CWF.WF1["Warning - Cannot open %s.\n"L, filename];
			GOTO out
			}];
	WHILE Subr.GetLine[sh, line] DO
		IF line.length = 0 
		OR Subr.Prefix[line, "//"L]
		OR Subr.Prefix[line, "--"L] THEN LOOP;
		[] ← DFSubr.StripLongName[line, host, directory, shortname, FALSE];
		IF exseq.size > exseq.maxsize THEN {
			CWF.WF1["Error - too many exception list entries in %s.\n"L, filename];
			EXIT;
			};
		exseq[exseq.size] ← [host: Subr.CopyString[host, longzone],
			directory: Subr.CopyString[directory, longzone],
			shortname: IF shortname.length > 0 THEN 
				Subr.CopyString[shortname, longzone] ELSE NIL];
		exseq.size ← exseq.size + 1;
		ENDLOOP;
	Stream.Delete[sh];
	EXITS
	out => NULL;
	};
	
-- dfseq must be sorted
CheckExceptions: PROC[dfseq: DFSubr.DFSeq] = {
	exseq: EXSeq ← NIL;
	i, j: CARDINAL;
	dfcur, dfinner: DFSubr.DF;
	nrelease, ncamefrom: CARDINAL;
	skip: BOOL;
	i ← 0;
	WHILE i < dfseq.size DO
		IF g.in.UserAbort[] THEN SIGNAL Subr.AbortMyself;
		dfcur ← @dfseq[i];
		nrelease ← ncamefrom ← 0;
		j ← i;
		skip ← FALSE;
		WHILE j < dfseq.size 
		AND LongString.EquivalentString[dfcur.shortname, dfseq[j].shortname]
		AND LongString.EquivalentString[dfcur.directory, dfseq[j].directory]
		AND LongString.EquivalentString[dfcur.host, dfseq[j].host]
		DO
			dfinner ← @dfseq[j];
			IF dfinner.readonly THEN {
				IF dfinner.releaseDirectory ~= NIL AND NOT dfinner.cameFrom THEN 
					CWF.WF3["Warning- %s in %s is ReadOnly but also has a %s statement.\n"L,
						dfinner.shortname, dfinner.recorder, 
						IF dfinner.cameFrom THEN "CameFrom"L ELSE "ReleaseAs"L];
				};
			IF dfinner.releaseDirectory ~= NIL AND NOT dfinner.cameFrom THEN {
				IF LongString.EquivalentString[dfseq[j].directory, dfseq[j].releaseDirectory]
				AND LongString.EquivalentString[dfseq[j].host, dfseq[j].releaseHost] THEN 
					CWF.WF2["Warning - %s in %s has directory and release directory that are identical.\n"L,
						dfseq[j].shortname, dfseq[j].recorder];
				nrelease ← nrelease + 1;
				};
			IF NOT dfinner.readonly AND dfinner.releaseDirectory = NIL THEN {
				Flush[];
				CWF.WF2["Error - %s in %s is not ReadOnly and has no release directory.\n"L,
					dfinner.shortname, dfinner.recorder];
				skip ← TRUE;
				};
			IF dfinner.cameFrom THEN
				ncamefrom ← ncamefrom + 1;
			j ← j + 1;
			ENDLOOP;
		-- if there are no releaseAs statements and no CameFrom statements,
		-- then this if ReadOnly everywhere
		-- (Remember that the sort is not stable, so we can't simply look at dfcur)
		IF nrelease = 0 AND NOT skip AND ncamefrom = 0 THEN {
			ex: BOOL ← FALSE;
			IF exseq = NIL THEN {
				CWF.WF0["Reading exception list From 'Release.ExceptionList'.\n"L];
				exseq ← ParseExceptionList["Release.ExceptionList"L];
				};
			FOR i: CARDINAL IN [0 .. exseq.size) DO
				IF Subr.Prefix[dfcur.directory, exseq[i].directory]
				AND LongString.EquivalentString[dfcur.host, exseq[i].host]
				AND (dfcur.directory.length = exseq[i].directory.length
				    OR (exseq[i].directory.length < dfcur.directory.length 
				       AND dfcur.directory[exseq[i].directory.length] = '>))
				AND (
				    exseq[i].shortname = NIL 
				    OR LongString.EquivalentString[dfcur.shortname, exseq[i].shortname])
				THEN {
					ex ← TRUE;
					EXIT;
					};
				ENDLOOP;
			IF NOT ex THEN {
				Flush[];
				CWF.WF3["Error - there are no ReleaseAs statements for [%s]<%s>%s.\n\t(referenced in"L, 
					dfcur.host, dfcur.directory, dfcur.shortname];
				FOR k: CARDINAL IN [i .. j) DO
					CWF.WF1[" %s"L, dfseq[k].recorder];
					ENDLOOP;
				CWF.WF0[").\n"L];
				};
			};
		i ← j;
		ENDLOOP;
	IF exseq ~= NIL THEN FreeExceptionList[@exseq];
	};
	
-- also complains if there is a CameFrom without an explicit date
CheckReleaseAs: PROC[dfseq: DFSubr.DFSeq] = {
	exseq: EXSeq ← ParseExceptionList["Release.Destinations"L];
	df: DFSubr.DF;
	IF exseq = NIL OR exseq.size = 0 THEN {
		CWF.WF0["No destination list found on 'Release.Destinations'.\n"L];
		RETURN;
		};
	FOR i: CARDINAL IN [0 .. dfseq.size) DO
		df ← @dfseq[i];
		IF df.cameFrom THEN {
			IF df.createtime = 0 THEN 
				CWF.WF3["Warning - %s contains an entry for %s in a CameFrom Directory, but it has no create time for %s.\n"L,
					df.recorder, df.shortname, df.shortname];
			-- look for CameFrom's that are on directories that are not release directories
			-- this looks suspicious
			FOR j: CARDINAL IN [0 .. exseq.size) DO
				IF Subr.Prefix[df.directory, exseq[j].directory]
				AND LongString.EquivalentString[df.host, exseq[j].host]
				AND (df.directory.length = exseq[j].directory.length
				    OR (exseq[j].directory.length < df.directory.length 
				       AND df.directory[exseq[j].directory.length] = '>))
					THEN EXIT;
				REPEAT
				FINISHED => CWF.WF4["CameFrom Warning - %s in %s is on [%s]<%s>, but is is a CameFrom and that is not the official release directory.\n"L,
					df.shortname, df.recorder, df.releaseHost, df.releaseDirectory];
				ENDLOOP;
			};
		IF df.releaseDirectory ~= NIL
		AND NOT df.cameFrom
		THEN {
			FOR j: CARDINAL IN [0 .. exseq.size) DO
				IF Subr.Prefix[df.releaseDirectory, exseq[j].directory]
				AND LongString.EquivalentString[df.releaseHost, exseq[j].host]
				AND (df.releaseDirectory.length = exseq[j].directory.length
				    OR (exseq[j].directory.length < df.releaseDirectory.length 
				       AND df.releaseDirectory[exseq[j].directory.length] = '>))
					THEN EXIT;
				REPEAT
				FINISHED => CWF.WF4["Warning - %s in %s is released onto [%s]<%s>, not the official release directory.\n"L,
					df.shortname, df.recorder, df.releaseHost, df.releaseDirectory];
				ENDLOOP;
			-- special case for >Top> directories
			IF NOT Subr.EndsIn[df.shortname, ".df"L]
			AND NOT Subr.EndsIn[df.shortname, ".boot"L]
			AND Subr.EndsIn[df.releaseDirectory, ">Top"L]	THEN 
				CWF.WF2["Warning - %s is released onto %s but is neither a DF file nor a boot file.\n"L,
					df.shortname, df.releaseDirectory];
			};
		ENDLOOP;
	FreeExceptionList[@exseq];
	};
	
-- called after the CheckFiles to look at bcd dates
CheckBcdDates: PROC[dfseq: DFSubr.DFSeq] = {
		f: BOOL ← TRUE;
		df: DFSubr.DF;
		FOR i: CARDINAL IN [0 .. dfseq.size) DO
			df ← @dfseq[i];
			IF Subr.EndsIn[df.shortname, ".bcd"L] 
			AND df.createtime < g.oldestBcdDate THEN {
				IF f THEN {
					g.out.PutF["The following files were created before the earliest Bcd date (%t):\n",
						IO.card[g.oldestBcdDate]];
					f ← FALSE;
					};
				g.out.PutF["\t%s of %t in %s\n", IO.string[df.shortname], IO.card[df.createtime],
					IO.string[df.recorder]];
				};
			ENDLOOP;
		};
	
FreeExceptionList: PROC[pexseq: POINTER TO EXSeq] = {
	FOR i: CARDINAL IN [0 .. pexseq.size) DO
		Subr.FreeString[pexseq[i].host];
		Subr.FreeString[pexseq[i].directory];
		Subr.FreeString[pexseq[i].shortname];
		ENDLOOP;
	Subr.LongZone[].FREE[pexseq];
	};
	
	--
PrintSeparator: PROC = {
	CWF.WF0["===============================\n"L];
	};
	
-- size in pages for btree (used to be 100)
InitialNumberOfPhase1BTreePages: CARDINAL = 1080;
	
MakeBTree: PROC = {
	fileHandle: BTreeSupportDefs.FileHandle;
	g.oldPhase1FileCacheExists ← TRUE;
	g.bTreeCap ← Directory.Lookup[fileName: "ReleaseTool.Phase1BTreeFile$"L, permissions: Directory.ignore
		! Directory.Error => {
			g.oldPhase1FileCacheExists ← FALSE;
			CONTINUE;
			}];
	IF NOT g.oldPhase1FileCacheExists THEN 
		g.bTreeCap ← Subr.NewFile["ReleaseTool.Phase1BTreeFile$"L, Subr.ReadWrite,
			InitialNumberOfPhase1BTreePages];
	fileHandle ← BTreeSupportExtraDefs.OpenFile[g.bTreeCap];
	g.bTreeHandle ← BTreeDefs.CreateAndInitializeBTree[fileH: fileHandle,
	    	initializeFile: NOT g.oldPhase1FileCacheExists, isFirstGreaterOrEqual: IsFirstGEQ,
	    	areTheyEqual: AreTheyEQ];
	-- IF NOT g.oldPhase1FileCacheExists THEN
		-- BTreeSupportDefs.SetLength[fileHandle, InitialNumberOfPhase1BTreePages];
	};
	
CleanupBTree: PROC = {
	IF g.bTreeCap ~= File.nullCapability THEN {
		BTreeSupportExtraDefs.CloseFile[BTreeDefs.ReleaseBTree[g.bTreeHandle]];
		g.bTreeCap ← File.nullCapability;
		g.oldPhase1FileCacheExists ← FALSE;
		};
	};
	
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;
	
entryHeight: CARDINAL = 15; 
entryVSpace: CARDINAL = 10; 
entryHSpace: CARDINAL = 10; 
	  
-- this is called by START code
BuildOuter: PROC = 
	   {
	   menu: Menus.Menu ← Menus.CreateMenu[];
	   g ← NEW[Global ← []];
	   g.verbose ← NEW[BOOL ← TRUE];
	   -- any Bcd before this is assumed to be Cedar 3.2 or earlier
	   [dt: g.oldestBcdDate] ← DateAndTime.Parse["4-Aug-82 8:00:00 PDT"];
	   g.container ← Containers.Create[info: [name: "ReleaseTool", iconic: FALSE, scrollable: FALSE]];
	   Menus.InsertMenuEntry[menu, Menus.CreateEntry["       Go", Go]]; 
	   Menus.InsertMenuEntry[menu, Menus.CreateEntry["Abort", MyAbort]]; 
	   Menus.InsertMenuEntry[menu, Menus.CreateEntry["Reset", MyReset]]; 
	   ViewerOps.SetMenu[g.container, menu];
	   -- this is the set of user buttons
	   BuildUserInput[];
	   ViewerOps.PaintViewer[g.container, all];
	   -- this is the typescript part
	   SetupTypescriptPart[];
	   [g.in, g.out, g.logFile] ← SetUpLogStreams[g.typeScript];
	   g.tty ← Subr.MakeTTYProcs[g.in, g.out, g.typeScript, MyConfirm];
	   [] ← CWF.SetWriteProcedure[ToolTTYProc];
	   destroyEventRegistration ← ViewerEvents.RegisterEventProc[MyDestroy, destroy];
	   };
	
BuildUserInput: PROC =
	   {
	   heightSoFar: CARDINAL; 
	   l: ViewerClasses.Viewer;
	   
	      CreateButton: PROC[bname, lname: Rope.Text, newLine: BOOL] 
	         RETURNS[button: Buttons.Button, label: Labels.Label] = 
	         {
	         x: CARDINAL;
	         IF newLine THEN {
	            IF l = NIL THEN 
	               heightSoFar ← entryVSpace/2
	            ELSE
	               heightSoFar ← heightSoFar + entryVSpace + l.wh;
		    x ← 0;
		    }
		 ELSE 
		    x ← l.wx + l.ww + entryHSpace;
		 l ← button ← Buttons.Create[info: [name: bname, parent: g.container,
		    border: FALSE, wx: x, wy: heightSoFar], proc: PushButton];
		 IF lname ~= NIL THEN
		    l ← label ← Labels.Create[info: [name: lname, parent: g.container,
	   	       wx: button.wx + button.ww + entryHSpace, wy: heightSoFar, border: TRUE]];
	   	 };
	
	   -- first line
	   [g.phaseButton, g.phaseLabel] ← CreateButton["Phase:", "OneThree", TRUE];
	   IF g.phase = One THEN Labels.Set[g.phaseLabel, "One"];
	   [g.useOldPhase1FileButton, g.useOldPhase1FileLabel] ←
	      CreateButton["UseOldPhase1BTree:", "FALSE", FALSE];
	   IF g.useOldPhase1FileCache THEN Labels.Set[g.useOldPhase1FileLabel, "TRUE"];
	   [g.useOldPhase3FileButton, g.useOldPhase3FileLabel] ←
	      CreateButton["UseOldPhase3BTree:", "FALSE", FALSE];
	   IF g.useOldPhase3FileCache THEN Labels.Set[g.useOldPhase3FileLabel, "TRUE"];
	   [g.updateBTreeButton, g.updateBTreeLabel] ←
	      CreateButton["UpdateBTrees:", "FALSE", FALSE];
	   IF g.updateBTree THEN Labels.Set[g.updateBTreeLabel, "TRUE"];
	 -- second line
	   [g.priorityButton, g.priorityLabel] ← CreateButton["Priority:", "Background", TRUE];
	   IF g.priority = Process.priorityNormal THEN Labels.Set[g.priorityLabel, "Normal"];
	   [g.verboseButton, g.verboseLabel] ← CreateButton["Verbose:", "FALSE", FALSE];
	   IF g.verbose↑ THEN Labels.Set[g.verboseLabel, "TRUE"];
	   [g.fileNameButton,] ← CreateButton["DFFileName:", NIL, FALSE];
	   l ← g.fileNameViewer ← ViewerTools.MakeNewTextViewer[info: [parent: g.container,
	      wx: l.wx+l.ww+entryHSpace, wy: heightSoFar, ww: 150, wh: entryHeight, 
	      data: NIL, scrollable: FALSE, border: FALSE], paint: FALSE];
	   Containers.ChildXBound[g.container, g.fileNameViewer];
	   heightSoFar ← heightSoFar+entryVSpace+l.wh;
	   ViewerOps.SetOpenHeight[g.container, heightSoFar];
	   };
	
SetupTypescriptPart: PROC = 
		{
		-- rule: Rules.Rule;
	    --  now the line above the typescript
		-- rule ← Rules.Create[g.container, 0, heightSoFar, 0, 1];
		-- Containers.ChildXBound[g.container, rule];
		-- heightSoFar ← heightSoFar+entryVSpace;
		-- now the typescript
		-- g.typeScript ← TypeScript.CreateChild[parent: g.container,
		     -- x: 0, y: heightSoFar, w: 0, h: 800, border: FALSE];	800 due to viewers bug
		-- Containers.ChildXBound[g.container, g.typeScript];
		-- Containers.ChildYBound[g.container, g.typeScript];
		-- ViewerOps.SetOpenHeight[g.container, heightSoFar + 200];
		--
		-- create separate typescript due to lack of Split command in TypeScript.CreateChild
		g.typeScript ← TypeScript.Create[info: [name: "Release Tool TypeScript", iconic: FALSE]];
		};
		
PushButton: Buttons.ButtonProc =  CHECKED
	   {
	   SELECT NARROW[parent, ViewerClasses.Viewer] FROM
	   g.phaseButton => {
	      phaseString: ARRAY EPhase OF Rope.Text ← [
			"One", "Two", "Three", "OneThree"];
	      g.phase ← IF g.phase = OneThree THEN One ELSE SUCC[g.phase];
	      Labels.Set[g.phaseLabel, phaseString[g.phase]];
	      };
	   g.priorityButton => {
	      g.priority ← IF g.priority = Process.priorityNormal THEN
	         Process.priorityBackground ELSE Process.priorityNormal;
	      Labels.Set[g.priorityLabel,
	   	IF g.priority = Process.priorityNormal THEN "Normal" ELSE "Background"];
	      };
	   g.fileNameButton => {
	      ViewerTools.SetSelection[g.fileNameViewer, NIL];
	      };
	   g.verboseButton => {
			g.verbose↑ ← NOT g.verbose↑;
	   		Labels.Set[g.verboseLabel, IF g.verbose↑ THEN "TRUE" ELSE "FALSE"];
	      	};
	    g.useOldPhase1FileButton => {
	      g.useOldPhase1FileCache ← NOT g.useOldPhase1FileCache;
	      Labels.Set[g.useOldPhase1FileLabel,
	      	 IF g.useOldPhase1FileCache THEN "TRUE" ELSE "FALSE"];
	      };
	   g.useOldPhase3FileButton => {
	      g.useOldPhase3FileCache ← NOT g.useOldPhase3FileCache;
	      Labels.Set[g.useOldPhase3FileLabel,
	      	IF g.useOldPhase3FileCache THEN "TRUE" ELSE "FALSE"];
	      };
	   g.updateBTreeButton => {
	      g.updateBTree ← NOT g.updateBTree;
	      Labels.Set[g.updateBTreeLabel,
	      	IF g.updateBTree THEN "TRUE" ELSE "FALSE"];
	      };
	   ENDCASE => ERROR;
	   };
	   
SetUpLogStreams: PROC[ts: TypeScript.TS] RETURNS[in, out, file: IO.Handle] =
	   {
	   -- since the file will be flushed frequently, we use the minimum
	   file ← FileIO.Open[fileName: "ReleaseTool.Log", accessOptions: overwrite,
	   			streamBufferParms: FileIO.minimumStreamBufferParms];
	   [in, out] ← ViewerIO.CreateViewerStreams[viewer: ts, name: "Release Tool TypeScript",
	   	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;
	};

ToolTTYProc: PROC[ch: CHAR] =
	   {
	   g.out.PutChar[ch];
	   };
	
Flush: PROC =
		{
		g.out.Flush[];
		};
		
FreeDFSeqAll: PROC =
	   {
	   dfseq: DFSubr.DFSeq ← g.dfseqall;
	   DFSubr.FreeDFSeq[@dfseq];
	   g.dfseqall ← NIL;
	   };
	   
MyAbort: Menus.MenuProc =  CHECKED
	   {
		g.in.SetUserAbort[];
		};
		
-- just terminates connections, etc. 
-- does not free memory
TemporaryStop: PROC = 
	   {
	   CleanupBTree[];
	   STPSubr.StopSTP[];
	   LeafSubr.StopLeaf[];
	   Flush[];
	   };
	   
NullTTYProc: PROC[ch: CHAR] = {};	-- prints nothing
	
-- cannot print anything in this, monitor is locked
MyDestroy: ViewerEvents.EventProc =  TRUSTED
	   {
	   p: PROCESS;
	   IF g = NIL OR event ~= destroy OR viewer ~= g.container THEN RETURN;
	   IF g.busy THEN {
	      MessageWindow.Append[message: "ReleaseTool still busy, try later.", clearFirst: TRUE];
	      MessageWindow.Blink[];
	      RETURN;
	      };
	   [] ← CWF.SetWriteProcedure[NullTTYProc];	-- turn off printing
	   g.busy ← TRUE;
	   CWF.WF0["Destroying ReleaseTool.\n"L];
	   TemporaryStop[];		 -- does flush
	   FreeDFSeqAll[];
	   Subr.SubrStop[];
	   p ← FORK DestroyTypeScript[g.typeScript];	-- separate process for monitor lock
	   Process.Detach[p];
	   g.logFile.Close[];
	   g↑ ← [];
	   g ← NIL;
	   ViewerEvents.UnRegisterEventProc[destroyEventRegistration, destroy];
	   };
	   
DestroyTypeScript: PROC[ts: TypeScript.TS] = {
		TypeScript.Destroy[ts];		-- destroys the typescript window
		};
		
MyReset: Menus.MenuProc =  TRUSTED
	   {
	   ENABLE UNWIND => g.busy ← FALSE;
	   IF g.busy THEN {
	      MessageWindow.Append[message: "ReleaseTool still busy, try later.", clearFirst: TRUE];
	      MessageWindow.Blink[];
	      RETURN;
	      };
	   g.busy ← TRUE;
	   CWF.WF0["Resetting memory... "L];
	   TemporaryStop[];
	   FreeDFSeqAll[];
	   Subr.SubrStop[];
	   [] ← Subr.SubrInit[256];
	   CWF.WF0["ReleaseTool memory reset.\n"L];
	   PrintSeparator[];
	   g.busy ← FALSE;
	   };
	
BuildOuter[];
}.