⬆️ ⬇️

Writing Custom MSBuild Task for Deploy (WMI included)

Good day! One fine day, we discovered that our MSBuild deployment project does not want to work in a new environment: he used MSBuild.ExtensionPack to create and manage sites and pools. Failed due to unavailability of DCOM. The environment could not be changed, so by the way I had the opportunity to write my own tasks for MSBuild: msdn.microsoft.com/en-us/library/t9883dzc.aspx , it was decided to write my own, which would work through WMI (available on Wednesday) Who cares, what happened, please under the cat.



Why MSBuild and WMI



There are environments in which we have no power to open ports and configure them as we want. However, in this environment, everything was set up to run WMI across the entire network, so the decision to use WMI was the most painless.

MSBuild It was used to deploy a simple site from the very beginning, so it was chosen not to rewrite the entire deployment to Nant, but to use an existing script and replace only non-working tasks.



How to write custom tasks for MSBuild



We connect to our build project Microsoft.Build.Framework, Microsoft.Build.Tasks.v4.0 and Microsoft.Build.Utilities.v4.0. Now there are 2 alternatives:

1 - inherit from ITask interface and then redefine a bunch of methods and properties.

2 - inherit from the abstract class Task and override the Execute method only.

As you can guess, the second method was chosen.

HelloWorld for own task:

using System; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; namespace MyTasks { public class SimpleTask : Task { public override bool Execute() { Log.LogMessage("Hello Habrahabr"); return true; } } } 


The Execute method returns true if the task was successful, and false otherwise. Of the useful properties available in the Task class, it is worth noting the Log property, which allows maintaining user interaction.

Parameters are also passed easily; it is enough to define an open property in this class (with an open getter and setter):

 using System; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; namespace MyTasks { public class SimpleTask : Task { public string AppPoolName { get; set; } [Output] public bool Exists { get; set; } public override bool Execute() { Log.LogMessage("Hello Habrahabr"); return true; } } } 


For our task to return something, the [Output] attribute must be added to the property.

So we can say that the simplicity of writing was also a plus of this solution. I will not dwell on how to manage IIS with the help of WMI, just note that we use the WebAdministration namespace, which is installed along with the Windows component “IIS Management Scripts and Tools”.

Under the spoilers, there is a listing of the basic task in which the logic of connecting to WMI and basic parameters of the task are encapsulated, such as:

  1. Machine - the name of the remote machine or localhost
  2. UserName - the name of the user under which we will connect to WMI
  3. Password - user password to connect to WMI
  4. TaskAction - the name of the action itself (Create, Stop, Start, CheckExists)


BaseWMITask
 using Microsoft.Build.Utilities; using System; using System.Collections.Generic; using System.Linq; using System.Management; using System.Text; using System.Threading; namespace MSBuild.WMI { /// <summary> /// This class will be used as a base class for all WMI MSBuild tasks. /// Contains logic for basic WMI operations as well as some basic properties (connection information, actual task action). /// </summary> public abstract class BaseWMITask : Task { #region Private Fields private ManagementScope _scope; #endregion #region Public Properties (Task Parameters) /// <summary> /// IP or host name of remote machine or "localhost" /// If not set - treated as "localhost" /// </summary> public string Machine { get; set; } /// <summary> /// Username for connecting to remote machine /// </summary> public string UserName { get; set; } /// <summary> /// Password for connecting to remote machine /// </summary> public string Password { get; set; } /// <summary> /// Specific action to be executed (Start, Stop, etc.) /// </summary> public string TaskAction { get; set; } #endregion #region Protected Members /// <summary> /// Gets WMI ManagementScope object /// </summary> protected ManagementScope WMIScope { get { if (_scope != null) return _scope; var wmiScopePath = string.Format(@"\\{0}\root\WebAdministration", Machine); //we should pass user as HOST\\USER var wmiUserName = UserName; if (wmiUserName != null && !wmiUserName.Contains("\\")) wmiUserName = string.Concat(Machine, "\\", UserName); var wmiConnectionOptions = new ConnectionOptions() { Username = wmiUserName, Password = Password, Impersonation = ImpersonationLevel.Impersonate, Authentication = AuthenticationLevel.PacketPrivacy, EnablePrivileges = true }; //use current user if this is a local machine if (Helpers.IsLocalHost(Machine)) { wmiConnectionOptions.Username = null; wmiConnectionOptions.Password = null; } _scope = new ManagementScope(wmiScopePath, wmiConnectionOptions); _scope.Connect(); return _scope; } } /// <summary> /// Gets task action /// </summary> protected TaskAction Action { get { return (WMI.TaskAction)Enum.Parse(typeof(WMI.TaskAction), TaskAction, true); } } /// <summary> /// Gets ManagementObject by query /// </summary> /// <param name="queryString">String WQL query</param> /// <returns>ManagementObject or null if it was not found</returns> protected ManagementObject GetObjectByQuery(string queryString) { var query = new ObjectQuery(queryString); using (var mos = new ManagementObjectSearcher(WMIScope, query)) { return mos.Get().Cast<ManagementObject>().FirstOrDefault(); } } /// <summary> /// Wait till the condition returns True /// </summary> /// <param name="condition">Condition to be checked</param> protected void WaitTill(Func<bool> condition) { while (!condition()) { Thread.Sleep(250); } } #endregion } } 




AppPool
 using Microsoft.Build.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Management; using System.Text; using System.Threading; namespace MSBuild.WMI { /// <summary> /// This class is used for operations with IIS ApplicationPool. /// Possible actions: /// "CheckExists" - check if the pool with the name specified in "AppPoolName" exists, result is accessible through field "Exists" /// "Create" - create an application pool with the name specified in "AppPoolName" /// "Start" = starts Application Pool /// "Stop" - stops Application Pool /// </summary> public class AppPool : BaseWMITask { #region Public Properties /// <summary> /// Application pool name /// </summary> public string AppPoolName { get; set; } /// <summary> /// Used as outpur for CheckExists command - True, if application pool with the specified name exists /// </summary> [Output] public bool Exists { get; set; } #endregion #region Public Methods /// <summary> /// Executes the task /// </summary> /// <returns>True, is task has been executed successfully; False - otherwise</returns> public override bool Execute() { try { Log.LogMessage("AppPool task, action = {0}", Action); switch (Action) { case WMI.TaskAction.CheckExists: Exists = GetAppPool() != null; break; case WMI.TaskAction.Create: CreateAppPool(); break; case WMI.TaskAction.Start: StartAppPool(); break; case WMI.TaskAction.Stop: StopAppPool(); break; } } catch (Exception ex) { Log.LogErrorFromException(ex); return false; } //WMI tasks are execute asynchronously, wait to completing Thread.Sleep(1000); return true; } #endregion #region Private Methods /// <summary> /// Gets ApplicationPool with name AppPoolName /// </summary> /// <returns>ManagementObject representing ApplicationPool or null</returns> private ManagementObject GetAppPool() { return GetObjectByQuery(string.Format("select * from ApplicationPool where Name = '{0}'", AppPoolName)); } /// <summary> /// Creates ApplicationPool with name AppPoolName, Integrated pipeline mode and ApplicationPoolIdentity (default) /// Calling code (MSBuild script) must first call CheckExists, in this method there's no checks /// </summary> private void CreateAppPool() { var path = new ManagementPath(@"ApplicationPool"); var mgmtClass = new ManagementClass(WMIScope, path, null); //obtain in-parameters for the method var inParams = mgmtClass.GetMethodParameters("Create"); //add the input parameters. inParams["AutoStart"] = true; inParams["Name"] = AppPoolName; //execute the method and obtain the return values. mgmtClass.InvokeMethod("Create", inParams, null); //wait till pool is created WaitTill(() => GetAppPool() != null); var appPool = GetAppPool(); //set pipeline mode (default is Classic) appPool["ManagedPipelineMode"] = (int)ManagedPipelineMode.Integrated; appPool.Put(); } /// <summary> /// Starts Application Pool /// </summary> private void StartAppPool() { GetAppPool().InvokeMethod("Start", null); } /// <summary> /// Stops Application Pool /// </summary> private void StopAppPool() { GetAppPool().InvokeMethod("Stop", null); } #endregion } } 




Website
 using Microsoft.Build.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Management; using System.Text; using System.Threading; using System.Threading.Tasks; namespace MSBuild.WMI { /// <summary> /// /// </summary> public class WebSite : BaseWMITask { #region Public Properties /// <summary> /// Web Site name /// </summary> public string SiteName { get; set; } /// <summary> /// Web Site physical path (not a UNC path) /// </summary> public string PhysicalPath { get; set; } /// <summary> /// Port (it's better if it's custom) /// </summary> public string Port { get; set; } /// <summary> /// Name of the Application Pool that will be used for this Web Site /// </summary> public string AppPoolName { get; set; } [Output] public bool Exists { get; set; } #endregion #region Public Methods /// <summary> /// Executes the task /// </summary> /// <returns>True, is task has been executed successfully; False - otherwise</returns> public override bool Execute() { try { Log.LogMessage("WebSite task, action = {0}", Action); switch (Action) { case WMI.TaskAction.CheckExists: Exists = GetWebSite() != null; break; case WMI.TaskAction.Create: CreateWebSite(); break; case WMI.TaskAction.Start: StartWebSite(); break; case WMI.TaskAction.Stop: StopWebSite(); break; } } catch (Exception ex) { Log.LogErrorFromException(ex); return false; } //WMI tasks are execute asynchronously, wait to completing Thread.Sleep(1000); return true; } #endregion #region Private Methods /// <summary> /// Creates web site with the specified name and port. Bindings must be confgiured after manually. /// </summary> private void CreateWebSite() { var path = new ManagementPath(@"BindingElement"); var mgmtClass = new ManagementClass(WMIScope, path, null); var binding = mgmtClass.CreateInstance(); binding["BindingInformation"] = ":" + Port + ":"; binding["Protocol"] = "http"; path = new ManagementPath(@"Site"); mgmtClass = new ManagementClass(WMIScope, path, null); // Obtain in-parameters for the method var inParams = mgmtClass.GetMethodParameters("Create"); // Add the input parameters. inParams["Bindings"] = new ManagementBaseObject[] { binding }; inParams["Name"] = SiteName; inParams["PhysicalPath"] = PhysicalPath; inParams["ServerAutoStart"] = true; // Execute the method and obtain the return values. mgmtClass.InvokeMethod("Create", inParams, null); WaitTill(() => GetApp("/") != null); var rootApp = GetApp("/"); rootApp["ApplicationPool"] = AppPoolName; rootApp.Put(); } /// <summary> /// Gets Web Site by name /// </summary> /// <returns>ManagementObject representing Web Site or null</returns> private ManagementObject GetWebSite() { return GetObjectByQuery(string.Format("select * from Site where Name = '{0}'", SiteName)); } /// <summary> /// Get Virtual Application by path /// </summary> /// <param name="path">Path of virtual application (if path == "/" - gets root application)</param> /// <returns>ManagementObject representing Virtual Application or null</returns> private ManagementObject GetApp(string path) { return GetObjectByQuery(string.Format("select * from Application where SiteName = '{0}' and Path='{1}'", SiteName, path)); } /// <summary> /// Stop Web Site /// </summary> private void StopWebSite() { GetWebSite().InvokeMethod("Stop", null); } /// <summary> /// Start Web Site /// </summary> private void StartWebSite() { GetWebSite().InvokeMethod("Start", null); } #endregion } } 




Call your own tasks from the build script



Now it remains to learn how to call these tasks from the build script. To do this, first of all, tell MSBuild where our assembly lies and what tasks we will use from there:

 <UsingTask TaskName="MSBuild.WMI.AppPool" AssemblyFile="MSBuild.WMI\bin\Debug\MSBuild.WMI.dll"/> 


Now you can use the MSBuild.WMI.AppPool task in the same way as the most common MSBuild commands.

 <MSBuild.WMI.AppPool TaskAction="CheckExists" AppPoolName="$(AppPoolName)" Machine="$(Machine)" UserName="$(User)" Password="$(Password)"> <Output TaskParameter="Exists" PropertyName="AppPoolExists"/> </MSBuild.WMI.AppPool> 


Under the spoiler - an example of the deploy.proj file, which is able to create a pool and a site (if there are none), stop them before deployment, and then start again.

deploy.proj
 <?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="4.0" DefaultTargets="Deploy" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <!-- common variables --> <PropertyGroup> <Machine Condition="'$(AppPoolName)' == ''">localhost</Machine> <User Condition="'$(User)' == ''"></User> <Password Condition="'$(User)' == ''"></Password> <AppPoolName Condition="'$(AppPoolName)' == ''">TestAppPool</AppPoolName> <WebSiteName Condition="'$(WebSiteName)' == ''">TestSite</WebSiteName> <WebSitePort Condition="'$(WebSitePort)' == ''">8088</WebSitePort> <WebSitePhysicalPath Condition="'$(WebSitePhysicalPath)' == ''">D:\Inetpub\TestSite</WebSitePhysicalPath> <AppPoolExists>False</AppPoolExists> </PropertyGroup> <UsingTask TaskName="MSBuild.WMI.AppPool" AssemblyFile="MSBuild.WMI\bin\Debug\MSBuild.WMI.dll"/> <UsingTask TaskName="MSBuild.WMI.WebSite" AssemblyFile="MSBuild.WMI\bin\Debug\MSBuild.WMI.dll"/> <!-- set up variables --> <Target Name="_Setup"> <MSBuild.WMI.AppPool TaskAction="CheckExists" AppPoolName="$(AppPoolName)" Machine="$(Machine)" UserName="$(User)" Password="$(Password)"> <Output TaskParameter="Exists" PropertyName="AppPoolExists"/> </MSBuild.WMI.AppPool> <MSBuild.WMI.WebSite TaskAction="CheckExists" SiteName="$(WebSiteName)" Machine="$(Machine)" UserName="$(User)" Password="$(Password)"> <Output TaskParameter="Exists" PropertyName="WebSiteExists"/> </MSBuild.WMI.WebSite> </Target> <!-- stop web site --> <Target Name="_StopSite"> <MSBuild.WMI.WebSite TaskAction="Stop" SiteName="$(WebSiteName)" Machine="$(Machine)" UserName="$(User)" Password="$(Password)" Condition="'$(WebSiteExists)'=='True'" /> <MSBuild.WMI.AppPool TaskAction="Stop" AppPoolName="$(AppPoolName)" Machine="$(Machine)" UserName="$(User)" Password="$(Password)" Condition="'$(AppPoolExists)'=='True'" /> </Target> <!-- stop and deploy web site --> <Target Name="_StopAndDeployWebSite"> <!-- stop (if it exists) --> <CallTarget Targets="_StopSite" /> <!-- create AppPool (if does not exist) --> <MSBuild.WMI.AppPool TaskAction="Create" AppPoolName="$(AppPoolName)" Machine="$(Machine)" UserName="$(User)" Password="$(Password)" Condition="'$(AppPoolExists)'=='False'" /> <!-- create web site (if does not exist)--> <MSBuild.WMI.WebSite TaskAction="Create" SiteName="$(WebSiteName)" Port="$(WebSitePort)" AppPoolName="$(AppPoolName)" Machine="$(Machine)" UserName="$(User)" Password="$(Password)" PhysicalPath="$(WebSitePhysicalPath)" Condition="'$(WebSiteExists)'=='False'" /> </Target> <!-- start all application parts --> <Target Name="_StartAll"> <MSBuild.WMI.AppPool TaskAction="Start" AppPoolName="$(AppPoolName)" Machine="$(Machine)" UserName="$(User)" Password="$(Password)" /> <MSBuild.WMI.WebSite TaskAction="Start" SiteName="$(WebSiteName)" Machine="$(Machine)" UserName="$(User)" Password="$(Password)" /> </Target> <!-- deployment implementation --> <Target Name="_DeployAll"> <CallTarget Targets="_StopAndDeployWebSite" /> <CallTarget Targets="_StartAll" /> </Target> <!-- deploy application --> <Target Name="Deploy" DependsOnTargets="_Setup"> <CallTarget Targets="_DeployAll" /> </Target> <!-- stop application --> <Target Name="StopApplication" DependsOnTargets="_Setup"> <CallTarget Targets="_StopWebSite" /> </Target> <!-- start application --> <Target Name="StartApplication" DependsOnTargets="_Setup"> <CallTarget Targets="_StartAll" /> </Target> </Project> 




To call deployment, simply transfer this msbuild.exe file:

 "C:\Program Files (x86)\MSBuild\12.0\Bin\msbuild.exe" deploy.proj 




Conclusions and references



You can say that writing your tasks and slipping them into MSBuild is not at all difficult. The range of actions that can perform such tasks is also very wide and allows you to use MSBuild for even the most trivial deployment operations, without requiring anything other than msbuild.exe. This project is laid out on the githaba with the example of the build file: github.com/StanislavUshakov/MSBuild.WMI You can expand and add new tasks!


')

Source: https://habr.com/ru/post/261603/



All Articles