ErrorAddr — Переменная Delphi

Содержание

Работа с реестром в Delphi

Добавление элементов в контекстное меню «Создать»

1. Создать новый документ, поместить его в папку Windows/ShellNew

2. В редакторе реестра найти расширение этого файла, добавить новый подключ, добавить туда строку: FileName в качестве значения которой указать имя созданного файла.

Путь к файлу который открывает не зарегистрированные файлы

1. Найти ключ HKEY_CLASSES_ROOT\Unknown\Shell

2. Добавить новый ключ Open

3. Под этим ключом еще ключ с именем command в котором изменить значение (По умолчанию) на имя запускаемого файла, к имени нужно добавить %1. (Windows заменит этот символ на имя запускаемого файла)

В проводнике контекстное меню «Открыть в новом окне»

1. Найти ключ HKEY_CLASSES_ROOT\Directory\Shell

2. Создать подключ: opennew в котором изменить значение (По умолчанию) на: «Открыть в новом окне»

3. Под этим ключом создать еще подключ command (По умолчанию) = explorer %1

Использование средней кнопки мыши Logitech в качестве двойного щелчка

Подключ HKEY_LOCAL_MACHINE\SoftWare\Logitech и там найти параметр DoubleClick заменить 000 на 001

Новые звуковые события

Например создает звуки на запуск и закрытие WinWord

HKEY_CURRENT_USER\AppEvents\Shemes\Apps добавить подключ WinWord и к нему подключи Open и Close.

Теперь в настройках звуков видны новые события

Путь в реестре для деинсталяции программ:

Работа с реестром в Delphi 1

В Delphi 2 и выше появился объект TRegistry при помощи которого очень просто работать с реестром. Но мы здесь рассмотрим функции API, которые доступны и в Delphi 1.

Реестр предназначен для хранения системных переменных и позволяет зарегистрировать файлы программы, что обеспечивает их показ в проводнике с соответствующей иконкой, вызов программы при щелчке на этом файле, добавление ряда команд в меню, вызываемое при нажатии правой кнопки мыши над файлом. Кроме того, в реестр можно внести некую свою информацию (переменные, константы, данные о инсталлированной программы . ). Программу можно добавить в список деинсталляции, что позволит удалить ее из менеджера «Установка/Удаление программ» панели управления.

Для работы с реестром применяется ряд функций API :

RegCreateKey (Key: HKey; SubKey: PChar; var Result: HKey): Longint;

Создать подраздел в реестре. Key указывает на «корневой» раздел реестра, в Delphi1 доступен только один — HKEY_CLASSES_ROOT, в в Delphi3 — все. SubKey — имя раздела — строится по принципу пути к файлу в DOS (пример subkey1\subkey2\ . ). Если такой раздел уже существует, то он открывается (в любом случае при успешном вызове Result содержит Handle на раздел). Об успешности вызова судят по возвращаемому значению, если ERROR_SUCCESS, то успешно, если иное — ошибка.

RegOpenKey(Key: HKey; SubKey: PChar; var Result: HKey): Longint;

Открыть подраздел Key\SubKey и возвращает Handle на него в переменной Result. Если раздела с таким именем нет, то он не создается. Возврат — код ошибки или ERROR_SUCCESS, если успешно.

RegCloseKey(Key: HKey): Longint;

Закрывает раздел, на который ссылается Key. Возврат — код ошибки или ERROR_SUCCESS, если успешно.

RegDeleteKey(Key: HKey; SubKey: PChar): Longint;

Удалить подраздел Key\SubKey. Возврат — код ошибки или ERROR_SUCCESS, если нет ошибок.

RegEnumKey(Key: HKey; index: Longint; Buffer: PChar;cb: Longint): Longint;

Получить имена всех подразделов раздела Key, где Key — Handle на открытый или созданный раздел (см. RegCreateKey и RegOpenKey), Buffer — указатель на буфер, cb — размер буфера, index — индекс, должен быть равен 0 при первом вызове RegEnumKey. Типичное использование — в цикле While, где index увеличивается до тех пор, пока очередной вызов RegEnumKey не завершится ошибкой (см. пример).

RegQueryValue(Key: HKey; SubKey: PChar; Value: PChar; var cb: Longint): Longint;

Возвращает текстовую строку, связанную с ключом Key\SubKey.Value — буфер для строки; cb- размер, на входе — размер буфера, на выходе — длина возвращаемой строки. Возврат — код ошибки.

RegSetValue(Key: HKey; SubKey: PChar; ValType: Longint; Value: PChar; cb: Longint): Longint;

Задать новое значение ключу Key\SubKey, ValType — тип задаваемой переменной, Value — буфер для переменной, cb — размер буфера. В Windows 3.1 допустимо только Value=REG_SZ. Возврат — код ошибки или ERROR_SUCCESS, если нет ошибок.

Объект INIFILES — работа с INI файлами.

Почему иногда лучше использовать INI-файлы, а не реестр?

  • 1. INI-файлы можно просмотреть и отредактировать в обычном блокноте.
  • 2. Если INI-файл хранить в папке с программой, то при переносе папки на другой компьютер настройки сохраняются. (Я еще не написал ни одной программы, которая бы не поместилась на одну дискету :)
  • 3. Новичку в реестре можно запросто запутаться или (боже упаси), чего-нибудь не то изменить.

Поэтому для хранения параметров настройки программы удобно использовать стандартные INI файлы Windows. Работа с INI файлами ведется при помощи объекта TIniFiles модуля IniFiles. Краткое описание методов объекта TIniFiles дано ниже.

Создать экземпляр объекта и связать его с файлом. Если такого файла нет, то он создается, но только тогда, когда произведете в него запись информации.

WriteBool(const Section, Ident: string; Value: Boolean);

Присвоить элементу с именем Ident раздела Section значение типа boolean

WriteInteger(const Section, Ident: string; Value: Longint);

Присвоить элементу с именем Ident раздела Section значение типа Longint

WriteString(const Section, Ident, Value: string);

Присвоить элементу с именем Ident раздела Section значение типа String

ReadSection (const Section: string; Strings: TStrings);

Прочитать имена всех корректно описанных переменных раздела Section (некорректно описанные опускаются)

ReadSectionValues(const Section: string; Strings: TStrings);

Прочитать имена и значения всех корректно описанных переменных раздела Section. Формат : имя_переменной = значение

EraseSection(const Section: string);

Удалить раздел Section со всем содержимым

ReadBool(const Section, Ident: string; Default: Boolean): Boolean;

Прочитать значение переменной типа Boolean раздела Section с именем Ident, и если его нет, то вместо него подставить значение Default.

ReadInteger(const Section, Ident: string; Default: Longint): Longint;

Прочитать значение переменной типа Longint раздела Section с именем Ident, и если его нет, то вместо него подставить значение Default.

ReadString(const Section, Ident, Default: string): string;

Прочитать значение переменной типа String раздела Section с именем Ident, и если его нет, то вместо него подставить значение Default.

Закрыть и освободить ресурс. Необходимо вызвать при завершении работы с INI файлом

Property Values[const Name: string]: string;

Доступ к существующему параметру по имени Name

Работа с реестром — теория

Работа с реестром в Delphi 1 D1 *

В Delphi 2 и выше появился объект TRegistry при помощи которого очень просто работать с реестром. Но я здесь рассмотрю функции API, которые доступны и в Delphi 1.
Реестр предназначен для хранения системных переменных и позволяет зарегистрировать файлы программы, что обеспечивает их показ в проводнике с соответствующей иконкой, вызов программы при щелчке на этом файле, добавление ряда команд в меню, вызываемое при нажатии правой кнопки мыши над файлом. Кроме того, в реестр можно внести некую свою информацию (переменные, константы, данные о инсталлированной программы. ). Программу можно добавить в список деинсталляции, что позволит удалить ее из менеджера «Установка/Удаление программ» панели управления.
Для работы с реестром применяется ряд функций API :
RegCreateKey (Key: HKey; SubKey: PChar; var Result: HKey): Longint;
Создать подраздел в реестре. Key указывает на «корневой» раздел реестра, в Delphi1 доступен только один — HKEY_CLASSES_ROOT, в в Delphi3 — все. SubKey — имя раздела — строится по принципу пути к файлу в DOS (пример subkey1\subkey2\ . ). Если такой раздел уже существует, то он открывается (в любом случае при успешном вызове Result содержит Handle на раздел). Об успешности вызова судят по возвращаемому значению, если ERROR_SUCCESS, то успешно, если иное — ошибка.
RegOpenKey(Key: HKey; SubKey: PChar; var Result: HKey): Longint;
Открыть подраздел Key\SubKey и возвращает Handle на него в переменной Result. Если раздела с таким именем нет, то он не создается. Возврат — код ошибки или ERROR_SUCCESS, если успешно.
RegCloseKey(Key: HKey): Longint;
Закрывает раздел, на который ссылается Key. Возврат — код ошибки или ERROR_SUCCESS, если успешно.
RegDeleteKey(Key: HKey; SubKey: PChar): Longint;
Удалить подраздел Key\SubKey. Возврат — код ошибки или ERROR_SUCCESS, если нет ошибок.
RegEnumKey(Key: HKey; index: Longint; Buffer: PChar;cb: Longint): Longint;
Получить имена всех подразделов раздела Key, где Key — Handle на открытый или созданный раздел (см. RegCreateKey и RegOpenKey), Buffer — указатель на буфер, cb — размер буфера, index — индекс, должен быть равен 0 при первом вызове RegEnumKey. Типичное использование — в цикле While, где index увеличивается до тех пор, пока очередной вызов RegEnumKey не завершится ошибкой (см. пример).
RegQueryValue(Key: HKey; SubKey: PChar; Value: PChar; var cb: Longint): Longint;

Возвращает текстовую строку, связанную с ключом Key\SubKey.Value — буфер для строки; cb- размер, на входе — размер буфера, на выходе — длина возвращаемой строки. Возврат — код ошибки.
RegSetValue(Key: HKey; SubKey: PChar; ValType: Longint; Value: PChar; cb: Longint): Longint;
Задать новое значение ключу Key\SubKey, ValType — тип задаваемой переменной, Value — буфер для переменной, cb — размер буфера. В Windows 3.1 допустимо только Value=REG_SZ. Возврат — код ошибки или ERROR_SUCCESS, если нет ошибок.
Пример:

Работа с реестром в Delphi 2+ D2+ *

Итак, поговорим о работе с реестром, на сей раз о работе с ним из Delphi 3-4. Как показал опыт и анализ работы с письмами, этот раздел представляет собой трудность в изучении и многих интересует, как работать с реестром (а в литературе, к сожалению, нет данных по работе с ним). Возьмем за основу работу с реестром в Delphi 4.

В Delphi 4 для работы с реестром имеется класс TRegistry. В отличии от своего «младшего брата» (TRegistry в Delphi 1) он может работать со всеми ключами реестра, а не только с HKEY_CLASSES_ROOT.

Рассмотрим работу с TRegistry по шагам.

  • Перед началом использования TRegistry необходимо объявить подключение модуля Registry в Uses. Эту операцию Delphi не желает делать автоматически
  • Необходимо объявить переменную типа TRegistry, для примера назовем ее REG Необходимо проинициализировать: REG := TRegistry.Create;
  • Работаем с REG
  • Уничтожаем объект REG.Destroy;

Если опустить пункт 4, то рабочий минимум сводится к приведенному ниже коду
var
REG : TRegistry;
begin
REG := TRegistry.Create;
REG.Destroy;
end;

Далее необходимо рассмотреть (вкратце) основные свойства и методы TRegistry

Свойства

Назначение

property RootKey: HKEY;

Самое полезное свойство. Допускает чтение и запись. Определяет корневой ключ реестра. Допустимые значения:
HKEY_CLASSES_ROOT (по умолчанию)
HKEY_CURRENT_USER HKEY_LOCAL_MACHINE
HKEY_USERS HKEY_CURRENT_CONFIG
HKEY_DYN_DATA
Лично я не верю в эти «по умолчанию», потому начинаю работу с присвоения значения RootKey property CurrentPath: String; Путь к текущему ключу property LazyWrite: Boolean; «Ленивая запись». По умолчанию = true, что означает, что на момент возврата из функции закрытия ключа не гарантируется, что изменения в нем записаны в реестр. Если false, то на момент возврата из функции закрытия ключа гарантируется, что все изменения уже записаны в реестр. property CurrentKey: HKEY; Открытый в настоящее время ключ реестра

Procedure CloseKey;
Закрывает открытый в данный момент ключ реестра и записывает в реестра все изменения (если таковые были), произведенные в данном ключе. Вызов CloseKey несколько раз или при отсутствии открытого ключа не вызывает ошибки.

function CreateKey(const Key: String): Boolean;
Создает ключ Key (где Key — путь с разделителями «\», например Software\Test. Имя корневого ключа в пути не указывается !! Он задается через property RootKey. Возвращает True при успешном создании.

function DeleteKey(const Key: String): Boolean;
Удалить ключ Key (аналогично CreateKey, только с точностью до наоборот)

function DeleteValue(const Name: String): Boolean;
Удалить параметр с именем Name текущего ключа. Очевидно, что предварительно необходимо открыть этот ключ.

function GetDataInfo(const ValueName: String; var Value: TRegDataInfo): Boolean;
Получить информацию о параметре ValueName текущего ключа — его тип и размер. При написании программ применяется редко, т.к. программист и сам знает, какого типа его параметры. А вот при написании разного рода утилит для просмотра и редактирования реестра он просто незаменим.

Типы:
TRegDataType = (rdUnknown, rdString, rdExpandString, rdInteger, rdBinary);
TRegDataInfo = record
RegData: TRegDataType; // Тип ключа
DataSize: Integer; // Размер данных
end;

Данная функция возвращает комплексную информацию о параметре, для получения данных о размере или типе можно применять GetDataSize и GetDataType

function GetDataSize(const ValueName: String): Integer;
Получить размер параметра ValueName текущего ключа в байтах. Если — при ошибке. Для строкового параметра размер учитывает в размере и один байт для #0, завершающего строку .

function GetDataType(const ValueName: String): TRegDataType;
Получить тип текущего ключа.

function GetKeyInfo(var Value: TRegKeyInfo): Boolean;
Получить информацию о ключе. Возвращает заполненную структуру:

Как очевидно из структуры, она предназначена для построения программ просмотра реестра. При успешном выполнении возвращает true.

procedure GetKeyNames(Strings: TStrings);
Заполняет указанный Strings списком под ключей текущего ключа. Применяется для построения программ просмотра реестра или в том случае, когда количество подключей неизвестно. Например, одна из моих программ создает в одном из ключей несколько подключей с одинаковой структурой, но их количество заранее неизвестно (настройки пользователей).

procedure GetValueNames(Strings: TStrings);
Заполняет указанный Strings списком параметров текущего ключа.

function HasSubKeys: Boolean;
Возвращает True, если текущий ключ имеет подключи и False в противном случае

function KeyExists(const Key: String): Boolean;
Возвращает True, если ключ Key существует. Полезная функция, рекомендуется применять ее перед открытием ключей.

function LoadKey(const Key, FileName: String): Boolean;
Создает ключ Key и загружает в него данные из файла с именем FileName. Полезно при написании инсталляторов. Возвращает True при успешном выполнении.

procedure MoveKey(const OldName, NewName: String; Delete: Boolean);
Копировать или переименовать ключ. В любом случае копирует все из ключа OldName в NewName (со всеми подключами). После копирования анализируется Delete, и если он true, то ключ OldName уничтожается со всем содержимым. Лично у меня не было потребности в применении данной функции — она не требуется программ и предначначена для построения редакторов реестра.

function OpenKey(const Key: String; CanCreate: Boolean): Boolean;
Очень важная функция — с нее начинается работа с ключом. Key — имя открываемого ключа. Если ключ с указанным именем не найден и CanCreate=true, то производится попытка создать ключ с указанным именем. Возвращает признак успешности открытия ключа, его обязательно следует анализировать.

function OpenKeyReadOnly(const Key: String): Boolean;
Тоже, что и OpenKey, но открытие идет в режиме «только чтение»

Внимание . Все функции типа Read** при вызове генерируют исключение, если параметр не найден. Это исключение следует отлавливать при помощи try except или проверять наличие параметра при помощи ValueExists перед его чтением.

function ReadBinaryData(const Name: String; var Buffer; BufSize: Integer): Integer;
Читает значение параметра с именем Name текущего (открытого) ключа в Buffer размером BufSize.

function ReadBool(const Name: String): Boolean;
Считать значение параметра с именем Name типа Boolean

function ReadDate(const Name: String): TDateTime;
Считать значение параметра с именем Name типа дата

function ReadDateTime(const Name: String): TDateTime;
Считать значение параметра с именем Name типа дата-время

function ReadTime(const Name: String): TDateTime;
Считать значение параметра с именем Name типа время

function ReadFloat(const Name: String): Double;
Считать значение параметра с именем Name типа Double

function ReadInteger(const Name: String): Integer;
Считать значение параметра с именем Name типа Integer

function ReadString(const Name: String): String;
Считать значение параметра с именем Name типа String

function RegistryConnect(const UNCName: String): Boolean;
Подключить сетевой реестр машины UNCName (формат: \\сетевое имя машины). Перед вызовом этой функции программа должна установить RootKey в значение HKEY_USERS или HKEY_LOCAL_MACHINE. При успешном соединении и открытии удаленного реестра его RootKey ставится в заданное перед вызовам значение свойства RootKey и возвращается True.

procedure RenameValue(const OldName, NewName: String);
Переименовать параметр текущего ключа с именем OldName в NewName.

function ReplaceKey(const Key, FileName, BackUpFileName: String): Boolean;
Заменить место хранения ключа. Обычно ключи хранятся в базовом файле реестра, нот вызовом данной функции можно задать в качестве места хранения ключа отдельный файл с именем FileName (его следует предварительно создать при помощи savekey). При каждой перезагрузке компьютера ключ Key будет загружаться значениями, считываемыми из файла этого файла FileName,т.е. по сути мы имеет дело с ульем (hive) в терминологии Windows NT. Определение: Улей — часть реестра (его ячейка). Улей является дискретным набором ключей, подключей и параметров, который находится вверху иерархии реестра. Улей поддерживается одиночным файлом. BackUpFileName — имя резервной копии, которая создается перед перезаписью данных ключа Key. Если кого интересуют подробности, то следует почитать книгу по реестру Windows NT, главы типа «Ульи и файлы» и «Целостность и восстановление улья в реестре». При разработке практических приложений я не разу не применял этот вызов.

function RestoreKey(const Key, FileName: String): Boolean;
Открывает указанный ключ и перезаписывает его данные и подключи данными из файла FileName.

function SaveKey(const Key, FileName: String): Boolean;
Сохраняет все параметры указанного ключа и всех его подключей в файле FileName. Может применяться совместно с LoadKey и RestoreKey для создания и восстановления ключей реестра.

function UnLoadKey(const Key: String): Boolean;
Удалить улей Key из реестра.

function ValueExists(const Name: string): Boolean;
Проверить, существует ли в текущем ключе параметр с именем Name. Весьма полезная функция, т.к. чтение несуществующего параметра приводит к исключительной ситуации

procedure WriteBinaryData(const Name: String; var Buffer; BufSize: Integer);
Записать в параметр с именем Name данные из буфера Buffer размером BufSize. Если параметр существовал, то он будет перезаписан. Если параметр не существовал, то он будет создан. Это справедливо и для всех последующих процедур записи параметров

Остальные процедуры записи — WriteBool, WriteCurrency,WriteDate,WriteDateTime, WriteExpandString, WriteFloat, WriteInteger, WriteString, WriteTime имеют по два параметра — (имя ключа, значение ключа).

Ну вот, класс описали, теперь приведем парочку примеров.

Данный пример создает (если его не было) или открывает ключ реестра HKEY_LOCAL_MACHINE\Software\Test и записывает в него два параметра типа Boolean и Integer.

Данный пример открывает ключ (контролируя, есть ли он) и пытается читать параметры с проверкой, существуют ли они.

ErrorAddr — Переменная Delphi

Статья описывает систему методов и действий, которые могут быть использованы при написании больших проектов на Delphi. Основная цель системы заключается в эффективном повышении качества кода и возможности быстрого поиска и исправления обнаруживаемых ошибок. В настоящее время, несмотря на большие возможности системы Delphi и библиотеки VCL, разработчикам не предложен простой и эффективный подход к написанию качественных программ.

На основании личного опыта автора была составлена система методов, которые позволяют значительно усилить контроль над выполнением программы, ее сопровождением и исправлением обнаруживаемых ошибок. Статья может быть полезна как для начинающих, так и для профессиональных программистов. Однако, предполагается, что читатель хорошо знаком со средой разработки Borland Delphi и языком Object Pascal, а также таким понятием как исключение.

Все предложенные здесь методы не являются единственно возможными и могут быть изменены и улучшены. Конкретный пример использования данной системы приведен в прилагаемом проекте.

2. Процедура ASSERT

В Object Pascal введена специализированная процедура Assert, назначение которой — помощь в отладке кода и контроле над выполнением программы. Процедура является аналогом макроса ASSERT, который широко применяется практически во всех программах, написанных с использованием C и C++ и их библиотек. Синтаксис процедуры Assert (макрос имеет похожий синтаксис) описан ниже.

Процедура проверяет логическое утверждение, передаваемое первым аргументом и, если это утверждение ложно, то процедура выводит на экран диагностическое сообщение с номером строки и именем модуля, где расположен вызов этой процедуры, и сообщение пользователя, которое опционально передается вторым аргументом. В некоторых системах разработки, например (MSVC+MFC), макрос Assert принудительно завершает выполнение программы после выдачи соответствующего диагностического сообщения. В других системах (например Delphi) стандартная процедура ограничивается лишь выдачей диагностического сообщения.

Действие стандартной процедуры Assert (как и соответствующего макроса) зависит от режима компиляции проекта. Обычно, в режиме отладки процедура действует как описано выше, в других же режимах, вызов данной процедуры и проверка условия игнорируются и не компилируются в проект. Считается, что исключение проверки в готовой программе позволяет повысить производительность и уменьшить размер программы.

В языках С и С++ отладочный режим компиляции задается определением (#define) соответствующей константы, обычно это _DEBUG. В Object Pascal отладочный режим включается специальной опцией компилятора. Кроме того, в С и С++ макрос ASSERT — это обычный макрос, ничем не отличающийся от множества других макросов. Макрос использует переменную компилятора __LINE__, что позволяет ему определить номер строки, в которой произошло нарушение проверки. В Object Pascal такой переменной нет, и за реализацию процедуры Assert полностью отвечает компилятор, что позволяет говорить о процедуре Assert, как об особенности компилятора и языка Object Pascal, а не как об обычной процедуре в составе библиотеки VCL.

Процедура Assert обычно применяется в следующих случаях:

  • в начале процедуры или функции для проверки правильности переданных аргументов;
  • в начале процедуры или функции для проверки правильности внутренних переменных;
  • в конце работы алгоритма для проверки правильности работы алгоритма;
  • для проверки правильности выполнения «надежных» функций, то есть тех функций, которые всегда должны выполняться успешно всегда, и их невыполнение рассматривается как фатальная ошибка программы. Хороший пример — функция CloseHandle вызываемая с верным дескриптором. Практически, можно не сомневаться в правильности выполнения этой функции, однако результат ее выполнения все-таки можно и нужно проверить. Подробнее об этом в главе «Категории ошибок».

В любом из этих случаев невыполнение передаваемого в процедуру Assert условия рассматривается как совершенно неожиданная, фатальная ошибка алгоритма, которой не должно быть ни при каких условиях, и которая не оставляет никаких шансов на дальнейшее правильное выполнение программы. Например, возможен такой код.

Первая процедура проверяет, является ли переданная ссылка на объект непустой. Вторая процедура проверяет индекс добавляемого элемента на принадлежность к ограниченному диапазону. Третья процедура проверяет правильность выполнения алгоритма. Ясно, что при невыполнении хотя бы одного из этих условий, необходимо считать, что данная процедура выполнилась неправильно, и дальнейшее правильное выполнение всей программы не представляется возможным. Ясно также, что, так как данную процедуру (процедуру AddElement) вызываем только мы (наша программа), такое событие никогда не должно происходить, в предположении, что алгоритм правилен и аргументы, передаваемые в функцию, верные. Однако, как известно: «Человек полагает, а Бог располагает», и при невнимательном программировании в большом проекте такой тип ошибок становиться основным.

Без применения этих проверок, программа бы выдала малоинформативное сообщение, например об исключении определенного типа, и конечно без указания места, где это исключение произошло. Поэтому, в большом проекте, интенсивное применение процедуры Assert позволяет быстро локализовать, идентифицировать и устранить возникшую ошибку в работе алгоритма.

В настоящее время (в пятой версии Delphi и несколько более ранних версиях), процедура Assert генерирует на месте своего вызова исключение типа EAssertionFailed и дальнейшее следование этого исключения осуществляется обычным образом — вверх по стеку процедур до ближайшего обработчика исключений. Исключения от процедуры Assert обрабатываются таким же образом, как и все другие — объект TApplication ловит все исключения и показывает их тип и их сообщения на экране. Однако такой подход трудно считать логичным. Исключения от процедуры Assert коренным образом отличаются от остальных исключений. Исключения от процедуры Assert свидетельствуют о серьезных ошибках в логике работы программы и требуют особого внимания, вплоть до принудительного завершения программы с выводом особого диагностического сообщения.

Реализация процедуры Assert в Object Pascal полностью возложена на компилятор (вернее на «приближенный ко двору» модуль system.pas), что несколько затрудняет изменение логики работы. Возможны три основных варианта внесения изменений в логику работы.

  1. Установка обработчика события TApplication.OnException и обработка в нем исключения с типом EAssertionFailed. Данный способ является наиболее простым, и менее гибким. Подходит, если все что необходимо — это вывести на экран особое сообщение и принудительно завершить программу.
  2. Прямая установка обработчика процедуры Assert, путем присвоения адреса своей процедуры системной переменной AssertErrorProc. Область применения практически та же самая, что и в предыдущем случае. Однако, в этом случае, возможны и более сложные манипуляции. Например, можно написать обработчик, который не генерирует исключение, а сразу выводит сообщение и принудительно завершает программу. Пример такого обработчика приведен ниже.
  3. Написание процедуры Assert заново. Самый гибкий и удобный вариант. Например, если вас не устраивает стандартный синтаксис процедуры Assert, и вы хотите передать в процедуру дополнительные параметры (например, тип ошибки, тип генерируемого исключения, код, и т.п.). При этом возникает два особых момента связанных с тем фактом, что за процедуру Assert отвечает компилятор. Во-первых, вы не сможете управлять включением и включением вызовов процедуры Assert в программе через стандартные опции. Процедура Assert будет выполняться всегда. Единственное, что вы можете сделать — это сразу выйти из процедуры, если режим работы программы не отладочный. Во-вторых, в самой процедуре невозможно узнать номер строки, в которой произошел вызов процедуры. Тем не менее, обе этих трудности преодолимы.

Необходимость включения и выключения вызовов процедуры Assert, связанных с режимом работы программы является само собой подразумевающейся. Считается, что выключение вызовов Assert в отлаженной программе позволяет уменьшить размер программы и увеличить скорость ее работы. Однако, экономия на размере и увеличение быстродействия являются весьма малыми (если вы не включаете процедуру Assert в тело каждого цикла). Происходящее на этом фоне, молчаливое съедание ошибок в программе ставит под сомнение необходимость отключать вызовы процедуры Assert в «готовой» программе. То, что вы не нашли ошибок при разработке и отладке программы вовсе не означает, что там их нет. Ошибки могут появиться у пользователя программы, например, в условиях, в которых программа не тестировалась. Как вы о них узнаете, если отключите вызовы Assert? Конечно, лукавое отключение предупреждений может на время сохранить вашу репутацию, и пользователь может и не узнать о тех ужасах, которые происходят в недрах вашей программы. А внезапный безымянный сбой вашей программы вы можете списать на особенности работы Windows. Однако качества вашей программе это не прибавит. Честная регистрация всех ошибок намного улучшит ваши программы.

Таким образом, первая трудность решена — ее просто не нужно решать. Работайте честно, всегда оставляйте вызовы функций Assert в программе, и они помогут диагностировать ошибки. Решение второй проблемы — указание номеров строк в сообщениях описано в следующей главе. Пример же простейшей альтернативной процедуры Assert приведен ниже. Функция называется AssertMsg и принимает те же параметры, что и стандартная процедура. Конечно, список этих параметров можно расширить, так же, как и изменить логику работы. Данная же процедура генерирует исключение по адресу вызова этой процедуры, а глобальный обработчик исключений выводит сообщение и завершает программу при получении исключения с определенном типом EFatalExcept.

3. Номера строк или уникальные метки ?

В большинстве систем разработки, в сообщении, выводимом при активизации процедуры Assert, выводится три параметра: сообщение пользователя, имя исходного модуля и номер строки, в котором находился вызов процедуры. При этом основной упор при поиске ошибки делается на номер строки и имя модуля, а сообщение пользователя является малоинформативным. Однако, если вы решили оставить вызовы процедуры Assert в готовой распространяемой программе, здесь таится проблема. При разработке программы вы будете выпускать десятки и десятки версий, и естественно, что исходный код будет сильно модифицироваться, а номера линий, на которых располагаются проверочные вызовы, будут постоянно изменяться. И если вдруг ваш пользователь сообщает о том, что в вашей программе сработала проверка на линии N в исходном модуле M, вы должны задаться вопросом, какая версия программы находится у пользователя? Даже если вы сохраните все исходные тексты абсолютно всех версий, такой подход нельзя назвать удобным и ясным, он таит в себе скрытую путаницу и неразбериху, поскольку вам придется отслеживать номера строк в разных версиях программы при просмотре развития программы.

Возникает идея — совсем отказаться от номеров строк — они таят в себе хаос, и использовать уникальные сообщения пользователя, то есть уникальные метки. В каждом вызове процедуры Assert передавать абсолютно уникальный, не повторяющийся идентификатор, и выводить его на экран при срабатывании проверки. После того как вам сообщат, что вашей программе активизировалась проверка с идентификатором N, вы можете проследить развитие кода в этом месте во всех версиях программы путем простого поиска идентификатора в исходных текстах. Зачастую даже, вовсе не обязательно распаковывать ту же версию исходных текстов программы, что и версия программы у пользователя. Если код в этом месте давно не менялся, вы можете искать и исправлять ошибку прямо в текущей версии исходных текстов.

Что выбрать в качестве уникальных идентификаторов? Можно выбрать любое не повторяющееся число или строку. Например, последовательно передавать числа от нуля и далее с инкрементом на единицу. Или передавать уникальные строки, которые можно придумывать на ходу. Однако, это утомительное занятие — помнить последний набранный номер или строку и не повторяться. Можно написать специальный модуль для редактора в среде Delphi, который будет автоматически вводить идентификаторы. Однако, если вы будете работать над проектом на разных машинах, то возникнет проблема синхронизации счетчиков.

Существует прекрасный вариант решения этой проблемы. В среде Delphi нажмите клавиши + + и вы получите строку похожую на эту: [‘<19619100-22b0-11d4-acd0-009027350d25>‘]. Это уникальный идентификатор GUID (Global Unique IDentificator), шестнадцатибайтное уникальное значение, переведенное в строковый вид с разграничителями и заключенное в квадратные скобки. Такое значение уникально, оно зависит от времени и от номера сетевой карты. Если сетевой карты нет, то номер генерируется программно. По крайней мере, Microsoft утверждает, что такой номер никогда не повторится. Каждый раз, когда вы нажмете соответствующую комбинацию клавиш, система сгенерирует новый уникальный код. Если удалить квадратные скобки по бокам, то получиться отличный уникальный строковый идентификатор, готовый к использованию, при этом вызов процедуры AssertMsg будет выглядеть подобным образом.

В данном случае сообщение пользователя не передается — передается только уникальная метка. В самом деле, какая разница пользователю, отчего именно погибла ваша программа? Пользователь лишь должен сообщить, что ваша программа работает с ошибкой и передать информацию, которая поможет эту ошибку найти. Конечно, подобный уникальный номер занимает некоторое место в сегменте констант, но, как показывает практика, не больше 5% от всего объема программы, и в добавление, такой номер очень удобно записывать с экрана. Кроме того, он действительно никогда не повторяется! Если же вы — эконом, и вам жалко тратить 38 байт на каждый идентификатор, можете передавать в процедуру AssertMsg четырехбайтное число, которое можно получить, взяв первые восемь шестнадцатеричных цифр из строкового идентификатора — именно они изменяются чаще всего.

4. Категории ошибок

В последнее время, практически во всех развитых объектно-ориентированных языках, появилась структурная обработка исключений. Эта возможность поддерживается операционной системой и позволяет перехватывать нештатные ситуации возникающие, как в обычном, так и в защищенном режиме системы. Язык Object Pascal полностью поддерживает все возможности по обработке исключений. Использование исключений при разработке программ рекомендуется документацией и поддерживается широким использованием исключений в VCL. Типичный пример обработки исключений приведен ниже.

Кроме того, существует возможность объявлять и генерировать свои типы исключений. Использование исключений поощряется и считается хорошим стилем. Однако, мало кто до конца понимает, что же делать с исключением, когда оно действительно возникло.

По умолчанию все исключения обрабатываются одинаково. Исключения, если они не перехвачены обработчиком пользователя, передаются в глобальный обработчик исключений TApplication.HandleException. Обработчик проверяет тип исключения, а затем выводит диагностическое сообщение, после чего выполнение программы продолжается.

Неважно, какое исключение возникло: нехватка памяти, неудачное выполнение API функции, обращение к несуществующему участку памяти, неудачное открытие файла или базы данных, все эти ошибки порождают исключения, которые обрабатываются одинаковым образом! Между тем, все эти ошибки можно и нужно разделить на классы, каждый из которых требует особого внимания и отдельной обработки.

Прежде всего, ошибки нужно разделить на две группы — фатальные ошибки и нефатальные или восстановимые ошибки. Фатальные ошибки — это ошибки «несовместимые с жизнью», после которых выполнение программы невозможно и бессмысленно. Фатальная ошибка всегда является неожиданной и подразумевается маловероятной. Например, любое срабатывание проверки Assert означает серьезное нарушение логики работы программы. Сбой при выделении памяти или ресурсов в большинстве случаев также является невосстановимым. Подавляющее большинство API функций при передаче в них правильных параметров можно отнести к «надежным» функциям, то есть таким функциям, в выполнении которых мы уверены, и невыполнение которых маловероятно, но приводит к фатальным ошибкам. Фатальные ошибки можно отнести к серьезным недостаткам либо в программе, либо в операционной системе.

Наоборот, нефатальные или восстановимые ошибки являются вполне возможными. Например, операции открытия файла, базы данных, какого-либо стороннего ресурса являются потенциально опасными, и ошибка при выполнении этих операций вполне возможна. Также возможны восстановимые исключения при конвертировании введенных пользователем строк в числа, и во многих других ситуациях. Нет никаких оснований для паники и совсем не нужно закрывать программу, лишь потому, что программа не смогла открыть файл или базу данных. Конечно, вы можете закрыть программу при неудачной операции, если без этого программа не может нормально работать, но перед этим вы должны вывести осмысленное предупреждение для пользователя. При этом тон и стиль сообщения при восстановимой ошибке должен быть совсем иным, чем при фатальной ошибке. Восстановимая ошибка является следствием временной недоступности или неисправности ресурса, а не следствием недостатка программы. Вы полностью предусматриваете такую ситуацию, контролируете и обрабатываете ее.

Грань между фатальными и восстановимыми ошибками очень тонка и неопределенна. В одном случае ошибку можно отнести к фатальным ошибкам, а в другом — к восстановимым. Но, тем не менее, каждая нештатная ситуация должна быть четко отнесена к одному из этих двух классов.

В пределе такого подхода, все функции должны быть объявлены ненадежными, и должно быть предсмотрено восстановление нормальной работы программы после любой нештатной ситуации. Однако, такое вряд ли возможно, так как в таком случае 99% от объема программы будут занимать проверки и восстановления после сбоя при выполнения любой из функции. Таким образом, для успешной и эффективной работы, необходимо как можно больше функций отнести к разряду надежных, и как можно меньше — к разряду ненадежных, требующих специальной обработки.

Если ошибка отнесена к фатальным ошибкам, мы должны установить соответствующую проверку, например Assert, извиниться и закрыть программу в случае ее появления. Если ошибка отнесена к восстановимым ошибкам, то она должна быть обработана соответствующим образом с выводом предупреждения и возможностью восстановления исходного состояния программы.

Таким образом, мы пришли к пониманию того, что существуют разные группы ошибок, с разным алгоритмом их обработки. Как уже упоминалось, все ошибки в библиотеке VCL обрабатываются одинаковым образом. Изменить существующее положение вещей можно разными способами. Например, в можно анализировать все типы исключений в обработчике TApplication.OnException. Исключения таких типов как, например, EAccessViolation, EListError, EAbstractError, EArrayError, EAssertionFailed и многих других можно рассматривать как фатальные ошибки, а исключения остальных типов рассматривать как восстановимые ошибки. При этом откат при восстановимых ошибках выполнять путем локального перехвата исключения конструкцией try-except-end, обработки и дальнейшей генерации исключения инструкцией raise. Однако такой способ не является гибким, так как один и тот же тип исключения в одной операции может рассматриваться как восстановимый, а в другой — как фатальный.

Улучшенной разновидностью такого способа является способ, когда все восстановимые исключения от VCL перехватываются и обрабатываются на местах, а фатальные исключения транспортируются в глобальный обработчик TApplication.OnException. Таким образом, глобальный обработчик рассматривает любое исключение как фатальное. Перехват восстановимых исключений не занимает много места, так как и было указано раньше, подавляющее большинство операций можно и нужно рассматривать как «надежные» операции, то есть приводящие к фатальным ошибкам.

Например, открытие файла можно проводить следующим образом.

Вызовы API функций не генерируют исключений, поэтому следует анализировать и обрабатывать результаты выполнения потенциально опасных функций, а «надежные» функции обертывать процедурой Win32Check, а еще лучше специальной процедурой Assert с указанием кода ошибки и уникальной метки.

Таким образом, ключом к успеху является правильное разделение всех вызовов функций в программе на две группы. Первая группа — потенциально опасные, «ненадежные» функции должны быть обработаны с особой тщательностью и возможностью восстановления программы после сбоя этих функций. Ошибки при их выполнении вполне возможны и должны рассматриваться как естественные. Вторая группа — «надежные» функции, невыполнение которых должно приводить к выводу диагностического сообщения и закрытию программы. Ошибки при выполнении надежных функций должны рассматриваться как фатальные.

Вы либо полностью контролируете ситуацию, с возможностью отката к нормальному состоянию программы — «нефатальная ошибка»; либо извиняетесь и закрываете программу, так как вы не можете корректно обработать эту ошибку — «фатальная» ошибка. Во втором случае вы должны сделать все возможное, чтобы информация об ошибке была как можно содержательнее, для того, чтобы вы могли ее исправить.

5. Тотальный контроль

В первой и второй главе обсуждалась важность применения в программах процедуры Assert с уникальными метками для быстрой локализации и идентификации ошибок. В предыдущей главе обсуждался принцип разделения всех нештатных ситуаций на две группы, требующих различного подхода. Следование этим принципам поможет вам улучшить качество кода при написании больших проектов.

Однако существует еще одна проблема, связанная с тем, что широко используемая библиотека VCL, а также множество других естественно не используют выше указанные принципы. Например, при выполнении следующего кода возникнет исключение от VCL с сообщением «List index out of bounds», так как мы запрашиваем на один элемент больше, чем есть в списке. Получив сообщение о такой ошибке от пользователя, вы вряд ли сможете определить место программы, где эта ошибка произошла. Поиск такой ошибки затруднен, даже если она возникла на вашем компьютере, на том, на котором вы разрабатываете программу. А если такая ошибка возникла у далекого пользователя, сложность возрастает во много раз, так как вам предстоит еще и «выбить» всю информацию об ошибке у человека далекого от Delphi в частности, и от программирования вообще.

Та же самая ситуация произойдет, если вы попытаетесь обратиться по «бешеному» указателю — указателю в котором хранится постороннее значение указывающее в никуда. Вы получите малоинформативное сообщение, которое никак не указывает на место, где эта ошибка произошла. Используя вышеописанные принципы, вы сможете перехватить эту ошибку, распознать ее как фатальную и закрыть программу. Однако вы не сможете выдать информативное сообщение о месте где эта ошибка произошла. И ее локализация и исправление будут сильно затруднены.

Одним из успешных вариантов решения такой проблемы является помещение каждой (каждой !) процедуры в конструкцию try-except-end. При этом локальный обработчик исключения ловит возникшую ошибку и заменяет ее своей с указанием уникального идентификатора. При этом предыдущий пример может выглядеть так.

Процедура AssertInternal «наследует» сообщение от возникшего исключения, плюс, добавляет к нему свой уникальный идентификатор. При возникновении ошибки, пользователю будет выдано соответствующее предупреждение, после чего программа закроется. Возможна запись предупреждающего сообщения в специальный файл, который впоследствии может быть выслан вам пользователем.

Получив информацию о возникшей ошибке вы можете:

  • с точностью до процедуры узнать место возникновения ошибки;
  • узнать тип исключения и параметры ошибки (например ошибочный индекс или адрес по которому произошел ошибочный доступ к памяти).

Такая информация значительно облегчает поиск и исправление ошибок в очень большом проекте.

Все вышеперечисленное являлось лишь приблизительным рецептом и пищей для размышления. Точный вариант комплексной системы обработки ошибок приведен в прилагаемом архиве. Модуль ATSAssert.pas содержит примеры процедур и функций для обработки фатальных ошибок. Модуль ATSDialogMain.pas содержит примеры их использования.

7. Пример модуля с функциями комплексной обработки ошибок

Объявления деклараций используемых функций

Объявляется перечень и формат функций для комлексного контроля за качеством кода. Глобальный обработчик исключений

Глобальный обработчик исключений устанавливает процедуру обработки на событие Application.OnException. Это позволяет перехватывать все исключения не пойманные конструкцией try-except-AssertInternal-end. Хотя таких непойманных исключений быть не должно, лучше все-таки поставить на них обработчик.

Если вы пишите библиотеку DLL в таком обработчике нет необходимости, так как все непойманные исключения будут обрабатывать в EXE-модуле. Основная процедура обработки ошибок

Основная процедура обработки ошибок. Формирует и выводит сообщение, записывает его в посмертный лог и закрывает программу. Если вы пишете библиотеку DLL, то для каждой библиотеки можете добавить к сообщению свой дополнительный префикс, например ‘A’, ‘B’, ‘C’ и т.д. Это поможет быстро определить в каком модуле произошла ошибка. Альтернативная процедура Assert Альтернативная процедура Assert для API-функций

Функция для тотального контроля исключений

Функция для перехвата исключений от VCL и неудачных обращений к памяти. Добаляет к тексту исключения уникальную метку и генерирует ошибку. Инициализация модуля

Создание и настройка глобального обработчика исключения на событии TApplication.OnException.

8. Пример использования системы комплексного контроля

В прилагаемом примере рассматриваются возможные ошибочные ситуации и пути их обработки с использованием комплексного контроля за качеством кода.

ErrorAddr — Переменная Delphi

Подключил DLL, в основном все получается, но не могу вызвать одну процедуру. В прототипе
procedure SyncRead( var Serv: PSafeArray); safecall;

Как мне правильно завести переменную Serv чтобы не ругался компилятор на несовпадение типов. Эта программа работает в VisualBasic там эта переменная объявлена как массив string

Как обычно — через pointer. И к процедуре лучше, наверно, дописать StdCall.
Да и размер массива вернуть не помешает.

Если можно, поподробннее какую конкретно строчку ввести после var и после Begin

ммммм.
Насчёт васика не уверен — но в принципе он должен использовать stdcall.
в процедуре докатай ;stdcall;safecall;

Передача данных через указатель — must be known. Ты же с winApi так общаешься.

А зачем тебе с васиком общаться? Имей в виду pascal string, Basic string и PChar — вещи очень разные.

var Serv: PSafeArray;
begin
Serv := PSafeArray(VarArrayAsPSafeArray(
VarArrayOf([«Первый», «Второй», «Третий»])));
SyncRead(Serv);
end;

А он мне пишет Undeclared identifier: «PSafeArray»
может надо подключить что-то в поле uses .
и потом, массив мне не надо заполняь, его заполнит
вызываемая процедура, нужно только указать
размерность массива и что данные типа string.
а если бы я не знал этого то как можно было бы узнать??


> tesseract © (21.01.06 22:12) [3]


> ;stdcall;safecall;

SafeCall — оно и есть StdCall, само по себе. Вот только странность — SafeCall имеет смысл только в COM, а тут вроде как речь об обычной ДЛЛ. Непонятно.

> Alex222 (22.01.06 11:50) [5]

Попробуй вот это, один из вариантов должен подойти:

function InvokeSyncRead1(ItemCount: integer): OleVariant;
var
pArr: PSafeArray;
begin
Result:= VarArrayCreate([0, Pred(ItemCount)], varOleStr);
pArr:= TVarData(Result).VAny;
SyncRead(pArr);
end;

function InvokeSyncRead2(ItemCount: integer): OleVariant;
var
pArr: PSafeArray;
begin
Result:= VarArrayCreate([0, Pred(ItemCount)], varOleStr);
pArr:= TVarData(Result).VPointer;
TVarData(Result).VAny:= nil;
SyncRead(pArr);
TVarData(Result).VAny:= pArr;
end;

Скорее всего первый, но проверь, если до и после вызова значение SyncRead pArr одинаково, то используй первый вариант, если pArr изменяется внутри SyncRead, то второй.


> А он мне пишет Undeclared identifier: «PSafeArray»
> может надо подключить что-то в поле uses .

Разумеется, нужно. А что именно — на то в среде поиск по файлам есть.

> а если бы я не знал этого то как можно было бы узнать??
посмотреть в том модуле откуда эта декларация (*_TLB ?), если она компилится то все используемые модули в ней указаны (а она компилится раз ругается у тебя на не совпадения типов. иначе бы еще раньше ругалось бы «Undeclared identifier»).

p.s. зажми Ctrl и кликни мышкой на этом типе (только из того модуля что компилится, не из своего). в какой модуль попал? его и себе подключай.


> а она компилится раз ругается у тебя на не совпадения типов.
> .. иначе бы еще раньше ругалось бы «Undeclared identifier»


> sniknik © (22.01.06 13:19) [8]


> Alex222 (22.01.06 11:50) [5]


> А он мне пишет Undeclared identifier: «PSafeArray»

?


> sniknik © (22.01.06 13:19) [8]

А, ну да, дошло наконец-то:)))


> SafeCall — оно и есть StdCall, само по себе. Вот только
> странность — SafeCall имеет смысл только в COM, а тут вроде
> как речь об обычной ДЛЛ. Непонятно

SafeCall к Com и относится отличается от StdCAll допзащитой межпроцессного взаимодействия.
Cdecl отличается тем что чистить память должен вызывающий роцедуру, а stdCall — вызываемая процедура.


> размерность массива и что данные типа string

Данные типа string — разные всё таки скажи что-ты используешь.
Com или Dll

Подключил в uses ActiveX
var Serv,Val,Err: PSafeArray;
Result:Variant;
begin serv:= TVarData(VarArrayCreate([0, Pred(1)], varDouble)).VPointer;
val:= TVarData(VarArrayCreate([0, Pred(1)], varVariant)).VPointer;
err:= TVarData(VarArrayCreate([0, Pred(1)], varDouble)).VPointer;
MyOPCGroup.SyncWrite(1,Serv,Val,Err);

TVarData(Result).VAny:= err;
edit1.Text:=Result[1]; — Дает ошибку Invalid argument при выполнении
теперь осталось научиться читать и писать в такой хитрый массив


> tesseract © (22.01.06 16:22) [11]

Это ты мне что-ли объясняешь?:) Забавно:)))

> теперь осталось научиться читать и писать в такой хитрый массив
SafeArrayGetElement
SafeArrayPutElement


> теперь осталось научиться читать и писать в такой хитрый
> массив

А зачем? Передавай все строки в WideString с разделителями строк например #13#10.
Потом рассортируешь.


> Это ты мне что-ли объясняешь?:) Забавно:)))

Ну надо же повыёживаться :-)

Можно и так:

TVarData(Result).VType:= varArray or varOleStr;
TVarData(Result).VAny:= err;

но с учетом типа елементов в Err. Или вместо

err:= TVarData(VarArrayCreate([0, Pred(1)], varDouble)).VPointer;

Result:= VarArrayCreate([0, Pred(1)], varDouble);
err:= TVarData(Result).VPointer

Так все работает:
use . ActiveX;
var Serv: PSafeArray;
i,ix:integer;
rgsabound: array[0..1] of TSafeArrayBound;
MyServer : TPCServer;
Begin
if not Assigned(MyServer) then
MyServer := TPCServer.Create(MyPCServer);
// 1-й вариант:
Serv:= TVarData(VarArrayCreate([1,5],varDouble)).VPointer;
// 2-й вариант:
rgsabound[0].lLbound:=1;
rgsabound[0].cElements:=5;
Serv:=SafeArrayCreate(VT_r8,1,rgsabound);
// Общее:
i:=1;ix:=33;
SafeArrayPutElement(Serv,i,ix);
xx.SyncWrite(1,Serv);
SafeArrayGetElement(Serv,i,ix);
edit1.Text:=inttostr(ix);

А теперь вопрос: как мне подключить обработчик события MyServer
На форме значка нет, чтобы на нем щелкнуть,класс взят из DLL
Заранее всем благодарен.

Блог GunSmoker-а

. when altering one’s mind becomes as easy as programming a computer, what does it mean to be human.

8 мая 2020 г.

Windows Error Reporting и Delphi

Windows Error Reporting (сокращённо: WER) — это набор технологий, встроенных в Windows, который собирает информацию о сбое в приложениях при их вылетах (а также о сбоях ядра) и отправляет её на сервера Microsoft. Разработчик программного обеспечения по этой информации может разработать и опубликовать соответствующее обновление. Затем конечный пользователь, отправляя отчёт, увидит, что для этой ошибки в программе доступно исправление, сможет скачать его и обновить программу.

В этой статье я хотел бы посмотреть на его историю, концепцию и как вы можете использовать его на практике для своих приложений Delphi (или, наоборот, не использовать).

Содержание

Что происходит при сбое

Сбой в приложении как правило представляет собой исключение, которое может быть или программным (вызываемым явно через raise , т.е. происходящим синхронно) или аппаратным (прерыванием от процессора, т.е. происходящим асинхронно — например, ошибочная попытка доступа к памяти).

Когда происходит исключение, система ищет обработчики в таком порядке:

    Если к программе подключен отладчик, система передаёт управление ему. Отладчик решает: остановить ли программу, продолжить ли её выполнение, или же показать уведомление.
  • Поскольку сообщение показывается из самого процесса, то в особо запущенных случаях процесс может быть завершён без показа сообщения.
  • (для простоты я опустил случай подключения отладчика ядра)

    История Windows Error Reporting

    16-битные Windows

    В далёкие-далёкие времена не было никаких утилит диагностики. Если программа вылетала — она вылетала совсем. В тяжёлых случаях программа могла утянуть с собой всю систему.

    К примеру, General Protection Fault (GPF, общее нарушение защиты, Unrecoverable Application Error, UAE) — это прерывание (fault), возникающее в случае попытки приложения получить доступ к не принадлежащей ему области памяти (сегодня известно как исключение Access Violation большинству разработчиков Delphi). При получении этого сигнала от процессора операционная система останавливает выполнение приложения, сообщает пользователю и продолжает выполнение других приложений. Но если в процессе обработки GPF (в обработчике GPF) будет возбуждено ещё одно GPF, процессор отправит сигнал «повторный GPF» (double fault), останавливая уже операционную систему. Если при этом снова произойдёт GPF (triple fault), процессор прекратит работу и его нельзя будет перезапустить (нужен будет перезапуск всего компьютера).

    В те времена основным способом исправить ошибку в программе было воспроизведение проблемы под отладчиком.

    Первая программа диагностики появилась в бета-версии 16-битной Windows 3.0. Она была создана Доном Корбиттом (Don Corbitt), который раньше работал в Borland и был частью TeamB, но потом ушёл в Microsoft, где и написал Доктора Ватсона (Dr. Watson) — первую утилиту сбора информации о вылете приложения в Windows. Как вы, вероятно, уже предположили, имя «Доктор Ватсон» взято у Доктора Ватсона — персонажа историй Артура Конана Дойля про Шерлока Холмса.

    Доктор Ватсон собирал информацию о системе, сбое и состоянии программы («симптомы»). Информация записывалась в отладочный лог-файл, который потом мог быть доставлен разработчикам программы для анализа.

    Конечно же, Доктор Ватсон очень понравился разработчикам программ. Мэтт Питрек (автор «Windows Internals» и «Undocumented Windows», тоже, кстати, работал в то время в Borland и тоже входил в TeamB) написал свою собственную версию, изначально называвшуюся «Доктор Франк» («Dr. Frank») — в честь Франка Борленда, основателя Borland. Доктор Франк имел кучу дополнительных возможностей, которые делали его круче Доктора Ватсона. Borland-у понравилась идея утилиты и они включили Доктора Франка в состав Borland C++ 3.1 — к сожалению, переименовав его в WinSpector.

    Компилятор Watcom C также стал поставляться со своим собственным аналогом Доктора Ватсона, называвшегося «Доктор Ватком» (Dr. Watcom).

    Пример необработанного исключения в Delphi 1, которое было поймано WinSpector:

    32-битные Windows

    Вплоть до Windows 2000 Доктора Ватсона нужно было запускать вручную до запуска программы, в которой происходил вылет. Доктор Ватсон просто работал в фоне и мог собирать информацию о системе, которая потом сбрасывалась в текстовый отчёт.

    В Windows 2000 был предусмотрен новый механизм. С помощью ключа реестра HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug ( HKLM\Software\Wow6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug — для 32-битных программ в 64-битной системе) стало возможным указывать т.н. «посмертный отладчик» (postmortem debugger) или JIT-отладчик (Just-In-Time — «как раз вовремя») — для подключения к процессу при его вылете. Этот ключ реестра — документирован в MSDN и TechNet. Вы могли зарегистрировать Доктора Ватсона в качестве такого отладчика, вызвав:
    В результате чего Доктор Ватсон регистрировал сам себя:

    После этого при вылете приложения система читала ключ реестра, запускала Доктора Ватсона, он подключался к приостановленному процессу, собирал информацию:

    При этом, если параметр Auto ключа реестра AeDebug был равен True (1), то зарегистрированный отладчик запускался сразу, иначе — система выводила обычное сообщение, но с одной дополнительной кнопкой: «Отмена» — для запуска отладчика. Да, вот так коряво была добавлена эта возможность в Windows. Никто не удосужился даже сделать подходящий диалог.

    Примечание: строго говоря, ключ реестра AeDebug был ещё в WinNT, а в линейке 9x его функциональность выполнял похожий раздел в Win.ini , тем не менее, ключ -i у Доктора Ватсона впервые появился именно в Windows 2000.

    В любом случае, эту информацию можно было затем просмотреть и отправить разработчику программы:

    Это был прообраз того, что затем стало службой Windows Error Reporting.

    P.S. Разумеется, если у вас был установлен полноценный отладчик, то никто не мешал вам зарегистрировать этот отладчик как посмертный, вместо Доктора Ватсона — что, собственно говоря, и делают Delphi (и Visual Studio). Более того, если в системе зарегистрирован посмертный отладчик, то любое приложение может форсированно его вызвать, сделав вызов системной функции DebugBreak , которая состоит всего из одной ассемблерной команды: $CC — программная (пользовательская) точка останова. Разумеется, если посмертный отладчик не зарегистрирован и программа не отлаживается, то такой код приведёт к обычному вылету приложения.

    Windows XP

    В Windows XP Доктор Ватсон был существенно расширен и вылизан. Кроме того, он сменил имя на «Problem Reports and Solutions» и представлен dwwin.exe (Microsoft Application Error Reporting) и FaultRep.dll .

    Во-первых, в Windows XP Доктор Ватсон зарегистрирован по умолчанию в качестве посмертного отладчика, его не нужно регистрировать вручную (несмотря на то, что Доктор Ватсон зарегистрирован в AeDebug / Debugger как drwtsn32.exe , фактически drwtsn32.exe является переходником к dwwin.exe , который и выполняет всю работу).

    Во-вторых, он может быть вызван из программы вручную — через функцию ReportFault .

    В-третьих, он добавляет события о вылетах в системный журнал.

    Наконец, в-четвёртых, он может быть сконфигурирован из апплета Система Панели Управления:

    Как вы можете видеть, тут есть куча опций — выключить, включить частично, полностью, указать исключения или белый список. Достаточно широкий набор опций.

    Если вы выключите отчёты полностью (сняв даже галочку с «уведомлять о критических ошибках»), то при вылете приложения система просто завершит процесс, не показав никакого сообщения и даже не сделав отметку в системном журнале. Это полезно, если система работает в основном без присутствия пользователя, либо когда появление дополнительных окон крайне не желательно (например, режим киоска).

    Примечание: однако, если вместо этого вы вручную удалите параметр Debugger ключа AeDebug (в котором и зарегистрирован Доктор Ватсон), то настройки, конечно, будут игнорироваться. Система покажет обычное окно о фатальном сбое в приложении:

    Если же вы выключите отчёты, но включите опцию «уведомлять о критических ошибках», то Доктор Ватсон будет показывать отчёты о вылетах приложений:

    но не будет показывать отчёты, сгенерированные вручную (через ReportFault ). События о вылетах также будут добавлены в системный лог:

    Включение же отчётов покажет в диалоге новую опцию: «Отправить отчёт».

    при нажатии на которую собранный отчёт отправляется на серверы Microsoft:

    а также добавит отдельное событие в системный лог:

    Но почему отчёт отправляется Microsoft, а не разработчику программы? Дело в том, что вылет в модуле (exe или DLL) может не быть виной этого модуля. Быть может просто другой модуль неверно нас вызвал. Например, вылет проводника может быть из-за кривого расширения оболочки. Вылет игры может быть обусловлен глюком в видеодрайвере и т.д. Вот почему отчёты отправляются в централизованное хранилище. Там они сортируются и к отчётам допускаются все разработчики, чьи модули (exe или DLL) были упомянуты в отчёте.

    Microsoft не использует данные отчётов для каких-либо маркетинговых анализов, анализов частоты ошибок в различных программах т.п. Все данные пользователей защищаются от постороннего доступа и используются только для поиска причины ошибки. Отчёты отправляются по защищённому SSL соединению и хранятся на защищённых серверах, которые не используются ни для каких других целей. Для доступа к отчётам нужно предоставить логин и пароль (задаваемые при регистрации в WinQual). Любой разработчик может видеть только отчёты для своих программ. Все компании, использующие WER, обязуются следовать аналогичным политикам. Разумеется, нет гарантий, что небольшая компания из трёх человек, занимающаяся разработкой shareware-софта, заинтересована в соблюдении вашей конфиденциальности столько, сколько сама Microsoft (хотя она и согласилась следовать соответствующим политикам). Кроме того, по-умолчанию данные отчёта не содержат никаких данных пользователя, кроме тех, которые случайно попадут в дамп. Т.е. персональные данные специально не собираются. В отчёт они могут попасть только случайно. Тем не менее, вы можете посмотреть данные отчёта перед отправкой в случае, если вы работали с важными данными перед вылетом программы.

    А зачем вообще нужно отправлять отчёты о вылетах приложения? Дело в том, что в противном случае разработчик увидит только небольшую часть проблем в своём приложении, про большинство проблем он не будет знать (ведь про них не сообщают). Если разработчик не знает про проблему — он её и не исправит.

    Кроме того, автоматическая система сбора отчётов может сообщить вам, какая ошибка встречается в вашей программе чаще всего. Иными словами, вы можете сразу же исправить ту ошибку, которая действительно важная, которая мешает жить наибольшему количеству ваших пользователей. Т.е. вы можете исправить одну ошибку вместо сотни других — но эффект будет тот же самый.

    В частности, после того как Microsoft реализовала механизм отправки отчётов в Windows XP, она позднее провела широкомасштабный анализ присланных данных, который показал, что 80% пользовательских проблем могут быть решены исправлением 20% наиболее «популярных» ошибок. Даже исправление 1% самых частых ошибок устранит 50% пользовательских проблем!

    Windows Vista и позднее

    Неудивительно, что в Windows Vista служба отправки отчётов из Windows XP была снова обновлена. Она получила новое название Windows Error Reporting (WER) и представлена wermgr.exe и WER.dll .

    Для начала, теперь WER вообще не нужно регистрироваться в системе. По умолчанию ключ AeDebug / Debugger вообще не существует, WER вызывается по умолчанию. Кроме того, в системе теперь есть специальная служба WerSvc («Windows Error Reporting Service» / «Служба регистрации ошибок Windows»), которая по умолчанию стоит на ручном (Manual) запуске и автоматически запускается системой при необходимости.

    Бывший Доктор Ватсон теперь поддерживает настройку через групповые политики («Административные шаблоны» / «Компоненты Windows» / «Windows Error Reporting»), включая возможность указания альтернативных серверов для сбора отчётов (корпоративные настройки) и локальных дампов.

    API был расширен и теперь приложения могут вручную сообщать не только о вылетах, но и о почти любой feedback-информации, прикреплять к отчёту дополнительные данные и файлы.

    Отчёты теперь включают в себя не только логи, но и минидампы — слепки памяти процесса, которые могут быть загружены в отладчик на машине разработчика для исследования вылета.

    Самое главное нововведение — появление локального центра отслеживания проблем. Он называется «Отчёты о проблемах и их решения», находится в Панели Управления:

    В Windows 7 он был сгруппирован с «Обслуживанием» центра решений Windows («Центр безопасности и обслуживания»):

    В Windows Vista можно было изменить все те же опции, что и в Windows XP:

    Но уже в Windows 7 набор опций был уменьшен:

    А в дальнейшем — и вовсе исчез (к примеру, в Windows 10 вообще нет настроек).

    Сами настройки (из реестра) никуда не делись, их всё ещё можно поменять. Просто был убран UI — хотя всегда есть вариант использовать групповые политики.

    В реестре эти настройки находятся здесь:

    • HKCU\Software\Microsoft\Windows\Windows Error Reporting
    • HKLM\Software\Microsoft\Windows\Windows Error Reporting
      HKLM\Software\WOW6432Node\Microsoft\Windows\Windows Error Reporting

    и официально документированы тут.

    Как я уже сказал, самое значительное изменение — появление локального центра сбора отчётов. Отчёты/дампы теперь хранятся локально в %ALLUSERSPROFILE%\Microsoft\Windows\WER\ (например: C:\ProgramData\Microsoft\Windows\WER\ ; а также они могут быть в %LOCALAPPDATA%\CrashDumps\ ) и могут быть отправлены в любой момент времени. Их можно просмотреть в так называемом «журнале стабильности работы»:

    Жирным отмечены отчёты, которые не были отправлены. Вы можете просмотреть технические сведения по любому отчёту:

    отправить любой отчёт или все сразу, а также удалить все отчёты. Опция «Проверить решения» заново отправит сигнатуры всех отчётов (что может быть очень долго!) и сообщит вам, если разработчики программ выпустили какие-либо исправления к этим отчётам.

    Замечу, что «журнал стабильности работы» — это отдельный, независимый журнал. Он никак не связан с системным логом «Приложения» (Event Log). События о вылетах добавляются и туда, и туда, но очищать журналы можно индивидуально.

    Поскольку WER теперь нельзя отключить удалением ключа AeDebug / Debugger , то записи в системном логе и журнале стабильности работы будут добавляться всегда — даже если служба WerSvc будет отключена (Disabled).

    Вариант «Никогда не проверять решения» ( Windows Error Reporting / Disabled = 1), как и ожидается, покажет простое сообщение об ошибке — с единственной опцией: закрыть программу.

    Следующий вариант «Всегда спрашивать» ( Windows Error Reporting / Disabled = 0; Windows Error Reporting / Concent / DefaultConsent = 1) предложит или закрыть программу или отправить отчёт:

    Нажатие на кнопку отправки запустит отправку отчёта:

    Тут надо заметить, что, на самом деле, в этот момент идёт не отправка непосредственно отчёта, а только проверка, нужно ли его отправлять. Т.е. на сервер отправляются идентификационные данные — т.н. первый уровень (Level One), состоящий из основных параметров, идентифицирующих вылет (имя приложения, модуля, их версии, адрес/смещение, тип вылета, время и т.п.). Если сервер сообщит, что эту проблему ещё никто не встречал, либо если разработчик захотел собрать дополнительные отчёты, то вам будет предложено отправить сам отчёт (это называется «отправка дополнительной информации») — т.н. второй уровень (Level Two), состоящий из детализированного отчёта, включая слепки памяти (минидампы процесса):

    Заметьте, что в отличие от Windows XP, в Windows Vista помимо собственно лога прикладывается ещё и дамп процесса (.mdmp), который можно потом загрузить в отладчик Visual Studio или WinDbg, чтобы исследовать проблему.

    В противном случае (о сбое уже сообщили, уровень два не отправляется) программа просто завершается (возможно, с финальным диалогом — см. ниже).

    Соответственно, при выборе любой из двух «автоматических» опций ( DefaultConsent = 2 для «авто-проверять» / «Parameters only»; и = 3 для «и отправлять» / «Parameters and safe data»), начальный вопрос «отправлять» / «закрывать» пропускается, а сразу идёт отправка. Разница между 2 и 3 состоит в том, что при 2 Windows автоматически отправляет только данные «уровня один» — параметры для идентификации отчёта. Все данные «уровня два» (т.е. сам отчёт) при этом отправляются только после явного разрешения пользователя. Вариант 3 же автоматически отправляет и «уровень один» и «уровень два», без запроса разрешения пользователя — но только данные, которые система смогла опознать как, вероятно, не содержащие личную информацию. Для отправки любой другой дополнительной информации Windows всё же спросит разрешения пользователя. На практике эти два положения обычно не отличаются.

    Замечу, что DefaultConsent также может принимать значение 4 — автоматически отправлять вообще все данные без запроса пользователя. Это значение нельзя установить через UI в Windows Vista/7, хотя система его прекрасно понимает (т.е. диалог «отправка дополнительной информации» никогда не показывается). Но это значение стоит по умолчанию в последних версиях Windows (например, Windows 10).

    Отключение службы WerSvc может повлиять на поведение отчётов. Например, Windows не сможет определить, был ли уже отправлен отчёт и каждый отчёт будет считать новым. Отправка отчётов внешне будет успешна, но, похоже, отчёты просто встают в очередь на отправку.

    В некоторых случаях при вылете приложения Windows также может предложить поменять опции на «авто»:

    Важно: если вы разрабатываете невизуальное приложение (или если ваше приложение вылетает до показа первого окна), то в случае, если вы установите опцию WER в любую из двух (трёх) «авто» позиций, диалог не будет показан, если в системе уже есть точно такой же вылет и отчёт по нему был отправлен! Иными словами, если (невидимое) приложение вылетело в первый раз — система сделает локальный отчёт и отправит его (автоматически). Диалог будет показан. Если же то же самое приложение вылетает ровно на этом же месте с той же проблемой второй раз — система просто втихую закроет (невидимое) приложение без показа сообщения! Очистка старых отчётов приведёт к тому, что вылет снова будет считаться «новым» и приведёт к показу сообщения об ошибке. Визуальные (оконные) приложения показывают диалог всегда. Вы также можете форсировать диалог для невизуальных приложений сделав вызов WerSetFlags(WER_FAULT_REPORTING_ALWAYS_SHOW_UI); при старте своего приложения.

    (И наоборот, если вы сделаете вызов SetErrorMode(SEM_NOGPFAULTERRORBOX); или (недокументированный) WerSetFlags(WER_FAULT_REPORTING_NO_UI); , то ваше приложение не будет показывать диалог WER, даже если оно визуальное.)

    В любом случае, в конечном итоге — если финальный диалог о вылете должен быть показан, то он выглядит так:

    Если при этом вы зарегистрировали посмертный отладчик через AeDebug / Debugger , то будет показана дополнительная кнопка отладки:

    Это последний диалог, после которого приложение будет закрыто (за исключением случаев, когда приложение самостоятельно инициировало обращение к WER — в этом случае дальнейшие действия определяет приложение).

    Приложение скрытно завершается — что делать?

    Как должно быть уже понятно, это возможно в следующих случаях:

    • Приложение само явно вызвало TerminateProcess или ExitProcess (прямо или опосредованно — например, через Halt ).
    • Приложение явно завершило все свои потоки (вышло из процедур потоков или же явно вызвало TerminateThread или ExitThread ). Это не бывает в Delphi, поскольку компилятор Delphi вставляет неявный вызов Halt в конец главного потока (т.е. всегда вызывает ExitProcess в конце работы главного потока), но это может случиться, если внешний процесс уничтожит главный поток в вашей программе.
    • Какой-то внешний процесс закрыл или уничтожил или ваш процесс или все потоки в нём.
    • В вашей программе произошло необработанное (фатальное) исключение, но в Доктор Ватсон / WER отключен диалог об ошибках.
    • В вашей программе произошло необработанное (фатальное) исключение, в системе зарегистрирован сторонний посмертный отладчик с автоматическим запуском, который не показал сообщения об ошибке.
    • В вашей программе произошло необработанное (фатальное) исключение, которое настолько серьёзно, что система даже не смогла показать сообщение, а посмертный отладчик не зарегистрирован.

    Что же в таком случае делать?

    В целом нужно попытаться определить, завершилось ли приложение штатно (вызвало Exit/Terminate, либо это сделал кто-то за него), либо же вылетело с необработанным исключением. Подробный список — ниже.

    Примечание: в списке ниже ключ реестра Windows Error Reporting\что-то обозначает ключ HKCU\Software\Microsoft\Windows\Windows Error Reporting\что-то , а при его отсутствии — HKLM\Software\Microsoft\Windows\Windows Error Reporting\что-то , либо HKLM\Software\Wow6432Node\Microsoft\Windows\Windows Error Reporting\что-то (для 32-битных приложений на 64-битной машине).

    1. Попробуйте запустить приложение под отладчиком. Убедитесь, что в отладчике не отключены уведомления об исключениях. Если приложение под отладчиком не вылетает или подключить отладчик нет возможности — см. шаги ниже.
    2. Первым делом удалите регистрацию посмертного отладчика в ключе реестра AeDebug , либо хотя бы сбросьте параметр Auto в 0.
    3. [Vista+] Убедитесь, что «Служба регистрации ошибок Windows» («Windows Error Reporting Service», WerSvc ) не отключена (не находится в состоянии Disabled; по-умолчанию у неё тип запуска — Manual, но для надёжности вы можете её запустить вручную).
    4. Запустите Доктор Ватсон в Windows 2000, настройки отчётов в Windows XP, настройки WER в Windows Vista и позднее — и включите визуальные оповещения (Windows 2000), отчёты об ошибках (Windows XP), запрос согласия, т.е. не включайте автоматическую отправку (Windows Vista и выше).
    5. [Vista+] Проверьте настройки групповых политик WER. Убедитесь, что UI не отключен, логгинг не отключен, согласие (consent) не установлено в автоматическую отправку без запросов ( DefaultConcent = 1). Не забудьте проверить как политики машины, так и пользователя.
    6. [Vista+] Убедитесь, что ключа реестра Windows Error Reporting\DebugApplications\* нет или он установлен в 1.
    7. [Vista+] Убедитесь, что ключа реестра Windows Error Reporting\DontShowUI нет или он установлен в 0.
    8. [Vista+] Убедитесь, что ключа реестра Windows Error Reporting\LoggingDisabled нет или он установлен в 0.
    9. [Vista+] Очистите все отчёты в журнале стабильности системы.
    10. Убедитесь, что вы не вызываете SetErrorMode с одним из следующих флагов: SEM_FAILCRITICALERRORS , SEM_NOGPFAULTERRORBOX , SEM_NOOPENFILEERRORBOX . Для надёжности сделайте вызов SetErrorMode(0); первым действием при запуске своего приложения.
    11. [Win7+] Убедитесь, что вы не вызываете SetThreadErrorMode с одним из следующих флагов: SEM_FAILCRITICALERRORS , SEM_NOGPFAULTERRORBOX , SEM_NOOPENFILEERRORBOX для ваших потоков. Для надёжности сделайте вызов SetThreadErrorMode(0); первым действием ваших потоков.
    12. [Vista+] Убедитесь, что ваш код не делает вызов WerSetFlags(WER_FAULT_REPORTING_NO_UI); .
    13. [Vista+] Сделайте вызов WerSetFlags(WER_FAULT_REPORTING_ALWAYS_SHOW_UI); первым действием при запуске своего приложения.
    14. Убедитесь, что глобальная переменная System.JITEnable равна 0. Для надёжности присвойте её 0 первым действием при старте приложения.
    15. Запустите ваше приложение и дайте ему вылететь. Если никакого диалога так и не появилось, то выполните нижеследующие шаги.
    16. Проверьте, есть ли записи о вылете приложения в системном логе «Приложения».
    17. Проверьте, нет ли свежих записей в «журнале стабильности системы» или логов в %APPDATA%\Microsoft\Windows\WER\ReportArchive\ / %APPDATA%\CrashDumps\ .
    18. Попробуйте назначить свой глобальный обработчик необработанных исключений через системную функцию SetUnhandledExceptionFilter .
    19. Установите точки останова или хуки (в рамках вашего процесса) на TerminateProcess , ExitProcess , а если это не помогло — то и на TerminateThread и ExitThread .
    20. Установите точки останова или хуки на системную функцию kernel32.KiUserExceptionDispatcher — если эта функция будет вызвана непосредственно перед вылетом, то 99% за то, что у вас произошло крайне серьёзное необработанное исключение, при котором система даже не смогла показать сообщение.
    21. Наконец, попробуйте установить глобальный хук (все процессы) на TerminateProcess , TerminateThread , чтобы узнать, не завершает ли ваш процесс кто-то ещё.
    22. Также попробуйте пересобрать приложение под x86-64 или использовать другую версию Delphi (как более новую, так и более старую).

    Что Delphi делает не так

    Концепция обработки ошибок в Delphi была создана в Delphi 1 и существенно не менялась с тех пор. Напомню, что в те времена не было возможности подключения посмертного отладчика, сбора информации о сбое и автоматической отправки отчёта. Поэтому если приложение вылетало — оно вылетало с концами.

    Поэтому Delphi исторически реализует концепцию «ни за что не вылетать». Делает это она путём расстановки блоков try / except везде, где только возможно. В частности, в оконных приложениях VCL обработка каждого сообщения заключена в явный блок try / except с вызовом Application.HandleException для обработки каждого необработанного (unhandled) исключения: Application.HandleException просто показывает сообщение (.Message) исключения (в новых версиях Delphi — вместе с вложенными):

    После чего приложение продолжает обычную работу (цикл выборки сообщений).

    Если VCL (или аналогичный фреймворк) не используется, либо если исключение происходит вне цикла выборки сообщений, то управление получает глобальный обработчик исключений RTL System._ExceptionHandler , который вызывает пользовательский обработчик из System.ExceptProc :
    В 99% случаев этот обработчик установлен в SysUtils.ExceptHandler , который показывает сообщение через SysUtils.ShowException , а затем завершает работу приложения с кодом возврата равным 1:
    SysUtils.ShowException показывает упрощённо-техническое сообщение:

    Если же модуль SysUtils не подключен, либо исключение возникло до инициализации модуля SysUtils , то System.ExceptProc будет не назначен (равен nil ), так что System._ExceptionHandler попытается обработать сообщение самостоятельно. Поскольку объект исключения определяется в том же модуле SysUtils , модуль System не может оперировать объектом исключения. Вместо этого он завершит приложение с «кодом ошибки» 217 (необработанное исключение) — через System._RunError :
    Что приведёт к завершению приложения. При этом System.Halt увидит, что установлен код ошибки, поэтому он сообщит об этом:

    Примечание: код 217, на самом деле, означает закрытие консольного приложения через Ctrl + Break . По совместительству он же используется для указания необработанного исключения. Также код может быть 230 — настоящий код для необработанных исключений, используется на не-Windows платформах. 204 — код для Invalid Pointer, вызывается менеджером памяти, если передать ему неверный указатель (например, указатель на уже удалённую память). А также частый код 216 — если необработанное исключения является аппаратным Access Violation. Для других аппаратных исключений также есть свои собственные коды, но на практике в 99% случаев вы увидите только 216, 217 или 204.

    Если ваш код будет создавать дополнительные фоновые потоки через BeginThread (а равно и через любые обёртки к нему, например, TThread или многопоточный фреймворк), то RTL также оборачивает функцию потока в try / except блок с вызовом System._ExceptionHandler . А TThread и вовсе оборачивает .Execute в явный try / except блок с сохранением необработанного исключения в свойство .FatalException , таким образом полностью гася его обработку и оставляя её на ваше усмотрение.

    Иными словами (почти) любой код Delphi оказывается обёрнут в обработчик RTL, все исключения обрабатываются либо вашим кодом, либо RTL, поэтому настоящих необработанных исключений в Delphi не бывает.

    В те времена (Windows 3.x) это считалось несомненным плюсом — и таковым и преподносилось в рекламе Borland: «посмотрите, как надёжны наши приложения — они не вылетают». А если вылетают — показывают что-то более удобоваримое, чем просто GPF.

    В современном мире повсеместного распространения интернета это оказывается уже не так здорово, как казалось когда то. Если ваше приложение не вылетает — оно не вызывает WER. Не вызывает WER — не отправляет отчёт. Не отправляет отчёт — вы не получаете отчёт. Результат? Вы или вообще не в курсе, что с вашим приложением что-то не так, либо получаете письмо от пользователя «программа не работает». Разве не было бы лучше немедленно узнавать о вылетах вашего приложения? Получать чёткие отчёты вида «проблема в строке 42»? Сортировать отчёты по частоте возникновения, чтобы видеть самые «горячие» проблемы?

    Поэтому указанное поведение Delphi — зло, от которого нужно избавляться. Мы посмотрим на это чуть позже.

    Когда приложение Delphi может привести к вызову WER?

    А пока — посмотрим на то, как приложение Delphi может вызывать WER, даже хотя оно почти полностью завёрнуто в обработчики исключений, и необработанным исключениям возникнуть, вроде бы, неоткуда.

    Необработанное исключение в не-RTL потоке

    Для начала, самый простой случай — код в потоке, создаваемом системной функцией CreateThread , по очевидным причинам не будет иметь обработчика исключений RTL (в отличие от RTL-функции BeginThread ).
    или Оба эти примера кода приведут к вылету приложения: будет вызван WER, создан/отправлен отчёт о вылете, приложение будет закрыто.

    P.S. Кстати, этот метод — простейший способ заставить работать Restart & Recovery для приложений-служб. Просто реализуйте логику службы в потоках, создаваемых через CreateThread в OnStart . Не используйте OnExecute .

    Повреждение стека/обработчика

    Случай посложнее. Иногда система просто не может вызвать обработчики исключений: и тогда единственное, что она может сделать — вызвать глобальный обработчик (WER).

    Например:
    Ошибки переполнения буфера могут привести к перезаписи важных адресов на стеке потока. В результате блоки try будут испорчены. И если в этот момент произойдёт исключение, система увидит на стеке мусор вместо указателя на блок except . Обработчик вызвать не удастся, так что будет вызван только WER.

    Пример с перезаписью стека справедлив только для x86-32, т.к. на x86-64 стек не используется для блоков try (вместо этого используются табличные исключения, где все блоки try зарегистрированы в отдельной таблице, не хранящейся в стеке).

    Переполнение стека

    Тем не менее, можно привести и аналогичные примеры. Например:
    Этот код вызывает просто переполнение стека (stack overflow). Часто — это обычное исключение, которое будет поймано ближайшим обработчиком и обработано. В данном случае — будет показано сообщение через Application.HandleException . Тем не менее, для обработки исключения тоже нужно свободное место на стеке. И если обработчик займёт слишком много места на стеке — получится исключение внутри обработчика исключения. Новое исключение будет передано выше, пока не дойдёт до самого верхнего уровня, где и будет обработано WER.

    К примеру, сообщение о переполнении стека с высокой долей вероятности будет успешно показано из приложения Delphi 7 на Windows XP, поскольку обработчик представляет собой простой MessageBox(E.Message) . Но в комбинации Delphi 10.1 Berlin на Windows 10 — приложение, вероятнее всего, вылетит в WER, поскольку там и обработчик немного сложнее и MessageBox устроен сложнее.

    Двойное переполнение стека

    Но даже если мы сведём обработчик к тривиальному:
    который, очевидно, всегда будет выполняться успешно, т.к. не занимает места на стеке. Даже если мы возьмём наименьшую версию Delphi и Windows — всё равно приложение может вылететь.

    Да, первое нажатие на кнопку возбудит исключение stack overflow, которое успешно будет обработано (пустым) обработчиком исключений. Но вспомните, что исключение stack overflow возбуждается только когда стек дорастает до защитной страницы, после доступа к которой защита снимается и возбуждается первое исключение (stack overflow). Если затем стек растёт и далее — то никакой защитной страницы уже нет, запись в стек наткнётся на зарезервированную страницу без доступа. Иными словами, если нажать кнопку второй раз — исключения stack overflow уже не будет. Будет фатальное Access Violation, будет вызван WER.

    Вызов UnhandledExceptionFilter

    Далее, поведение программы зависит и от версии Delphi. Указанная выше (в предыдущем разделе) логика с безусловным вызовом System.ExceptProc из System._ExceptionHandler справедлива лишь для старых версий Delphi. Относительно новые версии Delphi ведут себя так только при запуске под отладчиком. Если же программа запущена вне отладчика, то System._ExceptionHandler сначала вызовет системный UnhandledExceptionFilter — и вызовет System.ExceptProc только лишь если он вернул EXCEPTION_EXECUTE_HANDLER . Если же UnhandledExceptionFilter вернул EXCEPTION_CONTINUE_SEARCH , то System._ExceptionHandler не будет обрабатывать исключение и передаст его выше (т.е. исключение будет необработанным и его перехватит ОС, где в дело вступит WER). Если никто специально UnhandledExceptionFilter не назначал (Delphi его не назначает), то за его поведение отвечает WER, т.е. поведение зависит от ОС и настроек WER. К примеру, обработчик может ничего не делать и вернуть EXCEPTION_CONTINUE_SEARCH — и тогда исключение будет поднято выше, и вы увидите только диалог WER, но не run-time error. Часто обработчик сам обработает исключение (покажет диалог WER) и вернёт EXCEPTION_EXECUTE_HANDLER . И тогда вы увидите и диалог WER, и диалог run-time error. Воистину странное сочетание для пользователя!

    Замечу, что это поведение (консультация с UnhandledExceptionFilter из System._ExceptionHandler ) есть только под Windows, только при запуске вне отладчика, только при обработке исключения модулем System (т.е. не влияет на TThread и VCL), и почти, но не во всех версиях Delphi (правда, появилось оно очень давно).

    Т.е. предполагается, что если вы запустили программу вне отладчика, она вылетела, то это поведение позволит вам подключить отладчик Delphi для диагностики проблемы (напомню, многие среды разработки, и Delphi в их числе, регистрируют свои отладчики в качестве посмертных через ключ реестра AeDebug ). Именно поэтому эта опция ничего не делает, если вы уже отлаживаете свою программу: в конце концов, задача опции — подключить отладчик, а если вы уже отлаживаетесь, то подключать ничего не надо.

    Перенаправление на JIT-отладчик

    Далее, начиная с Delphi 5 в модуле System появляется новая глобальная переменная:
    по-умолчанию она равна 0 и означает, что все обработчики исключений работают как обычно. Если эта переменная отлична от нуля, то блоки except не будут вызываться если, во-первых, программа не отлаживается, во-вторых, исключение — аппаратное (для System.JITEnable = 1) или произвольное ( System.JITEnable = 2). Переменная System.JITEnable не изменяется кодом RTL/VCL и предназначена для изменения вами.

    Иными словами, эта настройка ничего не делает, если программа запущена под отладчиком. В этом случае программа будет работать как обычно, все блоки except будут выполняться, все обработчики будут запускаться. Но если программа запущена без отладчика, то эта опция позволяет выбрать как/чем обрабатывать исключения — встроенными обработчиками или отдавать исключения наружу. 1, соответственно, отдаёт наружу только аппаратные исключения (типа Access Violation), 2 — любые.

    Ну, а когда вышла Windows XP (а затем — и Vista), ровно эта же переменная ( System.JITEnable ) позволила вызывать WER и инициировать, таким образом, отправку отчётов.

    Очевидно, что переменная System.JITEnable не предназначена для использования в production-версии вашего кода. По крайней мере, при значении 2 — точно. Ведь в вашем коде написаны какие-то обработчики исключений, стоят блоки try / except , ваш код предполагает, что обработчики будут выполняться, ведь они выполняют какую-то работу по откату. Но если вы включаете опцию System.JITEnable , то ни один из ваших обработчиков не будет вызван.

    System.JITEnable предназначена для отладки ваших приложений. Если в вашем приложении происходит исключение/вылет, которое вы не можете поймать под отладчиком (происходит только при запуске вне отладчика), то вы можете включить System.JITEnable и запустить вашу программу вне отладчика. Пусть она вылетит, вы подключите к программе посмертный отладчик (отладчик Delphi, конечно же) и исследуете проблему на месте.

    Замечу, что даже если System.JITEnable отлична от нуля, то это просто передаст исключение «наверх», т.е. UnhandledExceptionFilter будет вызываться. И если вы (или кто-то ещё) назначит обработчик UnhandledExceptionFilter (через SetUnhandledExceptionFilter ) и он будет возвращать EXCEPTION_EXECUTE_HANDLER (для всех или только избранных исключений), то соответствующие блоки except всё же будут выполняться. Таким образом, вы всегда можете сделать тонкую настройку, выполнять обработку только избранных исключений. И в таком виде System.JITEnable вполне имеет право на жизнь и в production-коде.

    Ручной вызов WER

    В конце концов, ваш код может просто вызвать WER вручную — через его API. Примеры вызова мы посмотрим ниже.

    Настройка Delphi-приложений для отправки отчётов

    Зачем это делать

    Итак, как уже должно быть понятно, отправка отчётов при вылете приложения — есть хорошо, т.к. позволяет разработчикам программ узнавать о вылетах и проблемах в приложениях без участия пользователя, даёт точные технические описания и позволяет делать сортировку/статистику.

    Кроме того, вызов системного обработчика ошибок (будь это WER или что-то ещё) — необходимое условие в некоторых отдельных случаях. Например, при сертификации своей программы для Windows, для реакции системных сервисов перезапуска (Restart & Recovery, в т.ч. в службах).

    Поэтому с концепцией Delphi «ни за что не вылетать» нужно срочно что-то делать.

    Лирическое отступление для тех, кому вообще не нравится концепция отправки отчётов

    Если по каким-либо причинам вы не хотите отправлять отчёты и хотите всегда показывать своё сообщение пользователю:

    • Не используйте CreateThread . Всегда используйте BeginThread .
    • Не изменяйте System.JITEnable .
    • Установите:
      и/или:
      • Заключайте каждую функцию потока, созданного CreateThread / BeginThread / QueueUserWorkItem (и аналогичными функциями), в явный блок try / except .
      • Заключайте в блоки try / except каждую секцию initialization , каждую секцию finalization и блок begin / end .dpr проекта.

      В блоке except вы можете вызывать SysUtils.ShowException или свой собственный код.

    • Вызовите SetErrorMode(SEM_NOGPFAULTERRORBOX); при старте процесса (опционально можно добавить и другие флаги). См. также.
    • Обработайте терминальное исключение главного потока:
      или:
      Заключайте в блоки try / except каждую секцию initialization , каждую секцию finalization и блок begin / end .dpr проекта. В блоке except вы можете вызывать SysUtils.ShowException или свой собственный код.
    • Опционально: замените все обработчики исключений на свои. Убедитесь, что вы дополнительно показываете в сообщении/диалоге: имя модуля, его версию и смещение исключения внутри этого модуля (см. ниже).

    Примечание: а если вы принципиально не против идеи отчётов, но не хотите использовать WER, то — см. ниже.

    Для всех же нормальных разработчиков — давайте посмотрим, что мы можем сделать.

    Во-первых, в первом приближении очень хорошо сработала бы такая комбинация:
    В данном случае мы вызываем WER для всех перечисленных кодов аппаратных исключений и RTL обработчики — для всего остального. Список, конечно, приведён как пример, и его нужно изменить под ваше приложение. Например, если вы делаете определение запуска виртуальной машины через выполнение секретной инструкции, то код EXCEPTION_PRIV_INSTRUCTION (и, возможно, EXCEPTION_ILLEGAL_INSTRUCTION ) нужно убрать. Если вы проверяете аргументы выражений, то коды числовых исключений (деление на ноль, переполнение и т.п.) хорошо бы добавить. В тривиальном случае можно также считать что любое аппаратное исключение нужно передать WER. В сложном случае — фильтровать не только аппаратные, но и программные исключения (установив JITEnable в 2).

    Но это решение — половинчатое. Ведь нам хотелось бы вызывать WER для всех необработанных исключений, вне зависимости от их типа, и не вызывать для всех остальных (опять же, вне зависимости от типа). Поэтому решение с JITEnable стоит отложить.

    К сожалению, в Delphi не предусмотрено никакой возможности отменить только «глобальные» блоки try / except , нет никакого аналога опции JITEnable . Вместо этого нам придётся назначить свой обработчик и вызывать WER вручную — через API. Сама Microsoft такой подход считает допустимым, но не рекомендованным. Рекомендованный — конечно же, просто не ловить исключения, которые вы не знаете как обрабатывать.

    Итак, для начала нам нужна функция, которую мы будем вызывать для необработанных исключений:
    Как видите, тут ужасно много всего.

    Центральным местом тут является всего одна функция — ReportFault , которую и нужно нам вызвать. Проблема в том, что на вход функция просит пару ExceptionRecord и Context — это низкоуровневые понятия, которые недоступны из обычного блока except в Delphi. Поэтому большая часть кода посвящена преобразованию между этими двумя понятиями. К сожалению, весьма важный кусок информации — контекст процессора, не доступен. Чтобы его получить, нам придётся прибегнуть к нетривиальным ловушкам, рассмотрение которых выходит за рамки этой статьи.

    Очень важно, что этот код должен работать без выделения памяти (в куче). Во-первых, нам не известно состояние менеджера памяти в момент вызова нашего обработчика. Может быть, обработчик был вызван как раз потому, что произошло исключение в менеджере памяти. И что тогда? Снова выделять память? Во-вторых, менеджер памяти может быть уже финализирован к моменту вызова вашего обработчика. Например, если исключение возникло в секции initialization модуля, то сначала сработает блок except процедуры InitUnits модуля System , который завершит модули (и менеджер памяти — в том числе), а затем уже вызовет UnhandledExceptionFilter (и, следовательно — наш обработчик). Выделение памяти у уже отработавшего менеджера памяти, опять же, ничем хорошим не закончится.

    Именно по этой причине вызов GetProcAddress вынесен из кода UnhandledExceptionHandler — он выделяет память (см. реализацию GetProcAddress в модуле Windows ).

    Подсказка: если вы всё же хотите выделать память в своём обработчике — попробуйте установить альтернативный менеджер памяти. Для простоты это может быть переходник к HeapAlloc или VirtualAlloc .

    (Это я ещё не про все подводные камни рассказал. Да, как видите, глобальная обработка исключений — не самое простое дело.)

    Если вызов WER был неудачен (например, WER отключен), либо WER вообще отсутствует (Windows 2000 и ранее), то мы пытаемся показать сообщение как оно показывалось бы ранее — методом ShowException модуля SysUtils . Проблема опять же в том, что делать, если на руках нет объекта исключения Delphi, который можно было бы показать. Напомню, что в этом случае модуль System показывает run-time ошибку (обычно — 217, 216 или 204, как мы обсуждали это выше) — что, на мой взгляд, достаточно бесполезно. Поэтому вместо этого я предлагаю показывать код исключения — что и делает функция ShowExceptionRecord , которая является слегка видоизменённой ShowException .

    Данный пример использует API уровня Windows XP (хотя будет работать в любой ОС), но вы также можете расширить этот пример на уровень Vista, чтобы полностью настроить поведение WER. В Vista вы можете изменять части диалога, добавлять в отчёт информацию и файлы и многое другое.

    Итак, имея на руках функцию UnhandledExceptionHandler , мы можем назначить её в качестве глобального обработчика исключений: В данном примере UnhandledExceptionHandler устанавливается обработчиком для оконных приложений VCL, а для остальных мест — мы подразумеваем, что будет использован стандартный обработчик модуля System , который должен вызвать (в не самых древних Delphi) стандартный системный UnhandledExceptionFilter . Соответственно, мы назначаем свой обработчик, чтобы перехватить этот вызов и показать WER.

    Конечно, в последнем случае гораздо проще просто ничего не делать, т.к., как мы помним из написанного выше, в не самых древних версиях Delphi при прогоне без отладчика исключение останется необработанным и будет поднято до WER. Именно поэтому этот код помечен «опциональным». Тем не менее, если вы захотите поменять WER на, скажем, свой собственный механизм обработки/логгирования исключений, то вам этот код будет нужен, так что я заодно его и показал.

    Для совсем старых версий Delphi вы можете заменить (или дополнить) вызов SetUnhandledExceptionFilter на назначение обработчика ExceptProc .

    Вызов же SetWERVisible нужен чтобы окно WER появлялось бы всегда, даже если ошибка произошла в невизуальной части нашего приложения (см. обсуждение этого поведения выше).

    И если вы хотите получать отчёты, но не хотите использовать WER, то вы можете использовать полностью аналогичный код:
    В этом случае вызов SetUnhandledExceptionFilter становится обязательным, а не опциональным.

    Примеры выше были приведены для оконного VCL-приложения, где есть глобальный объект Application , предоставляющий событие OnException . Если вы пишите приложение с использованием другого фреймворка — вы должны адаптировать этот код, изменив назначение глобального обработчика. Например, FireMonkey тоже предоставляет свой собственный глобальный объект Application , у которого также есть событие OnException . Некоторые другие фреймворки предоставляют аналогичное событие. Например, IntraWeb предоставляет событие gServerController.OnLogException . Некоторые фреймворки в качестве такого события используют глобальную переменную Classes.ApplicationHandleException (например, OTL — Omni Thread Library) — так что вы можете назначить свой обработчик туда. Некоторые фреймворки предоставляют процедуру регистрации обработчика вроде RegisterExceptionFilter . Например — OTL и IntraWeb ( TIWServerControllerBase.RegisterExceptionCallback ). Некоторые фреймворки (в основном — многопоточные) не выпускают исключение наружу, а сохраняют его в свойстве типа .FatalException — например, TThread и OTL.

    Короче говоря, вам нужно изучить документацию фреймворка, найти там как фреймворк обрабатывает исключения. И если фреймворк ловит все необработанные исключения, то узнать как можно назначить свой глобальный обработчик, назначить его — и вызывать из него UnhandledExceptionHandler из примера выше.

    К сожалению, не всегда можно назначить свой обработчик. К примеру, службы (сервисы Win32) в Delphi не предоставляют ни события, ни какого-либо иного способа зарегистрировать обработчик. В большинстве случаев для обработки исключений вызывается не виртуальный метод LogMessage . В TServiceApplication есть динамический метод DoHandleException, но заменить его нет никакой возможности, т.к. класс TServiceApplication создаётся безусловно, он не извлекается из какой-либо глобальной переменной, в которую мы могли бы записать свой класс. Есть и другие фреймворки, где не предусмотрена возможность указания своего обработчика исключений.

    Во всех таких случаях нет чистого решения. Вы можете только установить ловушку на нужный код методом сплайсинга.

    И раз уж вы собираетесь вылетать, то сильно неплохо было бы настроить своё приложение на автоматический перезапуск. Делается это либо вручную (на Windows XP и младше), либо вы можете использовать Restart and Recovery API на Windows Vista и выше. Не забудьте только передавать флаг WER_SUBMIT_HONOR_RESTART . Рассмотрение Restart and Recovery API выходит за рамки этой статьи.

    Настройка посмертного отладчика

    Как я уже описывал, у вас есть возможность зарегистрировать любую программу на ваш выбор в качестве посмертного (postmortem) или JIT (Just-In-Time) отладчика. Когда приложение вылетает, система добавит кнопку «Отладка» («Debug») в диалог фатального вылета. Нажатие на эту кнопку запустит указанный вами отладчик, передав ему идентификатор процесса, в котором произошёл вылет. Отладчик сможет подключиться к процессу и исследовать его. Отладчик может быть классическим интерактивным — вроде отладчика Delphi. Или же это может быть автоматизированная утилита, которая просто соберёт информацию — вроде Доктора Ватсона.

    Посмертный отладчик регистрируется в ключе реестра HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug (или HKLM\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug — для 32-битных приложений в 64-битной системе). Для регистрации отладчика вам нужно создать или изменить значение Debugger в ключе AeDebug (строкового типа). Строка должна содержать корректную командную строку. Иными словами, если имя/путь отладчика включает в себя пробелы, его нужно заключать в кавычки. Путь обязательно должен быть полным и абсолютным.

    В командной строке нужно указать как минимум один параметр %ld — это шаблон, который будет заменён на PID процесса.

    Опционально можно добавить ещё два параметра: второй %ld будет заменён на описатель (handle) события, который отладчик может взвести, чтобы возобновить выполнения процесса. При этом считается, что отладчик исправил проблему, и процесс может продолжить работу. Если его не указывать, либо отладчик его не взведёт, то система будет ждать завершения процесса отладчика, после чего возобновит обычную работу WER, т.е. считается, что проблема не исправлена. В большинстве случаев это не очень полезная возможность, которой на практике обычно не пользуются.

    Наконец, можно добавить третий параметр %p , который будет заменён на адрес записи JIT_DEBUG_INFO в целевом процессе. Отладчик (или вы, вручную) может прочитать оттуда дополнительную информацию.

    Кроме того, вы можете создать строковый параметр Auto и установить его в ‘0’ или ‘1’. Несложно сообразить, что при Auto = 1 диалоговое окно не показывается, посмертный отладчик запускается сразу. При Auto = 0 (умолчание), соответственно, появляется обычное окно с дополнительной кнопкой «Отладка» («Debug»).

    Примечание: поскольку настройки посмертного отладчика хранятся в HKEY_LOCAL_MACHINE — вам потребуются права администратора. Поэтому все действия ниже нужно выполнять из программ, запущенных под администратором (и с элевацией прав).

    Delphi при установке регистрирует себя в качестве посмертного отладчика. Но если потом устанавливается другая среда разработки, то она может перезаписать регистрацию посмертного отладчика. К сожалению, в Delphi нет никакого способа автоматически перерегистрировать посмертный отладчик, вам придётся сделать это вручную.

    К примеру, для Delphi 5-7 установите Debugger в: только замените 70 (Delphi 7) на 50 (Delphi 5) или 60 (Delphi 6).

    Для более новых версий Delphi используйте: В настоящее время параметр %p не принимает ни одна версия Delphi.

    Указанный выше отладчик Delphi 5-7 ( bordbg70.exe ) является, по-сути, удалённым отладчиком (remote debugger) и поэтому зависит от соответствующей библиотеки bordbk ( bordbk50.dll , bordbk60.dll , bordbk61.dll , bordbk70.dll ), которую можно найти в папке C:\Program Files\Common Files\Borland Shared\Debugger\ (да, даже на 64-битной системе используется C:\Program Files\ , а не C:\Program Files (x86)\ ).

    Если при запуске отладчика Delphi 5-7 вы получаете сообщение о невозможности загрузки библиотеки bordbk, то соответствующую библиотеку нужно зарегистрировать вызовом tregsvr (лежит в папке \bin\ Delphi) или regsvr32 (два экземпляра лежат в папке C:\Windows\System32\ — под каждую разрядность; вызывать нужно, разумеется, 32-битный), например:

    Помимо Delphi посмертным отладчиком регистрируются и отладчики от Microsoft. Только в отличие от Delphi, многие из них имеют возможность автоматической перерегистрации. Например, отладчики WinDbg, CDB и NTSD могут быть зарегистрированы следующими командами:
    К сожалению, отладчик WinDbg при этом не регистрирует параметр для JIT_DEBUG_INFO , поэтому лучше всего отредактировать его регистрацию вручную:

    Отладчик Visual Studio не имеет возможности перерегистрации, его нужно регистрировать заново вручную. Для этого используется такая командная строка:

    Утилита ProcDump от SysInternals также может быть зарегистрирована посмертным отладчиком с помощью такой команды:
    Утилита ProcDump создаёт дамп процесса. По умолчанию создаётся мини-дамп: списки процессов, потоков, модулей, описателей. Дамп может быть расширен указанием опций -ma или -mp . Подробнее — см. справку по параметрам ProcDump.

    Если вы пользуетесь в основном Delphi, то вы регистрируете отладчик Delphi в качестве посмертного — и на этом всё. Если же вы используете несколько сред разработки, то, возможно, вы бы хотели переключаться между отладчиками. В этом случае вы можете написать свою утилиту-переходник и зарегистрировать её в качестве посмертного отладчика. Например:
    При запуске вы можете показать диалоговое окно со списком отладчиков, которые вы используете в работе. При выборе отладчика — запустите его через CreateProcess , передав ему отформатированные параметры командной строки (хотя бы так: «Args := StringReplace(StringReplace(StringReplace(Args, ‘%ld’, ParamStr(1), []), ‘%ld’, ParamStr(2), []), ‘%p’, ParamStr(3), []);»). Не забудьте наследовать описатель события (второй параметр %ld ) в целевой процесс. Дождитесь завершения процесса отладчика и выходите.

    Например, отладчик Visual Studio позволяет выбрать отладчик так:

    Этот список позволяет выбрать native-отладчик, управляемый (.NET) или Java-отладчик. Delphi там, само собой, нет. Это я просто пример привёл, как это визуально может выглядеть, если вы захотите сделать такую утилиту самостоятельно.

    Использование Threads Snapshot в качестве посмертного отладчика

    В качестве посмертного отладчика вы также можете использовать утилиту Threads Snapshot. Она входит в состав трейсера исключений EurekaLog. Если у вас нет EurekaLog, то вы можете скачать бесплатный Tools Pack.

    Для установки Threads Snapshot в качестве посмертного отладчика достаточно запустить её с параметром » /install «:

    Само собой, запускать нужно под администратором (и с элевацией прав).

    Когда утилита Threads Snapshot зарегистрирована в качестве посмертного отладчика, и вылетает любое приложение — вы можете нажать кнопку Debug для снятия снимка процесса:

    Нажатие на кнопку Debug запустит снятие снимка процесса:

    В конце утилита Threads Snapshot подготовит отчёт и спросит у вас, куда его сохранять. Отчёт будет сохранён в обычный .el формат (отчёт EurekaLog), который можно просмотреть в любом текстовом редакторе или в бесплатной утилите EurekaLog Viewer:

    Восстановить регистрацию предыдущего посмертного отладчика можно запуском Threads Snapshot с параметром » /uninstall «.

    Что я могу извлечь из отчёта WER?

    Во-первых, вы можете просмотреть отчёт или его часть локально. Используйте «центр обслуживания» / «журнал стабильности работы» в новых версиях Windows или системный лог — в старых.

    (Примечание: «Неправильный путь приложения» — это кривой перевод «Faulting Application Path»: «Путь к сбойнувшему приложению».)

    Альтернативно можно использовать сторонюю утилиту AppCrashView — она показывает существенно больше технических деталей.

    Сохраняемая в отчёте информация зависит от версии Windows, настроек WER и способа вызова WER из приложения (вызвало ли WER приложение само, через API, или нет; и если через API — то какие параметры указало).

    Тип вылета

    Код исключения

    Далее, смотрим код ошибки. Он же — код исключения. Самые частые коды: $C0000005 — это Access Violation, $0EEDFADE — исключение-объект Delphi. Могут быть и другие коды. Если вы видите неопознанный код ошибки — попробуйте использовать утилиту Error Lookup. Она входит в состав трейсера исключений EurekaLog. Если у вас нет EurekaLog, то вы можете скачать бесплатный Tools Pack.

    Класс исключения

    Хорошо, с аппаратными исключениями понятно — они отличаются кодом. Но как отличить одно исключение Delphi от другого? Ведь любое исключение, представленное объектом Delphi, в результате будет иметь один и тот же код $0EEDFADE? Ну, в ситуации если WER вызван системой или через API уровня WinXP (т.е. функцию ReportFault , как мы сделали это в примере выше) — никак. Вам придётся исследовать слепок (дамп) процесса. Если же вы вызываете WER вручную (аналогично нашему примеру выше) и используете API уровня Vista+ ( WerCreateReport / WerSubmitReport ), то вы можете скопировать имя класса и/или сообщение в один из десяти произвольных строковых параметров отчёта (модифицированный код из примера выше):
    Да, ужасно много работы ради замены всего двух параметров. Получился не самый тривиальный код, который, к тому же работает без выделения памяти в куче стандартного менеджера памяти. Увы, вызвать WER — это вам не класс с перезаписью виртуального метода унаследовать. Вы можете модифицировать этот код под ваши нужды. Например, выводить имя класса и сообщение через (не используемые WER) параметры WER_P8 и WER_P9, или заменить один из стандартных параметров WER_P0-WER_P7.

    И насчёт сообщения — будьте аккуратны. Параметры отчёта используются WER для группировки и классификации отчётов, а сообщение об ошибке может быть локализовано, что приведёт к ненужному дублированию отчётов. Экспериментируйте.

    В любом случае, в результате имеем:

    Для исключений Delphi теперь показывается имя класса и сообщение исключения. Для аппаратных исключений всё осталось по прежнему, дополнительной информации мы не добавляем.

    Напомню, эта кастомизация возможна только на Windows Vista и выше. На Windows XP отчёт не изменится.

    Смещение/адрес исключения

    Как только вы опознали что именно произошло, осталось понять где это произошло. Для этого посмотрите на параметры имя модуля (не приложения!) и смещение (offset) для этого модуля.

    Имя модуля покажет модуль, в котором было возбуждено исключение. Смещение — это смещение указателя (на код) от начала этого модуля. К примеру, если исключение возникло по адресу $004AF70A в модуле CrashMe5.exe , который загружен по (стандартному для .exe) адресу $00400000, то смещение будет равно: $004AF70A — $00400000 = $AF70A. И наоборот, если вы знаете смещение, то, узнав адрес загруженного модуля, можете узнать и где произошло исключение: $000AF70A + $00400000 = $004AF70A.

    Почему вообще используется какое-то смещение? Почему бы в отчёте просто не указать адрес?

    Потому что этот адрес не будет иметь для вас смысла. К примеру, на машине где произошёл вылет адрес оказался равен $77A0098E, а модуль был загружен по адресу $77990000. Ну а на вашей машине этот модуль оказался загруженным по адресу $5AD20000. И что теперь? Как вы найдёте место вылета? Адрес $77A0098E на вашей машине вообще не указывает внутрь модуля!

    Но если вы знаете смещение ($77A0098E — $77990000 = $7098E), то легко определите и адрес вылета: $5AD20000 + $7098E = $5AD9098E — именно в этом месте произошло исключение.

    Окей, но как же узнать, что за код выполнялся по этому адресу?

    Для этого вы можете использовать утилиту Address Lookup. Она входит в состав трейсера исключений EurekaLog. Если у вас нет EurekaLog, то вы можете скачать бесплатный Tools Pack. Запустите утилиту, укажите на модуль вылета и укажите смещение:

    Утилита покажет вам в каких модуле, классе, методе/функции и строке произошёл вылет. Чтобы это стало возможным утилите необходима отладочная информация. Она понимает множество форматов: map-файлы, TDS/TD32, JCL, DBG и другие. Но это также означает, что вам нужно заранее позаботится об этом. Включите генерацию map-файла (или используйте любой другой из поддерживаемых форматов). Разумеется, эти map-файлы (а также .tds, .dbg, .jdbg и т.п.) нужно будет хранить у себя. И не перепутать два разных файла от двух разных версий одной программы. Чтобы не перепутать — используйте поле версии в версионной информации модуля.

    Некоторые форматы поддерживают внедрение (собственный формат EurekaLog, TDS/TD32, JCL и т.п.) — вы можете использовать их. Тогда дополнительных файлов хранить не нужно (только сам модуль) и нельзя перепутать версии файлов, что тоже есть хорошо. Но увеличивается размер модуля, что не есть хорошо. Выбор за вами.

    Альтернативно, вы можете использовать и саму Delphi. Для этого запустите программу (ровно той версии, что упомянута в отчёте) и поставьте её на паузу (Run / Pause), затем откройте окно со списком модулей: View / Debug / Modules и выясните по какому адресу оказался загружен модуль на вашей машине.

    У вас есть базовый адрес, у вас есть смещение — вычислите полный адрес, сложив два значения. Теперь используйте Search / Go to address и введите полученный абсолютный адрес:

    Если вы всё сделали правильно, то IDE откроет нужный модуль и ткнёт вас в строчку с ошибкой.

    Однако у этого способа куча минусов. Во-первых, не только версия программы должна полностью совпадать с модулем, в котором произошёл вылет, вам также необходимо иметь копию всех исходников для этой версии, а также быть 100% уверенными, что при перекомпиляции вы получите точно ту же самую программу. В противном случае адреса будут отличаться и этот метод ткнёт вас в неверное место.

    Во-вторых, IDE попросту не поддерживает сторонние отладочные форматы. Поэтому если вылет произошёл во внешнем модуле (в частном случае — в модуле ОС), то IDE откроет CPU-отладчик и ткнёт вас в конкретную ассемблерную инструкцию. Никакой дополнительной информации она показать не сможет.

    На практике хранить все копии исходников, восстанавливать их для исследования каждого вылета, пересобирать программу, гарантировать/проверять её совпадение — крайне трудоёмко и чревато ошибками, поэтому этот способ (с IDE) стоит приберечь только на самый последний случай (когда заранее к нему не подготовились), а сильно предпочтительнее просто генерировать отладочную информацию в одном из форматов и хранить её (либо ещё лучше — внедрять), а затем использовать Address Lookup.

    P.S. Аналогичную технику можно использовать и для анализа любых других адресов в сообщениях об ошибках. Например, «Runtime error 217 at 004060CD» или «Access Violation at address 005D2500. «. К сожалению, Delphi сделана в этом отношении не совсем грамотно: она показывает абсолютный адрес, а не смещение. В итоге получается, что если ошибка произошла в .exe — вам (возможно) повезло: .exe (почти) всегда грузится по фиксированному адресу $00400000. Если же исключение произошло в DLL — вы пролетели, если только вам сильно не повезёт (каким-то образом вы узнаете базовый адрес DLL, или же DLL на вашей машине окажется загруженной по тому же адресу).

    Стек, переменные и другая информация

    Окей, с базовой информацией мы разобрались. Но что делать с самой вкусной частью пирога — стеком вызова и, возможно, значениями переменных? К сожалению, в Delphi вы не сделаете ничего. Но вы можете использовать отладчик Microsoft. Для этого вам потребуется следующее:

    1. Vista и выше. На Windows XP дампы не создаются.
    2. Отчёт вылета должен содержать в себе дамп процесса (дамп процесса также называется мини-дампом, противопоставляя себя полному дампу режима ядра). Дамп процесса — это «слепок» памяти и, возможно, объектов процесса. Если вы вызываете WER вручную, то параметры создания дампа вы указываете самостоятельно — в вызове WER. См. код-пример выше, где мы вызывали WerReportAddDump для добавления мини-дампа в отчёт. Если вылет происходит под управлением системы, то мини-дамп создаётся (или нет) согласно настройке WER. В любом случае, файл мини-дампа будет упомянут в отчёте (файл .mdmp или, реже, .dmp), а ссылки на локальные хранилища я приводил выше (в описании WER).
    3. Отладочная информация в формате, которую может понять отладчик Microsoft. Для этого вам нужно создавать отладочную информацию в формате MAP и/или TDS/TD32, а затем использовать конвертер в DBG или PDB. Для этого подойдёт какой-либо из вариантов утилиты map2dbg:
      • https://code.google.com/p/map2dbg/
      • https://github.com/andremussche/map2dbg
      • https://github.com/garethm/map2dbg
      • https://bitbucket.org/wpostma/map2dbg

      или утилиты tds2pdb:

      • https://github.com/andremussche/map2dbg/tree/master/tds2pdb
      • https://sourceforge.net/projects/tds2pdb/
    4. Сама Visual Studio или WinDbg.

    Я решил вынести этот вопрос за рамки статьи, потому что я не уверен, что эта информация нужна по следующим причинам:

    • Это достаточно трудоёмкий процесс.
    • Интеграция не идеальна. К примеру, Visual Studio мне так и не удалось заставить читать DBG/PDB, созданные указанными утилитами. Это работает для WinDbg, но для Delphi-ста изучать WinDbg (отладчик, управляемый в основном текстовыми командами) — весьма нетривиальная задача. Также есть проблемы с 64-разрядными файлами.
    • Намного проще добавить к отчёту произвольный файл(ы) (через WerReportAddFile) — куда вы можете записать стеки, дампы кучи, переменные, логи, скриншоты, рабочие файлы и вообще всё, что душа пожелает.

    Как мне получать отчёты WER?

    Для этого необходимо зарегистрировать в Microsoft свою программу (а точнее — каждую версию каждого исполняемого файла).

    В первую очередь для этого вам потребуется сертификат для цифровой подписи — только так вы можете подтвердить, что вы именно тот человек, который собрал программу. Ранее для этого требовался только особый сертификат от одобренного Microsoft поставщика: VeriSign — стоимостью $500 в год.

    К счастью, сегодня это не так. Вам подойдёт любой сертификат для цифровой подписи. Ключевые слова для поиска: code signing certificate или Microsoft Authenticode. Стоимость — что-то от $65 в год (при покупке на несколько лет) до $200 — в зависимости от поставщика. Остались и сертификаты за $500, но они теперь уже EV: «Extended Validation». Для них производится более тщательная проверка перед выдачей сертификата вас как компании. Такие подписи получают бонус от Smart-фильтров браузеров, а также требуются для подписи драйверов. Для обычного же разработчика вполне достаточно сертификатов за $65 в год. Только убедитесь, что вы покупаете сертификат именно для подписи исполняемых файлов Windows.

    Подписывать свои программы — есть хорошо в любом случае, так что, если вы профессионально разрабатываете софт, то сертификат для подписи у вас и так уже есть.

    Как только у вас появится сертификат, вам, возможно, придётся переконвертировать в подходящий формат. Это бывает редко, но иногда необходимо. Поскольку я понятия не имею, в каком формате у вас сертификат — идите в Google. Вам нужно сконвертировать в формат PFX.

    Далее, вам потребуется Windows SDK. Не хочу давать прямую ссылку, они меняются. Поищите в Google по ключевым: download Windows SDK. Устанавливаете SDK. Вас там интересует только утилита SignTool.

    Когда SDK поставили — можно подписывать файлы. Делать это нужно после каждой компиляции для релиза. Можете использовать для этого Post-Build Event в современных IDE, либо что-то вроде FinalBuilder.

    Если вы не используете FinalBuilder, то вызывать SignTool вам придётся вручную. Как-то так:
    «C:\Program Files (x86)\Windows Kits\8.0\bin\x86\signtool.exe» sign /f «C:\путь-к-сертификату\сертификат.pfx» /p «пароль» /tr timestamp-сервер «C:\путь-к-программе\модуль.exe» /d «Краткое описание программы» /du «http://сайт-программы
    где » timestamp-сервер » может быть: http://timestamp.comodoca.com , http://sha256timestamp.ws.symantec.com/sha256/timestamp , http://tsa.starfieldtech.com/ или любым другим timestamp-сервером, поддерживающим RFC 3161.

    Примечание: все современные сертификаты используют SHA-256 и не распознаются Windows XP и ниже (которая поддерживает только SHA-1).

    Как только вы подписали файл — можно регистрировать его в Microsoft. Веб-сайт Windows Error Reporting (бывший WinQual) сейчас переехал на sysdev.microsoft.com (который, в свою очередь, в настоящее время находится в процессе переезда на https://developer.microsoft.com/dashboard/hardware — который пока только для разработчиков драйверов). Само собой, потребуется учётная запись Microsoft (Live), а также необходимо будет использовать только Internet Explorer.

    Регистрация происходит путём подписи файла. Вам предложат скачать .exe файл, вы должны подписать его своим сертификатом и закачать на сайт. Профиль компании будет создан автоматически по сертификату, вы станете администратором. Если же доступа к сертификату у вас нет, то вы можете выбрать одну из уже зарегистрированных компаний и сможете присоединиться к ней после одобрения заявки администратором компании.

    Выглядит это примерно так (на скриншотах показан процесс для драйверов на developer.microsoft.com, поэтому и упоминается EV; я не могу сделать скриншоты для обычного сертификата, поскольку этот процесс я уже прошёл; напомню, что в настоящее время вам нужно заходить на sysdev.microsoft.com, где произойдёт аналогичный процесс):

    Как только вы успешно зарегистрировались и вошли — вам будет доступна консоль (dashboard).

    Большинство опций в ней предназначены для разработчиков драйверов и сертификации приложений — и потому нам не интересны. Фактически, нас интересуют только отчёты (Reports). Но прежде нам нужно сделать ещё две вещи.

    Первое, что вам придётся в ней сделать — подписать соглашение. Для этого откройте Administration / Legal agreements:

    Как и ранее — тут куча всего для драйверов и сертификации, что нам не интересно. Выберите в фильтре тип «Signable» (т.е. что можно подписать, но ещё не подписано) и найдите в списке WER. Я не могу показать процесс подписи именно для этого соглашения (потому что у меня оно уже подписано), но вот скриншот для другого соглашения:

    Важно: вы должны использовать только Internet Explorer и у вас должен стоять Adobe Acrobat.

    Фактически, тут вам нужно продублировать ваше имя и дату ( Ctrl + C / Ctrl + V ) и нажать «Submit».

    Наконец, последнее, что нам осталось сделать — зарегистрировать свои программы. Делается это не тривиально. Откройте раздел Reports (где будет написано, что у вас отчётов нет) и проскрольте вниз, ищите ссылку на «Product mapping Tool»:

    Тут вас поджидает сюрприз, ибо установщик запросит установленный Windows Live Essentials, который. не доступен для скачивания. Придётся искать оффлайн установщик по неофициальным каналам. Короче, гуглите.

    В любом случае, текущая версия Product Mapping Tool называется аж Microsoft Ecosystem Metadata Exchange (или MEME). Она попросит вас войти в вашу учётку и синхронизируется с сервером Microsoft.

    Ну вот с помощью её мы и добавляем каждый выпуск (release) нашей программы. Само собой, добавляем Product, а не Driver. Ну и если у вас прям сложная программа, то можете создать ещё Product Group, но это не обязательно.

    Не забываем добавить и сами файлы (которые уже должны быть подписаны вашим сертификатом). Ещё раз: файл обязан быть подписан! И быть подписан именно вашим сертификатом, на аккаунт которого вы вошли в MEME. Если файл не будет подписан или будет подписан любым другим сертификатом (включая двойную подпись) — MEME покажет ошибку.

    Ещё один сюрприз тут — утилиту нужно вручную запускать с правами администратора (запускать из: C:\Program Files (x86)\Microsoft Ecosystem Metadata Exchange\MetadataExchange\ ). По крайней мере, на Windows 10. Иначе попытка добавления файла закончится ошибкой недостаточных привилегий (утилита выдаст общее сообщение «ошибка — см. лог», а подробности можно найти в системном логе «Приложения»).

    Я понятия не имею, что будет, если версионная информация в исполняемом модуле не будет совпадать с мета-информацией, которую вы вводите в MEME. Лучше бы, наверное, чтобы она совпадала как можно лучше — особенно номера версий.

    Не забудьте опубликовать внесённые изменения — ссылка «Publish Changes» в заголовке:

    Публикуйтесь — и на этом всё! Конечно, процесс ассоциации нужно будет проводить заново для каждого релиза/версии каждой вашей программы. Но по крайней мере, вы уже зарегистрированы в системе.

    После этого можно деплоить и крашиться! После регистрации вид раздела Reports изменится на рабочий:

    Учтите только, что отчётам нужно время, чтобы пройти отправку и сортировку, появляются они не сразу. Задержка будет от нескольких дней до месяца.

    Вот как будет выглядеть раздел отчётов при поступлении багов:

    Это общий вид «Product group summary», где создано две версии одного продукта, одна из которых раскрыта. Как вы можете видеть, отчёты сортируются по количеству возникновения. Чем больше произошло вылетов — тем выше в списке запись. Есть также «Company hotlist» — куда попадают самые «популярные» быги из всех ваших продуктов. Подразумевается, что «Company hotlist» будет служить своеобразным списком TO-DO для исправления багов.

    Вы можете раскрыть любую запись:

    Будут показана статистика и возможность скачать отчёты.

    У записей также может не быть никаких данных, кроме статистики. Это потому, что они не собраны. Вы должны щёлкнуть «Request more data»:

    В любом случае, когда вы скачаете отчёт (.cab или .zip-файл — в зависимости от ОС), в нём будет:

    Вот два отчёта с разных систем. В обоих случаях присутствует дамп процесса ( minidump.mdmp ), общая информация ( WERInternalMetadata.xml ) и пользовательский файл, присоединённый через WerReportAddFile ( dump.txt ) — куда вы можете сохранить информацию об исключении, стеки вызовов и т.п.

    WERInternalMetadata.xml выглядит примерно так (полный вариант из отчёта с 6-ю файлами; вариант из отчёта с тремя файлами существенно короче):

    sysinfo.txt содержит статистику работы системы (что-то вроде performance counters). WERDataCollectionStatus.txt — служебный. А memory.csv содержит список процессов (со статистикой).

    Что если я хочу использовать отчёты, но не хочу использовать WER?

    Да, использовать WER для Delphi приложений довольно сложно:

    1. Достаточно сложно всё настроить и зарегистрироваться
    2. Требуется сертификат цифровой подписи
    3. Отчёты доставляются с задержкой, а сам сайт sysdev работает неторопливо
    4. Нужно явно регистрировать каждую публикуемую сборку приложения
    5. Отчёты с Windows XP (и ранее) практически бесполезны, поскольку не включают в себя дамп и не позволяют приложить пользовательские файлы
    6. Невозможно использовать отладчик Delphi или Visual Studio для анализа дампов, будет работать только WinDbg
    7. Невозможно анализировать дампы 64-разрядных процессов
    8. В случае неконтролируемого вылета (т.е. когда WER вызывает система, а не мы сами), пользовательских данных у отчёта не будет

    Поэтому вы можете захотеть использовать свой собственный механизм отправки и сбора отчётов. Для этого вам потребуется две вещи:

    1. Централизованный сервер для сбора отчётов, включая их сортировку/группировку и возможности просмотра
    2. Код отправки вылета приложения на сервер

    В качестве сервера я крайне рекомендую FogBugz — это единственная известная мне система отслеживания ошибок (среди прочих: Mantis, JIRA, Bugzilla, Redmine), которая была создана с прицелом на автоматический сбор отчётов. Только она поддерживает понятие bug hash / bugID / alias с автоматической группировкой, учётом (count / occurencies) и контролем сбора. Во всех остальных трекерах отсутствует часть возможностей. Будете делать отправку отчётов через REST API FogBugz — ищите по ключевым словам BugzScout и sScoutDescription.

    К счастью, мы уже рассмотрели все модификации кода, которые вам нужно сделать в вашем приложении выше — теперь вам осталось только поместить ваш код отправки отчёта вместо ReportFault / WerSubmitReport в функцию UnhandledExceptionHandler выше.

    Что если я не хочу изобретать при этом велосипед?

    Тогда вам нужно использовать готовое решение. Для Delphi есть два трейсера исключений с поддержкой отправки отчётов в FogBugz: EurekaLog и madExcept. Конечно, они поддерживают не только FogBugz, но и другие трекеры.

    Более того, если вы хотите использовать WER, то EurekaLog поддерживает и его — присоединяя свой отчёт как дополнительный файл. (Я не уверен, но возможно, что madExcept в режиме Windows Logo Compliant Mode делает что-то аналогичное.)

    Ну и само собой, трейсеры исключений дадут вам и контекст процессора и стек вызовов.

    Использование стандартных дженериков Delphi для работы с наборами данных

    Автор: Alex. Опубликовано в Программирование 18 Июнь 2015 . просмотров: 36364

    Начиная с версии 2009, в Delphi на уровне языка и компилятора появилась поддержка универсальных типов или дженериков (известных также как параметризованные типы), аналога шаблонов в C++. Вместе с этими изменениями появился юнит System.Generics.Collections, служащий для работы с массивами и группировки данных в словари, списки, стеки и очереди. Именно об этом юните и о работе с ним пойдёт здесь речь.

    Статья рассчитана на читателей, имеющих представление о том, что такое универсальный тип или шаблон. Здесь я буду рассматривать только использование юнита System.Generics.Collections. Будут рассмотрены основные классы, которые в нём реализованы, и даны примеры использования. Все приведённые примеры сделаны для Delphi XE7 и их работоспособность в других версиях Delphi не гарантируется.

    TArray

    Класс TArray юнита System.Generics.Collections содержит статические методы для поиска (BinarySearch) и сортировки массива (Sort). При поиске с помощью функции BinarySearch используется бинарный поиск с использованием O(log n) алгоритма, где n — количество элементов массива. Давайте рассмотрим пример использования класса TArray.

    Обратите внимание, что универсальный тип для массива (TArray ) определён в юните System, а в юните System.Generics.Collections определён лишь вспомогательный класс, который может только сортировать массив и искать в нём.

    Теперь рассмотрим вариант сортировки с использованием своего компаратора (сравнивателя). Допустим, что в примере нам нужно сортировать строки не по алфавиту, а по своему собственному алгоритму.

    TDictionary и TObjectDictionary

    Словарь TDictionary или TObjectDictionary – это коллекция пар ключ-значение. Разница между этими двумя классами в том, что второй класс умеет автоматически удалять экземпляры ключей-объектов и/или значений-объектов, т.е. вы можете использовать в качестве ключей или значений экземпляры объектов.

    Добавить ключ с соответствующим значением в словарь вы можете с помощью методов Add (вернёт ошибку, если попытаться добавить ключ повторно) или AddOrSetValue (заменит значение для ключа, если ключ уже есть в коллекции). Удалять элементы словаря можно с помощью Remove (удаление одного элемента) и Clear (полная очистка словаря). Полезными могут быть события OnKeyNotify и OnValueNotify, которые происходят при добавлении, изменении или удалении пары (следует учитывать, что для одной операции может произойти несколько событий).

    Добавление или удаление пары ключ-значение, а также чтение значения по ключу являются эффективными, близкими к O (1), т.к. для хранения пар используется хэш-таблица. Ключи не могут быть nil, а значения – могут.

    Узнать наличие в словаре ключа или значения можно с помощью методов TryGetValue (пытается считать значение по ключу), ContainsKey (проверяет наличие ключа) и ContainsValue (проверяет наличие значения). Прочитать значение по ключу можно с помощью свойства Items, узнать количество пар в словаре – с помощью свойства Count. Получить список всех ключей можно из свойства Keys, а значений – из свойства Values.

    Рассмотрим несколько вариантов использования словарей.

    Теперь рассмотрим более сложный вариант, когда в качестве ключа используется экземпляр класса. Также определим для ключа свой компаратор (сравниватель).

    TList и TObjectList

    Список TList или TObjectList – это упорядоченный список, доступ к элементам которого происходит по индексу. Разница между этими классами в том, что второй класс умеет автоматически удалять экземпляры элементов при их удалении из списка.

    В список можно добавлять или вставлять элементы, менять и удалять их. Можно добавлять nil. При изменении списка срабатывает событие OnNotify.

    Список можно сортировать, используя стандартные или свои компараторы. Можно искать в нём и делать реверсию.

    Свойство Count показывает количество элементов в списке, а Capacity – количество зарезервированных мест. Прочитать элемент по индексу можно с помощью свойства Items.

    Вот пример использования объекта TList.

    Использование объекта TObjectList очень похоже, поэтому я лишь покажу пример, который покажет дополнительные тонкие моменты.

    TThreadList

    TThreadList – это тоже список, но потокобезопасный, т.е. с ним можно смело работать сразу из нескольких потоков. На самом деле – это обёртка над классом TList. Набор методов для работы с элементами здесь очень скромный: Add (добавление элемента), Clear (очистка списка), Remove (удаление элемента) и RemoveItem (удаление элемента с указанием направления поиска). А чтобы работать со списком в полную силу (чтение всех элементов, поиск, сортировка), нужно получить доступ к списку TList, который хранится внутри TThreadList. Сделать это можно с помощью функции блокировки LockList, которая заблокирует список и вернёт указатель на список TList. После работы со списком TList, список нужно разблокировать с помощью метода UnlockList. Также здесь есть очень полезное свойство Duplicates (дубликаты), которое задаёт поведение списка при добавлении дубликатов: разрешать добавление дубликатов (dupAccept), игнорировать дубликаты, не добавляя их, (dupIgnore) или генерировать ошибку при добавлении дубликата (dupError). По умолчанию свойство Duplicates имеет значение dupIgnore.

    Вот пример работы со списком TThreadList (для создания потоков я использую класс TTask, о котором я уже рассказывал в статье «Параллельное программирование в Delphi XE7»).

    TStack и TObjectStack

    Стек TStack или TObjectStack – это стек элементов, работающий по принципу «последним пришёл — первым вышел» (last in — first out). Т.е. добавленные в стек элементы, вытаскиваются из него в обратном порядке. Стеки TStack и TObjectStack отличаются друг от друга тем, что второй стек предоставляет механизм автоматического удаления объектов удаляемых из стека.

    Стек может быть произвольного размера. В стек можно добавлять nil. При изменении стека срабатывает событие OnNotify. Свойство Count показывает общее количество элементов в стеке.

    Пример использования стека TStack.

    Использование стека TObjectStack аналогичное и рассматривать его я здесь не буду. Упомяну лишь, что здесь можно использовать метод Extract, вместо Pop, если не требуется автоматическое удаление извлекаемого элемента.

    TQueue и TObjectQueue

    Очередь TQueue или TObjectQueue позволяет вам добавлять элементы в конец, а вытаскивать их из начала. Т.е. из очереди элементы будут считываться в том же порядке, в котором они были туда добавлены. Разница между очередями TQueue или TObjectQueue состоит в том, что очередь TObjectQueue умеет автоматически удалять объекты при удалении элементов из очереди.

    Свойство Count показывает количество элементов в очереди. При добавлении или удалении элемента вызывается событие OnNotify. В очередь можно добавлять nil.

    Вот пример использования очереди TQueue.

    Использование стека TObjectQueue аналогичное и рассматривать его я здесь не буду. Здесь, так же как и в классах TObjectList и TObjectStack, можно использовать метод Extract вместо метода Dequeue, если не требуется автоматическое удаление извлекаемого элемента.

    TThreadedQueue

    TThreadedQueue — это ещё одна реализация очереди, но в отличие от TQueue или TObjectQueue, эта очередь предназначена для вставки и изъятия элементов из разных потоков. Для этой очереди задаётся ограничение на максимальное количество находящихся в ней элементов, и, если очередь максимально заполнена и какой либо поток пытается добавить ещё один элемент, то этот поток ожидает, пока в очереди появится свободное место или пока не истечёт время ожидания.

    Очередь TThreadedQueue подходит, например, для реализации какого либо сервера, который принимает сообщения от клиентов в одном потоке (или нескольких потоках) и складывает их в очередь, а затем берёт эти сообщения из очереди и обрабатывает их в другом потоке (или нескольких потоках).

    Вот пример использования очереди TThreadedQueue (для создания потоков я использую класс TTask, о котором я уже рассказывал в статье «Параллельное программирование в Delphi XE7»):

    А вот результат, который будет выведен на консоль:

    Теперь давайте разберёмся, как работает этот пример. Здесь чтение из очереди умышленно делается очень медленно, раз в 2 секунды. А записывающий поток пытается записать всё сразу. У него бы и получилось записать сразу все 9 сообщений, но у нас установлено ограничение на максимальный размер очереди – всего 5 элементов. Поэтому он записывает первые пять сообщений сразу, а при попытке записать шестое сообщение зависает в ожидании, пока в очереди не освободится место. Но мы опять же специально ограничили время ожидания всего одной секундой, поэтому через секунду он перестаёт ждать и выдаёт ошибку. То же самое происходит и со следующим седьмым сообщением. А вот к моменту отправки восьмого сообщения в очереди появляется свободное место и сообщение успешно записывается. С девятым опять случается неудача, потому, что только что на свободное место было записано сообщение 8 и очередь опять заполнена, а чтение происходит ну оооочень медленно.

    Если будете плотно использовать этот класс, то вам может пригодиться ещё функция DoShutDown, которая объявляет, что очередь остановлена (после вызова этой функции новые элементы в очередь не добавляются, т.е. при вызове метода PushItem ничего не происходит), и свойство ShutDown, с помощью которого вы можете проверить, остановлена очередь или нет. Здесь нужно заметить, что после остановки очереди вы всё равно сможете считать попавшие туда элементы.

    И в заключении об использовании стандартных дженериков Delphi.

    . можно сказать следующее. Рассмотренные в статье дженерики или универсальные типы очень сильно облегчают программирование. Ведь массивы, списки и словари используются в программировании постоянно. А при использовании дженериков вы облегчаете читабельность кода. Кроме того ошибки несоответствия добавляемых типов в такие словари или списки отлавливаются ещё на этапе компиляции. Кроме того каждый из рассмотренных классов специально оптимизирован для выполнения возложенной на него задачи. Так, что если вы ещё не используете дженерики, настала пора это сделать.

    Переменные Delphi

    Прежде чем изучать переменные Delphi, сначала введем понятие самой переменной. Итак, переменная означает область памяти, содержащей некоторые данные, которые впоследствии могут быть использованы программой.

    То есть, когда программа работает с некими данными, она, выражаясь точнее, манипулирует содержимым каждой ячейки памяти, другими словами, каждой переменной. Для обращения программы к переменной (допустим с той целью, чтоб получить входные данные для вычисления по формуле, и, в конце концов, сохранить полученный результат), переменная должна обладать идентификатором или именем.

    Имя переменной задается произвольным образом программистом. Именем переменной может выступать любая последовательность, включающая буквы латинского алфавита, цифры и некие специальные символы. Имя переменной должно начинаться только с буквы. Нельзя допускать использование пробела в имени переменной.

    Заметка. Компилятор языка Delphi, как и компилятор языка Pascal, не различает использование прописных и строчных букв в идентификаторах переменных, то есть используя имена PROGRAM, Program, program, можно ввести обозначение одной и той же переменной. Обычно программисты обозначают переменные Delphi таким образом, чтоб ее имя было более-менее логически связано с ее непосредственным назначением.

    Примеры переменных Delphi

    Пример 1. Переменным, которым назначено хранение корней и свободных коэффициентов произвольного квадратного уравнения, традиционно представляющегося в общем виде

    логично присвоение следующих имен: корням квадратного уравнения — x1 и х2, а свободным коэффициентам — соответственно a, b и c.

    Пример 2. Если программа содержит переменные, которым назначено хранение числовых данных о сумме покупки и величине скидки, то эти переменные можно обозначить идентификаторами (именами) соответственно totalsum и skidka. Чтобы использовать переменные в программе, написанной на любых языках программирования, в том числе и Delphi, ее необходимо объявить в разделе переменных var.

    Объявление переменных Delphi

    При объявлении переменной, программист устанавливает не только факт ее существования, но и задает ее тип, указывая тем сам диапазон возможных ее значений. Обычно переменную объявляют следующим образом:

    откуда: Имя является именем назначаемой переменной; Тип подразумевает тип данных; для их хранения как раз и используется переменная.

    Пример 3:

    Здесь двум переменным m и n присвоен тип real, а переменной k тип integer. В тексте исходной программы программист, как правило, объявляет каждую переменную, помещая ее в отдельной строке. Если в коде исходной программы присутствуют несколько переменных, которым присвоен один и тот же тип, то их имена перечисляют в одной строке, разделяя запятыми, и лишь после последней переменной, используя символ «:», указывают тип переменных:

    ErrorAddr — Переменная Delphi

    Язык:
    Русский
    English

    ErrorAddr, ExitCode и ExitProc (переменные) (модуль System)

    Переменные ExitProc, ExitCode и ErrorAddr используются для установки процедуры выхода.

    Указательная переменная ExitProc позволяет вам устанавливать процедуру выхода. Процедура выхода всегда вызывается при завершении программы.

    Процедура выхода не требует параметров и должна компилироваться с директивой дальнего вызова (FAR) .

    При условии корректной установки, процедура выхода становится частью цепочки процедур выхода. Процедуры выхода в такой цепочке выполняются в порядке, обратном установке.

    Чтобы сохранить цепь процедур выхода неповрежденной, вы должны сохранить текущее содержание переменной ExitProc перед изменением ее на адрес вашей собственной процедуры выхода.

    Первый оператор в вашей процедуре выхода должен переустановить сохраненное значение переменной ExitProc.

    Процедура выхода может получить информацию о завершении, исследуя переменную типа Integer ExitCode и указательную переменную ErrorAddr.

    ■ В случае нормального завершения, ExitCode = 0 и ErrorAddr = NIL.
    ■ В случае завершения через обращение к процедуре Halt , ExitCode
    содержит значение, переданное к Halt, а ErrorAddr = NIL.
    ■ В случае завершения из-за ошибки во время выполнения программы,
    ExitCode содержит код ошибки, а ErrorAddr содержит адрес оператора,
    в котором произошла ошибка.

    Последняя из процедур выхода (установленная библиотекой поддержки) закрывает стандартные файлы Input и Output. Если значение ErrorAddr не равно NIL, то выводится сообщение об ошибке во время выполнения программы.

    Как только библиотека поддержки вызвала все установленные процедуры выхода, программа завершает свое выполнение и возвращается в операционную систему, передавая как код возврата значение, сохраненное в переменной ExitCode.

    ErrorAddr — Переменная Delphi

    Проблема заключается в следующем: Раннее мной были написаны несколько функций (не использующих копмпонент Delphi) они успешно работали в этом приложении(для вывода результата их работы были использованы визуальные компоненты). Но сейчас в другом приложении они (функции) мне потребовались. Я скопировал этот unit в нужный мне проект, перед компиляцией мне выдаются Undeclared identifier: «k» и т.д. , хотя эти переменные определены внутри тех функций. В чём дело?

    Товарищи ученые помогите разобраться с подземным стуком.

    Неплохо еще и в Uses добавить .

    Но в предыдущем приложении они работали! И в новом эти переменные также определены

    Ошибка программистов Borland-a.
    С этим уже ничего не поделаешь.

    И ещё при открытии проекта Delphi выдаёт ошибку: Error creating form: Cannot open file .dfm
    Но у меня к этому файлу не привязана форма

    >MishaS © (15.07.03 18:25)


    > Но у меня к этому файлу не привязана форма

    Обрати внимание на след. строку в коде (.pas) формы

    Вернее это происходит при попытки первого запуске программы после открытия проекта

    Skier, я забыл убрать эту строчку. Теперь убрал и у меня эта ошибка больше не выскакивает. Но те ошибки о которых сообщалось ранее не исчезли

    >MishaS © (15.07.03 18:32)
    > Skier, я забыл убрать эту строчку. Теперь убрал и у меня
    > эта ошибка больше не выскакивает.

    А программу с корнем не пробовал удалять ?

    Это как и зачем

    > Я скопировал этот unit в нужный мне проект

    А прописали его в нужные uses в других модулях?

    >MishaS © (15.07.03 18:36)
    Это я ёрничал. Sorry :)
    Просто твой вопрос должен очень просто решаться, достаточно
    нескольких первых страниц любой хорошей книги по Delphi.
    А если честно бомбить форум такими вопросами не культурно, да и
    потом, самому доходить полезней для дела, в твоём вопросе уж
    точно.

    Я пока эти функции ещё не вызывал в других модулях, но в главном модуле прописал. Ошибки только внутри одной функции.

    >MishaS © (15.07.03 18:43)

    > Ошибки только внутри одной функции.

    Какой ? Код покажи.

    MishaS © (15.07.03 18:43)
    Ну так может нам на неё все-таки посмотреть, а?

    код функции я выслать не могу.
    Эта функция(в которой Delphi обнаруживает ошибки) вызывает остальные, в которых ошибок нет.
    В той же где Delphi обнаруживает ошибки раннее(в другом приложении, использующем эти функции) он не видит лишь часть определённых в Var переменных.

    Теперь всё работает. Оказывается Delphi иногда не замечает синтаксических ошибок. Я их исправил и он мне не стал выдавать ложные ошибки


    > Оказывается Delphi иногда не замечает синтаксических ошибок.
    > Я их исправил и он мне не стал выдавать ложные ошибки

    Лучший анекдот недели ! :)))

    Извини за несколько разковатый тон, но нету в делфи ошибок таких, которые специалист твоего класса может обнаружить. Посему, не трать понапрасну времени, и своего, и чужого. Два варианта — код сюда, или чтение книжек. Второй вариант предпочтительней.

    Зы — а дельфи, она практически всегда очень по делу комментерии к ошибкам синтаксиса в компилируемой программе делает.. Их тоже желательно, как бы сказать, попытаться осознать, те комментарии.

    Зы2 Я вот уже лет почти двадцать программингом занимаюсь, но мне до сих пор ничего еще не удалось написать такого, чтобы фрагмент кода, тем более с ошибками, нельзя было обнародовать.. Счастливый вы, наверно, человек, как минимум на ФСБ работаете..

    > MishaS © (15.07.03 19:19)
    > Оказывается Delphi иногда не замечает синтаксических ошибок

    Понимаете, такого просто не может быть. При наличии синтаксических ошибок (то есть, ошибок несоответствия исходного кода формальному описанию языка) компилятор просто не сможет построить объектный код и обязательно сообщит об этом. Любой компилятор, а не только дельфишный.

    Есть исключение, компиляторы Си, по крайней мере первые могли.

    > Anatoly Podgoretsky © (15.07.03 20:08)
    И как же они тогда компилировали??

    > Anatoly Podgoretsky © (15.07.03 20:08)

    Да, немного непонятно. По идее, синтаксическая ошибка приводит к нераспознанной лексеме — а значит, к невозможности сгенерить код.

    Вариант вижу, наверное, один — при нераспознании лексемы компилятор делает некое предположение о ней, которое устраняет неизвестность и в соответствии с которым все-таки генерится код. С выдачей сообщения, конечно.

    Такую особенность имел и компилятор PL/1. Правда, его попытки устранить ошибку обячно приводили к снежному кому других ошибок и в итоге все равно к краху. В моей практике был только один случай, когда ему действительно удалось правильно убрать ошибку.

    Оно и понятно — искусственный интеллект для этого нужен, однако.

    В постели он был малоизобретателен:
    Спал на полу, боку, и животе :)

    ЗЫ: А почему животные не целуются?

    Интеллект простой, просто выкидовалось. Ну наверно кроме сильно извращенного кода. Самое плохе что было, так это ни какой проверки на параметры функций. Можно было описать три параметра, а передать два или четыре и при том любого типа. Ошибки такого типа очень тяжело искать, если она не приводила к катастрофическим последствиям. Поэтому Си++ был большой шаг вперед, там хоть предупреждения давались.

    WinInet в Delphi. Как прочитать заголовки сервера?

    Сегодня решил дописать свой THTTPSender, который использует в работе WinInet. Первое, что необходимо было сделать — это научиться правильно читать код ответа сервера для того, чтобы проолжить работу над Google Data API. При этом важно было не только «поймать» код 200 или 400, но и определять момены, когда сервер выдает коды 301 или 302. Обычно при работе с WinInet этот момент (определение кодов перенаправления) опускается, т.к. чаще всего удобно, чтобы перенаправление происходило автоматически. Сегодня разберемся как отлавливать все коды стауса и чиать заголовки сервера. Для того, чтобы получить какую либо служебную информацию от сервера мы можем воспользоваться методом HttpQueryInfo. Рассмотрим входные параметры функции:

    • hRequest — Handle, который мы получаем при выполнении HttpOpenRequest или InternetOpenUrl.
    • dwInfoLevel — комбинация флагов, каждый из которых указывает, какая информация нам необходима
    • lpvBuffer — указатель на буфер в котоый будет происходить запись данных.
    • lpdwBufferLength — размер буфера
      lpdwIndex — индекс заголовка

    Сама функция HttpQueryInfo работает следующим образом: при выполнении происходит попытка чтения в буфер запрошенной информации. Если размер буфера оказывается мал, то вызывается исключение ERROR_INSUFFICIENT_BUFFER и переменная lpdwBufferLength будет содержать необходимую размерность буфера для приёма всех данных. В случае, если мы запрашиваем информацию, которую сервер вернуть не в состоянии, вызывается исключение ERROR_HTTP

    Немного поэкспериментировав с HttpQueryInfo обнаружил следующее: по документации MSDN для успешного выполнения функции необходимо указывать хэндл, который мы берем из HttpOpenRequest или InternetOpenUrl. Если использовать InternetOpenUrl, то так и есть — функция отрабатывает как надо и возвращает результат. Если я использую HttpOpenRequest, то функция отказывается возвращать данные (при этом GetLastError ничего не выдает), но срабатывает после выполнения метода HttpSendRequest. Не думаю, что это глюк, но учитывать этот момент стоит.

    Теперь, что касается исключение ERROR_HTTP_HEADER_NOT_FOUND . На это исключение можно довольно легко налететь. Например, мы запрашиваем информацию о последнем изменении страницы. Для этого мы устанавливаем флаг HTTP_QUERY_LAST_MODIFIED и пробуем выполнить метод. Если в заголовках сервера нет заголовка Last-Modified, то функция вернет False и в результате выполнения GetLastError мы получим исключение ERROR_HTTP_HEADER_NOT_FOUND . Поэтому следует очень осторожно пользоваться методом, когда есть вероятность того, что в заголовках нет необходимой информации.

    Чтобы получать всю информацию от сервера, которую он выдает, я пользуюсь флагом HTTP_QUERY_RAW_HEADERS_CRLF, при этом возвращается строка, содержащая все заголовки сервера и каждый заголовок разделен символами CR/LF (#10#13), что удобно, например, для использования результата в списках TStringList.

    Теперь перейдем непосредственно к программированию. Итак, пишем функцию для получения информации сервера. Назовем её, например GetQueryInfo. В процессе работы нам может потребоваться:

    1. Получние како-либо конкретной информации, например, код ответа сервера.
    2. Получение всей информации — всех заголовков.

    Соответственно, в первом случае, будет излишним взводить HTTP_QUERY_RAW_HEADERS_CRLF (код статуса занимает 6-8 байт, а заголовки могу насчитывать килобайты информации). Следовательно в качестве входных парамеров функции будут выступать:

    1. Хэндл (hRequest)
    2. Флаг (dwInfoLevel) тип параметра — integer.

    Теперь по самому алгоритму. Будем делать так:

    1. Задаем буферу минимальный размер, которого будет достаточно, чтобы прочитать код статуса (сама функция возвращает минимальное значение 8 байт)
    2. Пробуем выполнить функцию HttpQueryInfo
    3. Если возвращается ERROR_INSUFFICIENT_BUFFER ,
      тоизменяем размер буфера и повторно вызываем HttpQueryInfo.
    4. Если возвращается код ошибки отличный от 122 ( ERROR_INSUFFICIENT_BUFFER ), то завершаем работу без повторного вызова HttpQueryInfo.

    Все вышесказанное в Delphi выглядит следующим образом:

    Илон Маск рекомендует:  Что такое код changeclipboardchain
    Понравилась статья? Поделиться с друзьями:
    Кодинг, CSS и SQL