Files
comet/src/comet.pas
Ken Johnson da732a10bd
Some checks failed
Build and Release / build-and-release (push) Failing after 13m54s
Version 1.2.1: full BinkP/Argus parity, Comet augmentation, WebUI
Version scheme: Major.Minor.Build-Revision.

BinkP gains every major Argus/binkd extension:

- PLZ (zlib) compression with adaptive block sizing (4KB→16KB)
- NR mode inbound resume via .bkp-part partials (FSP-1029)
- ND/NDA deferred cleanup: mid-session abort preserves outbound (FSP-1038)
- MBT multi-batch: FREQ response rides same session via second EOB
- M_NUL TRF traffic advisory and M_NUL FREQ (FRL-1026)
- M_NUL NDL/PHN info strings (new Phone, NodelistFlags config)
- RFC 2822 date format for M_NUL TIME
- Strict M_GET validation and duplicate-file pre-check
- TBinkpPostAuthCallback: host can route InboundDir before transfer
  (models binkd select_inbound / complete_login)
- TCometBinkpResult: Authenticated / AuthMethod fields

Comet native extensions keep the protocol ahead of BinkP:

- INIT payload adds Location/Time/Phone/NodelistFlags (trailing
  strings, backward-compatible)
- LST file listing: NPKT_LSTREQ/LSTITEM/LSTEND + COPT_LST
- Transactional file cleanup: destructive actions deferred until
  successful session close (matches ND semantics)
- Shared CometRFCDateStr across protocols — no drift between
  BinkP TIME and Comet INIT.Time

Daemon:
- BinkP inbound now starts unsecure and promotes to secure only
  after auth (fixes pre-1.2.1 bug where SecInbound was selected
  unconditionally).

TCometFileProvider: GetPartialSize and OpenForReceiveNamed for
NR partials; defaults preserve the random-temp scheme for
providers that don't track partials (Fastway plugin safe).

WebUI: /src/web/ + /src/webui/ backend, modeled after the Argus
GUI. Live session activity, outbound polls, FREQ requests,
nodelist, config editor, scheduler, SSE event stream.
2026-04-21 09:37:03 -07:00

770 lines
21 KiB
ObjectPascal

{
Comet - Direct TCP File Transfer for FidoNet
comet.pas - Main program
Usage:
comet Run as daemon (listen + poll outbound)
comet -c config.cfg Use specified config file
comet call 1:213/723 Single outbound call to a node
comet -h Show help
comet -v Show version
Signal handling (Unix):
SIGHUP = Reload configuration
SIGTERM = Clean shutdown
SIGINT = Clean shutdown
Copyright (C) 2026 Ken Johnson
License: GPL-2.0
}
program comet;
{$mode objfpc}{$H+}
uses
{$IFDEF UNIX}
cmem, { C memory manager - MUST be first }
cthreads, { pthreads - MUST be before all units }
BaseUnix,
{$ENDIF}
{$IFDEF GO32V2}
cometlibc, { DJGPP libc stubs for Watt-32 }
{$ENDIF}
SysUtils, Classes, cometdef, cometcfg, cometpath, comettcp,
cometfrm, cometlog, cometses, cometxfer, cometbso, cometbinkp,
cometfile, cometnodelist, cometed25519, cometdaemon, cometio
{$IFNDEF GO32V2}, cometweb, cometwebauth, cometwebsse{$ENDIF};
const
DEFAULT_CFG = 'comet.cfg';
var
Daemon: TCometDaemon;
CfgPath: string;
CmdMode: (cmDaemon, cmCall, cmHelp, cmVersion, cmKeygen, cmShowKey
{$IFNDEF GO32V2}, cmWebPassword{$ENDIF});
CallTarget: string;
{$IFDEF UNIX}
{ Signal handlers }
procedure HandleSigTerm(Sig: cint); cdecl;
begin
if Daemon <> nil then
Daemon.Shutdown;
end;
procedure HandleSigHup(Sig: cint); cdecl;
begin
if Daemon <> nil then
Daemon.SignalReload;
end;
procedure InstallSignalHandlers;
var
Act: SigActionRec;
begin
FillChar(Act, SizeOf(Act), 0);
Act.sa_handler := SigActionHandler(@HandleSigTerm);
fpSigAction(SIGTERM, @Act, nil);
fpSigAction(SIGINT, @Act, nil);
Act.sa_handler := SigActionHandler(@HandleSigHup);
fpSigAction(SIGHUP, @Act, nil);
{ Ignore SIGPIPE - critical for TCP code }
Act.sa_handler := SigActionHandler(SIG_IGN);
fpSigAction(SIGPIPE, @Act, nil);
end;
{$ENDIF}
procedure ShowVersion;
begin
WriteLn(COMET_NAME, ' ', COMET_FULLVER,
' - Direct TCP File Transfer for FidoNet');
WriteLn('Copyright (C) 2026 Ken Johnson');
WriteLn('Port 24554 - Comet native and BinkP on same port');
end;
procedure ShowHelp;
begin
ShowVersion;
WriteLn;
WriteLn('Usage: comet [options] [command]');
WriteLn;
WriteLn('Commands:');
WriteLn(' (none) Run as daemon (listen + poll outbound)');
WriteLn(' call <address> Single outbound call to a FidoNet node');
WriteLn(' Address format: zone:net/node[.point]');
WriteLn(' keygen Generate ED25519 keypair for authentication');
WriteLn(' showkey Print public key derived from configured private key');
{$IFNDEF GO32V2}
WriteLn(' webui-password Set the WebUI admin password');
{$ENDIF}
WriteLn;
WriteLn('Options:');
WriteLn(' -c <file> Use specified config file');
WriteLn(' (default: comet.cfg in current directory)');
WriteLn(' -d Enable debug/trace logging');
WriteLn(' -q Quiet mode (errors only on console)');
WriteLn(' -v Show version and exit');
WriteLn(' -h, --help Show this help and exit');
WriteLn;
WriteLn('Signals (Unix):');
WriteLn(' SIGHUP Reload configuration');
WriteLn(' SIGTERM, SIGINT Clean shutdown');
WriteLn;
WriteLn('Configuration:');
WriteLn(' Edit comet.cfg or run CSETUP for interactive configuration.');
WriteLn(' See COMET.DOC for complete documentation of all options.');
WriteLn;
WriteLn('Supported outbound formats:');
WriteLn(' BSO (Binkley-Style Outbound) - primary');
WriteLn(' FrontDoor .MSG style');
WriteLn(' D''Bridge Q-file queue');
WriteLn;
WriteLn('Report bugs: https://github.com/kenj/comet/issues');
end;
procedure ParseArgs;
var
I: Integer;
Arg: string;
begin
CfgPath := DEFAULT_CFG;
CmdMode := cmDaemon;
CallTarget := '';
I := 1;
while I <= ParamCount do
begin
Arg := ParamStr(I);
if (Arg = '-h') or (Arg = '--help') then
begin
CmdMode := cmHelp;
Exit;
end
else if Arg = '-v' then
begin
CmdMode := cmVersion;
Exit;
end
else if Arg = '-c' then
begin
Inc(I);
if I <= ParamCount then
CfgPath := ParamStr(I)
else
begin
WriteLn('Error: -c requires a filename argument');
Halt(1);
end;
end
else if Arg = '-d' then
begin
CometLogSetDebug(True);
CometLogSetConsoleLevel(cllDebug);
end
else if Arg = '-q' then
begin
CometLogSetConsoleLevel(cllError);
end
else if Arg = 'call' then
begin
CmdMode := cmCall;
Inc(I);
if I <= ParamCount then
CallTarget := ParamStr(I)
else
begin
WriteLn('Error: call requires a FidoNet address (e.g., 1:213/723)');
Halt(1);
end;
end
else if Arg = 'keygen' then
begin
CmdMode := cmKeygen;
end
else if Arg = 'showkey' then
begin
CmdMode := cmShowKey;
end
{$IFNDEF GO32V2}
else if Arg = 'webui-password' then
begin
CmdMode := cmWebPassword;
end
{$ENDIF}
else
begin
WriteLn('Error: Unknown option: ', Arg);
WriteLn('Try: comet -h');
Halt(1);
end;
Inc(I);
end;
end;
{ ---- Event callback for standalone mode ---- }
{$IFNDEF GO32V2}
procedure HandleLogForWeb(Level: TCometLogLevel; const Msg: string);
begin
if Assigned(SSEBus) then
SSEBus.HandleLog(Ord(Level), Msg);
end;
{$ENDIF}
procedure HandleEvent(const Event: TCometEventData);
var
Pct: Integer;
CPSStr: string;
begin
{ Forward to WebUI SSE bus if active }
{$IFNDEF GO32V2}
if Assigned(SSEBus) then
SSEBus.HandleEvent(Event);
{$ENDIF}
case Event.EventType of
cetSessionStart:
LogInfo('Event: session with %s [%s] via %s',
[Event.RemoteName, Event.RemoteAddr, Event.Protocol]);
cetSessionAuth:
case Event.AuthMethod of
AUTH_ED25519:
if Event.Encrypted then
LogInfo('Event: auth ED25519 + encrypted')
else
LogInfo('Event: auth ED25519');
AUTH_CRAM: LogInfo('Event: auth CRAM-MD5');
AUTH_NOPWD: LogInfo('Event: auth passwordless (NOPWD)');
end;
cetFileStart:
if Event.Sending then
LogInfo('Event: sending %s (%s)',
[Event.FileName, CometFormatSize(Event.FileSize)])
else
LogInfo('Event: receiving %s (%s)',
[Event.FileName, CometFormatSize(Event.FileSize)]);
cetFileProgress:
begin
if Event.FileSize > 0 then
Pct := (Event.Position * 100) div Event.FileSize
else
Pct := 0;
if Event.CPS > 0 then
CPSStr := Format(' %s/s', [CometFormatSize(Event.CPS)])
else
CPSStr := '';
if Event.Sending then
Write(#13, Format(' TX: %s %d%% (%s/%s)%s ',
[Event.FileName, Pct, CometFormatSize(Event.Position),
CometFormatSize(Event.FileSize), CPSStr]))
else
Write(#13, Format(' RX: %s %d%% (%s/%s)%s ',
[Event.FileName, Pct, CometFormatSize(Event.Position),
CometFormatSize(Event.FileSize), CPSStr]));
end;
cetFileEnd:
begin
WriteLn; { New line after progress }
if Event.CPS > 0 then
CPSStr := Format(' @ %s/s', [CometFormatSize(Event.CPS)])
else
CPSStr := '';
if Event.Sending then
LogInfo('Event: sent %s%s', [Event.FileName, CPSStr])
else
LogInfo('Event: received %s%s', [Event.FileName, CPSStr]);
end;
cetSessionEnd:
; { Already logged by daemon as "Session complete:" }
end;
end;
procedure RunDaemon;
begin
Daemon := TCometDaemon.Create;
try
if not Daemon.LoadConfig(CfgPath) then
begin
LogFatal('Cannot load config: %s', [CfgPath]);
LogFatal('Run CSETUP to create a configuration, or copy COMET.SAM to comet.cfg');
Exit;
end;
{$IFDEF UNIX}
InstallSignalHandlers;
{$ENDIF}
Daemon.Run;
finally
Daemon.Free;
Daemon := nil;
end;
end;
procedure RunKeygen;
var
Seed: TED25519Seed;
PubKey: TED25519PublicKey;
PrivKey: TED25519PrivateKey;
begin
WriteLn('Generating ED25519 keypair...');
WriteLn;
ED25519RandomSeed(Seed);
ED25519CreateKeypair(Seed, PubKey, PrivKey);
WriteLn('Add to [System] in your comet.cfg:');
WriteLn(' PrivateKey = ', ED25519ToHex(Seed, 32));
WriteLn(' ; PublicKey = ', ED25519ToHex(PubKey, 32));
WriteLn;
WriteLn('Give the public key to remote nodes. They add it to their config:');
WriteLn(' [Node:', CometAddrToStr(Default(TCometAddress)), ']');
WriteLn(' PublicKey = ', ED25519ToHex(PubKey, 32));
{ Wipe sensitive data }
FillChar(Seed, 32, 0);
FillChar(PrivKey, 64, 0);
end;
procedure RunShowKey;
var
Cfg: TCometConfig;
Seed: TED25519Seed;
PubKey: TED25519PublicKey;
PrivKey: TED25519PrivateKey;
begin
if not CometCfgLoad(CfgPath, Cfg) then
begin
WriteLn('Error: Cannot load config: ', CfgPath);
Halt(1);
end;
if Cfg.PrivateKey = '' then
begin
WriteLn('No PrivateKey configured in [System] section.');
WriteLn('Run "comet keygen" to generate one.');
Halt(1);
end;
ED25519FromHex(Cfg.PrivateKey, Seed, 32);
ED25519CreateKeypair(Seed, PubKey, PrivKey);
WriteLn(ED25519ToHex(PubKey, 32));
FillChar(Seed, 32, 0);
FillChar(PrivKey, 64, 0);
end;
{$IFNDEF GO32V2}
procedure RunWebPassword;
var
Password, Salt, Hash: string;
begin
Write('Enter new WebUI admin password: ');
ReadLn(Password);
if Password = '' then
begin
WriteLn('Error: Password cannot be empty');
Halt(1);
end;
if Length(Password) < 6 then
begin
WriteLn('Error: Password must be at least 6 characters');
Halt(1);
end;
Salt := TCometWebAuth.GenerateSalt;
Hash := TCometWebAuth.HashPassword(Password, Salt);
if CometWebCfgSavePassword(CfgPath, Hash, Salt) then
begin
WriteLn('Password saved to ', CfgPath);
WriteLn('Ensure [WebUI] section has Enabled = yes to activate.');
end
else
begin
WriteLn('Error: Could not write to ', CfgPath);
WriteLn('Add these lines to your [WebUI] section manually:');
WriteLn(' PasswordHash = ', Hash);
WriteLn(' PasswordSalt = ', Salt);
end;
end;
{$ENDIF}
procedure RunCall;
var
Addr: TCometAddress;
Cfg: TCometConfig;
NodeIdx: Integer;
Sock: TCometSocket;
Host: string;
Port: Word;
State: TCometSessionState;
XS: TCometXferState;
HSResult: TCometHandshakeResult;
UseComet: Boolean;
Flav: TCometFlavour;
FloPath, PktPath, InDir: string;
FloEntries: TCometFloEntryArray;
I, XResult: Integer;
BResult: TCometBinkpResult;
BEntries: array of TBinkpSendEntry;
BEntryCount: Integer;
BEntry: TBinkpSendEntry;
TF: TextFile;
NL: TCometNodelist;
FileIO: TCometLocalFileProvider;
{$IFDEF UNIX}
Act: SigActionRec;
{$ENDIF}
begin
if not CometStrToAddr(CallTarget, Addr) then
begin
WriteLn('Error: Invalid FidoNet address: ', CallTarget);
Halt(1);
end;
{ Load config }
if not CometCfgLoad(CfgPath, Cfg) then
begin
WriteLn('Error: Cannot load config: ', CfgPath);
Halt(1);
end;
CometCfgApply(Cfg);
{ Find node config for host/port.
Priority: per-node config > nodelist > default }
NodeIdx := CometCfgFindNode(Cfg, Addr);
Host := '';
Port := COMET_PORT;
UseComet := True;
if NodeIdx >= 0 then
begin
Host := Cfg.Nodes[NodeIdx].Host;
if Cfg.Nodes[NodeIdx].Port <> 0 then
Port := Cfg.Nodes[NodeIdx].Port;
UseComet := not Cfg.Nodes[NodeIdx].NoComet;
end;
{ If no host from config, try nodelist lookup }
if Host = '' then
begin
if Cfg.NodelistDir <> '' then
begin
NL := Default(TCometNodelist);
CometNodelistInit(NL);
if CometNodelistLoadDir(NL, Cfg.NodelistDir) > 0 then
begin
if CometNodelistGetBinkp(NL, Addr, Host, Port) then
LogInfo('Nodelist lookup: %s -> %s:%d',
[CometAddrToStr(Addr), Host, Port]);
end;
CometNodelistFree(NL);
end;
if Host = '' then
begin
WriteLn('Error: No host configured for ', CometAddrToStr(Addr));
WriteLn('Add a [Node:', CometAddrToStr(Addr), '] section to ', CfgPath);
WriteLn(' or set Nodelist = /path/to/nodelist/ in config');
Halt(1);
end;
end;
if Host = '' then
begin
WriteLn('Error: No host/IP configured for ', CometAddrToStr(Addr));
Halt(1);
end;
{$IFDEF UNIX}
{ Ignore SIGPIPE - critical for TCP }
FillChar(Act, SizeOf(Act), 0);
Act.sa_handler := SigActionHandler(SIG_IGN);
fpSigAction(SIGPIPE, @Act, nil);
{$ENDIF}
{ Determine inbound directory }
if Cfg.SecInbound <> '' then
InDir := Cfg.SecInbound
else
InDir := Cfg.Inbound;
{ Ensure directories exist }
CometMakePath(InDir);
if Cfg.TempDir <> '' then CometMakePath(Cfg.TempDir);
{ Create file I/O provider for protocol engines }
FileIO := TCometLocalFileProvider.Create;
try
{ Connect to remote - single port, protocol auto-detected }
if UseComet then
LogInfo('Calling %s at %s:%d', [CometAddrToStr(Addr), Host, Port])
else
LogInfo('Calling %s at %s:%d (BinkP)', [CometAddrToStr(Addr), Host, Port]);
Sock := CometTcpConnect(Host, Port, 15000);
if Sock = COMET_TCP_INVALID then
begin
LogError('Cannot connect to %s', [CometAddrToStr(Addr)]);
Halt(1);
end;
if UseComet then
begin
{ ---- Sniff protocol before sending anything ---- }
{ Uses MSG_PEEK so the byte stays in the socket buffer.
BinkP answerers send M_NUL immediately (high bit set).
Comet answerers send banner immediately (low byte).
If no data, remote is Comet waiting for our banner. }
HSResult := CometSniffProtocol(Sock, 5);
if HSResult = chrBinkP then
begin
LogInfo('Remote speaks BinkP - proceeding on same connection');
UseComet := False;
end
else if HSResult = chrDisconnect then
begin
LogError('Connection lost during protocol detection');
CometTcpClose(Sock);
Halt(1);
end;
{ chrOK (Comet banner seen) or chrTimeout (no data) = proceed with Comet }
end;
if UseComet then
begin
{ ---- Comet handshake ---- }
CometSessionInit(State, Sock, True, Host, Port);
State.OurInit.Password := CometCfgGetPassword(Cfg, Addr);
HSResult := CometHandshake(State, Cfg);
if HSResult <> chrOK then
begin
LogError('Handshake failed: %d', [Ord(HSResult)]);
CometSessionDone(State);
CometTcpClose(Sock);
Halt(1);
end;
end;
if UseComet then
begin
{ ---- Comet protocol session ---- }
try
CometXferInit(XS, State, InDir, Cfg.TempDir,
CometAddSlash(Cfg.TempDir) + 'comet-abort.log', FileIO);
XS.FreqDir := Cfg.FreqDir;
XS.FreqAliases := Cfg.FreqAliases;
try
{ Send .?UT packet files }
for Flav := Low(TCometFlavour) to High(TCometFlavour) do
begin
if Flav = cfHold then Continue;
PktPath := BSONodeFile(Cfg.Outbound, Addr,
Cfg.Addresses[0].Zone, BSOPktExt(Flav));
if CometFileExists(PktPath) then
begin
XResult := CometTransfer(XS, PktPath, '');
if XResult = XFER_ABORT then Break;
if XResult = XFER_OK then
DeleteFile(PktPath);
end;
end;
{ Send files from .FLO flow files }
for Flav := Low(TCometFlavour) to High(TCometFlavour) do
begin
if Flav = cfHold then Continue;
FloPath := BSONodeFile(Cfg.Outbound, Addr,
Cfg.Addresses[0].Zone, BSOFloExt(Flav));
if not CometFileExists(FloPath) then Continue;
FloEntries := BSOReadFlo(FloPath);
for I := 0 to High(FloEntries) do
begin
if FloEntries[I].Sent then Continue;
if FloEntries[I].FilePath = '' then Continue;
if not FileExists(FloEntries[I].FilePath) then Continue;
XResult := CometTransfer(XS, FloEntries[I].FilePath, '');
if XResult = XFER_ABORT then Break;
if XResult = XFER_OK then
CometFileSent(FloPath, FloEntries[I].OrigPath,
FloEntries[I].FilePath, FloEntries[I].Action);
end;
end;
{ Send FREQ requests via NPKT_FREQ for each .REQ line }
PktPath := BSONodeFile(Cfg.Outbound, Addr,
Cfg.Addresses[0].Zone, '.req');
if CometFileExists(PktPath) then
begin
AssignFile(TF, PktPath);
{$I-} Reset(TF); {$I+}
if IOResult = 0 then
begin
while not EOF(TF) do
begin
ReadLn(TF, FloPath); { reuse FloPath as temp string }
FloPath := Trim(FloPath);
if (FloPath <> '') and (FloPath[1] <> ';') then
begin
LogInfo('FREQ request: %s', [FloPath]);
CometFrameSend(State.Sock, NPKT_FREQ, 0,
@FloPath[1], Length(FloPath));
end;
end;
CloseFile(TF);
end;
DeleteFile(PktPath);
end;
{ Send any FREQ response files queued during the session }
for I := 0 to XS.FreqCount - 1 do
begin
if FileExists(XS.FreqQueue[I]) then
begin
XResult := CometTransfer(XS, XS.FreqQueue[I], '');
if XResult = XFER_ABORT then Break;
end;
end;
{ End of batch - also receives any remaining files from remote }
CometTransfer(XS, '', '');
LogInfo('Session complete: sent %d files (%s), rcvd %d files (%s)',
[XS.FilesSent, CometFormatSize(XS.BytesSent),
XS.FilesRecvd, CometFormatSize(XS.BytesRecvd)]);
finally
CometXferDone(XS);
end;
finally
CometSessionDone(State);
CometTcpClose(Sock);
end;
end
else
begin
{ ---- BinkP fallback session ---- }
{ Build send queue with .FLO tracking (per-file cleanup on M_GOT) }
BEntryCount := 0;
SetLength(BEntries, 64);
for Flav := Low(TCometFlavour) to High(TCometFlavour) do
begin
if Flav = cfHold then Continue;
{ .?UT packet files: delete after send }
PktPath := BSONodeFile(Cfg.Outbound, Addr,
Cfg.Addresses[0].Zone, BSOPktExt(Flav));
if CometFileExists(PktPath) then
begin
FillChar(BEntry, SizeOf(BEntry), 0);
BEntry.FilePath := PktPath;
BEntry.Action := csaDelete;
if BEntryCount >= Length(BEntries) then
SetLength(BEntries, BEntryCount + 32);
BEntries[BEntryCount] := BEntry;
Inc(BEntryCount);
end;
{ Files from .FLO }
FloPath := BSONodeFile(Cfg.Outbound, Addr,
Cfg.Addresses[0].Zone, BSOFloExt(Flav));
if CometFileExists(FloPath) then
begin
FloEntries := BSOReadFlo(FloPath);
for I := 0 to High(FloEntries) do
begin
if FloEntries[I].Sent then Continue;
if (FloEntries[I].FilePath = '') or
not FileExists(FloEntries[I].FilePath) then Continue;
FillChar(BEntry, SizeOf(BEntry), 0);
BEntry.FilePath := FloEntries[I].FilePath;
BEntry.FloPath := FloPath;
BEntry.FloLine := FloEntries[I].OrigPath;
BEntry.Action := FloEntries[I].Action;
if BEntryCount >= Length(BEntries) then
SetLength(BEntries, BEntryCount + 32);
BEntries[BEntryCount] := BEntry;
Inc(BEntryCount);
end;
end;
end;
{ .REQ file request }
PktPath := BSONodeFile(Cfg.Outbound, Addr,
Cfg.Addresses[0].Zone, '.req');
if CometFileExists(PktPath) then
begin
FillChar(BEntry, SizeOf(BEntry), 0);
BEntry.FilePath := PktPath;
BEntry.Action := csaDelete;
if BEntryCount >= Length(BEntries) then
SetLength(BEntries, BEntryCount + 32);
BEntries[BEntryCount] := BEntry;
Inc(BEntryCount);
end;
SetLength(BEntries, BEntryCount);
BResult := BinkpRunOutbound(Sock, Cfg, Addr,
InDir, Cfg.TempDir, BEntries, FileIO);
if BResult.Success then
LogInfo('BinkP session complete: sent %d files (%s), rcvd %d files (%s)',
[BResult.FilesSent, CometFormatSize(BResult.BytesSent),
BResult.FilesRecvd, CometFormatSize(BResult.BytesRecvd)])
else
LogError('BinkP session failed: %s', [BResult.ErrorMsg]);
CometTcpClose(Sock);
end;
finally
FileIO.Free;
end;
end;
{ ---- Entry point ---- }
begin
Daemon := nil;
CometLogSetConsole(True);
CometLogSetConsoleTimestamp(False);
CometLogSetEventCallback(@HandleEvent);
{$IFNDEF GO32V2}
CometLogSetCallback(@HandleLogForWeb);
{$ENDIF}
ParseArgs;
case CmdMode of
cmHelp: ShowHelp;
cmVersion: ShowVersion;
cmDaemon: RunDaemon;
cmCall: RunCall;
cmKeygen: RunKeygen;
cmShowKey: RunShowKey;
{$IFNDEF GO32V2}
cmWebPassword: RunWebPassword;
{$ENDIF}
end;
end.