История об одном исключении, или как нам приходится отлаживать чужой код




Использование сторонних библиотек позволяет получить необходимую функциональность, не тратя время на разработку соответствующей логики. Бери и пользуйся! Но естественно, что одними только достоинствами дело не ограничивается, так что у такого подхода есть и другая, "тёмная", сторона. Одна из проблем, связанных с использованием сторонних библиотек - отсутствие контроля над вещами, которые происходит внутри. Всё началось с того, что пользователь отписал об необработанном исключении, возникавшем при проверке C# проекта...

Перед тем, как перейти к разбору полётов, нужно хотя бы приблизительно понять, как взаимодействуют PVS-Studio, Roslyn и MSBuild. Если вкратце - для открытия C++ и C# проектов PVS-Studio использует библиотеки MSBuild, для C# проектов дополнительно используются библиотеки Roslyn (который, в свою очередь, тоже использует MSBuild). Если вам захотелось больше деталей о взаимодействии этих компонентов, найти их вы сможете в статье "Поддержка Visual Studio 2017 и Roslyn 2.0 в PVS-Studio: иногда использовать готовые решения не так просто, как кажется на первый взгляд".

Как я уже говорил, началось всё с падения со следующим сообщением:

Unhandled Exception: System.IO.FileNotFoundException: Could not load 
file or assembly 'Microsoft.Build.Framework, Version=15.1.0.0, 
Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its 
dependencies. The system cannot find the file specified. ---> 
System.IO.FileNotFoundException: Could not load file or assembly
'Microsoft.Build.Framework, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The 
system cannot find the file specified.

Самое интересное здесь - нигде в сообщении об ошибке не фигурирует упоминание PVS-Studio, что сразу навело на подозрения о том, что мы напрямую с падением не связаны. Это хорошо. Но в результате при проверке проекта PVS-Studio всё же падает, а это плохо. Причём исключение 'прилетало' так, что перехватить и адекватно обработать его не удавалось. Значит, нужно разбираться и 'копать вглубь', т.е. настраивать отладочные проекты, чтобы исследовать исходники библиотек Roslyn и MSBuild.

Из того, что было понятно сразу - исключение 'прилетает' откуда-то изнутри Roslyn при попытке открытия проекта, но наличие упоминания Antlr4.Build.Tasks сразу дало понять, что проблема более специфичная.

Постепенно, исследуя код и пробираясь в дебри сторонних зависимостей, с которыми связан PVS-Studio, вырисовалась такая цепочка вызовов:

PVS-Studio -> Roslyn -> MSBuild -> Antlr4

Следует пояснить, каким образом сюда затесались зависимости от Antlr4, если PVS-Studio их не использует. В проектном файле, при проверке которого генерировалось исключение, использовался генератор кода Antlr4, представляющий собой пользовательский task MSBuild'a. Такой task можно импортировать в любой стандартный MSBuild проект, и вызывать его в качестве отдельного сборочного шага. Таким образом, для открытия проекта, помимо Roslyn и MSBuild, мы оказываемся завязанными и на подобные сторонние компоненты, необходимые для сборки. А так как Roslyn для открытия проекта, помимо его эвалюации, запускает различные предсборочные шаги, код из Antlr4 также начинает выполняться в рамках процесса нашего анализатора.

Код task'а, вызывающего исключение, доступен в открытом виде на GitHub'е. Это позволило пересобрать .dll-ку task'a с отладочными символами и посмотреть, что же происходит внутри.

Самым интересным (и полезным для нас) из того, что было внутри, оказался способ запуска стороннего процесса - он происходил в другом application domain'e. При инстанциации типа, содержащего метод для обработки stdout'а упомянутого запускаемого процесса, task'у потребовалась библиотека Microsoft.Build.Framework.dll. Здесь начинается наиболее интересная часть. Данная библиотека на момент падения уже загружена процессом нашего анализатора - она нужна как нам для парсинга проектных файлов MSBuild, так и Roslyn'у. Однако, как вы помните, мы находимся сейчас в другом AppDomain'е - а в нём эта библиотека ещё не загружена. И именно при попытке её загрузки происходит падение.

У внимательного читателя сразу возникнет вопрос - почему мы не можем загрузить библиотеку сейчас, если в основном AppDomain'е с её загрузкой всё было хорошо? Ответ в том, как именно этот AppDomain был создан - в качестве базовой директории в его конструктор при создании была передана директория, в которой лежит dll'ка task'а Antlr4, а не директория, в которой лежит исполняемый модуль PVS-Studio_Cmd.exe. Это, в свою очередь привело к изменению поведения подсистемы Fusion, которая отвечает за поиск и загрузку зависимых .NET сборок. Fusion ищет сборку в директории исполняемого файла (обычно это exe файл) и в Global Assembly Cache. Создание же AppDomain'а с другой базовой директорией привело к тому, что Fusion стал искать dll файл уже в другой директории, а не в директории исполняемого файла PVS-Studio_Cmd.exe. Да, стоит отметить, что PVS-Studio в своём дистрибутиве несёт значительное число MSBuild библиотек (в том числе и ту, о которой сейчас идёт речь) - о причине, почему так получилось, также можно почитать в статье о поддержке Visual Studio 2017, ссылка на которую была приведена выше. При этом, начиная с MSBuild версии 15, его библиотеки не регистрируются в GAC'е. Две эти причины (отсутствие библиотеки в GAC и изменение базовой директории AppDomain'а, в которой Fusion ищет зависимости) и привели к падению программы.

Так как исключение возникало в другом AppDomain'e, перехватить и обработать его в application domain'e анализатора нельзя. Точнее, можно, но в результате приложение всё равно 'упадёт'. Максимум, что мы может сделать - провести какие-то действия для более 'мягкого' падения или, например, сообщить об источнике проблемы (что мы и сделали).

Кстати, такая же ошибка возникала в среде разработки Microsoft Visual Studio 2017 при попытке собрать этот проект. Скорее всего, она давно существовала в task'е Antlr4, но оставалась незамеченной, т.к. предыдущие версии MSBuild регистрировали свои зависимости в GAC, и Fusion находил их там. На данный момент ошибка уже исправлена в репозитории Antlr4. Знаете, какое решение приняли разработчики task'а Antlr4 для исправления данной проблемы? Не плодить AppDomain'ы, и провести все необходимые операции в одном. Зачем было переусложнять логику изначально - вопрос открытый.

Иронично вышло. Технология AppDomain'ов, создававшаяся с целью повышения отказоустойчивости приложений за счёт создания изолированных областей исполнения, в итоге привела к невозможности хоть как-то уберечься в данной ситуации. Понятно, что подобная ситуация могла бы возникнуть и при возникновении необработанного исключения в потоке, созданном в сторонней библиотеке, даже когда этот поток создаётся в рамках основного domain'а.

Так или иначе, отсюда можно извлечь несколько уроков:

  • Не нужно изобретать велосипеды. Чем сложнее код, тем больше вероятность допустить в нём ошибку.
  • Готовьтесь заплатить определённую цену за использование сторонних библиотек. Порой она может быть достаточно велика.
  • Даже если проблема не в вашем коде, а в какой-то из зависимостей, проблема автоматически станет вашей головной болью, когда проявит себя.
  • Мир не идеален - к сожалению, есть вещи, которые вне нашего контроля. Например, исключения, 'прилетающие' из других доменов приложений или потоков.


Найдите ошибки в своем C, C++, C# и Java коде

Предлагаем попробовать проверить код вашего проекта с помощью анализатора кода PVS-Studio. Одна найденная в нём ошибка скажет вам о пользе методологии статического анализа кода больше, чем десяток статей.

goto PVS-Studio;


Найденные ошибки

Проверено проектов
361
Собрано ошибок
13 417

А ты совершаешь ошибки в коде?

Проверь с помощью
PVS-Studio

Статический анализ
кода для C, C++, C#
и Java

goto PVS-Studio;