Copyright 1996-2003 John D. Polstra.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgment:
* This product includes software developed by John D. Polstra.
* 4. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
<* PRAGMA LL *>
MODULE SupGUI;
IMPORT
AutoRepeat, CVProto, Detailer, ErrMsg, EventSync, FileUpdater,
FileWr, Fmt, FormsVBT, FSClient, IP, Logger, OSError, Pathname, Rd,
Rsrc, SplitLogger, SupFile, SupGUIBundle, SupMisc, Text, TextPort,
TextPortLogger, TextEditVBT, TextVBT, TextVBTLogger, Thread, Time,
TokScan, TreeList, Trestle, TrestleComm, TypeinVBT, UnixMisc,
Updater, VBT, Version, Wr;
CONST
LogName = "cvsup.log";
<* FATAL FormsVBT.Unimplemented *>
***************************************************************************
Main Controller.
***************************************************************************
TYPE
Controller = Thread.Closure OBJECT
fv: FormsVBT.T;
supFile: Pathname.T;
config: FSClient.Configuration;
METHODS
init(fv: FormsVBT.T;
supFile: Pathname.T;
config: FSClient.Configuration): Controller := ControllerInit;
OVERRIDES
apply := ControllerApply;
END;
PROCEDURE ControllerInit(self: Controller;
fv: FormsVBT.T;
supFile: Pathname.T;
config: FSClient.Configuration): Controller =
BEGIN
self.fv := fv;
self.supFile := supFile;
self.config := config;
RETURN self;
END ControllerInit;
PROCEDURE ControllerApply(self: Controller): REFANY =
<* FATAL EventSync.Error, FormsVBT.Error, Thread.Alerted *>
VAR
totalStats: TotalStats;
clock: Clock;
listerVBT: TextVBT.T;
detailerVBT: TextVBT.T;
messagesVBT: TextPort.T;
filterVBT: TypeinVBT.T;
statusLogger: Logger.T;
listerTrace: Logger.T;
detailerTrace: Logger.T;
messagesLogger: Logger.T;
client: FSClient.T;
clientThread: Thread.T;
aborterThread: Thread.T;
clientStatus: SupMisc.ThreadStatus;
filterText := "";
ts: TokScan.T;
BEGIN
listerVBT := FormsVBT.GetVBT(self.fv, "lister");
detailerVBT := FormsVBT.GetVBT(self.fv, "detailer");
messagesVBT :=
NARROW(FormsVBT.GetVBT(self.fv, "messages"), TextEditVBT.T).tp;
filterVBT :=
NARROW(FormsVBT.GetVBT(self.fv, "filter"), TypeinVBT.T);
(* Translate the command-line accept filter into a text string for
the display. *)
WITH accepts = self.config.override.accepts DO
FOR i := 0 TO accepts.size()-1 DO
IF i = 0 THEN
filterText := accepts.get(i);
ELSE
filterText := filterText & " " & accepts.get(i);
END;
END;
END;
LOCK VBT.mu DO
messagesVBT.setModel(TextPort.Model.Xterm);
filterVBT.setModel(TextPort.Model.Xterm);
FormsVBT.PutText(self.fv, "filter", filterText);
END;
listerTrace := NEW(TextVBTLogger.T).init(
listerVBT, level := Logger.Priority.Info);
detailerTrace := NEW(TextVBTLogger.T).init(
detailerVBT, level := Logger.Priority.Info);
statusLogger := NEW(TextVBTLogger.T).init(
FormsVBT.GetVBT(self.fv, "status"), level := Logger.Priority.Info);
messagesLogger := NEW(TextPortLogger.T).init(
messagesVBT, level := Logger.Priority.Info);
self.config.trace := NEW(SplitLogger.T).init(messagesLogger, statusLogger,
SET OF Logger.Priority{Logger.Priority.Emerg..Logger.Priority.Warning},
SET OF Logger.Priority{Logger.Priority.Emerg..Logger.Priority.Debug});
self.config.listerTrace :=
NEW(SplitLogger.T).init(messagesLogger, listerTrace);
self.config.detailerTrace :=
NEW(SplitLogger.T).init(messagesLogger, detailerTrace);
self.config.updaterTrace := messagesLogger;
(* Wait for the initial start command. *)
IF EventSync.Wait(self.fv, "quit=0 start=1 filter=1") = 1 THEN (* start *)
LOOP
(* Clear the trace and statistics displays. *)
LOCK VBT.mu DO
TextVBT.Put(listerVBT, "");
TextVBT.Put(detailerVBT, "");
TextPort.SetText(messagesVBT, "");
ResetStatsDisplay(self);
VBT.Release(filterVBT, VBT.KBFocus);
END;
totalStats := NEW(TotalStats).init(self.fv);
self.config.listerStats := NEW(ListerStats).init(self.fv, totalStats);
self.config.detailerStats :=
NEW(DetailerStats).init(self.fv, totalStats);
self.config.updaterStats := NEW(UpdaterStats).init(self.fv, totalStats);
totalStats.ls := self.config.listerStats;
totalStats.ds := self.config.detailerStats;
totalStats.us := self.config.updaterStats;
(* Build a filter list from the text in the TypeIn. *)
WITH accepts = self.config.override.accepts DO
FOR i := 0 TO accepts.size()-1 DO EVAL accepts.remhi() END;
ts := TokScan.New(FormsVBT.GetText(self.fv, "filter"));
<* FATAL TokScan.Error *>
BEGIN
WHILE ts.next(filterText) DO
accepts.addhi(filterText);
END;
END;
END;
clock := NEW(Clock).init(self.fv, "clock", self.config.updaterStats);
TRY
Msg(self, "Parsing supfile \"" & self.supFile & "\"");
self.config.collections := SupFile.Parse(self.supFile,
override := self.config.override,
mask := self.config.overrideMask);
client := NEW(FSClient.T).init(self.config);
(* Start the client. *)
clientThread := Thread.Fork(client);
AutoRepeat.Start(clock);
aborterThread :=
Thread.Fork(NEW(Aborter).init(self.fv, clientThread));
(* Wait for the client to finish. *)
clientStatus := Thread.Join(clientThread);
AutoRepeat.Stop(clock);
Thread.Alert(aborterThread);
EVAL Thread.Join(aborterThread);
EXCEPT SupFile.Error(msg) =>
Msg(self, msg);
END;
CASE EventSync.Wait(self.fv, "quit=0 start=1 filter=1 save=2") OF
| 0 => (* quit *)
EXIT;
| 1 => (* start *)
(* Continue *)
| 2 => (* save *)
TRY
SaveTrace(messagesVBT, LogName);
Msg(self, "Saved message window to \"" & LogName & "\"");
EXCEPT Error(msg) =>
Msg(self, msg);
END;
IF EventSync.Wait(self.fv, "quit=0 start=1 filter=1") = 0 THEN
(* quit *)
EXIT;
END;
ELSE <* ASSERT FALSE *> END;
END;
END;
LOCK VBT.mu DO Trestle.Delete(self.fv) END;
RETURN NIL;
END ControllerApply;
PROCEDURE Msg(self: Controller;
msg: TEXT;
append := FALSE) =
<* FATAL FormsVBT.Error, FormsVBT.Unimplemented *>
BEGIN
LOCK VBT.mu DO
FormsVBT.PutText(self.fv, "status", msg, append);
END;
END Msg;
PROCEDURE ResetStatsDisplay(self: Controller) =
<* LL = VBT.mu *>
<* FATAL FormsVBT.Error, FormsVBT.Unimplemented *>
CONST
UpdateRows = ARRAY OF TEXT{
"edit", "co", "rsync", "app", "touch",
"add", "del", "repl", "fixup", "other", "tot"
};
UpdateCols = ARRAY OF TEXT{
"Count", "FileKB", "WireKB", "Compr"
};
CommRows = ARRAY OF TEXT{
"lister", "detailer", "updater", "tot"
};
CommCols = ARRAY OF TEXT{
"CommIn", "CommOut", "WireIn", "WireOut", "WireTot"
};
VAR
name, value: TEXT;
BEGIN
FOR i := FIRST(UpdateRows) TO LAST(UpdateRows) DO
FOR j := FIRST(UpdateCols) TO LAST(UpdateCols) DO
name := UpdateRows[i] & UpdateCols[j];
IF Text.Equal(UpdateCols[j], "Compr") THEN
value := "0.0";
ELSE
value := "0";
END;
FormsVBT.PutText(self.fv, name, value);
END;
END;
FOR i := FIRST(CommRows) TO LAST(CommRows) DO
FOR j := FIRST(CommCols) TO LAST(CommCols) DO
name := CommRows[i] & CommCols[j];
IF Text.Equal(name, "listerCommIn")
OR Text.Equal(name, "listerWireIn")
OR Text.Equal(name, "updaterCommOut")
OR Text.Equal(name, "updaterWireOut") THEN
value := "-";
ELSE
value := "0";
END;
FormsVBT.PutText(self.fv, name, value);
END;
END;
FormsVBT.PutText(self.fv, "clock", "00:00:00");
FormsVBT.PutText(self.fv, "xferRate", " 0.0 KB/sec");
END ResetStatsDisplay;
PROCEDURE SaveTrace(tp: TextPort.T; fileName: TEXT)
RAISES {Error, Thread.Alerted} =
CONST
ChunkSize = 8 * 1024;
VAR
wr: Wr.T;
pos, limit, len: CARDINAL;
BEGIN
TRY
wr := FileWr.Open(fileName);
EXCEPT OSError.E(list) =>
RAISE Error("Cannot create \"" & fileName & "\": " &
ErrMsg.StrError(list));
END;
TRY
LOCK VBT.mu DO
len := TextPort.Length(tp);
pos := 0;
WHILE pos < len DO
limit := MIN(pos+ChunkSize, len);
TRY
Wr.PutText(wr, TextPort.GetText(tp, pos, limit));
EXCEPT Wr.Failure(list) =>
RAISE Error("Write failure on \"" & fileName & "\": " &
ErrMsg.StrError(list));
END;
pos := limit;
END;
END;
FINALLY
TRY
Wr.Close(wr);
EXCEPT Wr.Failure(list) =>
RAISE Error("Cannot close \"" & fileName & "\": " &
ErrMsg.StrError(list));
END;
END;
END SaveTrace;
***************************************************************************
Aborter.
***************************************************************************
TYPE
Aborter = Thread.Closure OBJECT
fv: FormsVBT.T;
clientThread: Thread.T;
METHODS
init(fv: FormsVBT.T;
clientThread: Thread.T): Aborter := AborterInit;
OVERRIDES
apply := AborterApply;
END;
PROCEDURE AborterInit(self: Aborter;
fv: FormsVBT.T;
clientThread: Thread.T): Aborter =
BEGIN
self.fv := fv;
self.clientThread := clientThread;
RETURN self;
END AborterInit;
PROCEDURE AborterApply(self: Aborter): REFANY =
<* FATAL EventSync.Error, FormsVBT.Error *>
BEGIN
TRY
EVAL EventSync.Wait(self.fv, "abort=0");
Thread.Alert(self.clientThread);
EXCEPT Thread.Alerted =>
(* Just exit. *)
END;
RETURN NIL;
END AborterApply;
***************************************************************************
Lister Statistics Display.
***************************************************************************
TYPE
ListerStats = TreeList.Stats OBJECT
fv: FormsVBT.T;
ts: TotalStats;
METHODS
init(fv: FormsVBT.T; ts: TotalStats): ListerStats := LSInit;
OVERRIDES
update := LSUpdate;
END;
PROCEDURE LSInit(self: ListerStats;
fv: FormsVBT.T;
ts: TotalStats): ListerStats =
BEGIN
EVAL TreeList.Stats.init(self);
self.fv := fv;
self.ts := ts;
RETURN self;
END LSInit;
PROCEDURE LSUpdate(self: ListerStats) =
<* FATAL FormsVBT.Error *>
BEGIN
LOCK VBT.mu DO
FormsVBT.PutText(self.fv, "listerCommOut",
Fmt.LongReal(self.bytesOut / 1024.0d0, Fmt.Style.Fix, 0));
FormsVBT.PutText(self.fv, "listerWireOut",
Fmt.LongReal(self.wireBytesOut / 1024.0d0, Fmt.Style.Fix, 0));
END;
self.ts.update();
END LSUpdate;
***************************************************************************
Detailer Statistics Display.
***************************************************************************
TYPE
DetailerStats = Detailer.Stats OBJECT
fv: FormsVBT.T;
ts: TotalStats;
METHODS
init(fv: FormsVBT.T; ts: TotalStats): DetailerStats := DSInit;
OVERRIDES
update := DSUpdate;
END;
PROCEDURE DSInit(self: DetailerStats;
fv: FormsVBT.T;
ts: TotalStats): DetailerStats =
BEGIN
EVAL Detailer.Stats.init(self);
self.fv := fv;
self.ts := ts;
RETURN self;
END DSInit;
PROCEDURE DSUpdate(self: DetailerStats) =
<* FATAL FormsVBT.Error *>
BEGIN
LOCK VBT.mu DO
FormsVBT.PutText(self.fv, "detailerCommIn",
Fmt.LongReal(self.bytesIn / 1024.0d0, Fmt.Style.Fix, 0));
FormsVBT.PutText(self.fv, "detailerWireIn",
Fmt.LongReal(self.wireBytesIn / 1024.0d0, Fmt.Style.Fix, 0));
FormsVBT.PutText(self.fv, "detailerCommOut",
Fmt.LongReal(self.bytesOut / 1024.0d0, Fmt.Style.Fix, 0));
FormsVBT.PutText(self.fv, "detailerWireOut",
Fmt.LongReal(self.wireBytesOut / 1024.0d0, Fmt.Style.Fix, 0));
END;
self.ts.update();
END DSUpdate;
***************************************************************************
Updater Statistics Display.
***************************************************************************
TYPE
UpdaterStats = Updater.Stats OBJECT
fv: FormsVBT.T;
ts: TotalStats;
METHODS
init(fv: FormsVBT.T; ts: TotalStats): UpdaterStats := USInit;
OVERRIDES
start := USStart;
update := USUpdate;
END;
PROCEDURE USInit(self: UpdaterStats;
fv: FormsVBT.T;
ts: TotalStats): UpdaterStats =
BEGIN
EVAL Updater.Stats.init(self);
self.fv := fv;
self.ts := ts;
RETURN self;
END USInit;
PROCEDURE USStart(<*UNUSED*> self: UpdaterStats) =
BEGIN
END USStart;
PROCEDURE USUpdate(self: UpdaterStats; type: FileUpdater.UpdateType) =
<* FATAL FormsVBT.Error *>
VAR
name: TEXT;
BEGIN
CASE type OF
| FileUpdater.UpdateType.Edit => name := "edit";
| FileUpdater.UpdateType.Checkout => name := "co";
| FileUpdater.UpdateType.Rsync => name := "rsync";
| FileUpdater.UpdateType.Append => name := "app";
| FileUpdater.UpdateType.Touch => name := "touch";
| FileUpdater.UpdateType.Create => name := "add";
| FileUpdater.UpdateType.Delete => name := "del";
| FileUpdater.UpdateType.Replace => name := "repl";
| FileUpdater.UpdateType.Fixup => name := "fixup";
| FileUpdater.UpdateType.Other => name := "other";
ELSE
name := NIL;
END;
LOCK VBT.mu DO
IF name # NIL THEN
WITH info = self.updateInfo[type] DO
FormsVBT.PutText(self.fv, name & "Count",
Fmt.Int(info.fileCount));
FormsVBT.PutText(self.fv, name & "WireKB",
Fmt.LongReal(info.wireBytes / 1024.0d0, Fmt.Style.Fix, 0));
FormsVBT.PutText(self.fv, name & "FileKB",
Fmt.LongReal(info.fileBytes / 1024.0d0, Fmt.Style.Fix, 0));
IF info.fileBytes # 0.0d0 THEN
FormsVBT.PutText(self.fv, name & "Compr",
Fmt.LongReal(100.0d0 *
(info.fileBytes - info.wireBytes) / info.fileBytes,
style := Fmt.Style.Fix, prec := 1));
END;
END;
END;
WITH info = self.totals DO
FormsVBT.PutText(self.fv, "totCount",
Fmt.Int(info.fileCount));
FormsVBT.PutText(self.fv, "totWireKB",
Fmt.LongReal(info.wireBytes / 1024.0d0, Fmt.Style.Fix, 0));
FormsVBT.PutText(self.fv, "totFileKB",
Fmt.LongReal(info.fileBytes / 1024.0d0, Fmt.Style.Fix, 0));
IF info.fileBytes # 0.0d0 THEN
FormsVBT.PutText(self.fv, "totCompr",
Fmt.LongReal(100.0d0 *
(info.fileBytes - info.wireBytes) / info.fileBytes,
style := Fmt.Style.Fix, prec := 1));
END;
FormsVBT.PutText(self.fv, "updaterCommIn",
Fmt.LongReal(info.commBytes / 1024.0d0, Fmt.Style.Fix, 0));
FormsVBT.PutText(self.fv, "updaterWireIn",
Fmt.LongReal(info.wireBytes / 1024.0d0, Fmt.Style.Fix, 0));
END;
END;
self.ts.update();
END USUpdate;
***************************************************************************
Total Statistics Display.
***************************************************************************
TYPE
TotalStats = MUTEX OBJECT
fv: FormsVBT.T;
ls: ListerStats := NIL;
ds: DetailerStats := NIL;
us: UpdaterStats := NIL;
METHODS
init(fv: FormsVBT.T): TotalStats := TSInit;
update() := TSUpdate;
END;
PROCEDURE TSInit(self: TotalStats;
fv: FormsVBT.T): TotalStats =
BEGIN
self.fv := fv;
(* The other fields have to be filled in manually. *)
RETURN self;
END TSInit;
PROCEDURE TSUpdate(self: TotalStats) =
<* FATAL FormsVBT.Error *>
VAR
commIn, wireIn, commOut, wireOut := 0.0d0;
listTot, detTot, updTot, wireTot: LONGREAL;
BEGIN
LOCK self.ls DO
commOut := commOut + self.ls.bytesOut;
wireOut := wireOut + self.ls.wireBytesOut;
listTot := self.ls.wireBytesOut;
END;
LOCK self.ds DO
commIn := commIn + self.ds.bytesIn;
wireIn := wireIn + self.ds.wireBytesIn;
commOut := commOut + self.ds.bytesOut;
wireOut := wireOut + self.ds.wireBytesOut;
detTot := self.ds.wireBytesIn + self.ds.wireBytesOut;
END;
LOCK self.us DO
commIn := commIn + self.us.totals.commBytes;
wireIn := wireIn + self.us.totals.wireBytes;
updTot := self.us.totals.wireBytes;
END;
wireTot := listTot + detTot + updTot;
LOCK VBT.mu DO
FormsVBT.PutText(self.fv, "listerWireTot",
Fmt.LongReal(listTot / 1024.0d0, Fmt.Style.Fix, 0));
FormsVBT.PutText(self.fv, "detailerWireTot",
Fmt.LongReal(detTot / 1024.0d0, Fmt.Style.Fix, 0));
FormsVBT.PutText(self.fv, "updaterWireTot",
Fmt.LongReal(updTot / 1024.0d0, Fmt.Style.Fix, 0));
FormsVBT.PutText(self.fv, "totCommIn",
Fmt.LongReal(commIn / 1024.0d0, Fmt.Style.Fix, 0));
FormsVBT.PutText(self.fv, "totWireIn",
Fmt.LongReal(wireIn / 1024.0d0, Fmt.Style.Fix, 0));
FormsVBT.PutText(self.fv, "totCommOut",
Fmt.LongReal(commOut / 1024.0d0, Fmt.Style.Fix, 0));
FormsVBT.PutText(self.fv, "totWireOut",
Fmt.LongReal(wireOut / 1024.0d0, Fmt.Style.Fix, 0));
FormsVBT.PutText(self.fv, "totWireTot",
Fmt.LongReal(wireTot / 1024.0d0, Fmt.Style.Fix, 0));
END;
END TSUpdate;
***************************************************************************
Clock.
***************************************************************************
TYPE
Clock = AutoRepeat.T OBJECT
fv: FormsVBT.T;
name: TEXT;
updaterStats: UpdaterStats;
started := FALSE;
startTime: Time.T;
seconds: CARDINAL;
METHODS
init(fv: FormsVBT.T; name: TEXT; updaterStats: UpdaterStats): Clock
:= ClockInit;
OVERRIDES
repeat := ClockTick;
END;
PROCEDURE ClockInit(self: Clock;
fv: FormsVBT.T;
name: TEXT;
updaterStats: UpdaterStats): Clock =
BEGIN
EVAL NARROW(self, AutoRepeat.T).init(0, 500);
self.fv := fv;
self.name := name;
self.updaterStats := updaterStats;
RETURN self;
END ClockInit;
PROCEDURE ClockTick(self: Clock) =
<* FATAL FormsVBT.Error *>
VAR
elapsed: Time.T;
hours, minutes, seconds: CARDINAL;
clockText: TEXT;
fileBytes: LONGREAL;
xferRate: LONGREAL;
xferRateText: TEXT;
BEGIN
IF NOT self.started THEN
self.startTime := Time.Now();
self.seconds := 0;
self.started := TRUE;
RETURN;
END;
elapsed := Time.Now() - self.startTime;
seconds := FLOOR(elapsed);
IF seconds # self.seconds THEN
self.seconds := seconds;
hours := seconds DIV 3600;
seconds := seconds MOD 3600;
minutes := seconds DIV 60;
seconds := seconds MOD 60;
clockText := Fmt.Pad(Fmt.Int(hours), 2, '0') & ":" &
Fmt.Pad(Fmt.Int(minutes), 2, '0') & ":" &
Fmt.Pad(Fmt.Int(seconds), 2, '0');
LOCK self.updaterStats DO
fileBytes := self.updaterStats.totals.fileBytes;
END;
xferRate := fileBytes / 1024.0d0 / elapsed;
xferRateText := Fmt.Pad(Fmt.LongReal(xferRate, Fmt.Style.Fix, 1), 7) &
" KB/sec";
LOCK VBT.mu DO
FormsVBT.PutText(self.fv, self.name, clockText);
FormsVBT.PutText(self.fv, "xferRate", xferRateText);
END;
END;
END ClockTick;
***************************************************************************
PROCEDURE Run(supFile: Pathname.T;
config: FSClient.Configuration)
RAISES {Error, Thread.Alerted} =
CONST
FormFile = "SupGUI.fv";
PathVar = "$CVSupPath";
VAR
fv: FormsVBT.T;
controller: Controller;
BEGIN
(* If the client host's hostname is set up wrong, such that it is
impossible to find out its IP address, then we will get an
"IP.FatalError" exception from the X11 initialization code. Find
out about it now, so that we can produce an intelligent error
message rather than a core dump. *)
VAR
ok := FALSE;
addr: IP.Address;
BEGIN
TRY
ok := IP.GetHostByName(UnixMisc.GetHostName(), addr);
EXCEPT ELSE END;
IF NOT ok THEN
RAISE Error("Cannot get IP address of my own host"
& " -- is its hostname correct?");
END;
END;
TRY
TRY
WITH path = Rsrc.BuildPath(PathVar, SupGUIBundle.Get()) DO
fv := NEW(FormsVBT.T).initFromRsrc(FormFile, path);
END;
EXCEPT
| Rd.Failure(list) =>
RAISE Error("Cannot read \"" & FormFile & "\": " &
ErrMsg.StrError(list));
| Rsrc.NotFound =>
RAISE Error("Cannot find the \"" & FormFile & "\" resource");
END;
LOCK VBT.mu DO
FormsVBT.PutText(fv, "clientVersion", Version.Name);
FormsVBT.PutText(fv, "protoVersion",
Fmt.Int(CVProto.Current.major) & "." &
Fmt.Int(CVProto.Current.minor));
FormsVBT.PutText(fv, "target", Version.Target);
END;
controller := NEW(Controller).init(
fv := fv,
supFile := supFile,
config := config);
Trestle.Install(fv);
EVAL Thread.Fork(controller);
Trestle.AwaitDelete(fv);
EXCEPT
| FormsVBT.Error(msg) =>
RAISE Error("FormsVBT error: " & msg);
| TrestleComm.Failure =>
RAISE Error("Lost the connection to the X server");
END;
END Run;
BEGIN
Supported := TRUE;
END SupGUI.