Skip to content

Latest commit

 

History

History
2650 lines (2154 loc) · 60.2 KB

File metadata and controls

2650 lines (2154 loc) · 60.2 KB

🔝 Retour au Sommaire

Surveillance des modifications du Registre Windows avec FreePascal/Lazarus

Introduction : Pourquoi surveiller le Registre ?

Surveiller les modifications du Registre Windows permet à votre application de réagir en temps réel aux changements de configuration. C'est particulièrement utile pour :

  • Détecter les changements de paramètres effectués par d'autres applications
  • Synchroniser plusieurs instances de votre application
  • Réagir aux modifications système (nouveau matériel, changement de configuration)
  • Surveiller la sécurité (détection de modifications non autorisées)
  • Déboguer et comprendre ce qui modifie vos clés

Principe de fonctionnement

Windows fournit des mécanismes pour être notifié quand une clé du Registre change :

Votre Application
       ↓
[Demande de surveillance sur HKEY_CURRENT_USER\Software\MonApp]
       ↓
Windows Registry Monitor
       ↓
[Attend les modifications...]
       ↓
Une autre application modifie la clé
       ↓
Windows envoie une notification
       ↓
Votre application est informée
       ↓
[Réaction au changement]

Les API Windows pour la surveillance

RegNotifyChangeKeyValue

La fonction principale pour surveiller le Registre est RegNotifyChangeKeyValue :

function RegNotifyChangeKeyValue(
  hKey: HKEY;                    // Handle de la clé à surveiller
  bWatchSubtree: BOOL;           // True = surveiller les sous-clés aussi
  dwNotifyFilter: DWORD;         // Ce qu'il faut surveiller
  hEvent: THandle;               // Event à signaler (optionnel)
  fAsynchronous: BOOL            // True = non bloquant
): Longint;

Les filtres de notification

Vous pouvez surveiller différents types de changements :

const
  REG_NOTIFY_CHANGE_NAME       = $00000001; // Ajout/suppression de sous-clés
  REG_NOTIFY_CHANGE_ATTRIBUTES = $00000002; // Changement d'attributs
  REG_NOTIFY_CHANGE_LAST_SET    = $00000004; // Changement de valeur
  REG_NOTIFY_CHANGE_SECURITY    = $00000008; // Changement de sécurité

  // Combinaison courante : surveiller tout
  REG_NOTIFY_ALL = REG_NOTIFY_CHANGE_NAME or
                   REG_NOTIFY_CHANGE_ATTRIBUTES or
                   REG_NOTIFY_CHANGE_LAST_SET or
                   REG_NOTIFY_CHANGE_SECURITY;

Surveillance simple (bloquante)

Exemple basique

program SurveillanceSimple;

{$mode objfpc}{$H+}

uses
  Windows, Registry, SysUtils;

procedure SurveillerCleBloquant;  
var
  Reg: TRegistry;
  KeyHandle: HKEY;
  Result: Longint;
begin
  Reg := TRegistry.Create(KEY_NOTIFY);
  try
    Reg.RootKey := HKEY_CURRENT_USER;

    if Reg.OpenKeyReadOnly('Software\MonApp\Settings') then
    begin
      KeyHandle := Reg.CurrentKey;

      WriteLn('Surveillance de la clé démarrée...');
      WriteLn('Modifiez la clé dans regedit pour tester');
      WriteLn('Appuyez sur Ctrl+C pour arrêter');
      WriteLn('');

      repeat
        // Attendre un changement (BLOQUANT)
        Result := RegNotifyChangeKeyValue(
          KeyHandle,                     // Clé à surveiller
          True,                          // Surveiller les sous-clés
          REG_NOTIFY_CHANGE_LAST_SET,    // Surveiller les valeurs
          0,                             // Pas d'event
          False                          // Mode synchrone (bloquant)
        );

        if Result = ERROR_SUCCESS then
        begin
          WriteLn(FormatDateTime('hh:nn:ss', Now) +
                  ' - Changement détecté !');

          // Ici, vous pourriez relire les valeurs pour voir ce qui a changé
          ReadAndDisplayChanges(Reg);
        end
        else
        begin
          WriteLn('Erreur de surveillance : ' + SysErrorMessage(Result));
          Break;
        end;

      until False;

      Reg.CloseKey;
    end
    else
      WriteLn('Impossible d''ouvrir la clé');

  finally
    Reg.Free;
  end;
end;

begin
  SurveillerCleBloquant;
end.

Surveillance asynchrone (non-bloquante)

Utilisation d'un Event Windows

Pour ne pas bloquer votre application, utilisez un Event :

unit RegistryMonitor;

{$mode objfpc}{$H+}

interface

uses
  Windows, Classes, SysUtils, Registry;

type
  TRegistryChangeEvent = procedure(Sender: TObject) of object;

  { TRegistryMonitor }
  TRegistryMonitor = class(TThread)
  private
    FRootKey: HKEY;
    FKeyPath: string;
    FWatchSubTree: Boolean;
    FNotifyFilter: DWORD;
    FOnChange: TRegistryChangeEvent;
    FEventHandle: THandle;
    FKeyHandle: HKEY;
    FActive: Boolean;

    procedure DoNotifyChange;
  protected
    procedure Execute; override;
  public
    constructor Create(ARootKey: HKEY; const AKeyPath: string);
    destructor Destroy; override;

    procedure Start;
    procedure Stop;

    property WatchSubTree: Boolean read FWatchSubTree write FWatchSubTree;
    property NotifyFilter: DWORD read FNotifyFilter write FNotifyFilter;
    property OnChange: TRegistryChangeEvent read FOnChange write FOnChange;
    property Active: Boolean read FActive;
  end;

implementation

{ TRegistryMonitor }

constructor TRegistryMonitor.Create(ARootKey: HKEY; const AKeyPath: string);  
begin
  inherited Create(True); // Créé suspendu

  FRootKey := ARootKey;
  FKeyPath := AKeyPath;
  FWatchSubTree := True;
  FNotifyFilter := REG_NOTIFY_CHANGE_LAST_SET or REG_NOTIFY_CHANGE_NAME;
  FActive := False;

  // Créer un event pour la notification
  FEventHandle := CreateEvent(nil, False, False, nil);

  FreeOnTerminate := False;
end;

destructor TRegistryMonitor.Destroy;  
begin
  Stop;

  if FEventHandle <> 0 then
    CloseHandle(FEventHandle);

  inherited;
end;

procedure TRegistryMonitor.Start;  
var
  Reg: TRegistry;
begin
  if FActive then Exit;

  // Ouvrir la clé
  Reg := TRegistry.Create(KEY_NOTIFY);
  try
    Reg.RootKey := FRootKey;

    if Reg.OpenKeyReadOnly(FKeyPath) then
    begin
      FKeyHandle := Reg.CurrentKey;
      FActive := True;

      // Démarrer le thread
      inherited Start;
    end
    else
      raise Exception.CreateFmt('Impossible d''ouvrir la clé : %s', [FKeyPath]);

  finally
    Reg.Free;
  end;
end;

procedure TRegistryMonitor.Stop;  
begin
  if not FActive then Exit;

  FActive := False;

  // Signaler l'event pour débloquer le thread
  SetEvent(FEventHandle);

  // Attendre la fin du thread
  if not Finished then
  begin
    Terminate;
    WaitFor;
  end;

  // Fermer la clé
  if FKeyHandle <> 0 then
  begin
    RegCloseKey(FKeyHandle);
    FKeyHandle := 0;
  end;
end;

procedure TRegistryMonitor.Execute;  
var
  Result: Longint;
begin
  while not Terminated and FActive do
  begin
    // Configurer la surveillance
    Result := RegNotifyChangeKeyValue(
      FKeyHandle,
      FWatchSubTree,
      FNotifyFilter,
      FEventHandle,
      True  // Asynchrone
    );

    if Result <> ERROR_SUCCESS then
    begin
      FActive := False;
      raise Exception.Create('Erreur RegNotifyChangeKeyValue : ' +
                            SysErrorMessage(Result));
    end;

    // Attendre l'event
    if WaitForSingleObject(FEventHandle, INFINITE) = WAIT_OBJECT_0 then
    begin
      if not Terminated and FActive then
      begin
        // Notification reçue
        Synchronize(@DoNotifyChange);
      end;
    end;
  end;
end;

procedure TRegistryMonitor.DoNotifyChange;  
begin
  if Assigned(FOnChange) then
    FOnChange(Self);
end;

end.

Utilisation du moniteur asynchrone

unit MainForm;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, StdCtrls, Windows, Registry,
  RegistryMonitor;

type
  { TForm1 }
  TForm1 = class(TForm)
    Memo1: TMemo;
    ButtonStart: TButton;
    ButtonStop: TButton;
    Label1: TLabel;

    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure ButtonStartClick(Sender: TObject);
    procedure ButtonStopClick(Sender: TObject);
  private
    FMonitor: TRegistryMonitor;
    procedure OnRegistryChange(Sender: TObject);
    procedure RefreshValues;
  public
  end;

var
  Form1: TForm1;

implementation

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);  
begin
  // Créer le moniteur
  FMonitor := TRegistryMonitor.Create(
    HKEY_CURRENT_USER,
    'Software\MonApp\Settings'
  );
  FMonitor.OnChange := @OnRegistryChange;

  ButtonStop.Enabled := False;

  // Afficher les valeurs initiales
  RefreshValues;
end;

procedure TForm1.FormDestroy(Sender: TObject);  
begin
  FMonitor.Free;
end;

procedure TForm1.ButtonStartClick(Sender: TObject);  
begin
  try
    FMonitor.Start;

    ButtonStart.Enabled := False;
    ButtonStop.Enabled := True;
    Label1.Caption := 'Surveillance active';
    Label1.Font.Color := clGreen;

    Memo1.Lines.Add(FormatDateTime('hh:nn:ss', Now) +
                    ' - Surveillance démarrée');

  except
    on E: Exception do
    begin
      ShowMessage('Erreur : ' + E.Message);
      Label1.Caption := 'Erreur';
      Label1.Font.Color := clRed;
    end;
  end;
end;

procedure TForm1.ButtonStopClick(Sender: TObject);  
begin
  FMonitor.Stop;

  ButtonStart.Enabled := True;
  ButtonStop.Enabled := False;
  Label1.Caption := 'Surveillance arrêtée';
  Label1.Font.Color := clDefault;

  Memo1.Lines.Add(FormatDateTime('hh:nn:ss', Now) +
                  ' - Surveillance arrêtée');
end;

procedure TForm1.OnRegistryChange(Sender: TObject);  
begin
  // Cette méthode est appelée quand le registre change
  Memo1.Lines.Add(FormatDateTime('hh:nn:ss', Now) +
                  ' - Changement détecté !');

  // Rafraîchir l'affichage des valeurs
  RefreshValues;
end;

procedure TForm1.RefreshValues;  
var
  Reg: TRegistry;
  Values: TStringList;
  i: Integer;
begin
  Memo1.Lines.Add('--- Valeurs actuelles ---');

  Reg := TRegistry.Create(KEY_READ);
  Values := TStringList.Create;
  try
    Reg.RootKey := HKEY_CURRENT_USER;

    if Reg.OpenKeyReadOnly('Software\MonApp\Settings') then
    begin
      try
        Reg.GetValueNames(Values);

        for i := 0 to Values.Count - 1 do
        begin
          if Reg.GetDataType(Values[i]) = rdString then
            Memo1.Lines.Add(Format('  %s = %s',
              [Values[i], Reg.ReadString(Values[i])]));
        end;

      finally
        Reg.CloseKey;
      end;
    end;

  finally
    Values.Free;
    Reg.Free;
  end;

  Memo1.Lines.Add('-------------------------');
end;

end.

Surveillance de plusieurs clés

Gestionnaire multi-clés

unit MultiRegistryMonitor;

{$mode objfpc}{$H+}

interface

uses
  Windows, Classes, SysUtils, Registry, Generics.Collections;

type
  TRegistryChangeInfo = record
    RootKey: HKEY;
    KeyPath: string;
    ChangeType: DWORD;
    Timestamp: TDateTime;
  end;

  TMultiRegistryChangeEvent = procedure(Sender: TObject;
    const ChangeInfo: TRegistryChangeInfo) of object;

  { TMonitoredKey }
  TMonitoredKey = class
  public
    RootKey: HKEY;
    KeyPath: string;
    Handle: HKEY;
    EventHandle: THandle;
    WatchSubTree: Boolean;
    NotifyFilter: DWORD;

    constructor Create(ARootKey: HKEY; const AKeyPath: string);
    destructor Destroy; override;
  end;

  { TMultiRegistryMonitor }
  TMultiRegistryMonitor = class(TThread)
  private
    FKeys: TObjectList<TMonitoredKey>;
    FOnChange: TMultiRegistryChangeEvent;
    FActive: Boolean;
    FLastChangeInfo: TRegistryChangeInfo;

    procedure DoNotifyChange;
    procedure SetupNotification(Key: TMonitoredKey);
  protected
    procedure Execute; override;
  public
    constructor Create;
    destructor Destroy; override;

    procedure AddKey(ARootKey: HKEY; const AKeyPath: string;
      AWatchSubTree: Boolean = True);
    procedure RemoveKey(const AKeyPath: string);
    procedure Start;
    procedure Stop;

    property OnChange: TMultiRegistryChangeEvent read FOnChange write FOnChange;
    property Active: Boolean read FActive;
  end;

implementation

{ TMonitoredKey }

constructor TMonitoredKey.Create(ARootKey: HKEY; const AKeyPath: string);  
begin
  RootKey := ARootKey;
  KeyPath := AKeyPath;
  Handle := 0;
  EventHandle := CreateEvent(nil, False, False, nil);
  WatchSubTree := True;
  NotifyFilter := REG_NOTIFY_CHANGE_LAST_SET or REG_NOTIFY_CHANGE_NAME;
end;

destructor TMonitoredKey.Destroy;  
begin
  if Handle <> 0 then
    RegCloseKey(Handle);

  if EventHandle <> 0 then
    CloseHandle(EventHandle);

  inherited;
end;

{ TMultiRegistryMonitor }

constructor TMultiRegistryMonitor.Create;  
begin
  inherited Create(True);

  FKeys := TObjectList<TMonitoredKey>.Create(True); // True = owns objects
  FActive := False;
  FreeOnTerminate := False;
end;

destructor TMultiRegistryMonitor.Destroy;  
begin
  Stop;
  FKeys.Free;
  inherited;
end;

procedure TMultiRegistryMonitor.AddKey(ARootKey: HKEY; const AKeyPath: string;
  AWatchSubTree: Boolean);
var
  Key: TMonitoredKey;
  Reg: TRegistry;
begin
  // Vérifier si la clé n'est pas déjà surveillée
  for Key in FKeys do
  begin
    if (Key.RootKey = ARootKey) and (Key.KeyPath = AKeyPath) then
    begin
      raise Exception.Create('Cette clé est déjà surveillée');
    end;
  end;

  // Créer la nouvelle clé surveillée
  Key := TMonitoredKey.Create(ARootKey, AKeyPath);
  Key.WatchSubTree := AWatchSubTree;

  // Ouvrir la clé du registre
  Reg := TRegistry.Create(KEY_NOTIFY);
  try
    Reg.RootKey := ARootKey;

    if Reg.OpenKeyReadOnly(AKeyPath) then
    begin
      Key.Handle := Reg.CurrentKey;
      FKeys.Add(Key);

      // Si la surveillance est active, configurer la notification
      if FActive then
        SetupNotification(Key);
    end
    else
    begin
      Key.Free;
      raise Exception.CreateFmt('Impossible d''ouvrir la clé : %s', [AKeyPath]);
    end;

  finally
    Reg.Free;
  end;
end;

procedure TMultiRegistryMonitor.RemoveKey(const AKeyPath: string);  
var
  i: Integer;
begin
  for i := FKeys.Count - 1 downto 0 do
  begin
    if FKeys[i].KeyPath = AKeyPath then
    begin
      FKeys.Delete(i);
      Break;
    end;
  end;
end;

procedure TMultiRegistryMonitor.Start;  
var
  Key: TMonitoredKey;
begin
  if FActive then Exit;

  if FKeys.Count = 0 then
    raise Exception.Create('Aucune clé à surveiller');

  // Configurer la surveillance pour toutes les clés
  for Key in FKeys do
    SetupNotification(Key);

  FActive := True;
  inherited Start;
end;

procedure TMultiRegistryMonitor.Stop;  
var
  Key: TMonitoredKey;
begin
  if not FActive then Exit;

  FActive := False;

  // Signaler tous les events pour débloquer le thread
  for Key in FKeys do
    SetEvent(Key.EventHandle);

  // Attendre la fin du thread
  if not Finished then
  begin
    Terminate;
    WaitFor;
  end;
end;

procedure TMultiRegistryMonitor.SetupNotification(Key: TMonitoredKey);  
var
  Result: Longint;
begin
  Result := RegNotifyChangeKeyValue(
    Key.Handle,
    Key.WatchSubTree,
    Key.NotifyFilter,
    Key.EventHandle,
    True
  );

  if Result <> ERROR_SUCCESS then
    raise Exception.CreateFmt('Erreur surveillance pour %s : %s',
      [Key.KeyPath, SysErrorMessage(Result)]);
end;

procedure TMultiRegistryMonitor.Execute;  
var
  Handles: array of THandle;
  i, WaitResult: Integer;
  Key: TMonitoredKey;
begin
  // Créer le tableau des handles d'events
  SetLength(Handles, FKeys.Count);
  for i := 0 to FKeys.Count - 1 do
    Handles[i] := FKeys[i].EventHandle;

  while not Terminated and FActive do
  begin
    // Attendre qu'un des events soit signalé
    WaitResult := WaitForMultipleObjects(
      Length(Handles),
      @Handles[0],
      False,  // Attendre UN event, pas tous
      INFINITE
    );

    if (WaitResult >= WAIT_OBJECT_0) and
       (WaitResult < WAIT_OBJECT_0 + DWORD(Length(Handles))) then
    begin
      // Identifier quelle clé a changé
      i := WaitResult - WAIT_OBJECT_0;
      Key := FKeys[i];

      // Préparer les informations de changement
      FLastChangeInfo.RootKey := Key.RootKey;
      FLastChangeInfo.KeyPath := Key.KeyPath;
      FLastChangeInfo.ChangeType := Key.NotifyFilter;
      FLastChangeInfo.Timestamp := Now;

      // Notifier le changement
      if not Terminated and FActive then
      begin
        Synchronize(@DoNotifyChange);

        // Reconfigurer la surveillance pour cette clé
        SetupNotification(Key);
      end;
    end;
  end;
end;

procedure TMultiRegistryMonitor.DoNotifyChange;  
begin
  if Assigned(FOnChange) then
    FOnChange(Self, FLastChangeInfo);
end;

end.

Utilisation du moniteur multi-clés

procedure TForm1.FormCreate(Sender: TObject);  
begin
  FMultiMonitor := TMultiRegistryMonitor.Create;
  FMultiMonitor.OnChange := @OnMultiRegistryChange;

  // Surveiller plusieurs clés
  FMultiMonitor.AddKey(HKEY_CURRENT_USER,
    'Software\MonApp\Settings', True);
  FMultiMonitor.AddKey(HKEY_CURRENT_USER,
    'Software\MonApp\RecentFiles', True);
  FMultiMonitor.AddKey(HKEY_CURRENT_USER,
    'Software\Microsoft\Windows\CurrentVersion\Run', False);

  FMultiMonitor.Start;
end;

procedure TForm1.OnMultiRegistryChange(Sender: TObject;
  const ChangeInfo: TRegistryChangeInfo);
var
  RootKeyName: string;
begin
  // Identifier la ruche
  case ChangeInfo.RootKey of
    HKEY_CLASSES_ROOT:  RootKeyName := 'HKCR';
    HKEY_CURRENT_USER:  RootKeyName := 'HKCU';
    HKEY_LOCAL_MACHINE: RootKeyName := 'HKLM';
    HKEY_USERS:         RootKeyName := 'HKU';
  else
    RootKeyName := 'Unknown';
  end;

  // Logger le changement
  Memo1.Lines.Add(Format('[%s] Changement dans %s\%s',
    [FormatDateTime('hh:nn:ss', ChangeInfo.Timestamp),
     RootKeyName,
     ChangeInfo.KeyPath]));

  // Réagir selon la clé modifiée
  if Pos('Settings', ChangeInfo.KeyPath) > 0 then
  begin
    Memo1.Lines.Add('  → Rechargement des paramètres...');
    ReloadSettings;
  end
  else if Pos('RecentFiles', ChangeInfo.KeyPath) > 0 then
  begin
    Memo1.Lines.Add('  → Mise à jour de la liste des fichiers récents...');
    UpdateRecentFilesList;
  end
  else if Pos('Run', ChangeInfo.KeyPath) > 0 then
  begin
    Memo1.Lines.Add('  → Programme ajouté/retiré du démarrage automatique');
  end;
end;

Détection des changements spécifiques

Comparaison avant/après

Pour savoir exactement ce qui a changé, vous devez comparer l'état avant et après :

unit RegistryChangeDetector;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Registry, Generics.Collections;

type
  TRegistryValue = record
    Name: string;
    DataType: TRegDataType;
    Value: Variant;
  end;

  TRegistrySnapshot = class
  private
    FValues: TDictionary<string, TRegistryValue>;
    FSubKeys: TStringList;
  public
    constructor Create;
    destructor Destroy; override;

    procedure TakeSnapshot(Reg: TRegistry);
    function CompareWith(Other: TRegistrySnapshot;
      out Added, Modified, Deleted: TStringList): Boolean;

    property Values: TDictionary<string, TRegistryValue> read FValues;
    property SubKeys: TStringList read FSubKeys;
  end;

  { TRegistryChangeDetector }
  TRegistryChangeDetector = class
  private
    FRootKey: HKEY;
    FKeyPath: string;
    FLastSnapshot: TRegistrySnapshot;

  public
    constructor Create(ARootKey: HKEY; const AKeyPath: string);
    destructor Destroy; override;

    procedure TakeInitialSnapshot;
    function DetectChanges(out Report: string): Boolean;
  end;

implementation

{ TRegistrySnapshot }

constructor TRegistrySnapshot.Create;  
begin
  FValues := TDictionary<string, TRegistryValue>.Create;
  FSubKeys := TStringList.Create;
end;

destructor TRegistrySnapshot.Destroy;  
begin
  FValues.Free;
  FSubKeys.Free;
  inherited;
end;

procedure TRegistrySnapshot.TakeSnapshot(Reg: TRegistry);  
var
  ValueNames: TStringList;
  i: Integer;
  RegValue: TRegistryValue;
begin
  FValues.Clear;
  FSubKeys.Clear;

  ValueNames := TStringList.Create;
  try
    // Capturer les valeurs
    Reg.GetValueNames(ValueNames);

    for i := 0 to ValueNames.Count - 1 do
    begin
      RegValue.Name := ValueNames[i];
      RegValue.DataType := Reg.GetDataType(ValueNames[i]);

      // Lire la valeur selon son type
      case RegValue.DataType of
        rdString, rdExpandString:
          RegValue.Value := Reg.ReadString(ValueNames[i]);

        rdInteger:
          RegValue.Value := Reg.ReadInteger(ValueNames[i]);

        rdBinary:
          begin
            // Pour simplifier, on stocke la taille
            RegValue.Value := Reg.GetDataSize(ValueNames[i]);
          end;
      else
        RegValue.Value := Null;
      end;

      FValues.Add(ValueNames[i], RegValue);
    end;

    // Capturer les sous-clés
    Reg.GetKeyNames(FSubKeys);

  finally
    ValueNames.Free;
  end;
end;

function TRegistrySnapshot.CompareWith(Other: TRegistrySnapshot;
  out Added, Modified, Deleted: TStringList): Boolean;
var
  Key: string;
  OldValue, NewValue: TRegistryValue;
begin
  Result := False;

  Added := TStringList.Create;
  Modified := TStringList.Create;
  Deleted := TStringList.Create;

  // Chercher les valeurs supprimées et modifiées
  for Key in FValues.Keys do
  begin
    if Other.Values.TryGetValue(Key, NewValue) then
    begin
      // La valeur existe dans les deux
      OldValue := FValues[Key];

      if (OldValue.DataType <> NewValue.DataType) or
         (OldValue.Value <> NewValue.Value) then
      begin
        Modified.Add(Key);
        Result := True;
      end;
    end
    else
    begin
      // La valeur a été supprimée
      Deleted.Add(Key);
      Result := True;
    end;
  end;

  // Chercher les valeurs ajoutées
  for Key in Other.Values.Keys do
  begin
    if not FValues.ContainsKey(Key) then
    begin
      Added.Add(Key);
      Result := True;
    end;
  end;
end;

{ TRegistryChangeDetector }

constructor TRegistryChangeDetector.Create(ARootKey: HKEY;
  const AKeyPath: string);
begin
  FRootKey := ARootKey;
  FKeyPath := AKeyPath;
  FLastSnapshot := nil;
end;

destructor TRegistryChangeDetector.Destroy;  
begin
  FLastSnapshot.Free;
  inherited;
end;

procedure TRegistryChangeDetector.TakeInitialSnapshot;  
var
  Reg: TRegistry;
begin
  FreeAndNil(FLastSnapshot);
  FLastSnapshot := TRegistrySnapshot.Create;

  Reg := TRegistry.Create(KEY_READ);
  try
    Reg.RootKey := FRootKey;

    if Reg.OpenKeyReadOnly(FKeyPath) then
    begin
      try
        FLastSnapshot.TakeSnapshot(Reg);
      finally
        Reg.CloseKey;
      end;
    end;

  finally
    Reg.Free;
  end;
end;

function TRegistryChangeDetector.DetectChanges(out Report: string): Boolean;  
var
  Reg: TRegistry;
  CurrentSnapshot: TRegistrySnapshot;
  Added, Modified, Deleted: TStringList;
  i: Integer;
begin
  Result := False;
  Report := '';

  if not Assigned(FLastSnapshot) then
  begin
    Report := 'Pas de snapshot initial';
    Exit;
  end;

  CurrentSnapshot := TRegistrySnapshot.Create;
  try
    Reg := TRegistry.Create(KEY_READ);
    try
      Reg.RootKey := FRootKey;

      if Reg.OpenKeyReadOnly(FKeyPath) then
      begin
        try
          CurrentSnapshot.TakeSnapshot(Reg);
        finally
          Reg.CloseKey;
        end;
      end;

    finally
      Reg.Free;
    end;

    // Comparer les snapshots
    Result := FLastSnapshot.CompareWith(CurrentSnapshot,
                                        Added, Modified, Deleted);

    if Result then
    begin
      Report := 'Changements détectés :' + sLineBreak;

      if Added.Count > 0 then
      begin
        Report := Report + sLineBreak + 'Valeurs ajoutées :';
        for i := 0 to Added.Count - 1 do
          Report := Report + sLineBreak + '  + ' + Added[i];
      end;

      if Modified.Count > 0 then
      begin
        Report := Report + sLineBreak + 'Valeurs modifiées :';
        for i := 0 to Modified.Count - 1 do
          Report := Report + sLineBreak + '  * ' + Modified[i];
      end;

      if Deleted.Count > 0 then
      begin
        Report := Report + sLineBreak + 'Valeurs supprimées :';
        for i := 0 to Deleted.Count - 1 do
          Report := Report + sLineBreak + '  - ' + Deleted[i];
      end;

      // Mettre à jour le snapshot pour la prochaine comparaison
      FLastSnapshot.Free;
      FLastSnapshot := CurrentSnapshot;
      CurrentSnapshot := nil; // Pour éviter la double libération
    end;

    Added.Free;
    Modified.Free;
    Deleted.Free;

  finally
    CurrentSnapshot.Free;
  end;
end;

Utilisation du détecteur de changements

procedure TForm1.FormCreate(Sender: TObject);  
begin
  // Créer le détecteur
  FChangeDetector := TRegistryChangeDetector.Create(
    HKEY_CURRENT_USER,
    'Software\MonApp\Settings'
  );

  // Prendre un snapshot initial
  FChangeDetector.TakeInitialSnapshot;

  // Créer le moniteur pour être alerté des changements
  FMonitor := TRegistryMonitor.Create(
    HKEY_CURRENT_USER,
    'Software\MonApp\Settings'
  );
  FMonitor.OnChange := @OnRegistryChangeDetailed;
  FMonitor.Start;
end;

procedure TForm1.OnRegistryChangeDetailed(Sender: TObject);  
var
  Report: string;
begin
  // Détecter exactement ce qui a changé
  if FChangeDetector.DetectChanges(Report) then
  begin
    Memo1.Lines.Add(FormatDateTime('hh:nn:ss', Now));
    Memo1.Lines.Add(Report);
    Memo1.Lines.Add('---');

    // Réagir selon les changements
    ProcessChanges(Report);
  end;
end;

procedure TForm1.ProcessChanges(const Report: string);  
begin
  // Analyser le rapport pour prendre des actions spécifiques
  if Pos('Theme', Report) > 0 then
  begin
    ShowMessage('Le thème a changé - application du nouveau thème...');
    ApplyTheme;
  end;

  if Pos('Language', Report) > 0 then
  begin
    ShowMessage('La langue a changé - rechargement des ressources...');
    ReloadLanguage;
  end;
end;

Surveillance avec journalisation

Logger automatique de modifications

unit RegistryChangeLogger;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Registry, Windows, DateUtils;

type
  { TRegistryChangeLogger }
  TRegistryChangeLogger = class
  private
    FLogFile: TextFile;
    FLogFileName: string;
    FMaxLogSize: Int64;
    FRotateCount: Integer;
    FEnabled: Boolean;

    procedure RotateLogIfNeeded;
    procedure WriteLogEntry(const Entry: string);

  public
    constructor Create(const LogFileName: string);
    destructor Destroy; override;

    procedure LogChange(RootKey: HKEY; const KeyPath: string;
                       const Description: string);
    procedure LogValueChange(RootKey: HKEY; const KeyPath, ValueName: string;
                            const OldValue, NewValue: string);
    procedure LogKeyAdded(RootKey: HKEY; const KeyPath: string);
    procedure LogKeyDeleted(RootKey: HKEY; const KeyPath: string);

    property MaxLogSize: Int64 read FMaxLogSize write FMaxLogSize;
    property RotateCount: Integer read FRotateCount write FRotateCount;
    property Enabled: Boolean read FEnabled write FEnabled;
  end;

implementation

{ TRegistryChangeLogger }

constructor TRegistryChangeLogger.Create(const LogFileName: string);  
begin
  FLogFileName := LogFileName;
  FMaxLogSize := 10 * 1024 * 1024; // 10 MB par défaut
  FRotateCount := 5; // Garder 5 anciennes versions
  FEnabled := True;

  // Créer ou ouvrir le fichier log
  AssignFile(FLogFile, FLogFileName);

  if FileExists(FLogFileName) then
  begin
    Append(FLogFile);
    RotateLogIfNeeded;
  end
  else
  begin
    Rewrite(FLogFile);
    WriteLn(FLogFile, '=== Registry Change Log Started ===');
    WriteLn(FLogFile, 'Date: ' + DateTimeToStr(Now));
    WriteLn(FLogFile, '===================================');
  end;

  Flush(FLogFile);
end;

destructor TRegistryChangeLogger.Destroy;  
begin
  if FEnabled then
  begin
    WriteLn(FLogFile, '=== Log Closed: ' + DateTimeToStr(Now) + ' ===');
    CloseFile(FLogFile);
  end;
  inherited;
end;

procedure TRegistryChangeLogger.RotateLogIfNeeded;  
var
  FileInfo: TSearchRec;
  i: Integer;
  OldName, NewName: string;
begin
  // Vérifier la taille du fichier
  if FindFirst(FLogFileName, faAnyFile, FileInfo) = 0 then
  begin
    try
      if FileInfo.Size > FMaxLogSize then
      begin
        CloseFile(FLogFile);

        // Supprimer le plus ancien
        if FileExists(FLogFileName + '.' + IntToStr(FRotateCount)) then
          DeleteFile(FLogFileName + '.' + IntToStr(FRotateCount));

        // Décaler les autres
        for i := FRotateCount - 1 downto 1 do
        begin
          OldName := FLogFileName + '.' + IntToStr(i);
          NewName := FLogFileName + '.' + IntToStr(i + 1);

          if FileExists(OldName) then
            RenameFile(OldName, NewName);
        end;

        // Renommer le fichier actuel
        RenameFile(FLogFileName, FLogFileName + '.1');

        // Créer un nouveau fichier
        Rewrite(FLogFile);
        WriteLn(FLogFile, '=== Log Rotated: ' + DateTimeToStr(Now) + ' ===');
      end;
    finally
      FindClose(FileInfo);
    end;
  end;
end;

procedure TRegistryChangeLogger.WriteLogEntry(const Entry: string);  
begin
  if not FEnabled then Exit;

  WriteLn(FLogFile, FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz', Now) +
                    ' | ' + Entry);
  Flush(FLogFile);

  RotateLogIfNeeded;
end;

procedure TRegistryChangeLogger.LogChange(RootKey: HKEY; const KeyPath: string;
  const Description: string);
var
  RootName: string;
begin
  case RootKey of
    HKEY_CLASSES_ROOT:  RootName := 'HKCR';
    HKEY_CURRENT_USER:  RootName := 'HKCU';
    HKEY_LOCAL_MACHINE: RootName := 'HKLM';
    HKEY_USERS:         RootName := 'HKU';
    HKEY_CURRENT_CONFIG: RootName := 'HKCC';
  else
    RootName := Format('0x%x', [RootKey]);
  end;

  WriteLogEntry(Format('CHANGE | %s\%s | %s',
                      [RootName, KeyPath, Description]));
end;

procedure TRegistryChangeLogger.LogValueChange(RootKey: HKEY;
  const KeyPath, ValueName: string; const OldValue, NewValue: string);
var
  Entry: string;
begin
  Entry := Format('VALUE_CHANGED | %s | "%s" -> "%s"',
                 [ValueName, OldValue, NewValue]);
  LogChange(RootKey, KeyPath, Entry);
end;

procedure TRegistryChangeLogger.LogKeyAdded(RootKey: HKEY; const KeyPath: string);  
begin
  LogChange(RootKey, KeyPath, 'KEY_ADDED');
end;

procedure TRegistryChangeLogger.LogKeyDeleted(RootKey: HKEY; const KeyPath: string);  
begin
  LogChange(RootKey, KeyPath, 'KEY_DELETED');
end;

Cas pratiques avancés

Synchronisation de configuration entre applications

unit ConfigSyncManager;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Windows, Registry, SyncObjs;

type
  { TConfigSyncManager }
  TConfigSyncManager = class
  private
    FMutex: TMutex;
    FMonitor: TRegistryMonitor;
    FOnConfigChanged: TNotifyEvent;
    FIgnoreNextChange: Boolean;

    procedure OnRegistryChange(Sender: TObject);

  public
    constructor Create;
    destructor Destroy; override;

    procedure WriteConfig(const Name, Value: string);
    function ReadConfig(const Name: string; const Default: string = ''): string;
    procedure StartSync;
    procedure StopSync;

    property OnConfigChanged: TNotifyEvent read FOnConfigChanged
                                          write FOnConfigChanged;
  end;

implementation

const
  CONFIG_KEY = 'Software\MonEntreprise\SharedConfig';
  MUTEX_NAME = 'MonEntreprise_ConfigSync_Mutex';

{ TConfigSyncManager }

constructor TConfigSyncManager.Create;  
begin
  // Créer un mutex pour la synchronisation inter-processus
  FMutex := TMutex.Create(nil, False, MUTEX_NAME);

  // Créer le moniteur
  FMonitor := TRegistryMonitor.Create(HKEY_CURRENT_USER, CONFIG_KEY);
  FMonitor.OnChange := @OnRegistryChange;

  FIgnoreNextChange := False;
end;

destructor TConfigSyncManager.Destroy;  
begin
  StopSync;
  FMonitor.Free;
  FMutex.Free;
  inherited;
end;

procedure TConfigSyncManager.WriteConfig(const Name, Value: string);  
var
  Reg: TRegistry;
begin
  // Acquérir le mutex pour éviter les conflits
  if FMutex.WaitFor(1000) = wrSignaled then
  begin
    try
      FIgnoreNextChange := True; // Ignorer notre propre modification

      Reg := TRegistry.Create;
      try
        Reg.RootKey := HKEY_CURRENT_USER;

        if Reg.OpenKey(CONFIG_KEY, True) then
        begin
          try
            Reg.WriteString(Name, Value);

            // Ajouter un timestamp pour indiquer la dernière modification
            Reg.WriteDateTime('LastModified_' + Name, Now);
            Reg.WriteString('LastModifiedBy_' + Name,
                           ExtractFileName(ParamStr(0)));

          finally
            Reg.CloseKey;
          end;
        end;

      finally
        Reg.Free;
      end;

    finally
      FMutex.Release;
    end;
  end
  else
    raise Exception.Create('Timeout lors de l''accès au mutex de configuration');
end;

function TConfigSyncManager.ReadConfig(const Name: string;
  const Default: string): string;
var
  Reg: TRegistry;
begin
  Result := Default;

  // Lecture rapide sans mutex (lecture seule)
  Reg := TRegistry.Create(KEY_READ);
  try
    Reg.RootKey := HKEY_CURRENT_USER;

    if Reg.OpenKeyReadOnly(CONFIG_KEY) then
    begin
      try
        if Reg.ValueExists(Name) then
          Result := Reg.ReadString(Name);

      finally
        Reg.CloseKey;
      end;
    end;

  finally
    Reg.Free;
  end;
end;

procedure TConfigSyncManager.StartSync;  
begin
  FMonitor.Start;
end;

procedure TConfigSyncManager.StopSync;  
begin
  FMonitor.Stop;
end;

procedure TConfigSyncManager.OnRegistryChange(Sender: TObject);  
begin
  if FIgnoreNextChange then
  begin
    FIgnoreNextChange := False;
    Exit;
  end;

  // Notification de changement externe
  if Assigned(FOnConfigChanged) then
    FOnConfigChanged(Self);
end;

Surveillance de sécurité

unit RegistrySecurityMonitor;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Windows, Registry;

type
  TSecurityLevel = (slNormal, slWarning, slCritical);

  TSecurityAlert = record
    Level: TSecurityLevel;
    Timestamp: TDateTime;
    KeyPath: string;
    Description: string;
    ProcessName: string;
  end;

  TSecurityAlertEvent = procedure(Sender: TObject;
    const Alert: TSecurityAlert) of object;

  { TRegistrySecurityMonitor }
  TRegistrySecurityMonitor = class
  private
    FCriticalKeys: TStringList;
    FMonitors: TList;
    FOnSecurityAlert: TSecurityAlertEvent;
    FLogFile: string;

    procedure MonitorChange(Sender: TObject);
    procedure CheckForSuspiciousActivity(const KeyPath: string);
    procedure RaiseAlert(Level: TSecurityLevel; const KeyPath,
                        Description: string);
    procedure LogAlert(const Alert: TSecurityAlert);

  public
    constructor Create;
    destructor Destroy; override;

    procedure AddCriticalKey(RootKey: HKEY; const KeyPath: string);
    procedure StartMonitoring;
    procedure StopMonitoring;

    property OnSecurityAlert: TSecurityAlertEvent read FOnSecurityAlert
                                                  write FOnSecurityAlert;
    property LogFile: string read FLogFile write FLogFile;
  end;

implementation

{ TRegistrySecurityMonitor }

constructor TRegistrySecurityMonitor.Create;  
begin
  FCriticalKeys := TStringList.Create;
  FMonitors := TList.Create;
  FLogFile := ExtractFilePath(ParamStr(0)) + 'security.log';

  // Ajouter les clés critiques par défaut
  AddCriticalKey(HKEY_LOCAL_MACHINE,
    'SOFTWARE\Microsoft\Windows\CurrentVersion\Run');
  AddCriticalKey(HKEY_CURRENT_USER,
    'Software\Microsoft\Windows\CurrentVersion\Run');
  AddCriticalKey(HKEY_LOCAL_MACHINE,
    'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon');
  AddCriticalKey(HKEY_LOCAL_MACHINE,
    'SYSTEM\CurrentControlSet\Services');
  AddCriticalKey(HKEY_CLASSES_ROOT,
    '.exe');
end;

destructor TRegistrySecurityMonitor.Destroy;  
begin
  StopMonitoring;

  // Libérer les moniteurs
  while FMonitors.Count > 0 do
  begin
    TObject(FMonitors[0]).Free;
    FMonitors.Delete(0);
  end;

  FMonitors.Free;
  FCriticalKeys.Free;
  inherited;
end;

procedure TRegistrySecurityMonitor.AddCriticalKey(RootKey: HKEY;
  const KeyPath: string);
var
  Monitor: TRegistryMonitor;
begin
  // Créer un moniteur pour cette clé
  Monitor := TRegistryMonitor.Create(RootKey, KeyPath);
  Monitor.OnChange := @MonitorChange;
  Monitor.WatchSubTree := True;

  FMonitors.Add(Monitor);
  FCriticalKeys.Add(Format('%d|%s', [RootKey, KeyPath]));
end;

procedure TRegistrySecurityMonitor.StartMonitoring;  
var
  i: Integer;
begin
  for i := 0 to FMonitors.Count - 1 do
    TRegistryMonitor(FMonitors[i]).Start;
end;

procedure TRegistrySecurityMonitor.StopMonitoring;  
var
  i: Integer;
begin
  for i := 0 to FMonitors.Count - 1 do
    TRegistryMonitor(FMonitors[i]).Stop;
end;

procedure TRegistrySecurityMonitor.MonitorChange(Sender: TObject);  
var
  Monitor: TRegistryMonitor;
begin
  Monitor := Sender as TRegistryMonitor;

  // Vérifier l'activité suspecte
  CheckForSuspiciousActivity(Monitor.KeyPath);
end;

procedure TRegistrySecurityMonitor.CheckForSuspiciousActivity(
  const KeyPath: string);
var
  Reg: TRegistry;
  Values: TStringList;
  i: Integer;
  SuspiciousFound: Boolean;
  Value: string;
begin
  SuspiciousFound := False;

  Reg := TRegistry.Create(KEY_READ);
  Values := TStringList.Create;
  try
    // Analyser les valeurs pour détecter des patterns suspects
    if Pos('\Run', KeyPath) > 0 then
    begin
      // Vérifier les nouvelles entrées de démarrage automatique
      Reg.RootKey := HKEY_CURRENT_USER;

      if Reg.OpenKeyReadOnly(KeyPath) then
      begin
        try
          Reg.GetValueNames(Values);

          for i := 0 to Values.Count - 1 do
          begin
            Value := Reg.ReadString(Values[i]);

            // Patterns suspects
            if (Pos('temp\', LowerCase(Value)) > 0) or
               (Pos('appdata\local\temp', LowerCase(Value)) > 0) or
               (Pos('powershell', LowerCase(Value)) > 0) or
               (Pos('cmd.exe', LowerCase(Value)) > 0) or
               (Pos('wscript', LowerCase(Value)) > 0) then
            begin
              RaiseAlert(slCritical, KeyPath,
                Format('Programme suspect au démarrage : %s = %s',
                      [Values[i], Value]));
              SuspiciousFound := True;
            end;
          end;

        finally
          Reg.CloseKey;
        end;
      end;
    end;

    if not SuspiciousFound then
    begin
      // Alerte normale pour les clés surveillées
      RaiseAlert(slWarning, KeyPath, 'Modification détectée dans une clé critique');
    end;

  finally
    Values.Free;
    Reg.Free;
  end;
end;

procedure TRegistrySecurityMonitor.RaiseAlert(Level: TSecurityLevel;
  const KeyPath, Description: string);
var
  Alert: TSecurityAlert;
begin
  Alert.Level := Level;
  Alert.Timestamp := Now;
  Alert.KeyPath := KeyPath;
  Alert.Description := Description;
  Alert.ProcessName := ExtractFileName(ParamStr(0));

  // Logger l'alerte
  LogAlert(Alert);

  // Notifier l'application
  if Assigned(FOnSecurityAlert) then
    FOnSecurityAlert(Self, Alert);
end;

procedure TRegistrySecurityMonitor.LogAlert(const Alert: TSecurityAlert);  
var
  F: TextFile;
  LevelStr: string;
begin
  case Alert.Level of
    slNormal:   LevelStr := 'NORMAL';
    slWarning:  LevelStr := 'WARNING';
    slCritical: LevelStr := 'CRITICAL';
  end;

  AssignFile(F, FLogFile);
  try
    if FileExists(FLogFile) then
      Append(F)
    else
      Rewrite(F);

    WriteLn(F, Format('[%s] [%s] %s | %s | %s',
      [FormatDateTime('yyyy-mm-dd hh:nn:ss', Alert.Timestamp),
       LevelStr,
       Alert.KeyPath,
       Alert.Description,
       Alert.ProcessName]));

  finally
    CloseFile(F);
  end;
end;

Performance et optimisation

Surveillance optimisée avec cache

unit OptimizedRegistryMonitor;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Windows, Registry, DateUtils;

type
  { TOptimizedRegistryMonitor }
  TOptimizedRegistryMonitor = class(TThread)
  private
    FKeyPaths: TStringList;
    FLastCheck: TDateTime;
    FCheckInterval: Integer; // Secondes
    FCache: TStringList;
    FUseCache: Boolean;
    FCacheTimeout: Integer; // Secondes

    procedure UpdateCache(const KeyPath, Data: string);
    function GetFromCache(const KeyPath: string; out Data: string): Boolean;
    procedure ClearExpiredCache;

  protected
    procedure Execute; override;

  public
    constructor Create;
    destructor Destroy; override;

    procedure AddKey(const KeyPath: string);
    procedure RemoveKey(const KeyPath: string);

    property CheckInterval: Integer read FCheckInterval write FCheckInterval;
    property UseCache: Boolean read FUseCache write FUseCache;
    property CacheTimeout: Integer read FCacheTimeout write FCacheTimeout;
  end;

implementation

type
  TCacheEntry = class
    Data: string;
    Timestamp: TDateTime;
  end;

{ TOptimizedRegistryMonitor }

constructor TOptimizedRegistryMonitor.Create;  
begin
  inherited Create(True);

  FKeyPaths := TStringList.Create;
  FCache := TStringList.Create;
  FCache.OwnsObjects := True;

  FCheckInterval := 5; // Vérifier toutes les 5 secondes
  FCacheTimeout := 30; // Cache valide 30 secondes
  FUseCache := True;
  FLastCheck := 0;

  FreeOnTerminate := False;
end;

destructor TOptimizedRegistryMonitor.Destroy;  
begin
  FCache.Free;
  FKeyPaths.Free;
  inherited;
end;

procedure TOptimizedRegistryMonitor.AddKey(const KeyPath: string);  
begin
  FKeyPaths.Add(KeyPath);
end;

procedure TOptimizedRegistryMonitor.RemoveKey(const KeyPath: string);  
var
  Index: Integer;
begin
  Index := FKeyPaths.IndexOf(KeyPath);
  if Index >= 0 then
  begin
    FKeyPaths.Delete(Index);

    // Retirer du cache aussi
    Index := FCache.IndexOf(KeyPath);
    if Index >= 0 then
      FCache.Delete(Index);
  end;
end;

procedure TOptimizedRegistryMonitor.UpdateCache(const KeyPath, Data: string);  
var
  Entry: TCacheEntry;
  Index: Integer;
begin
  if not FUseCache then Exit;

  Index := FCache.IndexOf(KeyPath);

  if Index >= 0 then
  begin
    Entry := TCacheEntry(FCache.Objects[Index]);
    Entry.Data := Data;
    Entry.Timestamp := Now;
  end
  else
  begin
    Entry := TCacheEntry.Create;
    Entry.Data := Data;
    Entry.Timestamp := Now;
    FCache.AddObject(KeyPath, Entry);
  end;
end;

function TOptimizedRegistryMonitor.GetFromCache(const KeyPath: string;
  out Data: string): Boolean;
var
  Index: Integer;
  Entry: TCacheEntry;
begin
  Result := False;
  Data := '';

  if not FUseCache then Exit;

  Index := FCache.IndexOf(KeyPath);
  if Index >= 0 then
  begin
    Entry := TCacheEntry(FCache.Objects[Index]);

    // Vérifier si le cache est encore valide
    if SecondsBetween(Now, Entry.Timestamp) <= FCacheTimeout then
    begin
      Data := Entry.Data;
      Result := True;
    end;
  end;
end;

procedure TOptimizedRegistryMonitor.ClearExpiredCache;  
var
  i: Integer;
  Entry: TCacheEntry;
begin
  if not FUseCache then Exit;

  for i := FCache.Count - 1 downto 0 do
  begin
    Entry := TCacheEntry(FCache.Objects[i]);

    if SecondsBetween(Now, Entry.Timestamp) > FCacheTimeout then
      FCache.Delete(i);
  end;
end;

procedure TOptimizedRegistryMonitor.Execute;  
var
  i: Integer;
  CachedData: string;
begin
  while not Terminated do
  begin
    // Vérifier l'intervalle
    if SecondsBetween(Now, FLastCheck) >= FCheckInterval then
    begin
      FLastCheck := Now;

      // Nettoyer le cache expiré
      ClearExpiredCache;

      // Vérifier chaque clé
      for i := 0 to FKeyPaths.Count - 1 do
      begin
        if Terminated then Break;

        // Essayer d'abord le cache
        if not GetFromCache(FKeyPaths[i], CachedData) then
        begin
          // Pas dans le cache ou expiré, lire du registre
          // ... (code de lecture du registre)

          // Mettre à jour le cache
          UpdateCache(FKeyPaths[i], CachedData);
        end;
      end;
    end;

    Sleep(100); // Petite pause pour ne pas surcharger le CPU
  end;
end;

end.

Bonnes pratiques pour la surveillance

1. Limiter le nombre de clés surveillées

// MAUVAIS - Surveiller tout le registre
Monitor.Create(HKEY_LOCAL_MACHINE, 'SOFTWARE');  
Monitor.WatchSubTree := True; // Surveille des milliers de clés !

// BON - Surveiller uniquement ce qui est nécessaire
Monitor.Create(HKEY_CURRENT_USER, 'Software\MonApp\Settings');  
Monitor.WatchSubTree := False; // Seulement cette clé

2. Utiliser des threads pour ne pas bloquer

// MAUVAIS - Bloque l'interface
procedure TForm1.Button1Click(Sender: TObject);  
begin
  RegNotifyChangeKeyValue(..., False); // Bloquant !
end;

// BON - Utiliser un thread
procedure TForm1.Button1Click(Sender: TObject);  
begin
  FMonitor := TRegistryMonitor.Create(...);
  FMonitor.Start; // Non bloquant
end;

3. Gérer les erreurs de permission

try
  Monitor.Start;
except
  on E: Exception do
  begin
    if Pos('Access denied', E.Message) > 0 then
      ShowMessage('Permissions insuffisantes pour surveiller cette clé')
    else
      ShowMessage('Erreur : ' + E.Message);
  end;
end;

4. Nettoyer les ressources

procedure TForm1.FormDestroy(Sender: TObject);  
begin
  // Toujours arrêter et libérer les moniteurs
  if Assigned(FMonitor) then
  begin
    FMonitor.Stop;
    FMonitor.Free;
  end;
end;

5. Éviter les boucles infinies

procedure OnRegistryChange;  
begin
  // MAUVAIS - Peut créer une boucle
  Reg.WriteString('LastCheck', DateTimeToStr(Now)); // Déclenche un autre changement !

  // BON - Utiliser un flag
  if not FUpdating then
  begin
    FUpdating := True;
    try
      Reg.WriteString('LastCheck', DateTimeToStr(Now));
    finally
      FUpdating := False;
    end;
  end;
end;

Résumé des points clés

  1. Deux approches : Synchrone (bloquante) ou asynchrone (avec threads)
  2. RegNotifyChangeKeyValue est l'API Windows de base
  3. Utiliser des Events pour la surveillance non-bloquante
  4. Comparer des snapshots pour détecter les changements exacts
  5. Logger les modifications pour l'audit et le débogage
  6. Surveiller la sécurité pour détecter les activités suspectes
  7. Optimiser avec un cache pour réduire les accès au registre
  8. Gérer les erreurs de permission et d'accès
  9. Libérer les ressources proprement
  10. Éviter les boucles infinies lors des modifications

Guide de dépannage

Problème : La surveillance ne détecte pas les changements

Causes possibles et solutions :

// Vérifier les permissions
procedure CheckMonitoringPermissions;  
var
  Reg: TRegistry;
begin
  Reg := TRegistry.Create(KEY_NOTIFY or KEY_READ);
  try
    Reg.RootKey := HKEY_LOCAL_MACHINE;

    if not Reg.OpenKeyReadOnly('SOFTWARE\TestKey') then
    begin
      ShowMessage('Impossible d''ouvrir la clé - vérifiez les permissions');

      // Essayer avec des permissions utilisateur
      Reg.RootKey := HKEY_CURRENT_USER;
      if Reg.OpenKeyReadOnly('Software\TestKey') then
        ShowMessage('Fonctionne avec HKEY_CURRENT_USER');
    end;

  finally
    Reg.Free;
  end;
end;

// Vérifier le filtre de notification
procedure CheckNotificationFilter;  
begin
  // S'assurer que le bon filtre est utilisé
  Monitor.NotifyFilter := REG_NOTIFY_CHANGE_LAST_SET;  // Pour les valeurs
  // ou
  Monitor.NotifyFilter := REG_NOTIFY_CHANGE_NAME;      // Pour les clés
  // ou
  Monitor.NotifyFilter := REG_NOTIFY_CHANGE_LAST_SET or
                         REG_NOTIFY_CHANGE_NAME;        // Pour tout
end;

Problème : Le programme se bloque

Solution : Utiliser la surveillance asynchrone

// PROBLÈME : Code bloquant
procedure TForm1.Button1Click(Sender: TObject);  
begin
  RegNotifyChangeKeyValue(Handle, True, REG_NOTIFY_ALL, 0, False);
  // L'interface est bloquée jusqu'à un changement !
end;

// SOLUTION : Utiliser un thread
procedure TForm1.Button1Click(Sender: TObject);  
begin
  TThread.CreateAnonymousThread(
    procedure
    begin
      RegNotifyChangeKeyValue(Handle, True, REG_NOTIFY_ALL, 0, False);

      TThread.Synchronize(nil,
        procedure
        begin
          ShowMessage('Changement détecté !');
        end
      );
    end
  ).Start;
end;

Problème : Fuite mémoire avec les moniteurs

Solution : Toujours nettoyer les ressources

type
  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    FMonitors: TObjectList<TRegistryMonitor>;
  end;

procedure TForm1.FormCreate(Sender: TObject);  
begin
  FMonitors := TObjectList<TRegistryMonitor>.Create(True); // True = owns objects

  // Ajouter des moniteurs
  FMonitors.Add(TRegistryMonitor.Create(...));
end;

procedure TForm1.FormDestroy(Sender: TObject);  
var
  Monitor: TRegistryMonitor;
begin
  // Arrêter tous les moniteurs avant de les libérer
  for Monitor in FMonitors do
    Monitor.Stop;

  FMonitors.Free; // Libère automatiquement tous les moniteurs
end;

Template complet prêt à l'emploi

Classe de surveillance complète

unit RegistryWatcher;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Windows, Registry, Generics.Collections, DateUtils;

type
  TChangeType = (ctValueAdded, ctValueModified, ctValueDeleted,
                 ctKeyAdded, ctKeyDeleted);

  TRegistryChange = record
    ChangeType: TChangeType;
    KeyPath: string;
    ValueName: string;
    OldValue: string;
    NewValue: string;
    Timestamp: TDateTime;
  end;

  TOnRegistryChange = procedure(Sender: TObject;
    const Change: TRegistryChange) of object;

  { TRegistryWatcher }
  TRegistryWatcher = class(TComponent)
  private
    FRootKey: HKEY;
    FKeyPath: string;
    FActive: Boolean;
    FWatchSubTree: Boolean;
    FThread: TThread;
    FOnChange: TOnRegistryChange;
    FSnapshot: TDictionary<string, string>;
    FEventHandle: THandle;

    procedure TakeSnapshot;
    procedure CompareSnapshots;
    procedure StartWatching;
    procedure StopWatching;
    procedure SetActive(AValue: Boolean);

  protected
    procedure DoChange(const Change: TRegistryChange); virtual;

  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

    procedure Start;
    procedure Stop;
    procedure Refresh;

  published
    property RootKey: HKEY read FRootKey write FRootKey default HKEY_CURRENT_USER;
    property KeyPath: string read FKeyPath write FKeyPath;
    property Active: Boolean read FActive write SetActive default False;
    property WatchSubTree: Boolean read FWatchSubTree write FWatchSubTree default True;
    property OnChange: TOnRegistryChange read FOnChange write FOnChange;
  end;

procedure Register;

implementation

type
  { TWatcherThread }
  TWatcherThread = class(TThread)
  private
    FWatcher: TRegistryWatcher;
    FKeyHandle: HKEY;
    FEventHandle: THandle;

  protected
    procedure Execute; override;

  public
    constructor Create(AWatcher: TRegistryWatcher; AEventHandle: THandle);
    destructor Destroy; override;
  end;

{ TRegistryWatcher }

constructor TRegistryWatcher.Create(AOwner: TComponent);  
begin
  inherited Create(AOwner);

  FRootKey := HKEY_CURRENT_USER;
  FKeyPath := '';
  FActive := False;
  FWatchSubTree := True;
  FSnapshot := TDictionary<string, string>.Create;
  FEventHandle := CreateEvent(nil, False, False, nil);
end;

destructor TRegistryWatcher.Destroy;  
begin
  Stop;

  if FEventHandle <> 0 then
    CloseHandle(FEventHandle);

  FSnapshot.Free;
  inherited;
end;

procedure TRegistryWatcher.Start;  
begin
  if FActive then Exit;

  if FKeyPath = '' then
    raise Exception.Create('KeyPath non défini');

  TakeSnapshot;
  StartWatching;
  FActive := True;
end;

procedure TRegistryWatcher.Stop;  
begin
  if not FActive then Exit;

  StopWatching;
  FActive := False;
end;

procedure TRegistryWatcher.Refresh;  
begin
  if FActive then
  begin
    CompareSnapshots;
    TakeSnapshot;
  end;
end;

procedure TRegistryWatcher.SetActive(AValue: Boolean);  
begin
  if FActive = AValue then Exit;

  if AValue then
    Start
  else
    Stop;
end;

procedure TRegistryWatcher.TakeSnapshot;  
var
  Reg: TRegistry;
  Values: TStringList;
  i: Integer;
  Key, Value: string;
begin
  FSnapshot.Clear;

  Reg := TRegistry.Create(KEY_READ);
  Values := TStringList.Create;
  try
    Reg.RootKey := FRootKey;

    if Reg.OpenKeyReadOnly(FKeyPath) then
    begin
      try
        Reg.GetValueNames(Values);

        for i := 0 to Values.Count - 1 do
        begin
          Key := Values[i];

          case Reg.GetDataType(Key) of
            rdString, rdExpandString:
              Value := Reg.ReadString(Key);
            rdInteger:
              Value := IntToStr(Reg.ReadInteger(Key));
          else
            Value := '[Binary Data]';
          end;

          FSnapshot.Add(Key, Value);
        end;

      finally
        Reg.CloseKey;
      end;
    end;

  finally
    Values.Free;
    Reg.Free;
  end;
end;

procedure TRegistryWatcher.CompareSnapshots;  
var
  NewSnapshot: TDictionary<string, string>;
  Reg: TRegistry;
  Values: TStringList;
  i: Integer;
  Key, OldValue, NewValue: string;
  Change: TRegistryChange;
begin
  NewSnapshot := TDictionary<string, string>.Create;
  try
    // Prendre un nouveau snapshot
    Reg := TRegistry.Create(KEY_READ);
    Values := TStringList.Create;
    try
      Reg.RootKey := FRootKey;

      if Reg.OpenKeyReadOnly(FKeyPath) then
      begin
        try
          Reg.GetValueNames(Values);

          for i := 0 to Values.Count - 1 do
          begin
            Key := Values[i];

            case Reg.GetDataType(Key) of
              rdString, rdExpandString:
                NewValue := Reg.ReadString(Key);
              rdInteger:
                NewValue := IntToStr(Reg.ReadInteger(Key));
            else
              NewValue := '[Binary Data]';
            end;

            NewSnapshot.Add(Key, NewValue);

            // Vérifier si c'est une nouvelle valeur ou une modification
            if FSnapshot.TryGetValue(Key, OldValue) then
            begin
              if OldValue <> NewValue then
              begin
                // Valeur modifiée
                Change.ChangeType := ctValueModified;
                Change.KeyPath := FKeyPath;
                Change.ValueName := Key;
                Change.OldValue := OldValue;
                Change.NewValue := NewValue;
                Change.Timestamp := Now;

                DoChange(Change);
              end;
            end
            else
            begin
              // Nouvelle valeur
              Change.ChangeType := ctValueAdded;
              Change.KeyPath := FKeyPath;
              Change.ValueName := Key;
              Change.OldValue := '';
              Change.NewValue := NewValue;
              Change.Timestamp := Now;

              DoChange(Change);
            end;
          end;

        finally
          Reg.CloseKey;
        end;
      end;

    finally
      Values.Free;
      Reg.Free;
    end;

    // Vérifier les valeurs supprimées
    for Key in FSnapshot.Keys do
    begin
      if not NewSnapshot.ContainsKey(Key) then
      begin
        // Valeur supprimée
        FSnapshot.TryGetValue(Key, OldValue);

        Change.ChangeType := ctValueDeleted;
        Change.KeyPath := FKeyPath;
        Change.ValueName := Key;
        Change.OldValue := OldValue;
        Change.NewValue := '';
        Change.Timestamp := Now;

        DoChange(Change);
      end;
    end;

    // Mettre à jour le snapshot
    FSnapshot.Free;
    FSnapshot := NewSnapshot;
    NewSnapshot := nil;

  finally
    NewSnapshot.Free;
  end;
end;

procedure TRegistryWatcher.StartWatching;  
begin
  FThread := TWatcherThread.Create(Self, FEventHandle);
  FThread.Start;
end;

procedure TRegistryWatcher.StopWatching;  
begin
  if Assigned(FThread) then
  begin
    FThread.Terminate;
    SetEvent(FEventHandle); // Débloquer le thread
    FThread.WaitFor;
    FThread.Free;
    FThread := nil;
  end;
end;

procedure TRegistryWatcher.DoChange(const Change: TRegistryChange);  
begin
  if Assigned(FOnChange) then
    FOnChange(Self, Change);
end;

{ TWatcherThread }

constructor TWatcherThread.Create(AWatcher: TRegistryWatcher;
  AEventHandle: THandle);
var
  Reg: TRegistry;
begin
  inherited Create(True);

  FWatcher := AWatcher;
  FEventHandle := AEventHandle;
  FreeOnTerminate := False;

  // Ouvrir la clé
  Reg := TRegistry.Create(KEY_NOTIFY);
  try
    Reg.RootKey := FWatcher.RootKey;

    if Reg.OpenKeyReadOnly(FWatcher.KeyPath) then
      FKeyHandle := Reg.CurrentKey
    else
      raise Exception.Create('Impossible d''ouvrir la clé pour surveillance');

  finally
    Reg.Free;
  end;
end;

destructor TWatcherThread.Destroy;  
begin
  if FKeyHandle <> 0 then
    RegCloseKey(FKeyHandle);

  inherited;
end;

procedure TWatcherThread.Execute;  
var
  Result: Longint;
begin
  while not Terminated do
  begin
    // Configurer la notification
    Result := RegNotifyChangeKeyValue(
      FKeyHandle,
      FWatcher.WatchSubTree,
      REG_NOTIFY_CHANGE_LAST_SET or REG_NOTIFY_CHANGE_NAME,
      FEventHandle,
      True
    );

    if Result <> ERROR_SUCCESS then
      Break;

    // Attendre l'event ou la terminaison
    if WaitForSingleObject(FEventHandle, INFINITE) = WAIT_OBJECT_0 then
    begin
      if not Terminated then
      begin
        // Un changement a eu lieu
        TThread.Synchronize(nil,
          procedure
          begin
            FWatcher.CompareSnapshots;
            FWatcher.TakeSnapshot;
          end
        );
      end;
    end;
  end;
end;

procedure Register;  
begin
  RegisterComponents('System', [TRegistryWatcher]);
end;

end.

Exemple d'utilisation simple

procedure TForm1.FormCreate(Sender: TObject);  
begin
  // Créer le watcher
  FWatcher := TRegistryWatcher.Create(Self);
  FWatcher.RootKey := HKEY_CURRENT_USER;
  FWatcher.KeyPath := 'Software\MonApp\Settings';
  FWatcher.OnChange := @OnRegistryChanged;

  // Démarrer la surveillance
  FWatcher.Start;
end;

procedure TForm1.OnRegistryChanged(Sender: TObject;
  const Change: TRegistryChange);
var
  ChangeTypeStr: string;
begin
  case Change.ChangeType of
    ctValueAdded:    ChangeTypeStr := 'Ajout';
    ctValueModified: ChangeTypeStr := 'Modification';
    ctValueDeleted:  ChangeTypeStr := 'Suppression';
    ctKeyAdded:      ChangeTypeStr := 'Nouvelle clé';
    ctKeyDeleted:    ChangeTypeStr := 'Clé supprimée';
  end;

  Memo1.Lines.Add(Format('[%s] %s - %s: %s -> %s',
    [FormatDateTime('hh:nn:ss', Change.Timestamp),
     ChangeTypeStr,
     Change.ValueName,
     Change.OldValue,
     Change.NewValue]));
end;

Checklist de mise en œuvre

✅ Avant de commencer

  • Identifier les clés exactes à surveiller
  • Déterminer le niveau de détail nécessaire (clé seule ou avec sous-clés)
  • Vérifier les permissions d'accès aux clés
  • Choisir entre surveillance synchrone ou asynchrone
  • Prévoir la gestion des erreurs

✅ Pendant le développement

  • Implémenter la surveillance de base
  • Ajouter la comparaison de snapshots si nécessaire
  • Mettre en place la journalisation
  • Tester avec différents types de modifications
  • Gérer les cas limites (clé supprimée, permissions perdues)

✅ Avant la mise en production

  • Optimiser la fréquence de vérification
  • Implémenter un cache si approprié
  • Tester les performances avec plusieurs clés
  • Vérifier l'absence de fuites mémoire
  • Documenter les clés surveillées et pourquoi

Performances et limites

Limites système

  • Nombre maximum d'events : Windows limite le nombre d'objets event
  • Performance : Surveiller trop de clés peut impacter les performances
  • Notifications manquées : Si trop de changements trop rapidement

Recommandations de performance

// Surveiller efficacement
const
  MAX_MONITORED_KEYS = 10; // Limiter le nombre de clés
  CHECK_INTERVAL = 5000;   // Ne pas vérifier trop souvent (ms)
  CACHE_TIMEOUT = 30;      // Utiliser un cache (secondes)

// Regrouper les notifications
type
  TBatchedMonitor = class
  private
    FChanges: TList<TRegistryChange>;
    FBatchTimer: TTimer;

    procedure FlushBatch;
  public
    procedure AddChange(const Change: TRegistryChange);
  end;

procedure TBatchedMonitor.AddChange(const Change: TRegistryChange);  
begin
  FChanges.Add(Change);

  // Traiter par batch toutes les 500ms
  FBatchTimer.Enabled := False;
  FBatchTimer.Interval := 500;
  FBatchTimer.Enabled := True;
end;

procedure TBatchedMonitor.FlushBatch;  
begin
  // Traiter tous les changements d'un coup
  ProcessAllChanges(FChanges);
  FChanges.Clear;
end;

Conclusion

La surveillance des modifications du Registre Windows est un outil puissant pour :

  • Synchroniser des configurations entre applications
  • Détecter des modifications non autorisées
  • Déboguer des problèmes de configuration
  • Auditer les changements système

Les points essentiels à retenir :

  1. Utilisez RegNotifyChangeKeyValue pour la surveillance de base
  2. Implémentez avec des threads pour ne pas bloquer
  3. Comparez des snapshots pour détecter les changements précis
  4. Gérez correctement les erreurs et permissions
  5. Optimisez pour éviter l'impact sur les performances

Avec les exemples et templates fournis, vous avez tout ce qu'il faut pour implémenter une surveillance robuste et efficace du Registre Windows dans vos applications FreePascal/Lazarus.

⏭️ COM/ActiveX et OLE