Helixoft Blog

Peter Macej - lead developer of VSdocman - talks about Visual Studio tips and automation

The MSBuild task for executing any Visual Studio command

As you know, almost every operation in Visual Studio is implemented as a named command. Visual Studio provides its built-in commands and extensions can add their own. For example, our VSdocman provides commands for compiling a documentation. You can invoke the commands from Command Window. Just start typing and it will offer you a list of available commands. You can optionally supply arguments for the command. When you press Enter, the command will be executed. For example, if you type Help.About, the About window will be displayed. Similarly, if you type VSdocman.CompileProject "SampleClassLibrary", it will compile a documentation for the SampleClassLibrary project.

While you can run any batch command in your build events, there's no way to execute a Visual Studio command during the build (at least, I'm not aware of any). So I created a reusable MSBuild task that you can use for executing any named VS command, when the build is performed inside Visual Studio.

There's no need to create a separate project or compile anything because it's implemented as a simple inline task. You just create a new msbuild .targets file named e.g. ExecuteVsCommand.targets. Copy the task definition into it, or simply download the ready made file from here.

ExecuteVsCommand.targets:

<?xml version="1.0" encoding="utf-8" ?>
<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <!--
  =========================================================================
 
  If the msbuild is executed from Visual Studio, execute specified named
  Visual Studio command.
  =========================================================================
  -->
 
  <!--Execute Visual Studio command-->
  <UsingTask TaskName="ExecuteVsCommand" Condition="'$(BuildingInsideVisualStudio)' == 'true'" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
      <CommandName ParameterType="System.String" Required="true" />
      <CommandArguments ParameterType="System.String" Required="false" />
    </ParameterGroup>
    <Task>
      <Reference Include="System.Core" />
      <Reference Include="System.Management" />
      <Reference Include="envdte" />
      <Reference Include="Microsoft.Build.Framework" />
      <Using Namespace="System" />
      <Code Type="Class" Language="cs">
        <![CDATA[
using System;
using System.IO;
using System.Security;
using System.Collections;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System.Diagnostics;
using System.Management;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text.RegularExpressions;

public class ExecuteVsCommand : Microsoft.Build.Utilities.Task
{

    [Required]
    public string CommandName { get; set; }

    public string CommandArguments { get; set; }

    /// <summary>
    /// Execute is part of the Microsoft.Build.Framework.ITask interface.
    /// When it's called, any input parameters have already been set on the task's properties.
    /// It returns true or false to indicate success or failure.
    /// </summary>
    public override bool Execute()
    {
        try
        {
            Log.LogMessage(MessageImportance.Normal, "Executing Visual Studio command: " + CommandName + " " + CommandArguments);

            int devenvPid = GetParentDevenvProcessId(Process.GetCurrentProcess());

            // Register the IOleMessageFilter to handle any threading errors.
            // See https://msdn.microsoft.com/en-us/library/ms228772
            MessageFilter.Register();

            EnvDTE.DTE dte = GetDTE(devenvPid);
            if (dte == null)
            {
                Log.LogError("Error: Couldn't get the current Visual Studio automation object.");
            }

            if (CommandArguments == null)
            {
                Log.LogMessage(MessageImportance.Normal, "No command arguments supplied");
                dte.ExecuteCommand(CommandName);
            }
            else
            {
                dte.ExecuteCommand(CommandName, CommandArguments);
            }

            
            Log.LogMessage(MessageImportance.Normal, "The command was successfully executed.");
        }
        catch (Exception ex)
        {
            Log.LogError("Command " + CommandName + ". " + ex.Message);
        }
        finally
        {
            // turn off the IOleMessageFilter.
            MessageFilter.Revoke();
        }


        // Log.HasLoggedErrors is true if the task logged any errors -- even if they were logged
        // from a task's constructor or property setter. As long as this task is written to always log an error
        // when it fails, we can reliably return HasLoggedErrors.
        return !Log.HasLoggedErrors;
    }


    private int GetParentDevenvProcessId(Process proc)
    {
        try
        {
            using (ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT * FROM Win32_Process WHERE ProcessId = " + proc.Id))
            {
                foreach (ManagementObject obj in searcher.Get())
                {
                    int parentId = Convert.ToInt32((UInt32)obj["ParentProcessId"]);
                    if (Process.GetProcessById(parentId).ProcessName.ToLower().Contains("devenv") )
                    {
                        return parentId;
                    }
                }
            }
        }
        catch (Exception ex)
        {
        }
        return -1;
    }


    [DllImport("ole32.dll")]
    private static extern int CreateBindCtx(uint reserved, out IBindCtx ppbc);

    public static EnvDTE.DTE GetDTE(int processId)
    {
        string progId = "!VisualStudio.DTE.10.0:" + processId.ToString();
        object runningObject = null;

        IBindCtx bindCtx = null;
        IRunningObjectTable rot = null;
        IEnumMoniker enumMonikers = null;

        try
        {
            Marshal.ThrowExceptionForHR(CreateBindCtx(reserved: 0, ppbc: out bindCtx));
            bindCtx.GetRunningObjectTable(out rot);
            rot.EnumRunning(out enumMonikers);

            IMoniker[] moniker = new IMoniker[1];
            IntPtr numberFetched = IntPtr.Zero;
            while (enumMonikers.Next(1, moniker, numberFetched) == 0)
            {
                IMoniker runningObjectMoniker = moniker[0];

                string name = null;

                try
                {
                    if (runningObjectMoniker != null)
                    {
                        runningObjectMoniker.GetDisplayName(bindCtx, null, out name);
                    }
                }
                catch (UnauthorizedAccessException)
                {
                    // Do nothing, there is something in the ROT that we do not have access to.
                }

                Regex monikerRegex = new Regex(@"!VisualStudio.DTE\.\d+\.\d+\:" + processId, RegexOptions.IgnoreCase);
                if (!string.IsNullOrEmpty(name) && monikerRegex.IsMatch(name))
                {
                    Marshal.ThrowExceptionForHR(rot.GetObject(runningObjectMoniker, out runningObject));
                    break;
                }
            }
        }
        finally
        {
            if (enumMonikers != null)
            {
                Marshal.ReleaseComObject(enumMonikers);
            }

            if (rot != null)
            {
                Marshal.ReleaseComObject(rot);
            }

            if (bindCtx != null)
            {
                Marshal.ReleaseComObject(bindCtx);
            }
        }

        return runningObject as EnvDTE.DTE;
    }


    /// <summary>
    /// See https://msdn.microsoft.com/en-us/library/ms228772
    /// </summary>
    public class MessageFilter : IOleMessageFilter
    {
        //
        // Class containing the IOleMessageFilter
        // thread error-handling functions.

        // Start the filter.
        public static void Register()
        {
            IOleMessageFilter newFilter = new MessageFilter();
            IOleMessageFilter oldFilter = null;
            CoRegisterMessageFilter(newFilter, out oldFilter);
        }

        // Done with the filter, close it.
        public static void Revoke()
        {
            IOleMessageFilter oldFilter = null;
            CoRegisterMessageFilter(null, out oldFilter);
        }

        //
        // IOleMessageFilter functions.
        // Handle incoming thread requests.
        int IOleMessageFilter.HandleInComingCall(int dwCallType,
          System.IntPtr hTaskCaller, int dwTickCount, System.IntPtr
          lpInterfaceInfo)
        {
            //Return the flag SERVERCALL_ISHANDLED.
            return 0;
        }

        // Thread call was rejected, so try again.
        int IOleMessageFilter.RetryRejectedCall(System.IntPtr
          hTaskCallee, int dwTickCount, int dwRejectType)
        {
            if (dwRejectType == 2)
            // flag = SERVERCALL_RETRYLATER.
            {
                // Retry the thread call immediately if return >=0 &
                // <100.
                return 99;
            }
            // Too busy; cancel call.
            return -1;
        }

        int IOleMessageFilter.MessagePending(System.IntPtr hTaskCallee,
          int dwTickCount, int dwPendingType)
        {
            //Return the flag PENDINGMSG_WAITDEFPROCESS.
            return 2;
        }

        // Implement the IOleMessageFilter interface.
        [DllImport("Ole32.dll")]
        private static extern int
          CoRegisterMessageFilter(IOleMessageFilter newFilter, out
          IOleMessageFilter oldFilter);
    }

    [ComImport(), Guid("00000016-0000-0000-C000-000000000046"),
    InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
    interface IOleMessageFilter
    {
        [PreserveSig]
        int HandleInComingCall(
            int dwCallType,
            IntPtr hTaskCaller,
            int dwTickCount,
            IntPtr lpInterfaceInfo);

        [PreserveSig]
        int RetryRejectedCall(
            IntPtr hTaskCallee,
            int dwTickCount,
            int dwRejectType);

        [PreserveSig]
        int MessagePending(
            IntPtr hTaskCallee,
            int dwTickCount,
            int dwPendingType);
    }

}

]]>
      </Code>
    </Task>
  </UsingTask>


</Project>

Place the file in your VS project folder, where you have your .*proj file. Now you can import and use the task in your project. You probably want to run the task in the BeforeBuild or AfterBuild targets. To do so, you need to manually edit your .csproj, vbproj or other project file as there is no GUI option in Visual Studio:

  1. Right click on your project in Solution explorer and select Unload Project
  2. Right click on the project (tagged as unavailable in Solution explorer) and click “Edit yourproj.csproj”

Find the the BeforeBuild or AfterBuild targets and uncomment them, because they are most likely commented out. Then call the ExecuteVsCommand task in one of them. And, of course, don't forget to import the ExecuteVsCommand.targets file. You have to do it before the targets where you put the task. Here's an example from my sample csproj file:

  <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
       Other similar extension points exist, see Microsoft.Common.targets.
  <Target Name="BeforeBuild">
  </Target>
  -->
  <Import Project="ExecuteVsCommand.targets" />
  <Target Name="AfterBuild">
    <ExecuteVsCommand CommandName="VSdocman.CompileProject" CommandArguments="&quot;SampleClassLibrary&quot;" Condition="'$(BuildingInsideVisualStudio)' == 'true'"/>
    <Message Text="The VS command is skipped because the build is not executed from Visual Studio." Condition="'$(BuildingInsideVisualStudio)' != 'true'" />
  </Target>

As you can see, I execute the VSdocman.CompileProject VS command after the build. Notice one important thing - the Condition attribute. It checks whether the build is executed inside Visual Studio IDE, in which case the $(BuildingInsideVisualStudio) property is set to true. The task is executed only in such a case. When the build is executed from msbuild command line, only a message is shown.

The ExecuteVsCommand task takes two arguments. The CommandName is required and it specifies the name of the VS command to be executed. The CommandArguments is optional and it specifies the arguments for the command. You can omit it if the command takes no arguments (as most VS commands do).

That's all. A little explanation for the task code. The task is executed in a msbuild process started by a parent Visual Studio process (devenv). As the first thing we get the devenv process ID with GetParentDevenvProcessId() method. Then we get the VS automation object (DTE) from this ID with GetDTE() method. Once we have the DTE object, we can call the ExecuteCommand() on it which executes specified VS command. To avoid various troubles, we also need to implement an IOleMessageFilter during the use of the DTE object.

And why did I play with this? Our VSdocman provides the command line utility which can be called in the post-build event during a build. But this operation is quite expensive for a large solution, because it needs to be loaded into memory and then compiled. But running the VSdocman.CompileSolutionToSingle (or other commands for compiling the documentation) command would be more efficient because the solution is already loaded in the current VS instance. So I wanted to test if it's possible to execute a VS command during the build.

If you have an idea how executing a VS command during the build may be useful, let us know in the comments.

Add comment


Security code
Refresh

 

Start generating your .NET documentation now!
DOWNLOAD
Free, fully functional trial