EurekaLog
Menu

The EurekaLog Developer Guide

17. EConfig and EModules: options at run time

When a report goes to the wrong folder, first ask which settings instance you changed. The build embeds project options in each reporting module. At run time the module has a default options object, and every raised exception captures its own copy. Changing the default after the exception was raised is too late for that exception.

Use EModules to read or change the live defaults and to identify a module in a report. Use EConfig only when you need EurekaLog's registry/INI configuration-storage abstraction; choosing an INI store does not turn the project's .eof into a runtime configuration file.

Start with section 17.3 for a runtime change, section 17.4 for a change to one exception, or section 17.7 for configuration storage. Chapter 25 explains which settings file a build embeds; chapter 2 covers the complete build and deployment workflow.

17.1 EModules - the unit

EModules provides module information functions used to build bug reports. Most of this unit can work in your application without EurekaLog enabled or installed.

That applies to the module-information routines below, not to the options accessor: CurrentEurekaLogOptions requires EurekaLog to be active before it can be called at all.

The unit declares two clearly separated groups: the options accessor, which returns the options of the current executable module as an object you can read and (partly) change, and the module facts - application name and description, compilation date and time, the module an exception address belongs to, and its version.

17.2 The options object

CurrentEurekaLogOptions

function CurrentEurekaLogOptions: TEurekaModuleOptions;   // EModules

Returns the current, global EurekaLog options for the executable module. Chapter 6 introduces this function with its blunt warning - if EurekaLog is not active, calling it produces the message Cannot use 'CurrentEurekaLogOptions' function in module "XYZ" without activate EurekaLog and then terminates the process immediately - so there is nothing to catch and continue from.

The returned options object follows these rules:

  • When the options exist. They are present if EurekaLog was installed - its code is in the build and the executable was processed.
  • Where their defaults come from. The project's EurekaLog settings, as set in the project options or in the standalone settings editor.
  • That they can change at any time - with an immediate qualification: some options are read-only, because they cannot be changed after activation.
  • That there is a copy per exception. CurrentEurekaLogOptions represents the global options, and those can also be held locally; when that happens, changes to the global options have no effect on the local copy. The documented example: a raised exception creates a copy of the options, so a later change to the global ones does not reach an exception that is already active. For such an exception the local copy is the target - the .Options property of a dialog, the AOptions argument of an event handler.
  • Prefer values that already work at startup. Environment variables in design-time options can avoid initialization code altogether. An exception may occur before your custom setup runs.

TEurekaModuleOptions

TEurekaModuleOptions = class(TInterfacedPersistent);   // EClasses

The class behind the accessor is declared in EClasses:

  • It represents EurekaLog options, usually stored as one instance per loaded executable module; you can also create a new instance yourself, for custom options.
  • You normally reach it through CurrentEurekaLogOptions, or through the Options property of other EurekaLog classes - dialogs, call stacks and the rest.
  • Options are stored as named-pair text, name=value, one option per line, with the value encoded so that non-printable characters and line breaks survive. Complex values can be cached.
  • Properties used in this chapter include OutputPath, ExceptionDialogType, SaveLogFile, SenderClasses, ExceptionsFilters, and CustomField. Appendix D supplies the option lookup; chapter 3 covers sender-specific configuration.
  • One performance note: if you need an option value constantly, save it into a variable rather than reading it repeatedly.

The accessor returns EurekaLog's live object, not a new object for the caller to free. Do not call Free on it or on an exception's Options. Treat each as belonging to the module or exception that supplied it. If you deliberately create your own TEurekaModuleOptions, that is a separate instance, not a replacement installed merely by creating it.

17.3 Changing an option from your code

For an application that can run as either a service or a GUI, configure common options at design time and change only the differences after deciding the launch mode. Put this procedure in a normal unit of the reporting executable or standalone DLL, then call it once from your startup code, before starting workers or accepting requests. Pass the mode your application has already determined; the procedure does not attempt to detect a service.

uses
  EBase, ETypes, EClasses, EModules;

procedure ConfigureReporting(const RunningAsService: Boolean);
var
  Options: TEurekaModuleOptions;
begin
  if not IsEurekaLogActive then
    Exit;
  Options := CurrentEurekaLogOptions;
  Options.SaveLogFile := True;
  if RunningAsService then
  begin
    Options.OutputPath := '%APPDATA%\MySoftware\Win32Service\';
    Options.ExceptionDialogType := edtService;
  end
  else
  begin
    Options.OutputPath := '%APPDATA%\MySoftware\GUIFrontEnd\';
    Options.ExceptionDialogType := edtMSClassic;
  end;
end;

Before building, open Project / EurekaLog options / Advanced / Code / Dialogs and include both the Service and MS Classic dialog implementations. Selecting a default dialog normally includes its unit automatically; the extra dialog used only by code needs explicit inclusion. Rebuild after changing that set. With both modes exercised, a report should be saved under the selected path and the service must not show an interactive error dialog.

%APPDATA% is resolved for the account running the process. A service running as Local System uses the system profile, not the logged-in user's profile; choose a path writable by the actual service account. Text-option environment-variable names are case-insensitive, and a path starting with . is relative to the executable, not the process's current directory. The Variables picker lists built-in pseudo-variables (names beginning with _) and common environment variables; an ordinary environment variable need not appear in the picker to be usable. Prefer these design-time substitutions when they remove a startup timing dependency.

IsEurekaLogActive also returns False when EurekaLog was not installed in the module; it checks global activation, not the per-thread state. This guard avoids the accessor's fatal inactive-module case. It does not make this unit suitable for a lightweight DLL: such DLLs must not link EModules, EClasses or EExceptionManager. Configure the host's options instead (chapter 16).

What cannot be changed this way

Three categories cannot be changed through ordinary options properties:

  • Options that affect compilation. Build options are decided while the file is compiled, so they cannot be changed at run time when the file already exists.
  • Options that need their own rules. Leaks control is the example: it requires special setup and is therefore controlled by other means, not through the options class. An option with no corresponding property may be compilation-only, settable only at initial setup (such as low-level hooks and the memory-manager filter), or changed through another unit (EMemLeaks for memory/leak controls, EFreeze for hang detection).
  • Features registered on startup. Hooks and debug information providers are registered at startup and cannot be customised at run time.

The same feature-in-build rule applies to senders: open Advanced / Code / Send engines, include every implementation you intend to select at run time, and rebuild. Selection and inclusion are separate operations. A custom sender must also be registered before selection (section 24.5). Chapter 3 explains how to configure the delivery destination and exercise the real upload.

Use EMemLeaks for supported memory/leak controls and EFreeze for hang-detection controls (chapters 12 and 14); changing an ordinary options property cannot install a missing memory-manager filter, hook, debug provider, dialog or sender into an already-built executable.

17.4 Global options, or the options of this exception

A raised exception holds its own copy of the options. The wrong version below looks reasonable, but changes future exceptions rather than the exception being handled:

// WRONG: Changing global options instead of options for E
CurrentEurekaLogOptions.ExceptionDialogType := edtNone;

// CORRECT: Changing options for E
ExceptionManager.Info(E).Options.ExceptionDialogType := edtNone;

and, inside an event handler, where the exception information object is handed to you:

// WRONG: Changing global options instead of current exception's options
CurrentEurekaLogOptions.SaveLogFile := False;

// CORRECT: Changing current exception's options
AExceptionInfo.Options.SaveLogFile := False;

The explanation of why the first form fails is the mechanism from section 17.2: altering the global options does not affect the options of already-raised exceptions, because those were copied when the exception was raised.

So the two levers are not alternatives; they are separate instances that start out equal. Set the defaults globally before anything is raised - for example, in a unit's initialization section - and adjust per exception inside handlers or wherever you hold an exception information object. For an exception that already exists, its own copy is the one to change; changing the globals again does not reach it. Use events and event handlers to alter an option for selected exceptions.

17.5 Custom keys in the options object

Besides the options EurekaLog defines, the options object carries keys of your own:

  • There is no predefined list of allowed option names. The suggested prefix for your own keys is Custom_ (or My_), because EurekaLog will never use an option name starting with Custom_ - so your names cannot collide with its settings.
  • Options whose names start with _ are not saved into the executable: design-time only, stored with the project options but not included in the final executable.
  • Read custom keys through CurrentEurekaLogOptions.CustomField. For example, enter Custom_ReportLabel="Service" on Advanced / Custom/Manual, rebuild, and read the value from an active reporting module:
uses
  EBase, EModules;

function ReportLabel: String;
begin
  Result := '';
  if IsEurekaLogActive then
    Result := CurrentEurekaLogOptions.CustomField['Custom_ReportLabel'];
end;

The accessor is CurrentEurekaLogOptions, as declared in EModules; no alternative accessor is needed. The custom value is data for your code: defining a key does not itself change report content or select a sender.

The options that decide how much EurekaLog is allowed to modify your project - the DoNotTouch family, written by hand into the settings file - are ordinary options of the same store; chapter 25 covers them with the rest of the settings-file material.

17.6 Which module am I, and when was it built

The rest of EModules answers questions about modules. The names, descriptions and file versions come from the version resource of the module in question; the date comes from EurekaLog's own data embedded in the build, with the PE headers as the fallback for a module that has none. These are the module facts a report states, so the routines are the natural companion of the options object.

GetApplicationName

function GetApplicationName: String;   // EModules

Returns the name of the .exe file, or an empty string if it is not available: the "File description" field of the version information, 'Embarcadero RAD Studio for Windows' being the sample output. It combines GetModuleDescriptionAndVersion information for the primary instance.

GetApplicationDescription

function GetApplicationDescription: String;   // EModules

Returns a description of the .exe file, or an empty string. Same field of the same resource, with the file name included: the sample output is 'bds.exe - (Embarcadero RAD Studio for Windows)', and the composition is stated as GetModuleDescription(PrimaryInstance).

GetCompilationDate

function GetCompilationDate(const AModule: HINST;
  const ALocalTime: Boolean; out ADate: TDateTime): Boolean;   // EModules

The compilation date of any loaded module of the current process, given its handle - the usual value being HInstance. ALocalTime is True for your current time zone and False for GMT/UTC; the function returns True if it obtained the date, False otherwise.

Its behaviour is more specific than its name. It reads the post-processing date out of EurekaLog's embedded data for the module - the documentation notes this is almost the same as the compilation date

  • and only for a module with no EurekaLog data does it try the PE headers. It is documented as always succeeding for a EurekaLog-enabled module, and as possibly failing for an invalid handle, a module without EurekaLog, or a module without a valid date in its headers. One caveat is called out: for a module without EurekaLog injected it may not always be possible to retrieve the date in the correct time zone.

GetCurrentModuleCompilationDate

function GetCurrentModuleCompilationDate: TDateTime;   // EModules

The compilation date and time of the current module in local time, or 0 if it is not available. Documented as a simple combination of GetCompilationDate with the primary instance, inheriting that function's limitations.

DATE and TIME

function __DATE__: String;   // EModules - for example '2021-02-07'
function __TIME__: String;   // EModules - for example '11:09:10'

The compilation date of the current project as 'yyyy-mm-dd' and its time as 'hh:mm:ss', both in GMT/UTC, both built from GetCompilationDate and the RTL's date formatting.

Unlike the C++ macros of the same names, these functions return the compilation date and time of the project, not of a source file.

GetExceptionModuleName

function GetExceptionModuleName(const AAddr: Pointer): String;   // EModules

The description of the module an exception address falls in, or an empty string. The address is normally ExceptAddr or an equivalent, and it is used to determine which module contains the code at that address. If the exception was raised in the .exe itself, the result equals GetApplicationDescription. This is the call that answers "the crash came from a DLL - which DLL".

GetExceptionModuleVersion

function GetExceptionModuleVersion(const AAddr: Pointer): String; overload;        // EModules
function GetExceptionModuleVersion(const AModuleName: String): String; overload;   // EModules

The file version of the module - '1.0.0.0' is the example, and an empty string is returned when the version information is absent. The first overload takes the same exception address as GetExceptionModuleName and finds the module from it; the second takes a full file name directly.

17.7 EConfig - the configuration storage

EConfig is described as the configuration management unit: basic routines for saving and loading EurekaLog configuration information, with the storage abstracted away. EurekaLog configuration can be stored either in the registry or in an .ini file, and this unit encapsulates that so callers are not aware of the underlying storage. Code uses ConfigReadXXX and ConfigWriteXXX without knowing which back end is in use; the typed ConfigReadXXX functions and ConfigWriteXXX procedures are wrappers around the two string routines - ConfigReadString for the readers, ConfigWriteString for the writers.

By default, EurekaLog configuration is stored in the registry under GeneralEurekaLogRegistryKey, '\Software\EurekaLab\EurekaLog\7.0'. Callers can change that with SetupConfig and identify the selected back end through ConfigIsRegistry, ConfigFileName and ConfigKeyPath.

The unit also names the routines that are not meant for callers: the high-level ConfigXXX routines call low-level ones according to the current storage configuration - the RegKeyXXX family, IniReadString/IniWriteString, the backward-compatibility routines ReadRawStrings/WriteRawStrings, and the helpers EncodeConfigValue/DecodeConfigValue. Use these low-level routines only to implement your own custom high-level wrapper; otherwise prefer ConfigReadString/ConfigWriteString.

Choosing the storage: SetupConfig

procedure SetupConfig(const AUseRegistry: Boolean;
  const AKeyPathOrFileName: String = '');   // EConfig

Changes where EurekaLog configuration is stored: True for the registry, False for an .ini file. The second parameter is the registry key path in the first case and the .ini file name in the second.

Call it only once at application startup, preferably in a unit's initialization section. This complete unit selects a private registry key, then illustrates a typed preference. It is for an application that intentionally uses EConfig as its configuration store:

unit ReportPreferences;

interface

function ReadReportLabel: String;
procedure WriteReportLabel(const Value: String);

implementation

uses
  EConfig;

function ReadReportLabel: String;
begin
  Result := ConfigReadString('Reporting', 'Label', 'Desktop');
end;

procedure WriteReportLabel(const Value: String);
begin
  ConfigWriteString('Reporting', 'Label', Value);
end;

initialization
  SetupConfig(True, '\Software\MySoftware\ReportPreferences');
end.

To use an INI file instead, replace that single startup call with SetupConfig(False, 'C:\ProgramData\MySoftware\ReportPreferences.ini') and provision its parent directory and account permissions first. Do not call both forms or switch storage in response to individual exceptions. Writes can fail with EOSError; handle that as an application configuration error rather than assuming a setting was persisted. These stored preferences are distinct from CurrentEurekaLogOptions and do not automatically alter its values.

EurekaLog Viewer, for example, changes the default location to its own registry key, GeneralEurekaLogViewerRegistryKey ('\Software\EurekaLab\Viewer\4.0'), and in standalone mode moves configuration to an external .ini file.

Asking where the configuration is

function ConfigIsRegistry: Boolean;   // EConfig
function ConfigFileName: String;      // EConfig
function ConfigKeyPath: String;       // EConfig

ConfigIsRegistry answers which back end is in use: True means the registry, and then ConfigKeyPath gives the key; False means an .ini file, and then ConfigFileName gives its name.

The two are deliberately narrow. ConfigFileName raises EAssertionFailed if called when ConfigIsRegistry is True; ConfigKeyPath raises EAssertionFailed if called when it is False. The pair is meant to be used behind a test on ConfigIsRegistry, never on its own, and the wrong call is an assertion rather than an empty string. ConfigKeyPath returns the key name with both a leading and a trailing '\', allowing a sub-path to be concatenated onto it. These are low-level routines for implementing your own high-level wrapper.

Reading and writing values

function ConfigReadString(const ASection, AName: String;
  const ADefault: String = ''): String;                        // EConfig
function ConfigReadInteger(const ASection, AName: String;
  const ADefault: Int64 = 0): Int64;                           // EConfig
function ConfigReadBool(const ASection, AName: String;
  const ADefault: Boolean = False): Boolean;                   // EConfig
function ConfigReadFloat(const ASection, AName: String;
  const ADefault: Extended = 0): Extended;                     // EConfig
procedure ConfigWriteString(const ASection, AName, AValue: String);                      // EConfig
procedure ConfigWriteInteger(const ASection, AName: String; const AValue: Int64);        // EConfig
procedure ConfigWriteBool(const ASection, AName: String; const AValue: Boolean);         // EConfig
procedure ConfigWriteFloat(const ASection, AName: String; const AValue: Extended);       // EConfig

The readers take a section, a key and an optional default, and return the key's value on success or the default when the key or section does not exist; the Integer, Bool and Float readers also fall back to the default when the stored value has the wrong format - a case ConfigReadString does not have, since what it returns can hold arbitrary bytes. The writers write one value to one key of one section. The parameter rules are constraints rather than advice: the section name cannot be empty and cannot contain '\' or '/', and the key name cannot be empty. On the write side the value is mandatory - for the numeric and boolean writers, deleting a value is ConfigDelete's job, there being no "empty value" form. ConfigWriteString does accept an empty string, but that is not a delete either: writing an empty string will not delete the value, and you must not encode it yourself, because it can hold arbitrary bytes and they will all be stored properly. Symmetrically, ConfigReadString returns a value that can hold arbitrary bytes and needs no decoding. All eight document System.SysUtils.EOSError as raised on failure, and the six typed ones repeat that they are wrappers - the Integer, Bool and Float readers around ConfigReadString, the matching writers around ConfigWriteString.

Deleting a key

procedure ConfigDelete(const ASection, AName: String);   // EConfig

Deletes a key from a section. Same constraints on the names, same EOSError on failure. The documentation's example is the natural one - clearing the keys that no longer correspond to anything, one by one, in a loop over the entries that disappeared.

Encoding, and why you normally do not touch it

function EncodeConfigValue(const AValue: String): String;   // EConfig
function DecodeConfigValue(const AValue: String): String;   // EConfig

EncodeConfigValue escapes special characters such as #0, line breaks and trailing spaces before data is saved. Its exact encoded format is unspecified: do not construct encoded values by hand. DecodeConfigValue reverses the encoding. These low-level routines are for custom wrappers and are usually unnecessary, because ConfigReadXXX/ConfigWriteXXX decode and encode automatically.

The raw back ends

function IniReadString(const AFileName: String; const ASection, AName: String;
  const ADefault: String = ''): String;
  deprecated 'Consider using ConfigWriteXXX/ConfigReadXXX routines';            // EConfig
procedure IniWriteString(const AFileName: String; const ASection, AName, AValue: String);
  deprecated 'Consider using ConfigWriteXXX/ConfigReadXXX routines';            // EConfig

The direct .ini accessors, both deprecated in the unit's own declarations with that same message. AFileName is a full absolute path. IniWriteString creates the file if it does not exist, as a Unicode .ini file; here an empty string does delete the value, the opposite of ConfigWriteString; and the value needs EncodeConfigValue for special characters to survive, while what the reader returns needs DecodeConfigValue first. IniWriteString also documents System.SysUtils.EOSError as raised on failure; IniReadString documents no exception.

function RegKeyCheck(const ARoot: HKey; const AKey: String): Boolean;   // EConfig
function RegKeyRead(const ARoot: HKey; const AKey, AName: String;
  const ADefault: String = ''): String;                                 // EConfig
procedure RegKeyWrite(const ARoot: HKey; const AKey, AName, AValue: String); overload;   // EConfig

The registry counterparts. ARoot is one of the predefined HKEY_XYZ constants - the documentation says usually HKEY_CURRENT_USER or HKEY_LOCAL_MACHINE - and AKey is a full absolute path to a key. RegKeyCheck returns True when the key exists, meaning at least read access to it. RegKeyRead returns the value, or the default on an error: no access, or value or key missing. RegKeyWrite writes a value; an empty string does not delete it, and it documents System.SysUtils.EOSError as raised on failure, as the other raw writers do. Here the encoding rules invert, and the documentation says why: the returned value does not need DecodeConfigValue, and the written value does not need EncodeConfigValue, because the registry can hold an exact binary copy of the data itself. The .ini file cannot - which is where the escaping comes from, and why the two families must not be mixed up.