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

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

Перед тем, как перейти к разбору полётов, нужно хотя бы приблизительно понять, как взаимодействуют PVS\-Studio, Roslyn и MSBuild\. Если вкратце \- для открытия C\+\+ и C\# проектов PVS\-Studio использует библиотеки MSBuild, для C\# проектов дополнительно используются библиотеки Roslyn \(который, в свою очередь, тоже использует MSBuild\)\. Если вам захотелось больше деталей о взаимодействии этих компонентов, найти их вы сможете в статье "[Поддержка Visual Studio 2017 и Roslyn 2\.0 в PVS\-Studio: иногда использовать готовые решения не так просто, как кажется на первый взгляд](https://pvs-studio.ru/ru/blog/posts/csharp/0503/)"\.

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

```cpp
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, вырисовалась такая цепочка вызовов:

```cpp
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](https://docs.microsoft.com/en-us/dotnet/framework/app-domains/application-domains)\. При инстанциации типа, содержащего метод для обработки 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'а\.

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

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