Files
comet/cometlog.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

467 lines
10 KiB
ObjectPascal

{
Comet - Direct TCP File Transfer for FidoNet
cometlog.pas - Logging subsystem
Supports simultaneous output to:
- Log file (with timestamps, rotatable)
- Console (stdout, with optional color)
- Debug file (verbose protocol-level trace)
Log levels: Fatal, Error, Warning, Info, Debug
Each output target has its own minimum level filter.
Log format (file):
2026-03-29 14:30:05 + Connecting to 1:213/723
2026-03-29 14:30:06 ! Connection refused
2026-03-29 14:30:06 * Falling back to BinkP
Prefix characters by level:
! Fatal/Error
# Warning
+ Info
* Debug
Thread-safe: all writes go through a critical section.
Copyright (C) 2026 Ken Johnson
License: GPL-2.0
}
unit cometlog;
{$mode objfpc}{$H+}
interface
uses
SysUtils;
type
TCometLogLevel = (
cllDebug = 0,
cllInfo = 1,
cllWarning = 2,
cllError = 3,
cllFatal = 4
);
const
{ Level names for display }
CometLogLevelName: array[TCometLogLevel] of string = (
'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'
);
{ Prefix chars for log file lines }
CometLogLevelPrefix: array[TCometLogLevel] of Char = (
'*', '+', '#', '!', '!'
);
{ ---- Initialization ---- }
{ Open the log file. Path is the full path to the log file.
Appends if file exists. Creates parent directories if needed. }
procedure CometLogOpen(const LogPath: string);
{ Open a separate debug log file for protocol-level tracing.
Only written to when debug logging is enabled. }
procedure CometLogOpenDebug(const DebugPath: string);
{ Close all log files }
procedure CometLogClose;
{ ---- Configuration ---- }
{ Set minimum log level for file output (default: cllInfo) }
procedure CometLogSetFileLevel(Level: TCometLogLevel);
{ Set minimum log level for console output (default: cllInfo) }
procedure CometLogSetConsoleLevel(Level: TCometLogLevel);
{ Enable/disable debug log (default: disabled) }
procedure CometLogSetDebug(Enabled: Boolean);
{ Enable/disable console output (default: enabled) }
procedure CometLogSetConsole(Enabled: Boolean);
{ Enable/disable timestamps in console output (default: disabled) }
procedure CometLogSetConsoleTimestamp(Enabled: Boolean);
{ ---- Logging ---- }
{ Log a message at the specified level }
procedure CometLog(Level: TCometLogLevel; const Msg: string);
procedure CometLog(Level: TCometLogLevel; const Fmt: string; const Args: array of const);
{ Convenience wrappers }
procedure LogDebug(const Msg: string);
procedure LogDebug(const Fmt: string; const Args: array of const);
procedure LogInfo(const Msg: string);
procedure LogInfo(const Fmt: string; const Args: array of const);
procedure LogWarning(const Msg: string);
procedure LogWarning(const Fmt: string; const Args: array of const);
procedure LogError(const Msg: string);
procedure LogError(const Fmt: string; const Args: array of const);
procedure LogFatal(const Msg: string);
procedure LogFatal(const Fmt: string; const Args: array of const);
{ Write to debug log only (protocol trace, hex dumps, etc.).
Only written if debug logging is enabled. }
procedure LogTrace(const Msg: string);
procedure LogTrace(const Fmt: string; const Args: array of const);
{ Log a hex dump of a buffer to the debug log }
procedure LogTraceHex(const Label_: string; Data: PByte; Len: LongInt);
implementation
var
LogFile: TextFile;
LogFileOpen: Boolean = False;
LogFilePath: string = '';
DebugFile: TextFile;
DebugFileOpen: Boolean = False;
DebugFilePath: string = '';
FileLevel: TCometLogLevel = cllInfo;
ConsoleLevel: TCometLogLevel = cllInfo;
DebugEnabled: Boolean = False;
ConsoleOn: Boolean = True;
ConsoleTS: Boolean = False;
LogLock: TRTLCriticalSection;
LogInited: Boolean = False;
procedure EnsureInit;
begin
if LogInited then Exit;
InitCriticalSection(LogLock);
LogInited := True;
end;
function LogTimestamp: string;
var
Now: TDateTime;
Y, M, D, HH, MM, SS, MS: Word;
begin
Now := SysUtils.Now;
DecodeDate(Now, Y, M, D);
DecodeTime(Now, HH, MM, SS, MS);
Result := Format('%04d-%02d-%02d %02d:%02d:%02d', [Y, M, D, HH, MM, SS]);
end;
{ ---- Initialization ---- }
procedure CometLogOpen(const LogPath: string);
begin
EnsureInit;
EnterCriticalSection(LogLock);
try
if LogFileOpen then
begin
CloseFile(LogFile);
LogFileOpen := False;
end;
LogFilePath := LogPath;
if LogFilePath = '' then Exit;
{ Create parent directory if needed }
ForceDirectories(ExtractFilePath(LogFilePath));
AssignFile(LogFile, LogFilePath);
{$I-}
if FileExists(LogFilePath) then
Append(LogFile)
else
Rewrite(LogFile);
{$I+}
if IOResult = 0 then
begin
LogFileOpen := True;
WriteLn(LogFile, '');
WriteLn(LogFile, LogTimestamp, ' + Comet log opened');
Flush(LogFile);
end;
finally
LeaveCriticalSection(LogLock);
end;
end;
procedure CometLogOpenDebug(const DebugPath: string);
begin
EnsureInit;
EnterCriticalSection(LogLock);
try
if DebugFileOpen then
begin
CloseFile(DebugFile);
DebugFileOpen := False;
end;
DebugFilePath := DebugPath;
if DebugFilePath = '' then Exit;
ForceDirectories(ExtractFilePath(DebugFilePath));
AssignFile(DebugFile, DebugFilePath);
{$I-}
if FileExists(DebugFilePath) then
Append(DebugFile)
else
Rewrite(DebugFile);
{$I+}
if IOResult = 0 then
begin
DebugFileOpen := True;
WriteLn(DebugFile, '');
WriteLn(DebugFile, '=== Comet debug session started ', LogTimestamp, ' ===');
Flush(DebugFile);
end;
finally
LeaveCriticalSection(LogLock);
end;
end;
procedure CometLogClose;
begin
if not LogInited then Exit;
EnterCriticalSection(LogLock);
try
if LogFileOpen then
begin
WriteLn(LogFile, LogTimestamp, ' + Comet log closed');
CloseFile(LogFile);
LogFileOpen := False;
end;
if DebugFileOpen then
begin
WriteLn(DebugFile, '=== Comet debug session ended ', LogTimestamp, ' ===');
CloseFile(DebugFile);
DebugFileOpen := False;
end;
finally
LeaveCriticalSection(LogLock);
end;
end;
{ ---- Configuration ---- }
procedure CometLogSetFileLevel(Level: TCometLogLevel);
begin
FileLevel := Level;
end;
procedure CometLogSetConsoleLevel(Level: TCometLogLevel);
begin
ConsoleLevel := Level;
end;
procedure CometLogSetDebug(Enabled: Boolean);
begin
DebugEnabled := Enabled;
end;
procedure CometLogSetConsole(Enabled: Boolean);
begin
ConsoleOn := Enabled;
end;
procedure CometLogSetConsoleTimestamp(Enabled: Boolean);
begin
ConsoleTS := Enabled;
end;
{ ---- Core logging ---- }
procedure CometLog(Level: TCometLogLevel; const Msg: string);
var
TS: string;
Prefix: Char;
Line: string;
begin
EnsureInit;
Prefix := CometLogLevelPrefix[Level];
TS := LogTimestamp;
Line := TS + ' ' + Prefix + ' ' + Msg;
EnterCriticalSection(LogLock);
try
{ Write to log file }
if LogFileOpen and (Level >= FileLevel) then
begin
WriteLn(LogFile, Line);
Flush(LogFile);
end;
{ Write to console }
if ConsoleOn and (Level >= ConsoleLevel) then
begin
if ConsoleTS then
WriteLn(Line)
else
WriteLn(Prefix, ' ', Msg);
end;
{ Write to debug log if enabled }
if DebugFileOpen and DebugEnabled then
begin
WriteLn(DebugFile, Line);
Flush(DebugFile);
end;
finally
LeaveCriticalSection(LogLock);
end;
end;
procedure CometLog(Level: TCometLogLevel; const Fmt: string; const Args: array of const);
begin
CometLog(Level, Format(Fmt, Args));
end;
{ ---- Convenience wrappers ---- }
procedure LogDebug(const Msg: string);
begin
CometLog(cllDebug, Msg);
end;
procedure LogDebug(const Fmt: string; const Args: array of const);
begin
CometLog(cllDebug, Fmt, Args);
end;
procedure LogInfo(const Msg: string);
begin
CometLog(cllInfo, Msg);
end;
procedure LogInfo(const Fmt: string; const Args: array of const);
begin
CometLog(cllInfo, Fmt, Args);
end;
procedure LogWarning(const Msg: string);
begin
CometLog(cllWarning, Msg);
end;
procedure LogWarning(const Fmt: string; const Args: array of const);
begin
CometLog(cllWarning, Fmt, Args);
end;
procedure LogError(const Msg: string);
begin
CometLog(cllError, Msg);
end;
procedure LogError(const Fmt: string; const Args: array of const);
begin
CometLog(cllError, Fmt, Args);
end;
procedure LogFatal(const Msg: string);
begin
CometLog(cllFatal, Msg);
end;
procedure LogFatal(const Fmt: string; const Args: array of const);
begin
CometLog(cllFatal, Fmt, Args);
end;
{ ---- Debug trace ---- }
procedure LogTrace(const Msg: string);
begin
if not DebugEnabled then Exit;
EnsureInit;
EnterCriticalSection(LogLock);
try
if DebugFileOpen then
begin
WriteLn(DebugFile, LogTimestamp, ' ', Msg);
Flush(DebugFile);
end;
finally
LeaveCriticalSection(LogLock);
end;
end;
procedure LogTrace(const Fmt: string; const Args: array of const);
begin
if not DebugEnabled then Exit;
LogTrace(Format(Fmt, Args));
end;
procedure LogTraceHex(const Label_: string; Data: PByte; Len: LongInt);
var
I: LongInt;
Line: string;
MaxDump: LongInt;
begin
if not DebugEnabled then Exit;
if not DebugFileOpen then Exit;
EnsureInit;
EnterCriticalSection(LogLock);
try
WriteLn(DebugFile, LogTimestamp, ' ', Label_, ' (', Len, ' bytes):');
MaxDump := Len;
if MaxDump > 256 then MaxDump := 256;
Line := '';
I := 0;
while I < MaxDump do
begin
if (I mod 16) = 0 then
begin
if I > 0 then WriteLn(DebugFile, Line);
Line := Format(' %04X: ', [I]);
end;
Line := Line + Format('%02X ', [Data[I]]);
Inc(I);
end;
if Line <> '' then
WriteLn(DebugFile, Line);
if Len > 256 then
WriteLn(DebugFile, Format(' ... (%d more bytes)', [Len - 256]));
Flush(DebugFile);
finally
LeaveCriticalSection(LogLock);
end;
end;
{ ---- Cleanup ---- }
finalization
if LogInited then
begin
CometLogClose;
DoneCriticalSection(LogLock);
end;
end.