Очередной «смешной случай из жизни» заставил немного понервничать.
Сопровождаемая нами программа имеет два окна: основное графическое и вспомогательное консольное. Свернутое при запуске консольное окно, как правило, нужно только при крахе системы, в нем выдаются стандартные сообщения об ошибках, и можно даже вызвать интерактивный отладчик, с помощью которого попытаться определить причину падения.
Однако при эксплуатации получилось не очень удобно. Дело в том, что если при крахе приложение снять, то, естественно, графическое окно исчезает, и пользователю, по крайней мере, сразу видно, что вся система рухнула. Правда и информация о крахе системы так же исчезает с одновременным закрытием и консольного окна. А вот если управление перехватывает отладчик в консольном (свернутом по умолчанию) окне, то графическое изображение на экране «замораживается» и при беглом взгляде может показаться, что все еще продолжает работать. Бывали случаи введения пользователя такой картиной в заблуждение. В конце концов, был найден компромисс: при исключении программа все-таки снимается, но вся информация, выдаваемая в консольном окне, дублируется еще и в файл протокола.
В нашей системе программирования так легко сделать, поскольку в системной библиотеке перед каждой выдачей очередного байта стандартного сообщения в стандартную консоль, идет проверка: а есть ли подпрограмма пользователя, которую нужно каждый раз вызывать перед очередной выдачей байта? Если есть – она вызывается с единственным параметром, этим самым байтом.
Поэтому достаточно лишь написать маленькую подпрограмму, выдающую свой входной параметр-байт в лог, как все сообщения об ошибках, идущие в консоль, будут и автоматически дублироваться в лог.
Таким образом, и пользователь не обманется неактуальной картинкой, и мы получим следы краха системы в своем единственном логе.
Конечно, в принципе могут быть исключения, например, из-за «разрушения» памяти, когда подобные действия «после смерти» программы станут невозможны или дадут неожиданные эффекты. Но на практике, после большинства исключений система все еще достаточно «живуча», чтобы суметь просто дописать лог.
Некоторое время такой подход вполне себя оправдывал, как вдруг при очередной ошибке вместо записи в протокол выдается сообщение уже самой Windows о фатальной ошибке с предложением отправить рапорт в Микрософт. Это значит, что исключение вообще не удалось перехватить! Кляня на чем свет и проклятую Windows и ее проклятых авторов-индусов («ну кто так строит?»), пытаемся понять, что произошло. Но оказывается, что Windows ни при чем! А виновата непредвиденная рекурсия.
Первичная ошибка возникла при выводе выражения в протокол, т.е. возникло исключение при вычислении этого выражения. Это к вопросу о том, что вообще-то нехорошо писать в лог выражения, лучше вычислить их все заранее и присвоить переменным. Тогда, по крайней мере, никогда не произойдет подобных исключений в момент самого формирования лога. В данном же случае получилось, что информация об этом исключении так же должна быть дописана и в тот же самый протокол. Т.е. получился вывод информации в файл протокола, так сказать, прямо посреди другого вывода в этот же самый протокол. Но такой случай тоже предусмотрен в системной библиотеке и, в свою очередь, вызывает исключение «рекурсивное обращение к файлу».
Увы, сообщение и о таком исключении из-за рекурсии вывода тоже попыталось занестись и в тот же самый файл.
Ну, а далее начался «рекурсивный спуск» – каждая попытка записать исключение «рекурсивное обращение к файлу» вызывала очередное такое же исключение потому, что опять пыталось записаться в лог в момент другой записи в лог и т.д. до тех пор, пока стек физически не исчерпался, и Windows не сняла задачу с сообщением о фатальной ошибке.
Мораль проста: ошибки бывают разные, в том числе даже смешные, как эта. А, главное, рекурсия, это не только красивый пример вычисления факториала в учебниках, но и иногда следствие непродуманных действий, которое может проявиться самым неожиданным образом. Ведь решение писать информацию об исключении в файл казалось простым, удачным, и ни в коем случае не рекурсивным. А реальность же оказалась иной.
SpiderEkb
Несколько странный подход к логированию. Мне казалось что лог должен идти отдельным процессом, а источник лога просто кидает туда данные по любому возможному каналу связи.
Ну и про исключения — средствами языка перехватываются только языковые исключения. Системные в иногда случаем ловятся через SIGNAL, но там сложнее с информацией что произошло.
Не помню уже есть ли в винде что-то типа #pragma exception_handler который позволяет устанавливать обработчик именно системных исключений с информацией о том что случилось.