mysticbbs/mystic/mis.pas

663 lines
17 KiB
ObjectPascal
Raw Normal View History

2012-02-13 15:28:44 -08:00
// ====================================================================
// Mystic BBS Software Copyright 1997-2012 By James Coyle
// ====================================================================
//
// This file is part of Mystic BBS.
//
// Mystic BBS is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Mystic BBS is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Mystic BBS. If not, see <http://www.gnu.org/licenses/>.
//
// ====================================================================
Program MIS;
2013-09-13 20:08:58 -07:00
{$I M_OPS.PAS}
2012-02-13 15:28:44 -08:00
Uses
{$IFDEF DEBUG}
HeapTrc,
LineInfo,
2012-02-13 15:28:44 -08:00
{$ENDIF}
{$IFDEF UNIX}
cThreads,
BaseUnix,
2012-02-13 15:28:44 -08:00
{$ENDIF}
DOS,
2012-02-13 15:28:44 -08:00
m_Output,
m_Input,
m_DateTime,
m_io_Base,
m_io_Sockets,
2012-02-13 15:28:44 -08:00
m_FileIO,
m_Strings,
m_Term_Ansi,
MIS_Common,
MIS_NodeData,
MIS_Server,
MIS_Client_Telnet,
MIS_Client_SMTP,
MIS_Client_POP3,
MIS_Client_FTP,
2013-08-17 19:06:47 -07:00
MIS_Client_NNTP,
2013-08-31 22:42:24 -07:00
MIS_Client_BINKP,
2013-09-03 05:02:29 -07:00
BBS_Records,
BBS_DataBase;
2012-02-13 15:28:44 -08:00
Const
FocusTelnet = 0;
FocusSMTP = 1;
FocusPOP3 = 2;
FocusFTP = 3;
FocusNNTP = 4;
2013-08-17 19:06:47 -07:00
FocusBINKP = 5;
FocusMax = 5;
2012-02-13 15:28:44 -08:00
Var
Keyboard : TInput;
2012-02-13 15:28:44 -08:00
TelnetServer : TServerManager;
FTPServer : TServerManager;
POP3Server : TServerManager;
SMTPServer : TServerManager;
NNTPServer : TServerManager;
2013-08-17 19:06:47 -07:00
BINKPServer : TServerManager;
2012-02-13 15:28:44 -08:00
FocusPTR : TServerManager;
FocusCurrent : Byte;
TopPage : Integer;
BarPos : Integer;
NodeData : TNodeData;
DaemonMode : Boolean = False;
2012-02-13 15:28:44 -08:00
{$I MIS_ANSIWFC.PAS}
Procedure ReadConfiguration;
Var
2013-05-08 23:12:55 -07:00
FileConfig : TFileBuffer;
DatLoc : String;
2012-02-13 15:28:44 -08:00
Begin
2013-05-08 23:12:55 -07:00
FileConfig := TFileBuffer.Create(SizeOf(RecConfig));
2013-05-20 02:35:04 -07:00
If Not FileConfig.OpenStream ('mystic.dat', 1, fmOpen, fmRWDN) Then Begin
DatLoc := GetEnv('mysticbbs');
If DatLoc <> '' Then DatLoc := DirSlash(DatLoc);
2013-05-20 02:35:04 -07:00
If Not FileConfig.OpenStream (DatLoc + 'mystic.dat', 1, fmOpen, fmRWDN) Then Begin
2013-03-07 07:39:10 -08:00
If Not DaemonMode Then Begin
Console.WriteLine (#13#10 + 'ERROR: Unable to read MYSTIC.DAT. This file must exist in the same');
Console.WriteLine ('directory as MIS');
Keyboard.Free;
Console.Free;
End;
FileConfig.Free;
2012-02-13 15:28:44 -08:00
Halt (1);
End;
2012-02-13 15:28:44 -08:00
End;
2013-05-20 02:35:04 -07:00
FileConfig.ReadBlock (bbsConfig, SizeOf(bbsConfig));
2012-02-13 15:28:44 -08:00
FileConfig.Free;
If bbsConfig.DataChanged <> mysDataChanged Then Begin
WriteLn('ERROR: Data files are not current and must be upgraded.');
Halt(1);
End;
DirChange(bbsConfig.SystemPath);
2012-02-13 15:28:44 -08:00
End;
Function GetFocusPtr : TServerManager;
Begin
Result := NIL;
Case FocusCurrent of
FocusTelnet : GetFocusPtr := TelnetServer;
FocusSMTP : GetFocusPtr := SMTPServer;
FocusPOP3 : GetFocusPtr := POP3Server;
FocusFTP : GetFocusPtr := FTPServer;
FocusNNTP : GetFocusPtr := NNTPServer;
2013-08-17 19:06:47 -07:00
FocusBINKP : GetFocusPtr := BINKPServer;
2012-02-13 15:28:44 -08:00
End;
End;
Procedure UpdateConnectionList;
Var
Count : Byte;
Attr : Byte;
PosY : Byte;
NI : TNodeInfoRec;
Begin
If FocusPtr = NIL Then Exit;
NodeData.SynchronizeNodeData;
2012-02-13 15:28:44 -08:00
PosY := 0;
For Count := TopPage to TopPage + 7 Do Begin
NodeData.GetNodeInfo(Count, NI);
Inc (PosY);
If Count = BarPos Then Attr := 31 Else Attr := 7;
Case FocusCurrent of
0 : If NI.Busy Then Begin
2012-02-13 15:28:44 -08:00
Console.WriteXY (3, 3 + PosY, Attr,
strPadL(strI2S(NI.Num), 3, '0') + ' ' +
strPadR(NI.User, 12, ' ') + ' ' +
strPadR(NI.Action, 18, ' ') + ' ' +
strPadL(NI.IP, 15, ' '));
End Else
If Count <= FocusPtr.ClientMax Then
Console.WriteXY (3, 3 + PosY, Attr, strPadL(strI2S(NI.Num), 3, '0') + strPadR(' Waiting', 48, ' '))
Else
Console.WriteXY (3, 3 + PosY, Attr, strRep(' ', 51));
1,
2,
3,
2013-08-17 19:06:47 -07:00
4,
5 : If (Count <= FocusPtr.ClientList.Count) And (FocusPtr.ClientList[Count - 1] <> NIL) Then Begin
2012-02-13 15:28:44 -08:00
Console.WriteXY (3, 3 + PosY, Attr,
strPadL(strI2S(Count), 3, '0') + ' ' +
strPadR(TFTPServer(FocusPtr.ClientList[Count - 1]).User.Handle, 31, ' ') + ' ' +
strPadL(TFTPServer(FocusPtr.ClientList[Count - 1]).Client.PeerIP, 15, ' '));
End Else
If Count <= FocusPtr.ClientMax Then
Console.WriteXY (3, 3 + PosY, Attr, strPadL(strI2S(Count), 3, '0') + strPadR(' Waiting', 48, ' '))
Else
Console.WriteXY (3, 3 + PosY, Attr, strRep(' ', 51));
End;
End;
End;
Procedure UpdateStatus;
Var
Offset : Integer;
Count : Integer;
Begin
If FocusPtr = NIL Then Exit;
FocusPtr.StatusUpdated := False;
2012-02-13 15:28:44 -08:00
// UPDATE CONNECTION STATS
Console.WriteXY (69, 7, 7, strPadR(strI2S(FocusPtr.ClientActive), 5, ' '));
Console.WriteXY (69, 8, 7, strPadR(strI2S(FocusPtr.ClientBlocked), 5, ' '));
Console.WriteXY (69, 9, 7, strPadR(strI2S(FocusPtr.ClientRefused), 5, ' '));
Console.WriteXY (69, 10, 7, strPadR(strI2S(FocusPtr.ClientTotal), 5, ' '));
// UPDATE STATUS MESSAGES
Offset := FocusPtr.ServerStatus.Count;
2012-02-13 15:28:44 -08:00
For Count := 22 DownTo 15 Do Begin
If Offset > 0 Then Begin
Dec(Offset);
2013-09-13 20:08:58 -07:00
Console.WriteXY (4, Count, 7, strPadR(FocusPtr.ServerStatus.Strings[Offset], 74, ' '));
2012-02-13 15:28:44 -08:00
End Else
Console.WriteXY (4, Count, 7, strPadR(' ', 74, ' '));
End;
UpdateConnectionList;
End;
Procedure SwitchFocus;
Begin
BarPos := 1;
TopPage := 1;
Repeat
If FocusCurrent = FocusMax Then FocusCurrent := 0 Else Inc(FocusCurrent);
Case FocusCurrent of
FocusTelnet : If TelnetServer <> NIL Then Break;
2013-03-22 20:17:33 -07:00
FocusSMTP : If SmtpServer <> NIL Then Break;
FocusPOP3 : If Pop3Server <> NIL Then Break;
FocusFTP : If FtpServer <> NIL Then Break;
FocusNNTP : If NNTPServer <> NIL Then Break;
2013-08-17 19:06:47 -07:00
FocusBINKP : If BINKPServer <> NIL Then Break;
2012-02-13 15:28:44 -08:00
End;
Until False;
2013-08-17 19:06:47 -07:00
Console.WriteXY (49, 1, 112, 'telnet/smtp/pop3/ftp/nntp/binkp');
2012-02-13 15:28:44 -08:00
Case FocusCurrent of
2013-08-17 19:06:47 -07:00
FocusTelnet : Console.WriteXY (49, 1, 113, 'TELNET');
FocusSMTP : Console.WriteXY (56, 1, 113, 'SMTP');
FocusPOP3 : Console.WriteXY (61, 1, 113, 'POP3');
FocusFTP : Console.WriteXY (66, 1, 113, 'FTP');
FocusNNTP : Console.WriteXY (70, 1, 113, 'NNTP');
FocusBINKP : Console.WriteXY (75, 1, 113, 'BINKP');
2012-02-13 15:28:44 -08:00
End;
FocusPtr := GetFocusPtr;
If FocusPtr <> NIL Then Begin
Console.WriteXY (69, 5, 7, strPadR(strI2S(FocusPtr.Port), 5, ' '));
Console.WriteXY (69, 6, 7, strPadR(strI2S(FocusPtr.ClientMax), 5, ' '));
UpdateStatus;
End;
End;
Procedure LocalLogin;
Const
BufferSize = 1024 * 4;
Var
Client : TIOSocket;
2012-02-13 15:28:44 -08:00
Res : LongInt;
Buffer : Array[1..BufferSize] of Char;
Done : Boolean;
Ch : Char;
Begin
Console.TextAttr := 7;
2012-02-13 15:28:44 -08:00
Console.ClearScreen;
2013-05-20 02:35:04 -07:00
// Console.WriteStr ('Connecting to 127.0.0.1... ');
2012-02-13 15:28:44 -08:00
Client := TIOSocket.Create;
2012-02-13 15:28:44 -08:00
2012-09-20 10:52:58 -07:00
Client.FTelnetClient := True;
2013-05-20 02:35:04 -07:00
If Not Client.Connect(bbsConfig.inetInterface{'127.0.0.1'}, bbsConfig.InetTNPort) Then
2012-02-13 15:28:44 -08:00
Console.WriteLine('Unable to connect')
Else Begin
Done := False;
Term := TTermAnsi.Create(Console);
Console.SetWindow (1, 1, 80, 24, True);
Console.WriteXY (1, 25, 112, strPadC('Local TELNET: ALT-X to Quit', 80, ' '));
Term.SetReplyClient(TIOBase(Client));
2012-02-13 15:28:44 -08:00
Repeat
If Client.WaitForData(0) > 0 Then Begin
Repeat
Res := Client.ReadBuf (Buffer, BufferSize);
If Res < 0 Then Begin
Done := True;
Break;
End;
Term.ProcessBuf(Buffer, Res);
Until Res <> BufferSize;
End Else
If Keyboard.KeyPressed Then Begin
Ch := Keyboard.ReadKey;
2012-02-13 15:28:44 -08:00
Case Ch of
#00 : Case Keyboard.ReadKey of
2012-02-13 15:28:44 -08:00
#45 : Break;
#71 : Client.WriteStr(#27 + '[H');
#72 : Client.WriteStr(#27 + '[A');
#73 : Client.WriteStr(#27 + '[V');
#75 : Client.WriteStr(#27 + '[D');
#77 : Client.WriteStr(#27 + '[C');
#79 : Client.WriteStr(#27 + '[K');
#80 : Client.WriteStr(#27 + '[B');
#81 : Client.WriteStr(#27 + '[U');
#83 : Client.WriteStr(#127);
End;
Else
Client.WriteBuf(Ch, 1);
If Client.FTelnetEcho Then Term.Process(Ch);
End;
End Else
WaitMS(5);
Until Done;
Term.Free;
End;
Client.Free;
Console.TextAttr := 7;
Console.SetWindow (1, 1, 80, 25, True);
FocusCurrent := FocusMax;
2013-05-25 10:56:32 -07:00
2012-02-13 15:28:44 -08:00
DrawStatusScreen;
2013-05-25 10:56:32 -07:00
2012-02-13 15:28:44 -08:00
SwitchFocus;
End;
{$IFDEF UNIX}
Procedure SetUserOwner;
Var
Info : Stat;
MysLoc : String;
Begin
MysLoc := GetEnv('mysticbbs');
If MysLoc <> '' Then MysLoc := DirSlash(MysLoc);
If fpStat(MysLoc + 'mis', Info) = 0 Then Begin
fpSetGID (Info.st_GID);
fpSetUID (Info.st_UID);
End;
End;
{$ENDIF}
2012-06-20 07:07:29 -07:00
Function ServerStartup : Boolean;
Begin
Result := False;
ReadConfiguration;
TelnetServer := NIL;
FTPServer := NIL;
POP3Server := NIL;
2013-08-17 19:06:47 -07:00
SMTPServer := NIL;
2012-06-20 07:07:29 -07:00
NNTPServer := NIL;
2013-08-17 19:06:47 -07:00
BINKPServer := NIL;
2012-06-20 07:07:29 -07:00
NodeData := TNodeData.Create(bbsConfig.INetTNNodes);
If bbsConfig.InetTNUse Then Begin
TelnetServer := TServerManager.Create(bbsConfig, bbsConfig.InetTNPort, bbsConfig.INetTNNodes, NodeData, @CreateTelnet);
TelnetServer.Server.FTelnetServer := True;
TelnetServer.ClientMaxIPs := bbsConfig.InetTNDupes;
2013-08-28 18:01:55 -07:00
TelnetServer.LogFile := 'telnet';
2012-06-20 07:07:29 -07:00
Result := True;
End;
If bbsConfig.InetSMTPUse Then Begin
SMTPServer := TServerManager.Create(bbsConfig, bbsConfig.INetSMTPPort, bbsConfig.inetSMTPMax, NodeData, @CreateSMTP);
SMTPServer.Server.FTelnetServer := False;
SMTPServer.ClientMaxIPs := bbsConfig.INetSMTPDupes;
2013-08-28 18:01:55 -07:00
SMTPServer.LogFile := 'smtp';
2012-06-20 07:07:29 -07:00
Result := True;
End;
If bbsConfig.InetPOP3Use Then Begin
POP3Server := TServerManager.Create(bbsConfig, bbsConfig.INetPOP3Port, bbsConfig.inetPOP3Max, NodeData, @CreatePOP3);
POP3Server.Server.FTelnetServer := False;
POP3Server.ClientMaxIPs := bbsConfig.inetPOP3Dupes;
2013-08-28 18:01:55 -07:00
POP3Server.LogFile := 'pop3';
2012-06-20 07:07:29 -07:00
Result := True;
End;
If bbsConfig.InetFTPUse Then Begin
FTPServer := TServerManager.Create(bbsConfig, bbsConfig.InetFTPPort, bbsConfig.inetFTPMax, NodeData, @CreateFTP);
FTPServer.Server.FTelnetServer := False;
FTPServer.ClientMaxIPs := bbsConfig.inetFTPDupes;
2013-08-28 18:01:55 -07:00
FTPServer.LogFile := 'ftp';
2012-06-20 07:07:29 -07:00
Result := True;
End;
If bbsConfig.InetNNTPUse Then Begin
NNTPServer := TServerManager.Create(bbsConfig, bbsConfig.InetNNTPPort, bbsConfig.inetNNTPMax, NodeData, @CreateNNTP);
NNTPServer.Server.FTelnetServer := False;
NNTPServer.ClientMaxIPs := bbsConfig.inetNNTPDupes;
2013-08-28 18:01:55 -07:00
NNTPServer.LogFile := 'nntp';
2012-06-20 07:07:29 -07:00
Result := True;
End;
2013-08-17 19:06:47 -07:00
If bbsConfig.InetBINKPUse Then Begin
BINKPServer := TServerManager.Create(bbsConfig, bbsConfig.InetBINKPPort, bbsConfig.inetBINKPMax, NodeData, @CreateBINKP);
BINKPServer.Server.FTelnetServer := False;
BINKPServer.ClientMaxIPs := bbsConfig.inetBINKPDupes;
2013-08-28 18:01:55 -07:00
BINKPServer.LogFile := 'binkp';
2013-08-17 19:06:47 -07:00
Result := True;
End;
{$IFDEF UNIX}
SetUserOwner;
{$ENDIF}
2013-05-20 02:35:04 -07:00
TempPath := bbsConfig.SystemPath + 'temp0' + PathChar;
DirCreate(TempPath);
2012-06-20 07:07:29 -07:00
End;
{$IFDEF UNIX}
2012-08-21 09:23:47 -07:00
(*
2012-06-18 05:59:19 -07:00
Procedure Snoop;
Begin
If FocusCurrent <> FocusTelnet Then Exit;
If FocusPtr.ClientList[BarPos - 1] <> NIL Then Begin
Term := TTermAnsi.Create(Console);
Console.TextAttr := 7;
Console.ClearScreen;
Console.SetWindow (1, 1, 80, 24, True);
Console.WriteXY (1, 25, 112, strPadC('Snooping : Press [ESC] to Quit', 80, ' '));
TTelnetServer(FocusPtr.ClientList[BarPos - 1]).Snooping := True;
Repeat Until Keyboard.ReadKey = #27;
If TTelnetServer(FocusPtr.ClientList[BarPos - 1]) <> NIL Then
TTelnetServer(FocusPtr.ClientList[BarPos - 1]).Snooping := False;
Term.Free;
Console.TextAttr := 7;
2012-06-20 07:07:29 -07:00
2012-06-18 05:59:19 -07:00
Console.SetWindow (1, 1, 80, 25, True);
FocusCurrent := FocusMax;
2012-06-20 07:07:29 -07:00
2012-06-18 05:59:19 -07:00
DrawStatusScreen;
2012-06-20 07:07:29 -07:00
2012-06-18 05:59:19 -07:00
SwitchFocus;
End;
End;
2012-08-21 09:23:47 -07:00
*)
Procedure DaemonEventSignal (Sig : LongInt); cdecl;
Begin
Case Sig of
SIGTERM : Begin
TelnetServer.Free;
SMTPServer.Free;
POP3Server.Free;
FTPServer.Free;
NNTPServer.Free;
2013-08-17 19:06:47 -07:00
BinkPServer.Free;
NodeData.Free;
Halt(0);
End;
End;
End;
Procedure ExecuteDaemon;
Var
PID : TPID;
SID : TPID;
Begin
WriteLn('- [MIS] Executing Mystic Internet Server in daemon mode');
2012-06-20 07:07:29 -07:00
PID := fpFork;
If PID < 0 Then Halt(1);
If PID > 0 Then Halt(0);
SID := fpSetSID;
If SID < 0 Then Halt(1);
Close (Input);
Close (Output);
2012-06-20 07:07:29 -07:00
//CLOSE STDERR?
If Not ServerStartup Then Begin
NodeData.Free;
Halt(1);
End;
fpSignal (SIGTERM, DaemonEventSignal);
Repeat
2012-06-20 07:07:29 -07:00
WaitMS(60000); // Heartbeat
2013-08-17 19:06:47 -07:00
// change to wait 45 and check for event
Until False;
End;
{$ENDIF}
2012-02-13 15:28:44 -08:00
Const
WinTitle = 'Mystic Internet Server';
Var
2012-07-14 12:04:43 -07:00
Count : Integer;
2012-02-13 15:28:44 -08:00
Begin
{$IFDEF UNIX}
DaemonMode := Pos('-D', strUpper(ParamStr(1))) > 0;
{$ENDIF}
2012-02-13 15:28:44 -08:00
Randomize;
{$IFDEF DEBUG}
SetHeapTraceOutput('mis.mem');
{$ENDIF}
2012-06-20 07:07:29 -07:00
{$IFDEF UNIX}
If DaemonMode Then ExecuteDaemon;
{$ENDIF}
2012-02-13 15:28:44 -08:00
2012-06-20 07:07:29 -07:00
Console := TOutput.Create(True);
Keyboard := TInput.Create;
2012-02-13 15:28:44 -08:00
2012-06-20 07:07:29 -07:00
Console.SetWindowTitle(WinTitle);
2012-02-13 15:28:44 -08:00
2013-08-29 03:04:20 -07:00
// process command lines here and exit
2012-06-20 07:07:29 -07:00
If Not ServerStartup Then Begin
Console.ClearScreen;
2013-08-17 19:06:47 -07:00
Console.WriteLine('ERROR: No servers are configured as active.');
2012-02-13 15:28:44 -08:00
NodeData.Free;
Keyboard.Free;
2012-02-13 15:28:44 -08:00
Console.Free;
Halt(10);
End;
2012-02-21 23:30:43 -08:00
Count := 0;
2012-02-13 15:28:44 -08:00
2012-06-20 07:07:29 -07:00
DrawStatusScreen;
2012-02-13 15:28:44 -08:00
2012-06-20 07:07:29 -07:00
FocusCurrent := FocusMax;
SwitchFocus;
2012-02-13 15:28:44 -08:00
Repeat
2012-06-18 05:59:19 -07:00
If Keyboard.KeyWait(500) Then
Case Keyboard.ReadKey of
#00 : Case Keyboard.ReadKey of
2012-02-13 15:28:44 -08:00
#72 : If BarPos > TopPage Then Begin
Dec(BarPos);
UpdateConnectionList;
End Else
If TopPage > 1 Then Begin
Dec(TopPage);
Dec(BarPos);
UpdateConnectionList;
End;
#75 : Begin
Dec (TopPage, 8);
Dec (BarPos, 8);
If TopPage < 1 Then TopPage := 1;
If BarPos < 1 Then BarPos := TopPage;
UpdateConnectionList;
End;
#77 : Begin
Inc (TopPage, 8);
Inc (BarPos, 8);
If TopPage + 7 > FocusPtr.ClientList.Count Then TopPage := FocusPtr.ClientList.Count - 7;
If BarPos > FocusPtr.ClientList.Count Then BarPos := FocusPtr.ClientList.Count;
If TopPage < 1 Then TopPage := 1;
UpdateConnectionList;
End;
#80 : If (BarPos < FocusPtr.ClientMax) and (BarPos < TopPage + 7) Then Begin
Inc(BarPos);
UpdateConnectionList;
End Else
If (TopPage + 7 < FocusPtr.ClientMax) Then Begin
Inc(TopPage);
Inc(BarPos);
UpdateConnectionList;
End;
End;
#09 : SwitchFocus;
2012-08-21 09:23:47 -07:00
// #13 : {$IFDEF UNIX}Snoop{$ENDIF};
2012-02-13 15:28:44 -08:00
#27 : Break;
#32 : LocalLogin;
End;
If (FocusPtr <> NIL) Then
If FocusPtr.StatusUpdated Then Begin
2012-02-13 15:28:44 -08:00
UpdateStatus;
Count := 1;
End Else
If Count = 10 Then Begin // force update every 10 seconds since mystic
UpdateStatus; // cannot yet talk to MIS directly
Count := 1;
End Else
Inc (Count);
Until False;
Console.TextAttr := 7;
Console.ClearScreen;
Console.WriteLine ('Mystic Internet Server Version ' + mysVersion);
Console.WriteLine ('');
Console.WriteStr ('Shutting down servers: TELNET');
TelnetServer.Free;
Console.WriteStr (' SMTP');
SMTPServer.Free;
Console.WriteStr (' POP3');
POP3Server.Free;
Console.WriteStr (' FTP');
FTPServer.Free;
Console.WriteStr (' NNTP');
NNTPServer.Free;
2013-08-17 19:06:47 -07:00
Console.WriteStr (' BINKP');
BINKPServer.Free;
2012-02-13 15:28:44 -08:00
Console.WriteLine (' (DONE)');
NodeData.Free;
Keyboard.Free;
2012-02-13 15:28:44 -08:00
Console.Free;
Halt(255);
2012-09-20 10:52:58 -07:00
End.