Проектирование Windows-приложений, сохраняющих логи и дампы при сбое
· Го Комура · Разработка Windows, Обработка исключений, Логирование, WER, Дамп сбоя, Расследование сбоев
Самое тяжёлое в расследовании неполадок Windows-приложения - это состояние, когда известно только, что приложение упало, но не осталось никаких следов того, почему это произошло.
Особенно остро эта проблема стоит в таких проектах:
- падение происходит только в окружении заказчика;
- падение случается лишь после долгой непрерывной работы;
- WPF / WinForms / службы Windows / резидентные приложения с низкой воспроизводимостью;
- задействованы COM, P/Invoke, нативные DLL, SDK сторонних поставщиков;
- удалось получить «только текст сообщения об исключении» без контекста того, что происходило непосредственно перед этим.
Однако сразу честно скажем: гарантировать сохранение лога силами одного лишь падающего процесса невозможно. Если учитывать повреждение стека, повреждение памяти, fast fail, принудительное завершение и отключение питания, последний in-process лог по своей природе - это best effort.
На практике стоит стремиться к конфигурации, которая не полагается исключительно на код внутри падающего процесса. То есть мыслить в трёх слоях:
- обычный хронологический лог;
- финальный маркер сбоя в момент падения;
- следы сбоя, которые оставляет ОС или отдельный процесс.
В этой статье, ориентируясь на настольные Windows-приложения, резидентные приложения, службы Windows и инструменты интеграции с оборудованием, мы разберём практические рекомендации, позволяющие не терять возможность расследования даже при падении из-за исключения, вызванного программной ошибкой.
1. Сначала вывод
Сначала перечислим только выводы.
- Важнее всего не ставить всё на один in-process обработчик, отвечающий за «последнюю строку лога».
- Самое безопасное сочетание на практике - это обычный лог + финальный маркер сбоя + WER LocalDumps.
- Для долгой непрерывной работы, интеграции с оборудованием, плагинов и смешения нативных SDK заметно усиливает конфигурацию добавление процесса-наблюдателя (watchdog / launcher / service).
- В обработчике сбоя железное правило - не делать ничего тяжёлого. Сжатие, отправка по HTTP, разрешение зависимостей через DI, диалоги UI, сборка сложного JSON - всё это исключается.
- В момент сбоя стоит лишь коротко записать что-то локально, а сжатие, загрузку и уведомления переносить на следующий запуск или отдельный процесс.
- Использование
ThreadExceptionв WinForms илиDispatcherUnhandledExceptionв WPF, чтобы внешне продлить работу приложения, опасно, если причина - программная ошибка. - И в .NET, и в native-коде для исключений, указывающих на возможное повреждение состояния, безопаснее по умолчанию выбирать «записать и завершить», а не «восстановить».
- Если вы собираете дампы, необходимо одновременно хранить PDB и файлы дистрибутива - иначе позже дамп будет невозможно прочитать.
Иными словами, наилучшая практика сводится к формуле: «Не пытаться сделать всё в момент падения. Разделить роли между временем до падения, моментом падения и временем после падения».
2. Почему одного лишь in-process нельзя сделать «гарантированным»
Если оставить этот момент расплывчатым, проектирование начинает шататься.
2.1 Контекст упавшего потока сам по себе может быть повреждён
Хуки необработанных исключений и фильтры исключений верхнего уровня иногда выполняются в контексте уже повреждённого потока. На этом этапе вполне обычны такие ситуации:
- стек уже небезопасен;
- дополнительное выделение памяти небезопасно из-за повреждения кучи;
- ожидание останавливает выполнение из-за блокировок, удерживаемых на момент возникновения исключения;
- объекты, от которых зависит сам logger, уже повреждены.
Поэтому безопаснее рассматривать финальный обработчик не как «место, где возможно всё», а как «место, где возможно очень немногое».
2.2 Fast fail и исключения, указывающие на повреждение состояния, рассчитаны на «минимальную активность in-process»
При повреждении памяти или фатальных условиях не стоит полагаться на обычную обработку исключений.
В частности, семейство native-функций __fastfail и аномалии, указывающие на повреждённое состояние, спроектированы так, чтобы «завершаться немедленно с минимально возможными накладными расходами».
Иными словами, естественная позиция такова: финальный in-process лог - это удача, если он вообще записался; основные свидетельства должны жить на стороне ОС / отдельного процесса.
2.3 Событие необработанного исключения .NET тоже не место для «тяжёлого восстановления»
AppDomain.UnhandledException в .NET удобен, но
то, что здесь допустимо делать, стоит ограничить короткой записью.
- На него может влиять блокировка, удерживаемая на момент возникновения исключения
- Не всё, включая исключения, связанные с повреждением состояния, можно безопасно перехватить
- Если насильно выстраивать здесь политику продолжения работы, легко продлить жизнь наполовину разрушенного процесса
Реалистично считать, что «событие необработанного исключения = финальное уведомление», а не «безопасная точка восстановления».
3. Рекомендуемая архитектура - разделение crash-time и after-restart
Проще всего разделить то, что делается в момент сбоя, и то, что делается после перезапуска.
| Фаза | Цель | Где выполняется | Что делать |
|---|---|---|---|
| Обычная работа | Сохранить хронологию | Внутри приложения | Структурированное логирование, heartbeat, граничные события |
| Момент сбоя | Оставить минимальные свидетельства | Внутри приложения + ОС | Финальный маркер сбоя, дамп WER |
| Сразу после завершения | Обнаружить непредвиденный выход | Отдельный процесс | Запись кода выхода, решение о перезапуске, уведомление |
| После следующего запуска | Выполнить тяжёлую последующую обработку | Новый, здоровый процесс | Сжатие, загрузка, уведомление пользователя, очистка старых логов |
При таком разделении проектирование становится заметно стабильнее.
3.1 Минимальная конфигурация
Для небольших бизнес-инструментов или внутренних приложений на WPF / WinForms зачастую достаточно вот такого набора для начала.
- Обычный лог: локальный файл, дописываемый только в конец
- Финальный маркер сбоя: отдельный короткий файл
- Дамп: WER LocalDumps
- При следующем запуске: показать сообщение «В прошлый раз приложение завершилось аварийно. Есть диагностическая информация»
3.2 Усиленная конфигурация
Стоит усилить конфигурацию на один уровень при таких требованиях:
- непрерывная работа 24/7;
- управление оборудованием, мониторинг, резидентная работа;
- много COM / P/Invoke / нативных SDK;
- есть дочерние процессы, плагины, выполнение скриптов;
- в окружении заказчика недопустимо «зависнуть и остаться в этом состоянии».
В этом случае разделение на:
- worker-процесс: основная обработка;
- launcher / watchdog / service: контроль запуска, запись выхода, перезапуск;
- WER LocalDumps: на стороне worker-а;
- следующий запуск или watchdog: сбор диагностической информации,
делает конфигурацию весьма практичной.
4. Практические рекомендации по обычному логу
Если пытаться справиться силами лишь последней строки лога в момент сбоя, обычно проигрываешь. По-настоящему выручает обычный лог вплоть до момента, предшествующего сбою.
4.1 Лог - это «информация, которую можно сопоставить позже», а не «текст для человека»
Перечислим минимальный набор данных, которые стоит включать в обычный лог.
- Временная метка UTC
- Время, прошедшее с момента старта процесса
- PID / TID
- Название приложения, версия, номер сборки, идентификатор коммита
- ID сеанса
- ID операции / job / correlation ID
- Название модуля / экрана / worker-а
- Последние внешние воздействия:
- запись в файл;
- обновление БД;
- отправка команды оборудованию;
- сетевые запросы
- Тип исключения, HRESULT / код ошибки Win32 / код исключения
- Краткое описание основных входных параметров
- Идентификаторы объектов, в той мере, в какой они не содержат секретов
Рекомендуем формат одно событие на строку, в виде JSON Lines или key=value.
Важнее не длинный текст для человека, а то, что позже можно сопоставить три файла между собой.
4.2 Критические события записывать синхронно
Если делать все записи обычного лога синхронными, это утяжеляет работу. Но если полностью полагаться на асинхронный буфер, всё разом исчезает в момент падения.
Поэтому на практике реалистично менять подход в зависимости от уровня важности.
- Мелкие события уровня
Information: буферизация допустима Warningи выше: делать flush пораньше- Важные граничные события: записывать синхронно:
- ProcessStart;
- ConfigLoaded;
- WorkerStarted;
- ExternalCommandSent;
- TransactionCommitted;
- RecoveryStarted;
- FatalPathEntered.
Суть в том, что хотя бы бизнес-значимые границы должны надёжно «доходить до земли».
4.3 Разделять «обычный лог, который пишется сейчас» и «финальный маркер сбоя»
Это очень важно.
Если пытаться уместить всё в один rolling-лог, случается следующее:
- шла ротация;
- запись оставалась в асинхронной очереди;
- сам logger умер сразу после исключения;
- строка лога оборвалась на середине.
Поэтому рекомендуем разделить хотя бы на два файла.
app-<session>.jsonlобычный хронологический логfatal-last.logилиfatal-<session>.logвыделен только для финального маркера сбоя
Уже одна лишь однозначность того, куда попадает последняя строка, сильно выручает на практике.
4.4 Место хранения лога - фиксированный локальный путь, никогда не сетевой ресурс
Полагаться в момент сбоя на UNC-путь, NAS, HTTP или облачный API опасно, потому что в дело вмешиваются:
- кратковременные обрывы сети;
- задержки DNS;
- истёкшие учётные данные;
- блокировка в потоке UI;
- нехватка прав у служебной учётной записи.
В момент сбоя записывайте сначала в фиксированный локальный путь. Отправка происходит после следующего запуска или из отдельного процесса.
4.5 Включать сеанс в имя файла
Одной даты недостаточно, потому что в течение одного дня приложение может перезапускаться много раз.
Например, вот так:
Logs\
MyApp_20260318_101530_pid1234_session-4f1c.jsonl
MyApp_fatal_20260318_101533_pid1234_session-4f1c.log
MyApp_watchdog_20260318.jsonl
Уже однозначность в вопросе «о каком именно запуске идёт речь» сильно меняет скорость анализа.
5. Практические рекомендации по финальному маркеру сбоя
Это не место, где нужно строить полнофункциональный logger. Это место, где записывают один раз, коротко, максимально надёжно.
5.1 Цель - «зафиксировать точку входа», а не «детали причины»
Информацию в финальном маркере сбоя выгоднее сузить.
- UTC момента возникновения
- PID / TID
- ID сеанса
- Версия / номер сборки
- Из какого хука это пришло:
AppDomain.UnhandledException;Application.ThreadException;DispatcherUnhandledException;SetUnhandledExceptionFilter;_set_invalid_parameter_handler;set_terminate
- Тип исключения или код исключения
- По возможности - краткое сообщение
- ID последней операции
- Имя файла обычного лога
- Предполагаемая папка для дампа
Этого достаточно.
5.2 Чего нельзя делать в обработчике сбоя
Практически всё из этого списка с высокой вероятностью становится миной.
- Разрешение logger-а через DI-контейнер
- Использование async / await
- Запуск Task
- Ожидание блокировок
- Сборка сложного JSON
- Работа с COM-объектами
- Показ диалогов UI
- Сжатие
- Отправка по HTTP / SMTP / Slack / Teams
- Анализ и суммирование дампа
- Подавление исключения с продолжением работы
Обработчик сбоя - это не продолжение обычного потока обработки. Стоит сводить его к «сделать минимальную локальную запись и завершиться».
5.3 Что делать в обработчике сбоя
Наоборот, действия здесь довольно простые.
- Предотвратить повторный вход
- Записать только одну строку
- Сделать flush
- Завершить работу
Именно в таком порядке.
По возможности используйте:
- заранее подготовленную выделенную папку;
- заранее проверенный на существование путь;
- место хранения с заранее проверенным ACL.
Чрезмерный flush в обычном логе утяжеляет работу, но у fatal-маркера объём предельно мал, поэтому здесь допустим усиленный flush.
В .NET это FileStream.Flush(true), в native-коде - FlushFileBuffers; проектировать легче, если относиться к этой единственной строке как к правилу «именно эта строка должна прямо сейчас дойти до земли».
5.4 Не пытаться продолжать работу
Для непредвиденных исключений, вызванных программной ошибкой, безопаснее рассматривать финальный обработчик как устройство записи, а не устройство восстановления.
Особенно стоит по умолчанию выбирать «не продолжать» в таких случаях:
- даже
NullReferenceExceptionилиInvalidOperationException, возникшее посреди обновления общего состояния; - непредвиденное исключение в потоке UI;
- непредвиденное исключение, просочившееся из цикла мониторинга или родительского цикла;
AccessViolationException;StackOverflowException;- аномалия на native-границе;
- invalid parameter / purecall / terminate в CRT.
Желание «не хочется, чтобы приложение падало», понятно, но выживание в наполовину разрушенном состоянии чаще тяжелее и для диагностики, и для эксплуатации.
При завершении стоит рассмотреть API немедленного завершения - Environment.FailFast в .NET, RaiseFailFastException или __fastfail в native-коде - и проектировать так, чтобы не полагаться на finally или обычную последующую очистку.
6. Особенности по фреймворкам
6.1 Общее для .NET: AppDomain.CurrentDomain.UnhandledException
Это полезно как финальное уведомление. Однако тяжёлого восстановления здесь стоит избегать.
Базовое использование простое.
- Записать финальный маркер сбоя
- При необходимости оставить минимальное сообщение в журнале событий Windows
- Не продолжать работу
- Не ждать и не повторять попытку здесь
UnhandledException удобен, но безопаснее не исходить из того, что отсюда можно вернуть приложение в здоровое состояние.
6.2 WinForms: Application.ThreadException
Сложность здесь в том, что он перехватывает необработанные исключения в потоке UI и позволяет внешне продолжить работу.
Использовать его для превращения ожидаемых ошибок бизнес-ввода в диалоги - одно дело, но он не подходит для продолжения работы после непредвиденных исключений, вызванных программной ошибкой.
Если приоритет - действительно докопаться до причины, безопаснее:
- делать в
ThreadExceptionтолько минимальную запись; - либо склоняться к
UnhandledExceptionMode.ThrowException; - и затем завершать процесс, оставляя дамп и логи.
6.3 WPF: Application.DispatcherUnhandledException
В WPF ситуация похожая.
- В основном затрагивает только исключения в потоке UI
- Установка
Handled = trueпозволяет внешне продолжить работу - Но если сделать это против программной ошибки, состояние экрана и внутреннее состояние легко расходятся
Поэтому в WPF тоже безопаснее использовать это как точку входа для записи, а не как средство продления жизни ради продолжения работы.
6.4 Не делать TaskScheduler.UnobservedTaskException основным путём
Это не «последний рубеж перед падением».
Его можно использовать как вспомогательное средство для обнаружения пропущенных исключений Task,
но как надёжный путь записи в момент сбоя он слаб.
Поэтому его стоит использовать для:
- раннего обнаружения пропусков наблюдения за исключениями;
- выявления пробелов в проектировании
Taskво время разработки, -
но не назначать главной ролью финального обработчика сбоя.
6.5 Native Win32 / C++: не переоценивать SetUnhandledExceptionFilter
На стороне native хочется полагаться на SetUnhandledExceptionFilter.
Однако он выполняется в контексте потока, вызвавшего сбой, поэтому на него влияют:
- недействительный стек;
- глубокая рекурсия;
- уже повреждённая куча;
- блокировки, удерживаемые в момент исключения.
Поэтому SetUnhandledExceptionFilter разумнее рассматривать как
точку входа best effort для получения финального уведомления.
6.6 В native C++ также стоит перехватывать пути завершения CRT
В native C++ наблюдение только за необработанным SEH оставляет пробелы.
Конкретно стоит держать в поле зрения:
_set_invalid_parameter_handler;_set_purecall_handler;set_terminate.
Это семейство существует, чтобы перехватывать «пути завершения», происходящие из C runtime и C++ runtime.
На практике разумный подход:
- записывать финальный маркер сбоя и в этих обработчиках;
- но не делать тяжёлого восстановления;
- надёжно завершать работу;
- основные свидетельства оставлять на WER / дамп.
7. Строим на фундаменте WER LocalDumps
Здесь на практике очень сильно помогает следующее.
7.1 Первая рекомендация - WER LocalDumps
В смысле «оставить минимальные, максимально надёжные свидетельства после падения» проще всего начать именно с WER LocalDumps.
Причины просты.
- Дамп может сохранять сама ОС
- Легко внедрить без дополнительных инструментов
- Настраивается для каждого приложения отдельно
- Позволяет вынести основные свидетельства сбоя за пределы in-process
То, чего не покажет один лишь лог:
- какой поток упал;
- на каком стеке произошло падение;
- на какой границе модуля это случилось;
- где именно проблема - в managed, native, COM или SDK-коде.
Возможность увидеть это позже - сильная сторона такого подхода.
7.2 Типичная настройка
Например, чтобы сохранять дампы для MyApp.exe в C:\CrashDumps\MyApp, можно сделать так:
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpFolder /t REG_EXPAND_SZ /d "C:\CrashDumps\MyApp" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpCount /t REG_DWORD /d 10 /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpType /t REG_DWORD /d 2 /f
Для начала вполне достаточно такого упрощённого подхода.
| Значение | Рекомендация для старта |
|---|---|
DumpFolder |
Выделенная папка |
DumpCount |
5-10 |
DumpType |
2 на машинах разработки; 1 или 2 на местах - в зависимости от объёма диска и требований конфиденциальности |
7.3 Обязательно проверяйте ACL места хранения дампа
Как и с логами, здесь тоже: настройка папки, в которую нельзя писать, лишена смысла.
Особенно для:
- служб Windows;
- дочерних процессов с разделением привилегий;
- ограниченных учётных записей на машинах на местах;
- случаев, связанных с UAC,
ACL места хранения - главная причина «пустых» попыток.
Место хранения стоит проверить по всем пунктам:
- предварительное создание;
- тест записи;
- ограничение числа хранимых копий;
- доступно ли это место для эксплуатационного персонала.
7.4 Когда нужно прикрепить текущий лог к отчёту WER
Если вы используете отчёты WER в адрес Microsoft или собственный конвейер WER, есть и способ через WerRegisterFile зарегистрировать текущий файл лога для включения в отчёт об ошибке.
Однако это стоит рассматривать как дополнительный канал, а не замену локального хранения. То, что действительно нужно в момент сбоя, - это в первую очередь максимально надёжная запись на самом устройстве.
Практичнее такой порядок:
- локальный обычный лог;
- локальный fatal-маркер;
- локальный дамп;
- при необходимости - также регистрация связанных файлов через путь отправки WER.
7.5 Сохранять не только дамп, но и версионную информацию
Даже имея дамп, если позже окажется, что:
- EXE / DLL той сборки уже нет;
- PDB отсутствуют;
- неизвестно, из какого коммита была собрана эта сборка, -
позиция становится слабой.
Как минимум стоит сохранять:
- распространённые бинарные файлы;
- соответствующие PDB;
- версию;
- дату и время сборки;
- идентификатор коммита;
- версию установщика.
Сбор дампов и хранение PDB - это парная задача.
8. Как относиться к MiniDumpWriteDump и собственным crash-репортерам
Бывают ситуации, когда нужна собственная реализация.
- Хочется кнопку «Сохранить диагностическую информацию» в UI
- Хочется объединить также логи и файлы конфигурации
- Хочется обрабатывать группу дочерних процессов вместе
- Хочется добавить собственное маскирование перед автоматической загрузкой
Однако самое важное здесь - не перегружать работой по снятию дампа саму падающую сторону.
8.1 Отдельный процесс лучше self-dump
MiniDumpWriteDump - мощный инструмент, но
вызывать его из отдельного процесса безопаснее, чем из самого упавшего процесса.
Типичная конфигурация выглядит так.
- worker обнаруживает аномалию;
- по возможности уведомляет helper через event или именованный канал (named pipe);
- helper снимает дамп worker-а;
- helper объединяет
tailлогов и файлы конфигурации; - helper после завершения помещает всё в очередь загрузки.
Так, даже если worker повреждён, сторона helper-а остаётся здоровой.
8.2 Если без in-process никак, выносите это на отдельный поток
Даже когда отдельный процесс невозможен, лучше, если выделить под снятие дампа отдельный поток.
Однако и тогда суть остаётся best effort. «Добавили собственную реализацию dump-а - значит, теперь на 100% безопасно» не следует из этого.
8.3 Тяжёлые действия переносите на следующий запуск
В собственном репортере часто хочется сделать:
- сжатие в zip;
- сопоставление с символьной информацией;
- загрузку на сервер;
- снимок экрана;
- получение дополнительной информации из БД.
Всё это переносится на время после перезапуска или на сторону helper-а, а не на момент сбоя.
9. Что меняется при добавлении процесса-наблюдателя
Для систем с долгой непрерывной работой процесс-наблюдатель окупается очень сильно.
9.1 Что записывает процесс-наблюдатель
Watchdog / launcher / родительская служба может записывать такую информацию:
- время запуска дочернего процесса;
- аргументы запуска;
- PID;
- версию отслеживаемого объекта;
- время последнего полученного heartbeat;
- время завершения;
- код выхода;
- число перезапусков;
- наличие дампа;
- был ли выполнен перезапуск.
Уже наличие этого позволяет достаточно ясно увидеть:
- действительно ли произошёл сбой;
- было ли это отключением ОС;
- закрыл ли приложение сам пользователь;
- было ли зависание с последующим принудительным завершением;
- сколько раз цикл ушёл в перезапуск.
9.2 Особенно подходящие случаи
Разделение стоит активно рассматривать в таких случаях:
- worker, несущий SDK стороннего поставщика;
- обработка изображений / видео / устройств ввода-вывода;
- родительские циклы мониторинга или опроса;
- выполнение скриптов или плагинов;
- хостинг существующих активов COM / ActiveX;
- мосты и взаимодействие между 64-бит и 32-бит.
Замыкание опасной обработки в одном worker-е упрощает и проектирование логов, и проектирование восстановления.
10. Частые антипаттерны
10.1 catch (Exception), который лишь логирует и продолжает работу
Самый распространённый и самый опасный вариант.
- Остаются частичные изменения
- Разрушается общее состояние
- Множатся последующие сбои
- Размывается настоящая точка причины
Вы получаете одну дополнительную строку лога, а взамен часто инцидент растягивается надолго.
10.2 Доверие только очереди асинхронного logger-а
Само по себе асинхронное логирование не плохо. Проблема в том, что даже на fatal-пути данные попадают в ту же очередь, и на этом всё заканчивается.
Если worker останавливается в момент сбоя, вся очередь исчезает вместе с ним.
Безопаснее иметь запасной путь, при котором только fatal-путь пишет напрямую.
10.3 Отправка по HTTP из обработчика сбоя
Реализовать это хочется, но это довольно опасно.
- DNS;
- TLS;
- прокси;
- аутентификация;
- тайм-ауты;
- ожидание повторной отправки -
всё это ложится на упавший контекст.
Отправку стоит выполнять после перезапуска.
10.4 Дамп есть, но он не связан с обычным логом
Это встречается часто.
- в имени файла дампа нет сеанса;
- в логе нет PID / сеанса;
- у watchdog-а нет PID;
- номера сборки не совпадают.
В результате три вида свидетельств выглядят как три отдельные, не связанные друг с другом истории.
10.5 Продление жизни через события необработанного исключения в WinForms / WPF
Внешне приложение «перестаёт падать», и поначалу это радует. Но на деле это часто создаёт состояние зомби:
- остаётся только экран;
- worker уже мёртв;
- остаётся только активная кнопка;
- неизвестно, сохранились ли данные.
10.6 Игнорирование native-путей завершения
Если успокоиться, полагаясь только на SetUnhandledExceptionFilter, можно упустить:
- invalid parameter;
- purecall;
- terminate;
- fast fail.
В native C++ лучше держать в поле зрения не только SEH, но и пути завершения CRT / C++ runtime.
11. Минимальный чек-лист внедрения
Если выполнены следующие пункты, конфигурация вполне боеспособна.
- Обычный лог записывает одно событие на строку
- В каждой строке лога есть UTC, PID, TID, версия и сеанс
- Записываются
ProcessStartиProcessExit - Важные граничные события синхронно проходят flush
- Есть выделенный файл для финального маркера сбоя
- fatal-путь не проходит через асинхронный logger
- WER LocalDumps настроен для каждого приложения
- Проверен ACL места хранения дампа
- PDB и файлы дистрибутива хранятся
- При следующем запуске можно обнаружить предыдущее аварийное завершение
- Сжатие / загрузка / уведомление выполняются после перезапуска или в отдельном процессе
- В native C++ учтены invalid parameter / purecall / terminate
- На тестовой машине выполнено намеренное падение и подтверждено, что свидетельства действительно сохраняются
Особенно важна последняя строка. Одного лишь проектирования недостаточно - обязательно нужно провести «испытание на реальный захват свидетельств».
12. Насколько глубоко тестировать
Пункты, которые стоит проверить, собраны в таблицу.
| Тест | Что проверять |
|---|---|
| Необработанное managed-исключение | Появляются ли одновременно обычный лог, fatal-маркер и дамп |
| Исключение в потоке UI | Ведёт ли себя путь событий WinForms / WPF как задумано |
| Исключение в потоке worker-а | Доходит ли до AppDomain.UnhandledException, может ли watchdog это обнаружить |
| Native-исключение | Действительно ли снимается дамп WER |
| invalid parameter / terminate | Остаётся ли минимальная запись даже на пути CRT / C++ runtime |
| Принудительное завершение (kill) | Может ли watchdog зафиксировать непредвиденный выход, даже если in-process ничего не может |
| Перезапуск | Работают ли уведомление, сбор и загрузка после следующего запуска |
Важно не «предположение, что при исключении лог должен появиться», а подтверждение «при этом условии остаётся именно этот файл».
13. Итог
Если вы хотите, чтобы Windows-приложение при падении из-за исключения, вызванного программной ошибкой, сохраняло информацию, необходимую для расследования, оси мышления довольно просты.
- Не полагаться исключительно на падающий процесс
- Разделить свидетельства на обычный лог, финальный маркер сбоя и следы на стороне ОС / отдельного процесса
- В момент сбоя лишь коротко записывать что-то локально
- Тяжёлую обработку переносить на время после перезапуска или на отдельный процесс
- Строить на фундаменте WER LocalDumps
- По умолчанию выбирать запись и завершение, а не продолжение работы
В конечном счёте конфигурация, которая остаётся прослеживаемой даже без последней строки, сильнее, чем попытка изо всех сил вытянуть эту последнюю строку.
И всё же последняя строка нужна, поэтому финальный маркер сбоя стоит коротко записывать в отдельный файл. А настоящие основные свидетельства стоит доверить дампу WER и обычному логу вплоть до момента, предшествующего сбою. Это весьма устойчивый способ работы на практике Windows-приложений.
Похожие статьи
- Введение в сбор крэш-дампов Windows-приложений - как выбирать между WER / ProcDump / WinDbg
- Когда возникает непредвиденное исключение - завершать приложение или продолжать работу? Таблица решений, с которой стоит начать
Справочные материалы
- Microsoft Learn: Collecting User-Mode Dumps https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
- Microsoft Learn: Using WER https://learn.microsoft.com/en-us/windows/win32/wer/using-wer
- Microsoft Learn: MiniDumpWriteDump function https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump
- Microsoft Learn: SetUnhandledExceptionFilter function https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setunhandledexceptionfilter
- Microsoft Learn: System.AppDomain.UnhandledException event https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-appdomain-unhandledexception
- Microsoft Learn: Application.ThreadException Event https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.application.threadexception
- Microsoft Learn: Application.DispatcherUnhandledException Event https://learn.microsoft.com/en-us/dotnet/api/system.windows.application.dispatcherunhandledexception
- Microsoft Learn: TaskScheduler.UnobservedTaskException Event https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskscheduler.unobservedtaskexception
- Microsoft Learn: Environment.FailFast https://learn.microsoft.com/en-us/dotnet/api/system.environment.failfast
- Microsoft Learn: Registering for Application Recovery https://learn.microsoft.com/en-us/windows/win32/recovery/registering-for-application-recovery
- Microsoft Learn: RegisterApplicationRecoveryCallback https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-registerapplicationrecoverycallback
- Microsoft Learn: WerRegisterFile https://learn.microsoft.com/en-us/windows/win32/api/werapi/nf-werapi-werregisterfile
- Microsoft Learn: _set_invalid_parameter_handler https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/set-invalid-parameter-handler-set-thread-local-invalid-parameter-handler
- Microsoft Learn: _set_purecall_handler https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/get-purecall-handler-set-purecall-handler
- Microsoft Learn: set_terminate https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/set-terminate-crt
- Microsoft Learn: __fastfail https://learn.microsoft.com/en-us/cpp/intrinsics/fastfail
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Введение в сбор дампов сбоев Windows — WER/ProcDump/WinDbg
Чтобы отслеживать трудно воспроизводимые сбои Windows-приложений, разбираем, когда использовать WER LocalDumps, ProcDump, MiniDumpWriteDu...
Реагирование на инциденты не заканчивается восстановлением — шаблон постмортема (предотвращения повторения) для небольших команд разработки
Считать инцидент закрытым сразу после исправления и извинений — гарантированный способ повторить его снова. Адаптируем blameless-постморт...
Спящий режим, гибернация, Modern Standby и долго работающие приложения — как проектированием предотвратить «остановилось ночью»
Разбираем, почему долго работающее Windows-приложение оказывается «остановленным к утру», начиная с различий между спящим режимом S3, гиб...
Если вам досталась система без исходного кода и без документации — практический план, как сопровождать её, не останавливая работу
Разбираем практический план начала эксплуатации и сопровождения бизнес-системы, у которой нет ни исходного кода, ни спецификаций. Охватыв...
MAX_PATH и подводные камни путей/имён файлов в Windows — лимит 260 символов, зарезервированные имена, конечная точка, регистр
Разбираем ограничения путей и имён файлов, которые часто стоят за классической ошибкой «файл не найден». Рассматриваем состав лимита MAX_...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Расследование ошибок и причин
Сбои, проявляющиеся только в окружении заказчика, аварийные завершения с низкой воспроизводимостью и анализ причин через сопоставление дампов и логов - тема, хорошо сочетающаяся с расследованием сбоев и анализом первопричин.
Разработка приложений для Windows
То, как проектировать обычное логирование, WER и watchdog для WPF, WinForms, резидентных приложений и служб Windows, напрямую связано с самой разработкой Windows-приложений.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Можно ли гарантированно сохранить лог силами самого падающего приложения?
- Нет, нельзя. Если учитывать повреждение стека, повреждение памяти, fast fail, принудительное завершение и отключение питания, последний in-process лог по своей природе - это best effort. На практике стоит разделить работу на три слоя: обычный хронологический лог, финальный маркер сбоя в момент падения и следы сбоя, оставляемые на стороне ОС или отдельного процесса, - и не полагаться исключительно на код внутри падающего процесса. Самое безопасное сочетание на практике - обычный лог + финальный маркер сбоя + WER LocalDumps.
- Чего нельзя делать внутри обработчика сбоя?
- Ничего тяжёлого. Разрешение logger-а через DI-контейнер, async/await, ожидание блокировок, сборка сложного JSON, работа с COM-объектами, диалоги UI, сжатие, отправка по HTTP/SMTP/Slack - всё это с высокой вероятностью становится миной. Обработчик сбоя должен делать всего четыре вещи: предотвратить повторный вход, записать одну строку, сделать flush и завершить работу. Тяжёлую последующую обработку - сжатие, загрузку, уведомления - стоит переносить на следующий запуск или на отдельный процесс.
- Как настроить WER LocalDumps?
- В реестре, в разделе HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\имя_приложения.exe, задаются DumpFolder (выделенная папка), DumpCount (примерно 5-10) и DumpType (2 на машинах разработки, 1 или 2 на местах в зависимости от объёма диска и требований конфиденциальности). Важно проверить ACL места хранения: типичная ошибка - указать папку, в которую не может писать служба Windows или ограниченная учётная запись. Сбор дампов и хранение PDB и файлов дистрибутива - парная задача: без любого из них позже прочитать дамп не получится.
- Можно ли перехватывать исключение через DispatcherUnhandledException в WPF и продолжать работу?
- Для непредвиденных исключений, вызванных программной ошибкой, это опасно. Установка Handled=true позволяет продолжить работу внешне, но легко создаёт состояние зомби: экран остаётся, а worker уже мёртв, кнопка остаётся активной, но неизвестно, сохранились ли данные. Событие необработанного исключения стоит использовать не как средство восстановления, а как точку входа для записи: записав финальный маркер сбоя, приложение должно завершиться, а не продолжать работу, а основные свидетельства должны оставаться в дампе WER и в обычном логе вплоть до момента сбоя. Для завершения стоит рассмотреть API немедленного завершения вроде Environment.FailFast.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки