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.
CurrentEurekaLogOptionsrepresents 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.Optionsproperty of a dialog, theAOptionsargument 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 theOptionsproperty 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, andCustomField. 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 (
EMemLeaksfor memory/leak controls,EFreezefor 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_(orMy_), because EurekaLog will never use an option name starting withCustom_- 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, enterCustom_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.