[Из песочницы] Событийная модель построения проектов и решений Visual Studio для разработчиков

Эта небольшая статья поможет: Ознакомиться с событийной моделью построения проектов и решений MS Visual Studio; Понять, как получить поддержку Command-Line режима devenv.exe для VSPackage (где он изначально не предусмотрен); Понять, как эмулировать схожую модель событий от MSBuild Tools и транслировать на главный плагин; Узнать, как работать по приоритетной подписке; Узнать варианты получения контекста построения при обработке событий Visual Studio / MSBuild Tools; Узнать об оценке MSBuild Property & MSBuild Property Functions; Получить общие сведения межмодульного взаимодействия на слое абстракции для разнородных компонентов системы. СинопсисМне довольно часто приходиться заниматься автоматизацией тех или иных процессов, поэтому не мудрено, что часть решений рано или поздно коснулись и Visual Studio.На самом деле, эта статья, или даже заметка — результат рабочего и уже давно написанного плагина, который еще года 2 назад являлся лишь побочным продуктом при работе над одним проектом на C++. Однако мой дебют на Хабрахабре будет, пожалуй, с этого.Недавно ко мне обратился со схожими потребностями (те, которые изначально был призван решать плагин) парень из DevDiv. В попытках решить его проблему весьма кастомной автоматизации стало понятным, что все-таки крайне необходимо осветить некоторые аспекты решения и взаимодействия между VS & MSBuild. Ведь этого материала изначально не было и вроде бы до сих пор нет в публичном доступе.

О проблемах Изначальная потребность, для нас обоих, была и есть работа с Solution-Context в рамках событийной модели студии.Те, кто работал с Visual Studio, конечно же, должны знать о Pre/Post-build событиях уровня проектов. Однако, они не в полной мере удовлетворяют потребности управления проектами для всего решения, в особенности, когда этих проектов несколько десятков и т.п., что типично прежде всего для C++ проектов.Есть несколько способов как решить подобную задачу. К примеру, выше упомянутый, сейчас осуществляет переход от решения, когда в Solution есть проект, от которого зависят все остальные.

Так почему MS до сих пор не выделит подобный контекст для своей IDE? На самом деле, все тоже самое, что и с форматом .sln, который до сих пор представляет собой записки охотника, нежели валидный xml документ с файлами проектов, или что-то более гибкое и изящное.Совместимость? Вероятно, однако ломать это надо было еще с приходом мажорных изменений в VS2010.Так и с solution-context. Просто так поднять события на верхний уровень не выйдет, т.к. понадобится решить задачу по управлению msbuild данными и многое другое для одного единого контекста, а дописывать это в свой загруженный таймлайн особо никто и не спешит.

Обработка событий проектов и решений MS Visual Studio (VS2010, VS2012, VS2013, VS2015) Для начала давайте рассмотрим вариант с EnvDTE. Для нашей задачи взор прежде всего устремляется на BuildEvents, который предоставляет доступную подписку на базовые события, такие как OnBuildBegin, OnBuildProjConfigBegin и тому подобное. То есть они и реализованы в виде публичных events.Однако нет, в нашем случае они сработают слишком поздно, когда пойдет оповещение всех своих слушателей DTE. Нам же необходимо получить управление над ситуацией так скоро, насколько это возможно в нашем пакете.

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

В общем виде — это такая же классика observer’а, реализованная посредством Advise методов для конкретных сервисов. Но обо всем по порядку.

Прежде всего нам необходимо обратить свое внимание на Microsoft.VisualStudio.Shell.Interop. Именно он поможет работать с базовыми «build-событиями» VS и прочими уровня solution, а именно — IVsUpdateSolutionEvents:

Для работы с вышеупомянутыми потребностями достаточно рассмотреть только базовый IVsUpdateSolutionEvents2, который доступен вплоть до VS2010. На самом деле желателен IVsUpdateSolutionEvents4 для работы с контекстом построения, однако он доступен только для VS2012 и старше.Примечание: Для полноценной работы также скорее всего потребуется IVsSolutionEvents, но этот момент не рассматривается этой статьей. Если все-таки потребуется кому-либо, могу показать базовые приемы для обслуживания этого слоя.

Итак, нам необходимо имплементировать следующие базовые вещи интерфейса IVsUpdateSolutionEvents2:

// должны получить управление до того как начнется любой build-action // и этой последний шанс отменить построение int UpdateSolution_Begin (ref int pfCancelUpdate); // вызывается для любого отмененного построения т.е. Cancel / Abort — пользователем или самой студией int UpdateSolution_Cancel (); // вызывается при любой окончательной остановке построения. // Обратите внимание, что вызов будет как завершение для нормального построения, так и после Cancel/Abort, т.е.: // * Begin → Done // * Begin → Cancel → Done int UpdateSolution_Done (int fSucceeded, int fModified, int fCancelCommand); Все эти методы прежде всего и подходят для реализации уровня solution-context.По проектам у нас предусмотрено: int UpdateProjectCfg_Begin (IVsHierarchy pHierProj, IVsCfg pCfgProj, IVsCfg pCfgSln, uint dwAction, ref int pfCancel); int UpdateProjectCfg_Done (IVsHierarchy pHierProj, IVsCfg pCfgProj, IVsCfg pCfgSln, uint dwAction, int fSuccess, int fCancel); Которые будут вызываться для каждого, конфигурация которого должна пойти на построение. Обратите внимание на pHierProj аргумент, это гибкий способ ссылаться в любое место откуда пришло управление.Базовая имплементация вашим пакетом не означает, что VS будет готова работать с IVsUpdateSolutionEvents. Как и было сказано выше, нам необходимо зарегистрировать нашего слушателя (кто имплементирует этот интерфейс):

public sealed class vsSolutionBuildEventPackage: Package, IVsSolutionEvents, IVsUpdateSolutionEvents2 { … public int UpdateSolution_Begin (ref int pfCancelUpdate) { return VSConstants.S_OK; } … } Сделать это необходимо с Advise методами, так для IVsUpdateSolutionEvents2 предусмотрен — AdviseUpdateSolutionEvents.Пример регистрации:

///

/// For IVsUpdateSolutionEvents2 events /// http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.shell.interop.ivssolutionbuildmanager2.aspx /// private IVsSolutionBuildManager2 spSolutionBM;

///

/// Contains the cookie for advising IVsSolutionBuildManager2 / IVsSolutionBuildManager /// http://msdn.microsoft.com/en-us/library/bb141335.aspx /// private uint _pdwCookieSolutionBM; … spSolutionBM = (IVsSolutionBuildManager2)ServiceProvider.GlobalProvider.GetService (typeof (SVsSolutionBuildManager)); spSolutionBM.AdviseUpdateSolutionEvents (this, out _pdwCookieSolutionBM); Следует заметить, что получить доступ к сервису SVsSolutionBuildManager можно и другими путями. Используйте то, что удобно.spSolutionBM, в примере выше, должен быть частью класса для протекции от GC.

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

Поддержка Command-Line режима Немудрено, что у кого-то возникла потребность работать с devenv.exe (точнее devenv.com, т.к. именно он занимается обработкой в консольном режиме) с нашим плагином. Однако VSPackages не имеет возможностей работать хоть как-то в таком режиме! Соответственно плагин просто останется неактивным, если мы попробуем построить решение в консоли как devenv «D:\App1\App1.sln» /Rebuild Debug Мне этот вопрос в первые был задан в Q/A галерее и, честно говоря, у меня не возникало ранее таких потребностей или даже желаний (т.к. можно работать с msbuild.exe и, кроме того, сервера автоматизации все равно будут предоставлять у себя ограниченную среду без всего такого).Однако, как мы видим, потребности у каждого из нас разные, тем более если это часть VS, то непорядок, надо поддержать.

Позднее в рамках поддержки CI серверов я занялся подобным вопросом с успешным его решением. То есть всю событийную модель проектов и решения мы все-таки можем обрабатывать в VSPackage! Кто бы что не утверждал:

«C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\devenv» «D:\App1.sln» /Rebuild Debug «C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\devenv» «D:\App1.sln» verbosity: diagnostic /Build Release [?]Однако…Решение предполагает использование Add-Ins обертки для трансляции событий; Именно Add-Ins предоставляют command-line режим, точнее VS готова работать с ним в этом режиме; Они же имеют точно такую же модель, поэтому нам нет ничего проще просто имплементировать старый добрый IVsUpdateSolutionEvents2 и произвести регистрацию как и для VSPackage в точке: void OnConnection (object application, ext_ConnectMode connectMode, object addInInst, ref Array custom); И произвести перенаправление на главную библиотеку, т.е., например: … public int UpdateSolution_Begin (ref int pfCancelUpdate) { return library.Event.onPre (ref pfCancelUpdate); }

public int UpdateProjectCfg_Begin (IVsHierarchy pHierProj, IVsCfg pCfgProj, IVsCfg pCfgSln, uint dwAction, ref int pfCancel) { return library.Event.onProjectPre (pHierProj, pCfgProj, pCfgSln, dwAction, ref pfCancel); } … Но, как всем давно известно — the Add-ins are deprecated in Visual Studio 2013. То есть такой трюк будет работать для VS2010, VS2012 и VS2013. Для планируемой VS2015 такие игры не пройдут.Об этом я уже писал на MS Connect Issue #1075033, однако можете попрощаться для тех, кому это было важно. VS2015 уже в RC, а задачка simply closed.

Идем дальше.

Эмуляция схожей модели событий от MSBuild tools Начнем с того, что MSBuild tools — безусловно более мощный инструмент и выше указанные проблемы никто и знать не должен. Мы можем спокойно обращаться с $(Configuration) & $(Platform) уровня solution, можем работать с targets и т.п Однако решение должно быть единым и мы не должны замечать разницу работы между VS IDE и построениями на CI/Special Build Servers. Поэтому рассмотрим возможность работать с вышеуказанными событиями в рамках msbuild tools.Нам недоступен ни DTE2-context, ни сервисы для регистрации событий, ничего такого, чтобы можно было общаться с VS, ну, а чего вы еще ждали. Да, конечно, мы можем получить, например, тот же DTE2-context c GetActiveObject, который в свою очередь использует:

HRESULT GetActiveObject ( _In_ REFCLSID rclsid, _Reserved_ void *pvReserved, _Out_ IUnknown **ppunk ); [?]Т.е. например так: (EnvDTE80.DTE2)System.Runtime.InteropServices.Marshal.GetActiveObject («VisualStudio.DTE.10.0»); Но это все будет работать только при наличии запущенного экземпляра IDE студии, что не представляет возможным для ограниченных сред, например, для CI.Поэтому предлагаю получить управление msbuild.exe посредством регистрации разрабатываемого транслятора как логгера. Для этого нам необходимо работать с Microsoft.Build.Framework.

Действительно, базовый IEventSource способен обеспечить все наши основные потребности:

public class EventManager: Logger { … public override void Initialize (IEventSource evt) { … } } Однако и тут есть ряд особенностей, которые необходимо знать.IEventSource.BuildStarted не подойдет, т.к. он срабатывает слишком рано, точнее, для того чтобы получить данные проекта, который должен будет обрабатываться, нам придется подождать. В VS — контекст входа обрабатывается с IVsSolutionEvents; Поэтому для PRE событий представляется возможным использовать только IEventSource.ProjectStarted, а он же в свою очередь должен использоваться для событий уровня проектов. Дело в том, что обработчик — ProjectStartedEventHandler рассылает также ProjectStartedEventArgs в качестве аргумента и .sln файл для работы с ним мы можем отследить, например, так: evt.ProjectStarted += new ProjectStartedEventHandler ((object sender, ProjectStartedEventArgs e){ e.ProjectFile; // should be .sln! … }); Т.е. вариант может быть с ожиданием пока не придет .sln на обработку, а он в свою очередь должен быть до прохождения файлов проектов.Мучения на этом только начинаются, т.к. для работы вам скорее всего понадобится работать с данными .sln, включая загрузку проектов для получения доступа к msbuild движку где-либо еще — этому может быть пример evaluation св-во msbuild и т.п.

Для работы с .sln файлами, увы, ничего нормального вы не увидите, точнее вот варианты, выбирайте на любой вкус:

Либо использовать устаревший т.е. он помечен как obsolete Microsoft.Build.BuildEngine.Project. При этому учитываете, что если вы не используете Microsoft.Build.BuildEngine, то это доп. ненужный reference для вашего проекта. Либо использовать рефлексию на internal методы — Microsoft.Build.BuildEngine.Shared, например:→ void ParseProject (string firstLine)→ void ParseFirstProjectLine (string firstLine, ProjectInSolution proj)→ crackProjectLine → PROJECTNAME + RELATIVEPATH Либо написать собственный небольшой парсер по примеру того же ParseProject. Выбор небогатый, однако как и говорилось выше, .sln в непонятном виде уже достаточно давно…Идем дальше. В проекте может понадобиться оценка свойств и функций msbuild (MSBuild Property Functions) и т.п., ну, а как же без них…

Если вам потребуется подобное, то для того, чтобы с этим работать, необходимо подготовить msbuild движок, а его необходимо сначала инициировать. Так как мы работаем с изолированной средой, необходимо также от msbuild.exe передать свойства, с которым он был иницирован.

Если вы используете .NET 4.0, вам придется это сделать вручную, т.е. вам необходимо определить Configuration, Platform, SolutionDir и т.п. для обработчика. А вот для платформы .NET 4.5 доступен ProjectStartedEventArgs.GlobalProperties.

После того, как изолированная среда полностью проиниализирована, мы, наконец, можем обрабатывать события проектов. Однако мы работаем с msbuild и мы работаем с targets. Поэтому вам необходимо подписаться на TargetStarted.

Для трансляции нам устроят PreBuildEvent и PostBuildEvent от полученного TargetName, например:

protected void onTargetStarted (object sender, TargetStartedEventArgs e) { switch (e.TargetName) { case «PreBuildEvent»: { … return; } } } Но и тут не все гладко. Дело в том, что мы не можем сослаться на проект, который сейчас обрабатывается с TargetStartedEventArgs, однако мы можем получить BuildEventContext, от которого можно получить ProjectInstanceId, а он в свою очередь существует и для ProjectStarted, т.е. мы можем просто запомнить ProjectId и далее ссылаться на него везде, где доступен BuildEventContext, например, просто: projects[e.ProjectId] = new Project () { Name = properties[«ProjectName»], File = e.ProjectFile, Properties = properties }; Собственно, теперь вы можете полностью транслировать событийную модель в VSPackage с изолированной средой и работать как есть: «C:\Program Files (x86)\MSBuild\12.0\bin\msbuild.exe» «app.sln» /l:»<логгер>.dll» /m:12 /t: Rebuild /p: Configuration= /p: Platform= Получения контекста построения На самом деле здесь есть ряд проблем или особенностей, которые хотелось тоже осветить. Но в целом это слишком специфично именно для нашего проекта, а для вашего случая может вообще ничего подобного и не понадобиться, поэтому можно пропустить.Так как наш плагин работает с событиями построения, не трудно догадаться, что может потребоваться и получение типа построения, с которым все это началось. Но для VS с этим не все так просто.

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

_buildEvents.OnBuildBegin += new _dispBuildEvents_OnBuildBeginEventHandler ((vsBuildScope Scope, vsBuildAction Action) => { buildType = (BuildType)Action; }); Потому что это слишком поздно.Вы можете попробовать работу с IVsUpdateSolutionEvents4, однако как я и написал в вопросе и ответе, только если вы не планируете совместимсоть со старыми версиями.

На тот момент выбор остановился на перехвате комманд от VS IDE, например:

_cmdEvents.BeforeExecute += new _dispCommandEvents_BeforeExecuteEventHandler ((string guid, int id, object customIn, object customOut, ref bool cancelDefault) => {

if (GuidList.VSStd97CmdID == guid || GuidList.VSStd2KCmdID == guid) { _c.updateContext ((BuildType)id); }

}); Не особо нравится, лучше использовать IVsUpdateSolutionEvents4, но единственный рабочий вариант для совместимости версий и сохранения приоритетной обработки, до чего смог додуматься на тот момент. Да и обрадовать другими решениями никто пока не спешит.Межмодульное взаимодействие Ну здесь на самом деле ничего примечательного не будет, все обыденно, однако в рамках статьи небольшой description.Как вы уже успели заметить, компоненты разносторонние и даже различные по своей модели обработки событий. На примере моего плагина это выглядит следующим образом:

imageЛист того, что там изображено, можно найти тут.

Основные моменты для ваших работ:

Выделяем общий публичный интерфейс на уровне проектов, для того, чтобы все необходимые компоненты могли предоставлять по нему реализацию. Так делает Shell.Interop, так делает EnvDTE и т.п.; Определяем, где будет располагаться главное ядро программы. Тот, кто и будет ответственнен за единую обработку данных; Определяем где-нибудь загрузчик библиотеки. Это может быть также как у нас внешний Provider и т.п.; Каждый из компонентов, соответсвенно, самостоятельно определяет, как ему подключаться к тому, где он будет использоваться, и в общем виде должен заниматься лишь пересылкой и трансляцией, если нужно, обрабатываемых данных. (полагаю, пример как работать с внешними lib приводить не нужно; вы можете найти либо в моих исходниках, либо вообще где угодно, это за рамками статьи). Тут есть важное замечание. Наш плагин изначально не предполагалось использовать в таком объеме, поэтому ядро системы располагается в VSPackage.Да, это удобно не разделять на отдельные компоненты, но чем это плохо? Дело в том, что моему VSPackage на схеме выше, приходиться тянуть за собой тяжелые зависимости на EnvDTE & EnvDTE80 (которые, в свою очередь, имеют еще более экзотические связи). Все это может, и скорее всего будет отсутствовать на серверах непрерывной интеграции, т.к. прежде всего поставляется стандартный msbuild tools, и другое подобное. То есть, не забывайте о легких самодостаточных компонентах и прозрачных интерфейсах. Я же пока не спешу с разделением, т.к. все это время, и оно пока того не стоит, он ведь и был изначально VSPackage решением, как-то так.

Доступ и оценка MSBuild Properties & MSBuild Property Functions Последний момент, который я хочу рассмотреть — это evaluation свойств. Вообще доступ к properties может происходить несколькими вариантами, например: А также другие.Наиболее гибкий вариант — использовать Microsoft.Build.Evaluation, т.к. здесь наиболее простой способ получить оценку средствами msbuild движка и работать с проектами, например:

public virtual string evaluate (string unevaluated, string projectName = null) { … lock (_lock) { try { … project.SetProperty (container, Scripts.Tokens.characters (_wrapProperty (ref unevaluated))); return project.GetProperty (container).EvaluatedValue; } finally { project.RemoveProperty (project.GetProperty (container)); } } } Однако, как уже можно было понять, для solution-context вам придется решать, как работать с данными от проектов. Я лично реализовал доп. расширение синтаксиса.То есть предварительно все-таки приходится производить разбор средствами своего анализатора, а далее направлять подготовленные данные, с которыми уже справится оригинальный движок msbuild.

Заключение Собственно, это первая заметка на Хабрахабр. Надеюсь, не последняя -_-.В материале рассмотрены ключевые моменты, чтобы понять, что нужно делать, какие варианты решений существуют, и с какими проблемами и особенностями придется столкнуться по работе с событийной моделью построений в VS, а также его связанных компонентах devenv & MSBuild tools, на примере реального рабочего решения.

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

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

Дополнительные ссылки по теме

© Habrahabr.ru