Monday, 10 August 2026

How to update a Windows Service

 I've been developing Windows services using Delphi for many years now and something I have come across over the past few years is that some developers when updating a service think it is necessary to uninstall and reinstall the service rather than stopping the service, updating the exe and starting the service. 

Here are. the reasons why it's best not to reinstall:

Uninstalling and reinstalling a Windows Service for a simple update is considered bad practice because it strips away all service-level configuration and state managed by the Windows Service Control Manager (SCM).

Here are the primary reasons why stopping, replacing the executable, and restarting is the standard approach:

1. Loss of Service Configuration & Permissions

When you uninstall a service (e.g., using `sc delete` or `installutil /u`), Windows removes the entire registry key for that service under `HKLM\SYSTEM\CurrentControlSet\Services\`. This wipes out critical configuration settings:

* **Service Account & Credentials:** Custom Log On credentials (e.g., specific domain accounts or managed service accounts) revert to default or must be manually re-entered, requiring sensitive passwords during deployments.

* **Startup Type:** Settings like *Automatic (Delayed Start)* or *Manual* are lost.

* **Failure Actions & Recovery:** Custom auto-restart triggers, recovery actions, and reboot schedules on process crash are erased.

* **Dependencies:** Relationships showing which other services depend on this service (or which services it depends on) are broken.

* **Custom Security Descriptors (DACLs):** Custom permissions set on the service itself to dictate who can start, stop, or manage it are deleted.

2. Registry Fragility and "Marked for Deletion" Issues

When you command Windows to delete a service while any process (such as Event Viewer, Services MMC snap-in, or Task Manager) holds an open handle to that service’s registry key, Windows cannot complete the deletion immediately. Instead, it marks the service as **"Marked for Deletion."**

If your deployment script immediately attempts to reinstall the service under the same name:

* The installation will fail with `Error 1072 (ERROR_SERVICE_MARKED_FOR_DELETE)`.

* Resolving this often requires closing every handle or performing a full system reboot, turning an automated deployment into manual troubleshooting.

3. Disruption to Event Logs and System History

Uninstalling can orphan or disrupt Windows Event Viewer log subscriptions and diagnostic tracing associated with the service's registry entry, making post-deployment audit trails harder to trace continuously.

The Recommended Process

To update a service binary cleanly, follow this sequence:

1. **Stop the service:** `sc stop MyService` or `Stop-Service MyService`

2. **Wait for graceful shutdown:** Ensure the process terminates so file locks on the executable are released.

3. **Overwrite/Update the binaries:** Replace `MyService.exe` and associated DLLs in the installation folder.

4. **Start the service:** `sc start MyService` or `Start-Service MyService`


**When *is* an uninstall/reinstall appropriate?**

Only when structural metadata about the service itself changes—such as renaming the internal service name, altering low-level service architecture, or performing a major software release that completely changes the deployment path and configuration structure.


These are the comments from Gemini. I've also asked ChatGPT, Copilot and Grok and they all come back with similar reasons. 
I'm going to try to find out why these developers think it is best to uninstall/install every time an update is done.

Monday, 23 March 2026

Why use a code formatter

For years I've used the Delphi code formatter (CTRL+D) for the reasons below and still cannot understand why some developers don't use it and spend time manually indenting, adding removing spaces etc. When developers don't use code formatters (that are setup the same) it leads to messy codebases, inconsistent code, arguments about spacing and formatting and waists time.

Here are the Pros and Cons of using a code formatter.

The Pros: Why Formatters are Essential

  • Drastically Improved Code Reviews: Without a formatter, pull requests often get bogged down by "nitpick" comments like, "Can you add a space here?" or "Please use single quotes." Formatters eliminate this entirely. Code reviews can focus 100% on logic, architecture, and security.

  • Zero Cognitive Load: Formatting manually wastes time. A developer should be thinking about solving complex business problems, not counting indentation spaces or calculating line lengths.

  • A Unified Codebase: A codebase should read as if it were written by a single person. When multiple developers use their own styles, the code becomes visually jarring and harder to read. Formatters create a predictable standard.

  • Fewer Merge Conflicts: Inconsistent formatting is a massive driver of Git merge conflicts. If Developer A uses tabs and Developer B uses spaces, touching the same file will trigger massive conflicts just based on invisible whitespace.

  • Faster Onboarding: New developers don't have to read a 10-page style guide to understand how the team formats code. They just hit save, and the tool does it for them.

The Cons: Why Some Developers Resist

While the pros heavily outweigh the cons, it is helpful to understand why some developers might be pushing back:

  • Loss of Contextual Readability: Formatters apply rigid rules. Occasionally, a developer might format a complex array or mathematical matrix in a specific, non-standard way to make it more readable for humans. A formatter will aggressively crush this custom formatting back into the standard shape. (Workaround: Most formatters have a // ignore comment you can use for specific blocks of code).

  • Initial Configuration Arguments: Setting up a formatter often forces a team to have the dreaded "Tabs vs. Spaces" or "80 vs. 120 character line limit" arguments. Some developers hate giving up their personal preferences.

  • Git Blame Pollution: If you introduce a formatter to an older codebase, the first run will touch almost every file. This means git blame will show the person who ran the formatter as the last author of every line, obscuring the actual author. (Workaround: You can use a .git-blame-ignore-revs file to tell Git to ignore the massive formatting commit).

  • The "Loss of Control" Feeling: Some developers take deep pride in the craftsmanship of manually crafting their code. An automated tool re-arranging their work can feel intrusive to them.


Wednesday, 18 February 2026

SQL Server - How to capture database errors

One very useful addition to a database I have found recently is to capture database errors in a try...catch block and storing the information in a table. Here is what I did:

Table to store the error information:

CREATE TABLE dbo.DBErrors (
  DBErrorID INT IDENTITY
 ,Raised DATETIME NULL CONSTRAINT DF_DBErrors_Raised DEFAULT (GETDATE())
 ,Number INT NULL
 ,Severity INT NULL
 ,StateNumber INT NULL
 ,StoredProcedure NVARCHAR(128) NULL
 ,Line INT NULL
 ,ErrorMessage NVARCHAR(4000) NULL
 ,CONSTRAINT PK_DBErrors PRIMARY KEY CLUSTERED (DBErrorID)
) ON [PRIMARY]

Then I wrote a stored procedure to write to the table:

CREATE PROCEDURE dbo.DBErrorInsert
AS
BEGIN
DECLARE @DBErrorsExist bit;
  SET NOCOUNT ON;
  
  SELECT TOP (1) @DBErrorsExist = TableExists FROM dbo.efn_TableExists('DBErrors');
IF (@DBErrorsExist = 1)
BEGIN
INSERT INTO DBErrors (Number, Severity, StateNumber, StoredProcedure, Line,   ErrorMessage) VALUES ( 
ERROR_NUMBER(),  
ERROR_SEVERITY(),  
ERROR_STATE(),  
ERROR_PROCEDURE(),  
ERROR_LINE(),  
ERROR_MESSAGE());
    RETURN -2;
END
  ELSE
  BEGIN
    RETURN -1;
  END;
END;

Then where I wanted to capture any errors i.e. in a stored procedure I did the following:

BEGIN TRY
  ... Stored proc code
END TRY
BEGIN CATCH
  EXEC dbo.DBErrorInsert;
END CATCH

When an error occurs in the try...catch block it writes the error information including the line the error was triggered to a record in the table.



Tuesday, 17 February 2026

SQL Server - List all fixed length fields in a database

I noticed that a field in a database had been set to a nchar(), this means it's a fixed length and in a result query had trailing spaces. The 2 options I had were to change the field type to nvarchar() or add a RTRIM() to the field and alias it. To identify any other fields that were like this I ran the following query:

SELECT 
    TABLE_SCHEMA,
    TABLE_NAME, 
    COLUMN_NAME, 
    DATA_TYPE, 
    CHARACTER_MAXIMUM_LENGTH AS Defined_Length
FROM INFORMATION_SCHEMA.COLUMNS
WHERE DATA_TYPE IN ('char', 'nchar')
ORDER BY TABLE_NAME;

Friday, 13 February 2026

Mouse Wheel and TScrollBox

We had an issue where there was a TScrollbox on a form and the mouse wheel would not do the vertical scroll. The simplest solution I found for this was to do the following in the forms 'OnMouseWheel' event.

if MyScrollBox.BoundsRect.Contains(MyScrollBox.Parent.ScreenToClient(MousePos)) then
begin
  MyScrollBox.VertScrollBar.Position := MyScrollBox.VertScrollBar.Position -                   WheelDelta;
  Handled := True;
end; 



Thursday, 13 November 2025

SQL Server - Query to return list of tables and the space they use on disk

Below is a useful MS SQL Server query I use to get the list of database tables and information on how much disk space they use. The results are ordered by the size on disk descending.

SELECT 
    t.NAME AS TableName,
    s.Name AS SchemaName,
    p.rows AS RowCounts,
    CAST(ROUND(((SUM(a.total_pages) * 8.0) / 1024), 2) AS DECIMAL(18,2)) AS TotalSpaceMB,
    CAST(ROUND(((SUM(a.used_pages) * 8.0) / 1024), 2) AS DECIMAL(18,2)) AS UsedSpaceMB,
    CAST(ROUND(((SUM(a.data_pages) * 8.0) / 1024), 2) AS DECIMAL(18,2)) AS DataSpaceMB
FROM 
    sys.tables t
INNER JOIN      
    sys.indexes i ON t.OBJECT_ID = i.object_id
INNER JOIN 
    sys.partitions p ON i.object_id = p.OBJECT_ID AND i.index_id = p.index_id
INNER JOIN 
    sys.allocation_units a ON p.partition_id = a.container_id
LEFT OUTER JOIN 
    sys.schemas s ON t.schema_id = s.schema_id
WHERE 
    t.is_ms_shipped = 0
GROUP BY 
    t.Name, s.Name, p.Rows
ORDER BY 
    TotalSpaceMB DESC;

Wednesday, 12 March 2025

When to pass a dependency to a class?

I was speaking to another developer the other day and he insisted that if a class needs an instance of another class it should always be passed in an 'Init' method and not in the constructor. I disagreed with him on this and thought I should clarify when I think a dependency should be passed.

There are 2 options when passing a dependency to a class:

  1. Passing it as a parameter in the Create constructor.
  2. Passing it in a separate 'Init' method after creation. It does not have to be called 'Init'. 
When to use the 'Create' method and its advantages:
  • When the class absolutely needs the other class to operate properly.
  • Ensures the object is in a valid state immediately after creation.
  • Prevents accidental use of an uninitialized object.
  • Simplicity, it can lead to cleaner and more concise code.
When to use an 'Init' method and its advantages:
  • When the dependency could be seen as optional. The class can operate or partly operate without the other class. This can be done with the Create method, but passing it as nil.
  • Need delayed initialization.   
In most cases, the Create method (constructor) approach is preferred for better safety and reliability.


Monday, 13 January 2025

Should you write a website using Delphi?

Over the years I've written various websites some of which used Delphi to publish the pages. This method used HTML templates, JS and CSS files and the data from the database to stitched them together to produce the pages. The question is if I was to write a website from scratch would I still use this method or would I look at using a Javascript framework like ReactJS, Angular or Svelte? 

I'm sure the answer to this question would depend on the spec for the website, but here are some advantages and disadvantages of writing a website using Delphi.

Advantages:
  • If you have existing applications written in Delphi you can use existing code like common functions, classes and units that relate to your business, so there is no need to rewrite code in another language.
  • Time to develop the site could be shorter than using a framework like ReactJS, especially if you need to learn a new language.
  • Possible to implement JS libraries like Bootstrap.
  • Have front and backend code in the same project. Have a single service that is responsible for retrieving data from the database then using that data to produce the front end page.
Disadvantages:
  • You still have to write code in HTML, JS and CSS.
  • Compared to popular web development languages, Delphi lacks a vibrant ecosystem of web-specific tools, libraries, and frameworks.
  • Testing changes while developing is slow compared to other languages like ReactJS, which automatically refreshes the page you are viewing when you save a change.
  • Modern web development frameworks often come with built-in security measures (e.g., CSRF protection, SQL injection prevention). In Delphi, many of these must be implemented manually, increasing the risk of vulnerabilities.
  • Delphi's future as a web development platform is uncertain compared to the robust growth of modern web development languages.
  • Difficult to find Delphi developers, especially ones with web site development experience.
  • A lack of beginner-friendly tutorials, guides, and resources for web development in Delphi can make onboarding new developers more difficult.
In Conclusion:

While Delphi has its strengths, its disadvantages for web development primarily revolve around the lack of modern web development focus, limited developer availability, higher costs, and the complexity of implementing web-specific features. For teams already invested in Delphi or needing tight integration with Delphi-based systems, it can be a viable option, but for new projects or those targeting modern web standards, other technologies may be more practical.

Below is a table from ChatGPT with some security vulnerabilities & considerations:


Sunday, 8 December 2024

Should naming procedures Setup and Teardown be reserved for unit testing?

I recently came across some code a developer had done where they had named 2 public procedures 'Setup' and 'Teardown', when I fist saw these I instinctively thought of unit tests and thought that class might relate to unit testing. After looking into the code it was apparent that these procedures where meant to be called after an object of the class was created and before the object was freed, but had nothing to do with unit testing.

Even though there are no restrictions on using these words for procedures I do think it is best practice not to use them in production code and name them something different like 'InitializeResources' and 'CleanupResources'. I think keeping procedures 'Setup' and 'Teardown' specifically for testing maintains a clear distinction between testing and application logic, which can be beneficial for maintainability.

Wednesday, 24 July 2024

Why change how a Web Broker service works?

I have a web service (web broker) that works fine and each request is sent to a corresponding object that relates to the request and the response is returned, for example the /customerinfo request (GET) calls an object that is private to the TWebModule that returns customer information.

procedure TMyWebModule.CustomerInfoAction(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
    Response.Content := FCustomers.CustomerInfo(Request);
end; 

BTW, this is not the actual code, but is just an example. This works fine and has been working fine for some time. However, another developer has come up with what they consider a better way fo doing this. First, they add a property of TWebRequest to all objects that use TWebRequest, so in this example the FCustomer object has a WebRequest property. Then in the BeforeDispatch set the WebRequest property of all the web module objects (not just the one that relates to the request) to the web request. The advantage of this is so that you do not need to pass in the WebRequest in the action e.g.

procedure TMyWebModule.CustomerInfoAction(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
    Response.Content := FCustomers.CustomerInfo;
end; 

They have also proposed that we could also set the TWebResponse on all the objects to that then all you need to do is this.

procedure TMyWebModule.CustomerInfoAction(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
    FCustomers.CustomerInfo;
end; 

The other change that is being proposed is to add validation to the TWebRequest, by having a new class that descends from TWebRequest, lets call it TMyWebRequest and this will have validation code, so that you can call something like MyWebRequest.Validate it will do some validation. Currently there is a validation class that takes the TWebRequest, but this would be removed and the TMyWebRequest would validate itself.

In the AfterDispatch method it will iterate through all the objects and set the MyWebRequest property to nil.

There are a few things I am not keen about with making these changes. 

  • I don't like the idea of descending from TWebRequest just to add some validation methods, and much prefer having validation classes that are responsible for doing the validation.
  • Currently passing TWebRequest makes it clear and explicit that the parameter is expected, making it easier to trace.
  • The current way keeps the responsibility of handling the request and response with the action.
  • It is setting the MyWebRequest property of all the objects, even though it is only required for the object that will be called, for example it sets the MyWebRequest property on the Order, Product, System and Customer objects even though it is only the Customer object that will be called with that requrest.
  • It means that a developer can use both methods, this could cause confusion.
  • It will take some time to change all the services to work this new way. 

The benefits I can see from doing this are:

  • Reduce the number of parameters passed.
I cannot see any real benefits from making these changes, or am I missing something?


Friday, 24 November 2023

How to write try finally blocks

 I recently came across code another developer had done and they write try..finally blocks like the following:

procedure TSomeClass.DoSomething: string;
var
    myClass: TMyClass;
begin
    myClass := nil;
    try
        myClass := TMyClass.Create;
        // Do stuff
    finally
        myClass.Free;
    end;
end;

Were I would write it as follows:

procedure TSomeClass.DoSomething: string;
var
    myClass: TMyClass;
begin
    myClass := TMyClass.Create;
    try
        // Do stuff
    finally
        myClass.Free;
    end;
end;

I always create the object on the line before the try and would not set the object to nil just before creating it. I believe setting the object to nil before creating it and putting the create after the try to not be the correct way and do not know the reasoning why the developer does it like this.


Saturday, 12 February 2022

Writing to and reading from the windows event Log

I've written some classes that writes to and reads from the Windows event log. The TRBWindowsEventLogs class contains the writer and reader objects, when creating the object from this class it requires the application name to be passed, this is can be different is required to the application, but is also used to retrieve the log entries.

unit uWindowsEvents;

interface

uses classes, Windows, SvcMgr, Vcl.StdCtrls, Generics.Collections;

type

  { /----------------------------------------------------------------------------------------------------------------- }
  TRBWindowsEvent = class(TObject)
  strict private
    fRecordNumber: integer;
    fMessage: string;
    fComputerName: string;
    fEventData: string;
    fLogFile: string;
    fCategory: string;
    fEventCode: integer;
  public
    property Category: string read fCategory write fCategory;
    property ComputerName: string read fComputerName write fComputerName;
    property EventCode: integer read fEventCode write fEventCode;
    property Message: string read fMessage write fMessage;
    property RecordNumber: integer read fRecordNumber write fRecordNumber;
    property LogFile: string read fLogFile write fLogFile;
    property EventData: string read fEventData write fEventData;

    procedure Populate(aEvent: OLEVariant);
  end;

  { /----------------------------------------------------------------------------------------------------------------- }
  IRBEventReaderOutput = interface
    ['{4ADA872A-C1DA-4B3B-BE67-0F628C61039C}']
    procedure AddEventLog(aEventLog: TRBWindowsEvent);
    procedure SaveToFile(aFileName: string);
    function OutputString: string;
  end;

  { /----------------------------------------------------------------------------------------------------------------- }
  TRBWindowsEventLogsReader = class(TObjectList<TRBWindowsEvent>)
  strict private
    fApplicationName: string;
    fMaxNumberOfEntries: integer;

    function EventQuery: string;

    procedure GetWindowsEventLogs;
    procedure AddErrorMessage(aErrorMessage: string);
  public
    property MaxNumberOfEntries: integer read fMaxNumberOfEntries write fMaxNumberOfEntries;

    constructor Create(aApplicationName: string; aMaxNumberOfEntries: integer);

    procedure PopulateEvents(aReaderOutput: IRBEventReaderOutput);
  end;

  { /----------------------------------------------------------------------------------------------------------------- }
  TRBWindowsEventLogsWriter = class(TObject)
  strict private
    fWindowsEventLogger: TEventLogger;
  public
    constructor Create(aApplicationName: string);
    destructor Destroy; override;

    procedure WriteInformationToWindowsEvents(aMessage: string);
    procedure WriteErrorToWindowsEvents(aMessage: string);
  end;

  { /----------------------------------------------------------------------------------------------------------------- }
  TRBWindowsEventLogs = class(TObject)
  strict private
    fApplicationName: string;

    fWriter: TRBWindowsEventLogsWriter;
    fReader: TRBWindowsEventLogsReader;

  public
    property Writer: TRBWindowsEventLogsWriter read fWriter;
    property Reader: TRBWindowsEventLogsReader read fReader;

    constructor Create(aApplicationName: string); overload;
    constructor Create(aApplicationName: string; aMaxNumberOfEntries: integer); overload;
    destructor Destroy; override;
  end;

implementation

uses SysUtils, ComObj, ActiveX, System.Variants, DateUtils;

{ TRBWindowsEvent }

procedure TRBWindowsEvent.Populate(aEvent: OLEVariant);
var
  insertion: array of String;
  i: integer;
begin
  fCategory := string(aEvent.Category);
  fComputerName := string(aEvent.ComputerName);
  fEventCode := integer(aEvent.EventCode);
  fMessage := string(aEvent.Message);
  fRecordNumber := integer(aEvent.RecordNumber);
  fLogFile := string(aEvent.LogFile);

  if not VarIsNull(aEvent.InsertionStrings) then
  begin
    insertion := aEvent.InsertionStrings;
    for i := VarArrayLowBound(insertion, 1) to VarArrayHighBound(insertion, 1) do
    begin
      fEventData := fEventData + insertion[i];
    end;
  end;
end;

{ TRBWindowsEvents }

constructor TRBWindowsEventLogs.Create(aApplicationName: string);
begin
  Create(aApplicationName, 100);
end;

constructor TRBWindowsEventLogs.Create(aApplicationName: string; aMaxNumberOfEntries: integer);
begin
  inherited Create;
  fApplicationName := aApplicationName;
  fWriter := TRBWindowsEventLogsWriter.Create(aApplicationName);
  fReader := TRBWindowsEventLogsReader.Create(aApplicationName, aMaxNumberOfEntries);
end;

destructor TRBWindowsEventLogs.Destroy;
begin
  FreeAndNil(fReader);
  FreeAndNil(fWriter);
  inherited;
end;

{ TRBWindowsEventLogsWriter }

constructor TRBWindowsEventLogsWriter.Create(aApplicationName: string);
begin
  inherited Create;
  fWindowsEventLogger := TEventLogger.Create(aApplicationName);
end;

destructor TRBWindowsEventLogsWriter.Destroy;
begin
  FreeAndNil(fWindowsEventLogger);
  inherited;
end;

procedure TRBWindowsEventLogsWriter.WriteErrorToWindowsEvents(aMessage: string);
begin
  fWindowsEventLogger.LogMessage(aMessage, EVENTLOG_ERROR_TYPE);
end;

procedure TRBWindowsEventLogsWriter.WriteInformationToWindowsEvents(aMessage: string);
begin
  fWindowsEventLogger.LogMessage(aMessage, EVENTLOG_INFORMATION_TYPE);
end;

{ TRBWindowsEventLogsReader }

constructor TRBWindowsEventLogsReader.Create(aApplicationName: string; aMaxNumberOfEntries: integer);
begin
  inherited Create;
  fApplicationName := aApplicationName;
  fMaxNumberOfEntries := aMaxNumberOfEntries;
end;

function TRBWindowsEventLogsReader.EventQuery: string;
begin
  Result := 'SELECT * FROM Win32_NTLogEvent Where SourceName = "' + fApplicationName +
    '" AND Logfile = "Application" AND TimeGenerated >= "' + DateTimeToStr(IncDay(Now(), -1)) + '"';
end;

procedure TRBWindowsEventLogsReader.AddErrorMessage(aErrorMessage: string);
var
  event: TRBWindowsEvent;
begin
  event := TRBWindowsEvent.Create;
  event.Category := 'Error';
  event.Message := aErrorMessage;
  Add(event);
end;

procedure TRBWindowsEventLogsReader.GetWindowsEventLogs;
const
  wbemForwardOnly = 32;
  wbemReturnImmediately = 16;
var
  SWbemLocator: OLEVariant;
  WMIService: OLEVariant;
  WbemObjectSet: OLEVariant;
  WbemObject: OLEVariant;
  oEnum: IEnumvariant;
  iValue: LongWord;
  iCount: integer;
  event: TRBWindowsEvent;
begin
  try
    Clear;
    iCount := 0;
    SWbemLocator := CreateOleObject('WbemScripting.SWbemLocator');
    WMIService := SWbemLocator.ConnectServer('localhost', 'root\CIMV2', '', '');
    WbemObjectSet := WMIService.ExecQuery(EventQuery(), 'WQL', wbemReturnImmediately + wbemForwardOnly);
    oEnum := IUnknown(WbemObjectSet._NewEnum) as IEnumvariant;
    while oEnum.Next(1, WbemObject, iValue) = 0 do
    begin
      event := TRBWindowsEvent.Create;
      event.Populate(WbemObject);
      Add(event);
      WbemObject := Unassigned;
      inc(iCount);
      if iCount > fMaxNumberOfEntries then
      begin
        Break;
      end;
    end;
  except
    on E: EOleException do
      AddErrorMessage(Format('EOleException %s %x', [E.Message, E.ErrorCode]));
    on E: Exception do
      AddErrorMessage(E.Classname + ':' + E.Message);
  end;
end;

procedure TRBWindowsEventLogsReader.PopulateEvents(aReaderOutput: IRBEventReaderOutput);
var
  event: TRBWindowsEvent;
begin
  GetWindowsEventLogs;
  for event in Self do
  begin
    aReaderOutput.AddEventLog(event);
  end;
end;

end.


To use these classes I've created some output classes implemented from the IRBEventReaderOutput interface.

unit uWindowsEventsOutput;

interface

uses classes, Windows, uWindowsEvents, System.JSON;

type
  { /----------------------------------------------------------------------------------------------------------------- }
  TStringsReaderOutput = class(TInterfacedObject, IRBEventReaderOutput)
  strict private
    fStrings: TStringList;
  public
    procedure AddEventLog(aEventLog: TRBWindowsEvent);
    procedure SaveToFile(aFileName: string);
    function OutputString: string;

    constructor Create;
    destructor Destroy; override;
  end;

  { /----------------------------------------------------------------------------------------------------------------- }
  TJSONReaderOutput = class(TInterfacedObject, IRBEventReaderOutput)
  strict private
    fJSON_Array: TJSONArray;
  public
    procedure AddEventLog(aEventLog: TRBWindowsEvent);
    procedure SaveToFile(aFileName: string);
    function OutputString: string;

    constructor Create;
    destructor Destroy; override;
  end;

  { /----------------------------------------------------------------------------------------------------------------- }
  TCSVReaderOutput = class(TInterfacedObject, IRBEventReaderOutput)
  strict private
    fCSVString: string;
    procedure AddHeader;
    procedure AddLine(aLine: string);
  public
    procedure AddEventLog(aEventLog: TRBWindowsEvent);
    procedure SaveToFile(aFileName: string);
    function OutputString: string;
  end;

implementation

uses SysUtils;

{ TStringsReaderOutput }

constructor TStringsReaderOutput.Create;
begin
  inherited Create;
  fStrings := TStringList.Create;
end;

destructor TStringsReaderOutput.Destroy;
begin
  FreeAndNil(fStrings);
  inherited;
end;

function TStringsReaderOutput.OutputString: string;
begin
  Result := fStrings.CommaText;
end;

procedure TStringsReaderOutput.SaveToFile(aFileName: string);
begin
  fStrings.SaveToFile(aFileName);
end;

procedure TStringsReaderOutput.AddEventLog(aEventLog: TRBWindowsEvent);
begin
  fStrings.Add('Category: ' + aEventLog.Category);
  fStrings.Add('Computer Name: ' + aEventLog.ComputerName);
  fStrings.Add('Event Code: ' + aEventLog.EventCode.ToString);
  fStrings.Add('Message: ' + aEventLog.Message);
  fStrings.Add('Record Number: ' + aEventLog.RecordNumber.ToString);
  fStrings.Add('Log File: ' + aEventLog.LogFile);
  fStrings.Add('Event Data');
  fStrings.Add(aEventLog.EventData);
  fStrings.Add('-------------------------');
end;


{ TJSONReaderOutput }

constructor TJSONReaderOutput.Create;
begin
  inherited Create;
  fJSON_Array := TJSONArray.Create;
end;

destructor TJSONReaderOutput.Destroy;
begin
  FreeAndNil(fJSON_Array);
  inherited;
end;

function TJSONReaderOutput.OutputString: string;
begin
  if Assigned(fJSON_Array) then
  begin
    Result := fJSON_Array.ToJSON;
  end;
end;

procedure TJSONReaderOutput.SaveToFile(aFileName: string);
var
  sl: TStringList;
begin
  sl := TStringList.Create;
  try
    sl.Text := fJSON_Array.ToJSON;
    sl.SaveToFile(aFileName);
  finally
    sl.Free;
  end;
end;

procedure TJSONReaderOutput.AddEventLog(aEventLog: TRBWindowsEvent);
var
  JSON_Object: TJSONObject;
begin
  JSON_Object := TJSONObject.Create;
  JSON_Object.AddPair('category', aEventLog.Category);
  JSON_Object.AddPair('computerName', aEventLog.ComputerName);
  JSON_Object.AddPair('eventCode', aEventLog.EventCode.ToString);
  JSON_Object.AddPair('message', aEventLog.Message);
  JSON_Object.AddPair('recordNumber', aEventLog.RecordNumber.ToString);
  JSON_Object.AddPair('logFile', aEventLog.LogFile);
  JSON_Object.AddPair('eventData', aEventLog.EventData);
  fJSON_Array.Add(JSON_Object);
end;


{ TCSVReaderOutput }

procedure TCSVReaderOutput.AddEventLog(aEventLog: TRBWindowsEvent);
var
  s: string;
  procedure AddValue(aValue: string);
  begin
    s := s + aValue + ',';
  end;

begin
  AddHeader;
  AddValue(aEventLog.Category);
  AddValue(aEventLog.ComputerName);
  AddValue(aEventLog.EventCode.ToString);
  AddValue(aEventLog.Message);
  AddValue(aEventLog.RecordNumber.ToString);
  AddValue(aEventLog.LogFile);
  AddValue(aEventLog.EventData);
  AddLine(s);
end;

procedure TCSVReaderOutput.AddHeader;
begin
  if fCSVString = '' then
  begin
    AddLine('category,computerName,eventCode,message,recordNumber,logFile,eventData,');
  end;
end;

procedure TCSVReaderOutput.AddLine(aLine: string);
begin
  fCSVString := fCSVString + aLine + chr(13) + chr(10);
end;

function TCSVReaderOutput.OutputString: string;
begin
  Result := fCSVString;
end;

procedure TCSVReaderOutput.SaveToFile(aFileName: string);
var
  sl: TStringList;
begin
  sl := TStringList.Create;
  try
    sl.Text := fCSVString;
    sl.SaveToFile(aFileName);
  finally
    sl.Free;
  end;
end;

end.

Here are some examples of how to use the output classes.

To write to the Events Log.

procedure TfrmWindowsEvents.AddLogBtnClick(Sender: TObject);
begin
  fWindowsEvents.Writer.WriteInformationToWindowsEvents(MessageEdt.Text);
end;

To read from the Events Log

procedure TfrmWindowsEvents.StringBtnClick(Sender: TObject);
var
  stringsReader: IRBEventReaderOutput;
begin
  stringsReader := TStringsReaderOutput.Create;

  fWindowsEvents.Reader.PopulateEvents(stringsReader);
  stringsReader.SaveToFile('stringsoutput.txt');
  MemoEvents.Lines.CommaText := stringsReader.OutputString;
end;

procedure TfrmWindowsEvents.JsonBtnClick(Sender: TObject);
var
  jsonReader: IRBEventReaderOutput;
begin
  jsonReader := TJSONReaderOutput.Create;

  fWindowsEvents.Reader.PopulateEvents(jsonReader);
  jsonReader.SaveToFile('jsonoutput.txt');
  MemoEvents.Lines.Text := jsonReader.OutputString;
end;

procedure TfrmWindowsEvents.CsvBtnClick(Sender: TObject);
var
  csvReader: IRBEventReaderOutput;
begin
  csvReader := TCSVReaderOutput.Create;

  fWindowsEvents.Reader.PopulateEvents(csvReader);
  csvReader.SaveToFile('csvoutput.csv');
  MemoEvents.Lines.Text := csvReader.OutputString;
end;

One thing to note is that querying the events can be very slow depending on the amount of logs in the database. You can improve this in the Event Viewer application by selecting Windows Logs > Application and from either the main menu 'Action' or from the right click menu select 'Clear Logs'.