Repository navigation
Expand file tree
/
Copy pathLoggerPro.ConsoleAppender.pas
More file actions
452 lines (414 loc) · 13.7 KB
/
Copy pathLoggerPro.ConsoleAppender.pas
File metadata and controls
452 lines (414 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
// *************************************************************************** }
//
// LoggerPro
//
// Copyright (c) 2010-2026 Daniele Teti
//
// https://github.com/danieleteti/loggerpro
//
// Contributors for this file:
// Fulgan - https://github.com/Fulgan
// David Cornelius
//
// ***************************************************************************
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// ***************************************************************************
unit LoggerPro.ConsoleAppender;
interface
uses
System.Classes,
System.SysUtils,
LoggerPro,
System.SyncObjs;
type
/// <summary>
/// Cross-platform console appender with optional color support.
/// On Windows: Uses Windows Console API for colors and can create/attach consoles for GUI apps.
/// On Linux/macOS: Uses ANSI escape codes for colors.
/// </summary>
TLoggerProConsoleAppender = class(TLoggerProAppenderBase)
strict private
class var FLock: TCriticalSection;
{$IFDEF MSWINDOWS}
class var FConsoleAllocated: Int64;
{$ENDIF}
class constructor Create;
class destructor Destroy;
protected
FUTF8Output: Boolean;
FRendererHandlesColors: Boolean;
FForceNewConsole: Boolean;
{$IFDEF MSWINDOWS}
FSavedOutputCP: Cardinal;
fColors: array [TLogType.Debug .. TLogType.Fatal] of Integer;
fSavedColors: Integer;
{$ELSE}
fColors: array [TLogType.Debug .. TLogType.Fatal] of string;
{$ENDIF}
procedure SetColor(const aLogType: TLogType);
procedure ResetColor;
procedure SetupColorMappings; virtual;
/// <summary>
/// Writes a line of text as UTF-8 bytes directly to stdout, bypassing Writeln.
/// </summary>
procedure WriteUTF8Line(const aText: string);
/// <summary>
/// Writes text as UTF-8 bytes directly to stdout without appending a line break.
/// Used for ANSI color escape codes.
/// </summary>
procedure WriteUTF8Raw(const aText: string);
public
procedure Setup; override;
procedure TearDown; override;
procedure WriteLog(const aLogItem: TLogItem); override;
/// <summary>
/// When True, writes UTF-8 bytes directly to stdout instead of using Writeln.
/// Prevents Unicode mangling on Linux (POSIX locale) and Windows (console code page).
/// </summary>
property UTF8Output: Boolean read FUTF8Output write FUTF8Output;
/// <summary>
/// When True, the renderer emits its own ANSI color codes. The appender
/// skips its per-line SetColor/ResetColor so the terminal sees the
/// renderer's multi-color output. Set by WithColors / WithColorScheme.
/// </summary>
property RendererHandlesColors: Boolean read FRendererHandlesColors write FRendererHandlesColors;
/// <summary>
/// Windows only. When True, Setup skips AttachConsole(ATTACH_PARENT_PROCESS)
/// and goes straight to AllocConsole. Use this for GUI applications that
/// must always pop up a fresh, visible console window, regardless of
/// whether the parent process (e.g. the Delphi IDE) already has one.
/// No effect on POSIX or for console-subsystem apps (IsConsole = True).
/// </summary>
property ForceNewConsole: Boolean read FForceNewConsole write FForceNewConsole;
end;
TLoggerProConsoleLogFmtAppender = class(TLoggerProConsoleAppender)
public
constructor Create(ALogItemRenderer: ILogItemRenderer = nil); override;
function FormatLog(const ALogItem: TLogItem): string; override;
end;
/// <summary>
/// Simple cross-platform console appender without colors.
/// Uses plain Writeln, works on all platforms (Windows, Linux, macOS).
/// </summary>
TLoggerProSimpleConsoleAppender = class(TLoggerProAppenderBase)
strict private
FUTF8Output: Boolean;
FUseStdErr: Boolean;
{$IFDEF MSWINDOWS}
FSavedOutputCP: Cardinal;
{$ENDIF}
/// <summary>
/// Writes a line of text as UTF-8 bytes directly to the selected stream,
/// bypassing Writeln.
/// </summary>
procedure WriteUTF8Line(const aText: string);
public
procedure Setup; override;
procedure TearDown; override;
procedure WriteLog(const aLogItem: TLogItem); override;
/// <summary>
/// When True, writes UTF-8 bytes directly instead of using Writeln.
/// Prevents Unicode mangling on Linux (POSIX locale) and Windows (console code page).
/// </summary>
property UTF8Output: Boolean read FUTF8Output write FUTF8Output;
/// <summary>
/// When True, log lines are written to stderr instead of stdout.
/// Default: False. Typical use case: MCP servers and Unix daemons where
/// stdout is reserved for protocol/data and diagnostic output belongs on
/// stderr. No colors are emitted regardless of the target stream.
/// </summary>
property UseStdErr: Boolean read FUseStdErr write FUseStdErr;
end;
TLoggerProSimpleConsoleLogFmtAppender = class(TLoggerProSimpleConsoleAppender)
public
constructor Create(ALogItemRenderer: ILogItemRenderer = nil); override;
function FormatLog(const ALogItem: TLogItem): string; override;
end;
{$IFDEF MSWINDOWS}
function AttachConsole(PID: Cardinal): LongBool; stdcall;
{$ENDIF}
implementation
uses
{$IFDEF MSWINDOWS}
Winapi.Windows,
{$ENDIF}
{$IFDEF POSIX}
Posix.Unistd,
{$ENDIF}
LoggerPro.Renderers,
LoggerPro.AnsiColors;
{$IFDEF MSWINDOWS}
const
ATTACH_PARENT_PROCESS = Cardinal(-1);
function AttachConsole; external kernel32 name 'AttachConsole';
// FOREGROUND_*, BACKGROUND_*, FOREGROUND_INTENSITY, BACKGROUND_INTENSITY
// come from Winapi.Windows - no need to redeclare.
function GetCurrentColors: Integer;
var
info: CONSOLE_SCREEN_BUFFER_INFO;
begin
Result := -1;
if GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), info) then
Result := info.wAttributes;
end;
{$ENDIF}
// POSIX color codes come from LoggerPro.AnsiColors (FORE_*, STYLE_*).
procedure InternalWriteUTF8(const aText: string; const aUseStdErr: Boolean = False);
var
lBytes: TBytes;
{$IFDEF MSWINDOWS}
hOut: THandle;
lBytesWritten: Cardinal;
lRemaining, lOffset: Integer;
{$ENDIF}
begin
if aText = '' then
Exit;
lBytes := TEncoding.UTF8.GetBytes(aText);
if Length(lBytes) = 0 then
Exit;
{$IFDEF MSWINDOWS}
if aUseStdErr then
hOut := GetStdHandle(STD_ERROR_HANDLE)
else
hOut := GetStdHandle(STD_OUTPUT_HANDLE);
if hOut = INVALID_HANDLE_VALUE then
Exit;
// Raw byte write through WriteFile. Works for both consoles (Windows 10+
// with VT enabled interprets the ANSI escape codes in the stream) and
// redirected pipes/files. Looped to drain partial writes that WriteFile
// can perform under load.
lOffset := 0;
lRemaining := Length(lBytes);
while lRemaining > 0 do
begin
lBytesWritten := 0;
if not WriteFile(hOut, lBytes[lOffset], lRemaining, lBytesWritten, nil) then
Exit;
if lBytesWritten = 0 then
Exit;
Inc(lOffset, lBytesWritten);
Dec(lRemaining, lBytesWritten);
end;
{$ELSE}
if aUseStdErr then
__write(STDERR_FILENO, @lBytes[0], Length(lBytes))
else
__write(STDOUT_FILENO, @lBytes[0], Length(lBytes));
{$ENDIF}
end;
{ TLoggerProConsoleAppender }
class constructor TLoggerProConsoleAppender.Create;
begin
FLock := TCriticalSection.Create;
{$IFDEF MSWINDOWS}
FConsoleAllocated := 0;
{$ENDIF}
end;
class destructor TLoggerProConsoleAppender.Destroy;
begin
try
FLock.Enter;
FreeAndNil(FLock);
except
// No exception checking here or the app might blow up with a RTE 217
end;
end;
procedure TLoggerProConsoleAppender.Setup;
begin
inherited;
SetupColorMappings;
{$IFDEF MSWINDOWS}
// Default: this appender did not save the console state. Only the FIRST
// appender that enters the class-level guard block actually saves colors.
// Subsequent appenders must NOT overwrite console attributes in TearDown
// (otherwise their zero-value fSavedColors would set black on black).
fSavedColors := -1;
if TInterlocked.Read(FConsoleAllocated) < 2 then
begin
FLock.Enter;
try
if TInterlocked.Increment(FConsoleAllocated) = 1 then
begin
// For GUI apps (IsConsole = False) we need a console handle.
// - ForceNewConsole = True -> always AllocConsole (pops up a fresh
// window even when launched from the Delphi IDE, whose parent
// process bds.exe owns a console that AttachConsole would silently
// attach to, leaving the user with no visible window).
// - ForceNewConsole = False -> default behavior: try to attach to
// the parent's console (cmd.exe, PowerShell, ...) and only
// allocate a new one if there isn't one.
if not IsConsole then
begin
if FForceNewConsole then
AllocConsole
else if not AttachConsole(ATTACH_PARENT_PROCESS) then
AllocConsole;
end;
fSavedColors := GetCurrentColors;
TInterlocked.Increment(FConsoleAllocated);
end;
finally
FLock.Leave;
end;
end;
if FUTF8Output then
begin
FSavedOutputCP := GetConsoleOutputCP;
SetConsoleOutputCP(CP_UTF8);
end;
{$ENDIF}
if FRendererHandlesColors then
EnableANSIColorConsole;
end;
procedure TLoggerProConsoleAppender.SetupColorMappings;
begin
{$IFDEF MSWINDOWS}
fColors[TLogType.Debug] := FOREGROUND_GREEN or FOREGROUND_INTENSITY;
fColors[TLogType.Info] := FOREGROUND_RED or FOREGROUND_GREEN or FOREGROUND_BLUE or FOREGROUND_INTENSITY;
fColors[TLogType.Warning] := FOREGROUND_RED or FOREGROUND_GREEN; // Dark yellow/orange
fColors[TLogType.Error] := FOREGROUND_RED or FOREGROUND_INTENSITY;
fColors[TLogType.Fatal] := FOREGROUND_RED or FOREGROUND_BLUE or FOREGROUND_INTENSITY;
{$ELSE}
fColors[TLogType.Debug] := FORE_DARKGREEN;
fColors[TLogType.Info] := FORE_WHITE;
fColors[TLogType.Warning] := FORE_DARKYELLOW;
fColors[TLogType.Error] := FORE_RED;
fColors[TLogType.Fatal] := FORE_MAGENTA;
{$ENDIF}
end;
procedure TLoggerProConsoleAppender.SetColor(const aLogType: TLogType);
begin
{$IFDEF MSWINDOWS}
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), fColors[aLogType]);
{$ELSE}
if FUTF8Output then
WriteUTF8Raw(fColors[aLogType])
else
Write(fColors[aLogType]);
{$ENDIF}
end;
procedure TLoggerProConsoleAppender.ResetColor;
begin
{$IFDEF MSWINDOWS}
if fSavedColors > -1 then
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), fSavedColors)
else
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_BLUE or FOREGROUND_GREEN or FOREGROUND_RED);
{$ELSE}
if FUTF8Output then
WriteUTF8Raw(STYLE_RESETALL)
else
Write(STYLE_RESETALL);
{$ENDIF}
end;
procedure TLoggerProConsoleAppender.TearDown;
begin
{$IFDEF MSWINDOWS}
// When the renderer handles colors via ANSI VT codes, DO NOT touch
// Win32 SetConsoleTextAttribute here. Mixing the two color APIs
// leaves Windows Terminal / cmd in an inconsistent state.
if not FRendererHandlesColors then
begin
if fSavedColors > -1 then
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), fSavedColors)
else
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_BLUE or FOREGROUND_GREEN or FOREGROUND_RED);
end;
if FUTF8Output then
SetConsoleOutputCP(FSavedOutputCP);
{$ENDIF}
end;
procedure TLoggerProConsoleAppender.WriteLog(const aLogItem: TLogItem);
var
lText: string;
begin
lText := FormatLog(aLogItem);
FLock.Enter;
try
if not FRendererHandlesColors then
SetColor(aLogItem.LogType);
WriteUTF8Line(lText);
if not FRendererHandlesColors then
ResetColor;
finally
FLock.Leave;
end;
end;
procedure TLoggerProConsoleAppender.WriteUTF8Line(const aText: string);
begin
InternalWriteUTF8(aText + sLineBreak);
end;
procedure TLoggerProConsoleAppender.WriteUTF8Raw(const aText: string);
begin
InternalWriteUTF8(aText);
end;
{ TLoggerProConsoleLogFmtAppender }
constructor TLoggerProConsoleLogFmtAppender.Create(ALogItemRenderer: ILogItemRenderer);
begin
inherited Create(TLogItemRendererLogFmt.Create);
end;
function TLoggerProConsoleLogFmtAppender.FormatLog(const ALogItem: TLogItem): string;
begin
if Assigned(FOnLogRow) then
FOnLogRow(ALogItem, Result)
else
Result := FLogItemRenderer.RenderLogItem(ALogItem);
end;
{ TLoggerProSimpleConsoleAppender }
procedure TLoggerProSimpleConsoleAppender.Setup;
begin
inherited;
{$IFDEF MSWINDOWS}
if FUTF8Output then
begin
FSavedOutputCP := GetConsoleOutputCP;
SetConsoleOutputCP(CP_UTF8);
end;
{$ENDIF}
end;
procedure TLoggerProSimpleConsoleAppender.TearDown;
begin
{$IFDEF MSWINDOWS}
if FUTF8Output then
SetConsoleOutputCP(FSavedOutputCP);
{$ENDIF}
end;
procedure TLoggerProSimpleConsoleAppender.WriteLog(const aLogItem: TLogItem);
begin
if FUTF8Output then
WriteUTF8Line(FormatLog(aLogItem))
else if FUseStdErr then
Writeln(ErrOutput, FormatLog(aLogItem))
else
Writeln(FormatLog(aLogItem));
end;
procedure TLoggerProSimpleConsoleAppender.WriteUTF8Line(const aText: string);
begin
InternalWriteUTF8(aText + sLineBreak, FUseStdErr);
end;
{ TLoggerProSimpleConsoleLogFmtAppender }
constructor TLoggerProSimpleConsoleLogFmtAppender.Create(ALogItemRenderer: ILogItemRenderer);
begin
inherited Create(TLogItemRendererLogFmt.Create);
end;
function TLoggerProSimpleConsoleLogFmtAppender.FormatLog(const ALogItem: TLogItem): string;
begin
if Assigned(FOnLogRow) then
FOnLogRow(ALogItem, Result)
else
Result := FLogItemRenderer.RenderLogItem(ALogItem);
end;
end.