Taming the VSX beast from PowerShell
Using VSX from PowerShell is not always a pleasant experience. Most stuff in VSX is still good old COM the System.__ComObject types are flying around. Everything can be casted to everything (for example EnvDTE to EnvDTE2) if you are in C#, but PowerShell can’t make spaghetti of it.
Enter Power Console. In Power Console there are some neat tricks available to help you out of VSX trouble. And most of the trouble solving is done in… PowerShell. You just need to know what to do.
To get out of trouble do the following:
- Head over to the Power Console site
- Right-click on the Download button, and save the PowerConsole.vsix file to your disk
- Rename PowerConsole.vsix to PowerConsole.zip and unzip
- Look in the Scripts folder for the file Profile.ps1 which is full of PowerShell/VSX magic
The PowerShell functions that perform the VSX magic are:
- <#
- .SYNOPSIS
- Get an explict interface on an object so that you can invoke the interface members.
- .DESCRIPTION
- PowerShell object adapter does not provide explict interface members. For COM objects
- it only makes IDispatch members available.
- This function helps access interface members on an object through reflection. A new
- object is returned with the interface members as ScriptProperties and ScriptMethods.
- .EXAMPLE
- $dte2 = Get-Interface $dte ([EnvDTE80.DTE2])
- #>
- function Get-Interface
- {
- Param(
- $Object,
- [type]$InterfaceType
- )
- [Microsoft.VisualStudio.PowerConsole.Host.PowerShell.Implementation.PSTypeWrapper]::GetInterface($Object, $InterfaceType)
- }
- <#
- .SYNOPSIS
- Get a VS service.
- .EXAMPLE
- Get-VSService ([Microsoft.VisualStudio.Shell.Interop.SVsShell]) ([Microsoft.VisualStudio.Shell.Interop.IVsShell])
- #>
- function Get-VSService
- {
- Param(
- [type]$ServiceType,
- [type]$InterfaceType
- )
- $service = [Microsoft.VisualStudio.Shell.Package]::GetGlobalService($ServiceType)
- if ($service -and $InterfaceType) {
- $service = Get-Interface $service $InterfaceType
- }
- $service
- }
- <#
- .SYNOPSIS
- Get VS IComponentModel service to access VS MEF hosting.
- #>
- function Get-VSComponentModel
- {
- Get-VSService ([Microsoft.VisualStudio.ComponentModelHost.SComponentModel]) ([Microsoft.VisualStudio.ComponentModelHost.IComponentModel])
- }
The same Profile.ps1 file contains a nice example of how to use these functions:
A lot of other good samples can be found on the Power Console site at the home page.
Now there are two things you can do with respect to the Power Console specific function GetInterface() on line 22:
- Make sure that Power Console is installed and load the assembly Microsoft.VisualStudio.PowerConsole.Host.PowerShell.Implementation.dll
- Fire-up reflector and investigate the GetInterface() function to isolate the GetInterface code into your own library that only contains this functionality (I did this, it is a lot of work!)
For this post we use the first approach, the second approach is left for the reader as an exercise:-)
To try it out I want to present a maybe bit unusual case: I want to be able to access Visual Studio from a PowerShell script that is executed from the MSBuild script building a project.
In the .csproj of my project I added the following line:
<!-- Macaw Software Factory targets -->
<Import Project="..\..\..\..\tools\DotNet2\MsBuildTargets\Macaw.Mast.Targets" />
The included targets file loads the PowerShell MSBuild Task (CodePlex) that is used to fire a PowerShell script on AfterBuild. Below a relevant excerpt from this targets file:
- <Project DefaultTargets="AfterBuild" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
- <UsingTask AssemblyFile="PowershellMSBuildTask.dll" TaskName="Powershell"/>
- <Target Name="AfterBuild" DependsOnTargets="$(AfterBuildDependsOn)">
- <!-- expand $(TargetDir) to _TargetDir, otherwise error on including in arguments list below -->
- <CreateProperty Value="$(TargetDir)">
- <Output TaskParameter="Value" PropertyName="_TargetDir" />
- </CreateProperty>
- <Message Text="OnBuildSuccess = $(@(IntermediateAssembly))"/>
- <Powershell Arguments="
- MastBuildAction=build;
- MastSolutionName=$(SolutionName);
- MastSolutionDir=$(SolutionDir);
- MastProjectName=$(ProjectName);
- MastConfigurationName=$(ConfigurationName);
- MastProjectDir=$(ProjectDir);
- MastTargetDir=$(_TargetDir);
- MastTargetName=$(TargetName);
- MastPackageForDeployment=$(MastPackageForDeployment);
- MastSingleProjectBuildAndPackage=$(MastSingleProjectBuildAndPackage)
- "
- VerbosePreference="Continue"
- Script="& (Join-Path -Path "$(SolutionDir)" -ChildPath "..\..\..\tools\MastDeployDispatcher.ps1")" />
- </Target>
- </Project>
The MastDeployDispatcher.ps1 script is a Macaw Solutions Factory specific script, but you get the idea. To test in which context the PowerShell script is running I added the following lines op PowerShell code to the executed PowerShell script:
$process = [System.Diagnostics.Process]::GetCurrentProcess()
Write-Host "Process name: $($a.ProcessName)"
Which returns:
Process name: devenv
So we know our PowerShell script is running in the context of the Visual Studio process. I wonder if this is still the case if you set the maximum number of parallel projects builds to a value higher than 1 (Tools->Options->Projects and Solutions->Build and Run). I did put the value on 10, tried it, and it still worked, but I don’t know if there were more builds running at the same time.
My first step was to try one of the examples on the Power Console home page: show “Hello world” using the IVsUIShell.ShowMessageBox() function.
I added the following code to the PowerShell script:
- [void][reflection.assembly]::LoadFrom("C:\Users\serge\Downloads\PowerConsole\Microsoft.VisualStudio.PowerConsole.Host.PowerShell.Implementation.dll")
- [void][reflection.assembly]::LoadWithPartialName("Microsoft.VisualStudio.Shell.Interop")
- function Get-Interface
- {
- Param(
- $Object,
- [type]$InterfaceType
- )
- [Microsoft.VisualStudio.PowerConsole.Host.PowerShell.Implementation.PSTypeWrapper]::GetInterface($Object, $InterfaceType)
- }
- function Get-VSService
- {
- Param(
- [type]$ServiceType,
- [type]$InterfaceType
- )
- $service = [Microsoft.VisualStudio.Shell.Package]::GetGlobalService($ServiceType)
- if ($service -and $InterfaceType) {
- $service = Get-Interface $service $InterfaceType
- }
- $service
- }
- $msg = "Hello world!"
- $shui = Get-VSService `
- ([Microsoft.VisualStudio.Shell.Interop.SVsUIShell]) `
- ([Microsoft.VisualStudio.Shell.Interop.IVsUIShell])
- [void]$shui.ShowMessageBox(0, [System.Guid]::Empty,"", $msg, "", 0, `
- [Microsoft.VisualStudio.Shell.Interop.OLEMSGBUTTON]::OLEMSGBUTTON_OK,
- [Microsoft.VisualStudio.Shell.Interop.OLEMSGDEFBUTTON]::OLEMSGDEFBUTTON_FIRST, `
- [Microsoft.VisualStudio.Shell.Interop.OLEMSGICON]::OLEMSGICON_INFO, 0)
When I build the project I get the following:
So we are in business! It is possible to access the Visual Studio object model from a PowerShell script that is fired from the MSBuild script used to build your project. What you can do with that is up to your imagination. Note that you should differentiate between a build done on your developer box, executed from Visual Studio, and a build executed by for example your build server, or executing from MSBuild directly.