Archives
-
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:
Extract from Profile.ps1- <#
- .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:
Macaw.Mast.Targets- <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:
PowerShell from MSBuild- [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.
-
Powershell: Finding items in a Visual Studio project
In the Macaw Solutions Factory we execute a lot of PowerShell code in the context of Visual Studio, meaning that we can access the Visual Studio object model directly from our PowerShell code.
There is a great add-in for Visual Studio that provides you with a PowerShell console within Visual Studio that also allows you to access the Visual Studio object model to play with VSX (Visual Studio Extensibility). This add-in is called Power Console.
If you paste the function below in this Power Console, you can find a (selection of) project items in a specified Visual Studio project.
For example:
FindProjectItems -SolutionRelativeProjectFile 'Business.ServiceInterfaces\Business.ServiceInterfaces.csproj' -Pattern '*.asmx' | select-object RelativeFileName
returns:
RelativeFileName ----------------
Internal\AnotherSoapService.asmx
SampleSoapService.asmxWhat I do is that I extend the standard Visual Studio ProjectItem objects with two fields: FileName (this is the full path to the item) and RelativeFileName (this is the path to the item relative to the project folder (line 53-55). I return a collection of Visual Studio project items, with these additional fields.
A great way of testing out this kind of code is by editing it in Visual Studio using the PowerGuiVSX add-in (which uses the unsurpassed PowerGui script editor), and copying over the code into the Power Console.
Find project items- function FindProjectItems
- {
- param
- (
- $SolutionRelativeProjectFile,
- $Pattern = '*'
- )
- function FindProjectItemsRecurse
- {
- param
- (
- $AbsolutePath,
- $RelativePath = '',
- $ProjectItem,
- $Pattern
- )
- $projItemFolder = '{6BB5F8EF-4483-11D3-8BCF-00C04F8EC28C}' # Visual Studio defined constant
- if ($ProjectItem.Kind -eq $projItemFolder)
- {
- if ($ProjectItem.ProjectItems -ne $null)
- {
- if ($RelativePath -eq '')
- {
- $relativeFolderPath = $ProjectItem.Name
- }
- else
- {
- $relativeFolderPath = Join-Path -Path $RelativePath -ChildPath $ProjectItem.Name
- }
- $ProjectItem.ProjectItems | ForEach-Object {
- FindProjectItemsRecurse -AbsolutePath $AbsolutePath -RelativePath $relativeFolderPath -ProjectItem $_ -Pattern $Pattern
- }
- }
- }
- else
- {
- if ($ProjectItem.Name -like $pattern)
- {
- if ($RelativePath -eq '')
- {
- $relativeFileName = $ProjectItem.Name
- }
- else
- {
- if ($RelativePath -eq $null) { Write-Host "Relative Path is NULL" }
- $relativeFileName = Join-Path -Path $RelativePath -ChildPath $ProjectItem.Name
- }
- $fileName = Join-Path -Path $AbsolutePath -ChildPath $relativeFileName;
- $ProjectItem |
- Add-Member -MemberType NoteProperty -Name RelativeFileName -Value $relativeFileName -PassThru |
- Add-Member -MemberType NoteProperty -Name FileName -Value $fileName -PassThru
- }
- }
- }
- $proj = $DTE.Solution.Projects.Item($SolutionRelativeProjectFile)
- $projPath = Split-Path -Path $proj.FileName -Parent
- if ($proj -eq $null) { throw "No project '$SolutionRelativeProjectFile' found in current solution" }
- $proj.ProjectItems | ForEach-Object {
- FindProjectItemsRecurse -AbsolutePath $projPath -ProjectItem $_ -Pattern $Pattern
- }
- }
-
PowerShell internal functions
Working with PowerShell for years already, never knew that this would work! Internal functions in PowerShell (they probably have a better name):
function x
{
function y
{
"function y"
}
y
}PS> x
function y
PS> y
ERROR!
-
Returning an exit code from a PowerShell script
Returning an exit code from a PowerShell script seems easy… but it isn’t that obvious. In this blog post I will show you an approach that works for PowerShell scripts that can be called from both PowerShell and batch scripts, where the command to be executed can be specified in a string, execute in its own context and always return the correct error code.
Below is a kind of transcript of the steps that I took to get to an approach that works for me. It is a transcript of the steps I took, for the conclusions just jump to the end.
In many blog posts you can read about calling a PowerShell script that you call from a batch script, and how to return an error code. This comes down to the following:
c:\temp\exit.ps1:
Write-Host "Exiting with code 12345"
exit 12345c:\temp\testexit.cmd:
@PowerShell -NonInteractive -NoProfile -Command "& {c:\temp\exit.ps1; exit $LastExitCode }"
@echo From Cmd.exe: Exit.ps1 exited with exit code %errorlevel%Executing c:\temp\testexit.cmd results in the following output:
Exiting with code 12345
From Cmd.exe: Exit.ps1 exited with exit code 12345But now we want to call it from another PowerShell script, by executing PowerShell:
c:\temp\testexit.ps1:
PowerShell -NonInteractive -NoProfile -Command c:\temp\exit.ps1
Write-Host "From PowerShell: Exit.ps1 exited with exit code $LastExitCode"Executing c:\temp\testexit.ps1 results in the following output:
Exiting with code 12345
From PowerShell: Exit.ps1 exited with exit code 1This is not what we expected… What happs? If the script just returns the exit code is 0, otherwise the exit code is 1, even if you exit with an exit code!?
But what if we call the script directly, instead of through the PowerShell command?
We change exit.ps1 to:
Write-Host "Global variable value: $globalvariable"
Write-Host "Exiting with code 12345"
exit 12345And we change testexit.ps1 to:
$global:globalvariable = "My global variable value"
& c:\temp\exit.ps1
Write-Host "From PowerShell: Exit.ps1 exited with exit code $LastExitCode"Executing c:\temp\testexit.ps1 results in the following output:
Global variable value: My global variable value
Exiting with code 12345
From PowerShell: Exit.ps1 exited with exit code 12345This is what we wanted! But now we are executing the script exit.ps1 in the context of the testexit.ps1 script, the globally defined variable $globalvariable is still known. This is not what we want. We want to execute it is isolation.
We change c:\temp\testexit.ps1 to:
$global:globalvariable = "My global variable value"
PowerShell -NonInteractive -NoProfile -Command c:\temp\exit.ps1
Write-Host "From PowerShell: Exit.ps1 exited with exit code $LastExitCode"Executing c:\temp\testexit.ps1 results in the following output:
Global variable value:
Exiting with code 12345
From PowerShell: Exit.ps1 exited with exit code 1We are not executing exit.ps1 in the context of testexit.ps1, which is good. But how can we reach the holy grail:
- Write a PowerShell script that can be executed from batch scripts an from PowerShell
- That return a specific error code
- That can specified as a string
- Can be executed both in the context of a calling PowerShell script AND (through a call to PowerShell) in it’s own execution space
We change c:\temp\testexit.ps1 to:
$global:globalvariable = "My global variable value"
PowerShell -NonInteractive -NoProfile -Command { c:\temp\exit.ps1 ; exit $LastExitCode }
Write-Host "From PowerShell: Exit.ps1 exited with exit code $LastExitCode"This is the same approach as when we called it from the batch script. Executing c:\temp\testexit.ps1 results in the following output:
Global variable value:
Exiting with code 12345
From PowerShell: Exit.ps1 exited with exit code 12345This is close. But we want to be able to specify the command to be executed as string, for example:
$command = "c:\temp\exit.ps1 -param1 x -param2 y"
We change c:\temp\exit.ps1 to: (support for variables, test if in its own context)
param( $param1, $param2)
Write-Host "param1=$param1; param2=$param2"
Write-Host "Global variable value: $globalvariable"
Write-Host "Exiting with code 12345"
exit 12345If we change c:\temp\testexit.ps1 to:
$global:globalvariable = "My global variable value"
$command = "c:\temp\exit.ps1 -param1 x -param2 y"
Invoke-Expression -Command $command
Write-Host "From PowerShell: Exit.ps1 exited with exit code $LastExitCode"We get a good exit code, but we are still executing in the context of testexit.ps1.
If we use the same trick as in calling from a batch script, that worked before?
We change c:\temp\testexit.ps1 to:
$global:globalvariable = "My global variable value"
$command = "c:\temp\exit.ps1 -param1 x -param2 y"
PowerShell -NonInteractive -NoProfile -Command { $command; exit $LastErrorLevel }
Write-Host "From PowerShell: Exit.ps1 exited with exit code $LastExitCode"Executing c:\temp\testexit.ps1 results in the following output:
From PowerShell: Exit.ps1 exited with exit code 0
Ok, lets use the Invoke-Expression again. We change c:\temp\testexit.ps1 to:
$global:globalvariable = "My global variable value"
$command = "c:\temp\exit.ps1 -param1 x -param2 y"
PowerShell -NonInteractive -NoProfile -Command { Invoke-Expression -Command $command; exit $LastErrorLevel }
Write-Host "From PowerShell: Exit.ps1 exited with exit code $LastExitCode"Executing c:\temp\testexit.ps1 results in the following output:
Cannot bind argument to parameter 'Command' because it is null.
At :line:3 char:10
+ PowerShell <<<< -NonInteractive -NoProfile -Command { Invoke-Expression -Command $command; exit $LastErrorLevel }From PowerShell: Exit.ps1 exited with exit code 1
We should go back to executing the command as a string, so not within brackets (in a script block). We change c:\temp\testexit.ps1 to:
$global:globalvariable = "My global variable value"
$command = "c:\temp\exit.ps1 -param1 x -param2 y"
PowerShell -NonInteractive -NoProfile -Command $command
Write-Host "From PowerShell: Exit.ps1 exited with exit code $LastExitCode"Executing c:\temp\testexit.ps1 results in the following output:
param1=x; param2=y
Global variable value:
Exiting with code 12345
From PowerShell: Exit.ps1 exited with exit code 1Ok, we can execute the specified command text as if it is a PowerShell command. But we still have the exit code problem, only 0 or 1 is returned.
Lets try something completely different. We change c:\temp\exit.ps1 to:
param( $param1, $param2)
function ExitWithCode
{
param
(
$exitcode
)$host.SetShouldExit($exitcode)
exit
}Write-Host "param1=$param1; param2=$param2"
Write-Host "Global variable value: $globalvariable"
Write-Host "Exiting with code 12345"
ExitWithCode -exitcode 12345
Write-Host "After exit"What we do is specify to the host the exit code we would like to use, and then just exit, all in the simplest utility function.
Executing c:\temp\testexit.ps1 results in the following output:
param1=x; param2=y
Global variable value:
Exiting with code 12345
From PowerShell: Exit.ps1 exited with exit code 12345Ok, this fulfills all our holy grail dreams! But couldn’t we make the call from the batch script also simpler?
Change c:\temp\testexit.cmd to:
@PowerShell -NonInteractive -NoProfile -Command "c:\temp\exit.ps1 -param1 x -param2 y"
@echo From Cmd.exe: Exit.ps1 exited with exit code %errorlevel%Executing c:\temp\testexit.cmd results in the following output:
param1=x; param2=y
Global variable value:
Exiting with code 12345
From Cmd.exe: Exit.ps1 exited with exit code 12345This is even simpler! We can now just call the PowerShell code, without the exit $LastExitCode trick!
========================= CONCLUSIONS ============================
And now the conclusions after this long long story, that took a lot of time to find out (and to read for you):
- Don’t use exit to return a value from PowerShell code, but use the following function:
- Call script from batch using:
PowerShell -NonInteractive -NoProfile -Command "c:\temp\exit.ps1 -param1 x -param2 y"
function ExitWithCode
{
param
(
$exitcode
)$host.SetShouldExit($exitcode)
exit
}- echo %errorlevel%
- Call from PowerShell with: (Command specified in string, execute in own context)$command = "c:\temp\exit.ps1 -param1 x -param2 y"
PowerShell -NonInteractive -NoProfile -Command $command
$LastExitCode contains the exit code - Call from PowerShell with: (Direct command, execute in own context)
PowerShell -NonInteractive -NoProfile -Command { c:\temp\exit.ps1 -param1 x -param2 y } $LastExitCode contains the exit code - Call from Powershell with: (Command specified in string, invoke in caller context)
Invoke-Expression -Command $command- $LastExitCode contains the exit code
- Call from PowerShell with: (Direct command, execute in caller context)
& c:\temp\exit.ps1 -param1 x -param2 y $LastExitCode contains the exit code