Files
comet/cometdaemon.pas
Ken Johnson bade0eb593 Comet 1.00 - Initial commit: complete standalone FidoNet TCP mailer
Standalone FidoNet mailer daemon implementing the Comet protocol
(TCP variant of the Nova protocol family) with BinkP/1.1 fallback.
Written in Free Pascal for DOS/Win/OS2/Linux/FreeBSD.

15 source files, ~10K lines:
- Protocol: length-prefixed frames, SHA-256/384/512, CRC-32,
  bidirectional transfer with sliding window, adaptive block sizing
- Session: own TCP handshake with BinkP auto-detection on port 26638
- Outbound: BSO (Binkley), FrontDoor, D'Bridge format support
- Daemon: multi-session with thread pool, outbound scanner
- Paths: DOS<->Linux bridge with case-insensitive lookup, drive mapping
- Config: INI-style with heavily documented sample (COMET.SAM)

All 17 Nova interop bug fixes baked in from the start.
18/18 tests passing (CRC-32, SHA-256/384/512, frame encode/decode).
2026-03-29 20:02:37 -07:00

933 lines
25 KiB
ObjectPascal

{
Comet - Direct TCP File Transfer for FidoNet
cometdaemon.pas - Multi-session daemon core
Manages:
- Inbound listener: accepts TCP connections on port 26638,
detects Comet vs BinkP protocol, spawns session threads
- Outbound scanner: polls BSO outbound directory, initiates
calls to nodes with pending mail
- Session threads: one per active connection, runs handshake
then file transfer, cleans up on completion
- BSY locking: prevents duplicate sessions to same node
Thread model:
- Main thread runs the listener accept loop
- Outbound scanner runs on a timer within the main loop
- Each inbound/outbound session gets its own TThread
- Thread-safe session list with critical section
On DOS (no threads): runs single-session in the main loop.
Copyright (C) 2026 Ken Johnson
License: GPL-2.0
}
unit cometdaemon;
{$mode objfpc}{$H+}
interface
uses
SysUtils, Classes, cometdef, cometcfg, cometpath, comettcp, cometfrm,
cometses, cometxfer, cometbso, cometbinkp, cometfile, cometlog;
const
COMET_MAX_SESSIONS = 32; { Hard limit on concurrent sessions }
COMET_BSY_STALE_MIN = 10; { Stale BSY lock age in minutes }
type
{ Session status }
TCometSessStatus = (
cssConnecting, { TCP connected, handshake in progress }
cssActive, { Handshake done, transferring files }
cssClosing, { Session ending, cleanup in progress }
cssDone { Thread finished, ready for removal }
);
{ Session direction }
TCometSessDir = (
csdInbound, { They called us }
csdOutbound { We called them }
);
{ Session info (visible to main thread) }
TCometSessInfo = record
Active: Boolean;
Direction: TCometSessDir;
Status: TCometSessStatus;
RemoteAddr: TCometAddress; { Remote node address (after handshake)}
RemoteIP: string; { Remote IP address }
RemotePort: Word;
StartTime: TDateTime;
FilesSent: Integer;
FilesRecvd: Integer;
BytesSent: Int64;
BytesRecvd: Int64;
end;
{ Forward declaration }
TCometDaemon = class;
{ Session thread }
TCometSessionThread = class(TThread)
private
FDaemon: TCometDaemon;
FSock: TCometSocket;
FDirection: TCometSessDir;
FRemoteIP: string;
FRemotePort: Word;
FTargetAddr: TCometAddress; { For outbound: who we're calling }
FSlotIndex: Integer; { Index in daemon's session array }
FUseComet: Boolean; { True=Comet, False=BinkP only }
protected
procedure Execute; override;
procedure RunInbound;
procedure RunOutbound;
procedure RunSession(var State: TCometSessionState);
public
constructor Create(ADaemon: TCometDaemon; ASock: TCometSocket;
ADirection: TCometSessDir; const ARemoteIP: string;
ARemotePort: Word; ASlotIndex: Integer);
end;
{ Main daemon class }
TCometDaemon = class
private
FCfg: TCometConfig;
FListenSock: TCometSocket;
FRunning: Boolean;
FShutdown: Boolean;
{ Session management }
FSessions: array[0..COMET_MAX_SESSIONS - 1] of TCometSessInfo;
FThreads: array[0..COMET_MAX_SESSIONS - 1] of TCometSessionThread;
FSessLock: TRTLCriticalSection;
{ Outbound scanning }
FLastPoll: TDateTime;
procedure CleanFinishedSessions;
function FindFreeSlot: Integer;
function IsNodeInSession(const Addr: TCometAddress): Boolean;
procedure HandleInbound;
procedure PollOutbound;
procedure CallNode(const Addr: TCometAddress; const Host: string;
Port: Word);
public
constructor Create;
destructor Destroy; override;
{ Load configuration }
function LoadConfig(const CfgPath: string): Boolean;
{ Start the daemon (blocking - runs until shutdown) }
procedure Run;
{ Signal shutdown (call from signal handler or other thread) }
procedure Shutdown;
{ Get current session info for status display }
function GetSessionInfo(Index: Integer): TCometSessInfo;
function GetActiveSessionCount: Integer;
{ Update a session slot (called by session threads) }
procedure UpdateSession(SlotIndex: Integer; const Info: TCometSessInfo);
procedure MarkSessionDone(SlotIndex: Integer);
property Config: TCometConfig read FCfg;
property Running: Boolean read FRunning;
end;
implementation
{ ---- TCometSessionThread ---- }
constructor TCometSessionThread.Create(ADaemon: TCometDaemon;
ASock: TCometSocket; ADirection: TCometSessDir;
const ARemoteIP: string; ARemotePort: Word; ASlotIndex: Integer);
begin
inherited Create(True); { Create suspended }
FreeOnTerminate := True;
FDaemon := ADaemon;
FSock := ASock;
FDirection := ADirection;
FRemoteIP := ARemoteIP;
FRemotePort := ARemotePort;
FSlotIndex := ASlotIndex;
end;
procedure TCometSessionThread.Execute;
begin
try
case FDirection of
csdInbound: RunInbound;
csdOutbound: RunOutbound;
end;
except
on E: Exception do
LogError('Session thread exception: %s', [E.Message]);
end;
{ Clean up socket }
CometTcpClose(FSock);
{ Unlock BSY if outbound }
if FDirection = csdOutbound then
BSOUnlock(FDaemon.FCfg.Outbound, FTargetAddr,
FDaemon.FCfg.Addresses[0].Zone);
{ Mark slot as done }
FDaemon.MarkSessionDone(FSlotIndex);
end;
procedure TCometSessionThread.RunInbound;
var
State: TCometSessionState;
PeekBuf: string;
HSResult: TCometHandshakeResult;
Info: TCometSessInfo;
BResult: TCometBinkpResult;
InDir: string;
begin
LogInfo('Inbound connection from %s:%d', [FRemoteIP, FRemotePort]);
{ Step 1: Read banner to detect protocol }
if not CometSendBanner(FSock) then
begin
LogWarning('Failed to send banner to %s', [FRemoteIP]);
Exit;
end;
HSResult := CometRecvBanner(FSock, FDaemon.FCfg.TimeoutHandshake, PeekBuf);
case HSResult of
chrBinkP:
begin
LogInfo('BinkP detected from %s - running BinkP session', [FRemoteIP]);
if FDaemon.FCfg.SecInbound <> '' then
InDir := FDaemon.FCfg.SecInbound
else
InDir := FDaemon.FCfg.Inbound;
BResult := BinkpRunInbound(FSock, FDaemon.FCfg,
InDir, FDaemon.FCfg.TempDir, PeekBuf);
if BResult.Success then
begin
Info := FDaemon.GetSessionInfo(FSlotIndex);
Info.FilesSent := BResult.FilesSent;
Info.FilesRecvd := BResult.FilesRecvd;
Info.BytesSent := BResult.BytesSent;
Info.BytesRecvd := BResult.BytesRecvd;
if Length(BResult.RemoteAddrs) > 0 then
Info.RemoteAddr := BResult.RemoteAddrs[0];
Info.Status := cssClosing;
FDaemon.UpdateSession(FSlotIndex, Info);
end;
Exit;
end;
chrOK:
; { Comet protocol - continue }
else
LogWarning('Banner exchange failed with %s: %s',
[FRemoteIP, IntToStr(Ord(HSResult))]);
Exit;
end;
{ Step 2: Full Comet handshake (INIT/INITACK) }
CometSessionInit(State, FSock, False, FRemoteIP, FRemotePort);
try
HSResult := CometHandshake(State, FDaemon.FCfg);
if HSResult <> chrOK then
begin
LogWarning('Handshake failed with %s: %s',
[FRemoteIP, IntToStr(Ord(HSResult))]);
Exit;
end;
{ Update session info with remote address }
if Length(State.RemoteInit.Addresses) > 0 then
begin
FTargetAddr := State.RemoteInit.Addresses[0];
Info := FDaemon.GetSessionInfo(FSlotIndex);
Info.RemoteAddr := FTargetAddr;
Info.Status := cssActive;
FDaemon.UpdateSession(FSlotIndex, Info);
end;
{ Step 3: Run file transfer }
RunSession(State);
finally
CometSessionDone(State);
end;
end;
procedure TCometSessionThread.RunOutbound;
var
State: TCometSessionState;
HSResult: TCometHandshakeResult;
Info: TCometSessInfo;
BResult: TCometBinkpResult;
InDir: string;
SendFiles: TStringList;
Flav: TCometFlavour;
FloPath, PktPath: string;
FloEntries: TCometFloEntryArray;
I: Integer;
begin
LogInfo('Outbound call to %s at %s:%d (%s)',
[CometAddrToStr(FTargetAddr), FRemoteIP, FRemotePort,
BoolToStr(FUseComet, 'Comet', 'BinkP')]);
if FUseComet then
begin
{ ---- Comet protocol path ---- }
CometSessionInit(State, FSock, True, FRemoteIP, FRemotePort);
try
State.OurInit.Password := CometCfgGetPassword(FDaemon.FCfg, FTargetAddr);
HSResult := CometHandshake(State, FDaemon.FCfg);
if HSResult <> chrOK then
begin
LogWarning('Outbound handshake failed with %s: %s',
[CometAddrToStr(FTargetAddr), IntToStr(Ord(HSResult))]);
Exit;
end;
Info := FDaemon.GetSessionInfo(FSlotIndex);
Info.Status := cssActive;
FDaemon.UpdateSession(FSlotIndex, Info);
RunSession(State);
finally
CometSessionDone(State);
end;
end
else
begin
{ ---- BinkP fallback path ---- }
if FDaemon.FCfg.SecInbound <> '' then
InDir := FDaemon.FCfg.SecInbound
else
InDir := FDaemon.FCfg.Inbound;
{ Build list of files to send from BSO outbound }
SendFiles := TStringList.Create;
try
for Flav := Low(TCometFlavour) to High(TCometFlavour) do
begin
if Flav = cfHold then Continue;
{ Add .?UT packet files }
PktPath := BSONodeFile(FDaemon.FCfg.Outbound, FTargetAddr,
FDaemon.FCfg.Addresses[0].Zone, BSOPktExt(Flav));
if CometFileExists(PktPath) then
SendFiles.Add(PktPath);
{ Add files from .FLO flow files }
FloPath := BSONodeFile(FDaemon.FCfg.Outbound, FTargetAddr,
FDaemon.FCfg.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 <> '') and
FileExists(FloEntries[I].FilePath) then
SendFiles.Add(FloEntries[I].FilePath);
end;
end;
end;
Info := FDaemon.GetSessionInfo(FSlotIndex);
Info.Status := cssActive;
FDaemon.UpdateSession(FSlotIndex, Info);
BResult := BinkpRunOutbound(FSock, FDaemon.FCfg, FTargetAddr,
InDir, FDaemon.FCfg.TempDir, SendFiles);
if BResult.Success then
begin
{ Mark sent files in flow files }
for Flav := Low(TCometFlavour) to High(TCometFlavour) do
begin
if Flav = cfHold then Continue;
PktPath := BSONodeFile(FDaemon.FCfg.Outbound, FTargetAddr,
FDaemon.FCfg.Addresses[0].Zone, BSOPktExt(Flav));
if FileExists(PktPath) then
DeleteFile(PktPath);
FloPath := BSONodeFile(FDaemon.FCfg.Outbound, FTargetAddr,
FDaemon.FCfg.Addresses[0].Zone, BSOFloExt(Flav));
if FileExists(FloPath) then
BSOCleanFlo(FloPath);
end;
Info := FDaemon.GetSessionInfo(FSlotIndex);
Info.FilesSent := BResult.FilesSent;
Info.FilesRecvd := BResult.FilesRecvd;
Info.BytesSent := BResult.BytesSent;
Info.BytesRecvd := BResult.BytesRecvd;
Info.Status := cssClosing;
FDaemon.UpdateSession(FSlotIndex, Info);
end;
finally
SendFiles.Free;
end;
end;
end;
procedure TCometSessionThread.RunSession(var State: TCometSessionState);
var
XS: TCometXferState;
FloPath: string;
FloEntries: TCometFloEntryArray;
Flav: TCometFlavour;
PktPath: string;
I: Integer;
XResult: Integer;
InboundDir: string;
TF: file;
Info: TCometSessInfo;
begin
{ Determine inbound directory: secure if password matched }
if State.Session.PasswordOk and (FDaemon.FCfg.SecInbound <> '') then
InboundDir := FDaemon.FCfg.SecInbound
else
InboundDir := FDaemon.FCfg.Inbound;
CometXferInit(XS, State, InboundDir, FDaemon.FCfg.TempDir,
CometAddSlash(FDaemon.FCfg.TempDir) + 'comet-abort.log');
try
if FDirection = csdOutbound then
begin
{ Send our files to the remote }
{ First send any .?UT packet files }
for Flav := Low(TCometFlavour) to High(TCometFlavour) do
begin
if Flav = cfHold then Continue; { Don't send hold mail }
PktPath := BSONodeFile(FDaemon.FCfg.Outbound, FTargetAddr,
FDaemon.FCfg.Addresses[0].Zone, BSOPktExt(Flav));
if CometFileExists(PktPath) then
begin
XResult := CometTransfer(XS, PktPath, '');
if XResult = XFER_ABORT then Exit;
if XResult = XFER_OK then
DeleteFile(PktPath);
end;
end;
{ Then send files from .FLO flow files }
for Flav := Low(TCometFlavour) to High(TCometFlavour) do
begin
if Flav = cfHold then Continue;
FloPath := BSONodeFile(FDaemon.FCfg.Outbound, FTargetAddr,
FDaemon.FCfg.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 Exit;
if XResult = XFER_OK then
begin
{ Mark sent in flow file }
case FloEntries[I].Action of
csaDelete:
DeleteFile(FloEntries[I].FilePath);
csaTruncate:
begin
{ Truncate to zero }
AssignFile(TF, FloEntries[I].FilePath);
{$I-} Rewrite(TF, 1); {$I+}
if IOResult = 0 then CloseFile(TF);
end;
end;
BSOMarkSent(FloPath, FloEntries[I].OrigPath);
end;
end;
{ Clean processed entries }
BSOCleanFlo(FloPath);
end;
end;
{ Signal end of batch - this also handles receiving remaining files }
CometTransfer(XS, '', '');
{ Update final stats }
Info := FDaemon.GetSessionInfo(FSlotIndex);
Info.FilesSent := XS.FilesSent;
Info.FilesRecvd := XS.FilesRecvd;
Info.BytesSent := XS.BytesSent;
Info.BytesRecvd := XS.BytesRecvd;
Info.Status := cssClosing;
FDaemon.UpdateSession(FSlotIndex, Info);
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;
end;
{ ---- TCometDaemon ---- }
constructor TCometDaemon.Create;
var
I: Integer;
begin
inherited Create;
FListenSock := COMET_TCP_INVALID;
FRunning := False;
FShutdown := False;
FLastPoll := 0;
InitCriticalSection(FSessLock);
for I := 0 to COMET_MAX_SESSIONS - 1 do
begin
FillChar(FSessions[I], SizeOf(TCometSessInfo), 0);
FThreads[I] := nil;
end;
end;
destructor TCometDaemon.Destroy;
begin
if FListenSock <> COMET_TCP_INVALID then
CometTcpClose(FListenSock);
DoneCriticalSection(FSessLock);
inherited Destroy;
end;
function TCometDaemon.LoadConfig(const CfgPath: string): Boolean;
begin
Result := CometCfgLoad(CfgPath, FCfg);
if Result then
CometCfgApply(FCfg);
end;
procedure TCometDaemon.Run;
var
Deadline: TDateTime;
begin
if Length(FCfg.Addresses) = 0 then
begin
LogFatal('No FidoNet addresses configured');
Exit;
end;
if FCfg.Inbound = '' then
begin
LogFatal('No inbound directory configured');
Exit;
end;
if FCfg.Outbound = '' then
begin
LogFatal('No outbound directory configured');
Exit;
end;
{ Ensure directories exist }
CometMakePath(FCfg.Inbound);
if FCfg.SecInbound <> '' then
CometMakePath(FCfg.SecInbound);
CometMakePath(FCfg.Outbound);
if FCfg.TempDir <> '' then
CometMakePath(FCfg.TempDir);
{ Start listener }
FListenSock := CometTcpListen(FCfg.BindAddr, FCfg.ListenPort);
if FListenSock = COMET_TCP_INVALID then
begin
LogFatal('Cannot listen on port %d', [FCfg.ListenPort]);
Exit;
end;
LogInfo('Comet %s started - listening on port %d', [COMET_VERSION, FCfg.ListenPort]);
LogInfo('Address: %s System: %s SysOp: %s',
[CometAddrToStr(FCfg.Addresses[0]), FCfg.SysName, FCfg.SysOp]);
FRunning := True;
{ Main loop }
while not FShutdown do
begin
{ Accept inbound connections }
HandleInbound;
{ Poll outbound on interval }
if (FCfg.PollInterval > 0) and
(Now > FLastPoll + (FCfg.PollInterval / 86400.0)) then
begin
PollOutbound;
FLastPoll := Now;
end;
{ Clean up finished session threads }
CleanFinishedSessions;
{ Brief sleep to prevent CPU spin }
Sleep(100);
end;
{ Shutdown: wait for active sessions }
LogInfo('Shutting down - waiting for active sessions...');
{ Give sessions a few seconds to finish }
begin
Deadline := Now + (10.0 / 86400.0);
while (GetActiveSessionCount > 0) and (Now < Deadline) do
begin
CleanFinishedSessions;
Sleep(200);
end;
end;
if FListenSock <> COMET_TCP_INVALID then
CometTcpClose(FListenSock);
FRunning := False;
LogInfo('Comet daemon stopped');
end;
procedure TCometDaemon.Shutdown;
begin
FShutdown := True;
end;
{ ---- Session slot management ---- }
function TCometDaemon.FindFreeSlot: Integer;
var
I: Integer;
begin
Result := -1;
for I := 0 to COMET_MAX_SESSIONS - 1 do
begin
if not FSessions[I].Active then
begin
Result := I;
Exit;
end;
end;
end;
function TCometDaemon.IsNodeInSession(const Addr: TCometAddress): Boolean;
var
I: Integer;
begin
Result := False;
EnterCriticalSection(FSessLock);
try
for I := 0 to COMET_MAX_SESSIONS - 1 do
begin
if FSessions[I].Active and
(FSessions[I].RemoteAddr.Zone = Addr.Zone) and
(FSessions[I].RemoteAddr.Net = Addr.Net) and
(FSessions[I].RemoteAddr.Node = Addr.Node) and
(FSessions[I].RemoteAddr.Point = Addr.Point) then
begin
Result := True;
Exit;
end;
end;
finally
LeaveCriticalSection(FSessLock);
end;
end;
function TCometDaemon.GetSessionInfo(Index: Integer): TCometSessInfo;
begin
Result := Default(TCometSessInfo);
if (Index < 0) or (Index >= COMET_MAX_SESSIONS) then Exit;
EnterCriticalSection(FSessLock);
try
Result := FSessions[Index];
finally
LeaveCriticalSection(FSessLock);
end;
end;
function TCometDaemon.GetActiveSessionCount: Integer;
var
I: Integer;
begin
Result := 0;
EnterCriticalSection(FSessLock);
try
for I := 0 to COMET_MAX_SESSIONS - 1 do
if FSessions[I].Active then Inc(Result);
finally
LeaveCriticalSection(FSessLock);
end;
end;
procedure TCometDaemon.UpdateSession(SlotIndex: Integer;
const Info: TCometSessInfo);
begin
if (SlotIndex < 0) or (SlotIndex >= COMET_MAX_SESSIONS) then Exit;
EnterCriticalSection(FSessLock);
try
FSessions[SlotIndex] := Info;
finally
LeaveCriticalSection(FSessLock);
end;
end;
procedure TCometDaemon.MarkSessionDone(SlotIndex: Integer);
begin
if (SlotIndex < 0) or (SlotIndex >= COMET_MAX_SESSIONS) then Exit;
EnterCriticalSection(FSessLock);
try
FSessions[SlotIndex].Status := cssDone;
finally
LeaveCriticalSection(FSessLock);
end;
end;
procedure TCometDaemon.CleanFinishedSessions;
var
I: Integer;
begin
EnterCriticalSection(FSessLock);
try
for I := 0 to COMET_MAX_SESSIONS - 1 do
begin
if FSessions[I].Active and (FSessions[I].Status = cssDone) then
begin
FSessions[I].Active := False;
FThreads[I] := nil; { Thread is FreeOnTerminate }
end;
end;
finally
LeaveCriticalSection(FSessLock);
end;
end;
{ ---- Inbound handling ---- }
procedure TCometDaemon.HandleInbound;
var
ConnInfo: TCometConnInfo;
Slot: Integer;
SessInfo: TCometSessInfo;
Thread: TCometSessionThread;
begin
if not CometTcpAccept(FListenSock, ConnInfo) then Exit;
{ Check session limit }
EnterCriticalSection(FSessLock);
try
if GetActiveSessionCount >= FCfg.MaxSessions then
begin
LogWarning('Max sessions reached, rejecting %s:%d',
[ConnInfo.RemoteIP, ConnInfo.RemotePort]);
CometTcpClose(ConnInfo.Socket);
Exit;
end;
Slot := FindFreeSlot;
if Slot < 0 then
begin
CometTcpClose(ConnInfo.Socket);
Exit;
end;
{ Reserve the slot }
FillChar(SessInfo, SizeOf(SessInfo), 0);
SessInfo.Active := True;
SessInfo.Direction := csdInbound;
SessInfo.Status := cssConnecting;
SessInfo.RemoteIP := ConnInfo.RemoteIP;
SessInfo.RemotePort := ConnInfo.RemotePort;
SessInfo.StartTime := Now;
FSessions[Slot] := SessInfo;
{ Create and start session thread }
Thread := TCometSessionThread.Create(Self, ConnInfo.Socket,
csdInbound, ConnInfo.RemoteIP, ConnInfo.RemotePort, Slot);
FThreads[Slot] := Thread;
finally
LeaveCriticalSection(FSessLock);
end;
Thread.Start;
end;
{ ---- Outbound polling ---- }
procedure TCometDaemon.PollOutbound;
var
Items: TCometOutboundItemArray;
I: Integer;
NodeIdx: Integer;
Host: string;
Port: Word;
begin
if FCfg.Outbound = '' then Exit;
Items := BSOScanOutbound(FCfg.Outbound, FCfg.Addresses[0].Zone);
for I := 0 to High(Items) do
begin
{ Skip hold-only, busy, or already-in-session nodes }
if Items[I].IsBusy then Continue;
if (not Items[I].HasMail) and (not Items[I].HasFiles) then Continue;
if IsNodeInSession(Items[I].Address) then Continue;
{ Check session limit }
if GetActiveSessionCount >= FCfg.MaxSessions then Break;
{ Determine how to reach this node }
NodeIdx := CometCfgFindNode(FCfg, Items[I].Address);
if NodeIdx >= 0 then
begin
Host := FCfg.Nodes[NodeIdx].Host;
Port := FCfg.Nodes[NodeIdx].Port;
if Port = 0 then Port := COMET_PORT;
end
else
begin
{ No config entry - would need nodelist lookup.
For now, skip nodes without explicit config. }
Continue;
end;
if Host = '' then Continue; { No IP/hostname known }
CallNode(Items[I].Address, Host, Port);
end;
end;
procedure TCometDaemon.CallNode(const Addr: TCometAddress;
const Host: string; Port: Word);
var
Sock: TCometSocket;
Slot: Integer;
SessInfo: TCometSessInfo;
Thread: TCometSessionThread;
NodeIdx: Integer;
BinkpPort: Word;
UseComet: Boolean;
begin
{ Try to lock the node }
if not BSOLock(FCfg.Outbound, Addr, FCfg.Addresses[0].Zone,
COMET_BSY_STALE_MIN) then
begin
LogDebug('Node %s is busy', [CometAddrToStr(Addr)]);
Exit;
end;
{ Check per-node settings for protocol preference }
NodeIdx := CometCfgFindNode(FCfg, Addr);
UseComet := True;
BinkpPort := FCfg.BinkpPort;
if NodeIdx >= 0 then
begin
if FCfg.Nodes[NodeIdx].NoComet then UseComet := False;
if FCfg.Nodes[NodeIdx].BinkpPort <> 0 then
BinkpPort := FCfg.Nodes[NodeIdx].BinkpPort;
end;
Sock := COMET_TCP_INVALID;
{ Try Comet protocol first (unless NoComet set for this node) }
if UseComet then
begin
LogInfo('Calling %s at %s:%d (Comet)', [CometAddrToStr(Addr), Host, Port]);
Sock := CometTcpConnect(Host, Port, 15000);
end;
{ If Comet connect failed and BinkP is enabled, try BinkP port }
if (Sock = COMET_TCP_INVALID) and FCfg.BinkpEnabled then
begin
if UseComet then
LogInfo('Comet connect failed, trying BinkP on port %d', [BinkpPort])
else
LogInfo('Calling %s at %s:%d (BinkP)', [CometAddrToStr(Addr), Host, BinkpPort]);
Sock := CometTcpConnect(Host, BinkpPort, 15000);
if Sock = COMET_TCP_INVALID then
begin
LogWarning('Cannot connect to %s at %s (tried ports %d and %d)',
[CometAddrToStr(Addr), Host, Port, BinkpPort]);
BSOUnlock(FCfg.Outbound, Addr, FCfg.Addresses[0].Zone);
Exit;
end;
{ Connected on BinkP port - mark as non-Comet so the session thread
knows to run BinkP directly instead of Comet handshake }
UseComet := False;
end;
if Sock = COMET_TCP_INVALID then
begin
LogWarning('Cannot connect to %s at %s:%d',
[CometAddrToStr(Addr), Host, Port]);
BSOUnlock(FCfg.Outbound, Addr, FCfg.Addresses[0].Zone);
Exit;
end;
{ Reserve session slot }
EnterCriticalSection(FSessLock);
try
Slot := FindFreeSlot;
if Slot < 0 then
begin
CometTcpClose(Sock);
BSOUnlock(FCfg.Outbound, Addr, FCfg.Addresses[0].Zone);
Exit;
end;
FillChar(SessInfo, SizeOf(SessInfo), 0);
SessInfo.Active := True;
SessInfo.Direction := csdOutbound;
SessInfo.Status := cssConnecting;
SessInfo.RemoteAddr := Addr;
SessInfo.RemoteIP := Host;
SessInfo.RemotePort := Port;
SessInfo.StartTime := Now;
FSessions[Slot] := SessInfo;
Thread := TCometSessionThread.Create(Self, Sock,
csdOutbound, Host, Port, Slot);
Thread.FTargetAddr := Addr;
Thread.FUseComet := UseComet;
FThreads[Slot] := Thread;
finally
LeaveCriticalSection(FSessLock);
end;
Thread.Start;
end;
end.