My wish for the readers of this post is that you find this completely irrelevant and wonder why folks would wish to inflict powershell v2 on themselves now that we are on a much improved v5. However the reality is that many many machines are still running windows 7 and server 2008R2 without an upgraded powershell.
As I was working on Boxstarter 2.6 to support Chocolatey 0.9.9 which now ships as a .net 4 assembly, I had to be able to load it inside of Powershell 2 since I still want to support virgin win7/2008R2 environments. Without "help", this will fail because Powershell 2 hosts .Net 3.5. I really don't want to ask users to install an updated WMF prior to using Boxstarter because that violates the core mission of Boxstarter which is to setup a machine from scratch.
Adjusting CLR version system wide
So after some investigation I found several posts telling me what I already knew which included the following solutions:
- Upgrade to a WMF 3 or higher
- Create or edit a Powershell.exe.config file in C:\WINDOWS\System32\WindowsPowerShell\v1.0 setting the supportedRuntime to .net 4
- Edit the hklm\software\microsoft\.netframework registry key to only use the latest CLR
I have already mentioned why option 1 was not an option. Options 2 and 3 are equally unpalatable if you do not "own" the system since both change system wide behavior. I just want to change the behavior when my application is running.
An application scoped solution
So after more digging I found an obscure, and seemingly undocumented environment variable that can impact the version of the .net runtime loaded: $env:COMPLUS_version. If you set this variable to "v4.0.30319" and then spawn a new process, that process will use the specified version of the .net runtime.
PS C:\Users\Administrator> $PSVersionTable Name Value ---- ----- CLRVersion 2.0.50727.5420 BuildVersion 6.1.7601.17514 PSVersion 2.0 WSManStackVersion 2.0 PSCompatibleVersions {1.0, 2.0} SerializationVersion 1.1.0.1 PSRemotingProtocolVersion 2.1 PS C:\Users\Administrator> $env:COMPLUS_version="v4.0.30319" PS C:\Users\Administrator> & powershell { $psVersionTable } Name Value ---- ----- PSVersion 2.0 PSCompatibleVersions {1.0, 2.0} BuildVersion 6.1.7601.17514 CLRVersion 4.0.30319.17929 WSManStackVersion 2.0 PSRemotingProtocolVersion 2.1 SerializationVersion 1.1.0.1
A script that runs commands in .net 4
So given that this works, I created a Enter-DotNet4 command that allows one to run ad hoc scripts inside .net 4. Here it is:
function Enter-Dotnet4 { <# .SYNOPSIS Runs a script from a process hosting the .net 4 runtile .DESCRIPTION This function will ensure that the .net 4 runtime is installed on the machine. If it is not, it will be downloaded and installed. If running remotely, the .net 4 installation will run from a scheduled task. If the CLRVersion of the hosting powershell process is less than 4, such as is the case in powershell 2, the given script will be run from a new a new powershell process tht will be configured to host the CLRVersion 4.0.30319. .Parameter ScriptBlock The script to be executed in the .net 4 CLR .Parameter ArgumentList Arguments to be passed to the ScriptBlock .LINK http://boxstarter.org #> param( [ScriptBlock]$ScriptBlock, [object[]]$ArgumentList ) Enable-Net40 if($PSVersionTable.CLRVersion.Major -lt 4) { Write-BoxstarterMessage "Relaunching powershell under .net fx v4" -verbose $env:COMPLUS_version="v4.0.30319" & powershell -OutputFormat Text -ExecutionPolicy bypass -command $ScriptBlock -args $ArgumentList } else { Write-BoxstarterMessage "Using current powershell..." -verbose Invoke-Command -ScriptBlock $ScriptBlock -argumentlist $ArgumentList } } function Enable-Net40 { if(!(test-path "hklm:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319")) { if((Test-PendingReboot) -and $Boxstarter.RebootOk) {return Invoke-Reboot} Write-BoxstarterMessage "Downloading .net 4.5..." Get-HttpResource "http://download.microsoft.com/download/b/a/4/ba4a7e71-2906-4b2d-a0e1-80cf16844f5f/dotnetfx45_full_x86_x64.exe" "$env:temp\net45.exe" Write-BoxstarterMessage "Installing .net 4.5..." if(Get-IsRemote) { Invoke-FromTask @" Start-Process "$env:temp\net45.exe" -verb runas -wait -argumentList "/quiet /norestart /log $env:temp\net45.log" "@ } else { $proc = Start-Process "$env:temp\net45.exe" -verb runas -argumentList "/quiet /norestart /log $env:temp\net45.log" -PassThru while(!$proc.HasExited){ sleep -Seconds 1 } } } }
This will install .net 4.5 if not already installed and then spawn a new powershell process to run the given commands with the .net 4 runtime hosted.
Does not work in a remote shell
One scenario where this does not work is if you are remoted on a Powershell v2 machine. The .net4 CLR will almost immediately crash. My guess is that this is related to the fact that remote shells have an inherently different hosting model and run under wsmprovhost.exe or winrshost.exe.
The workaround for this in Boxstarter is to call the chocolatey.dll in a Scheduled Task instead of using Enter-DotNet4 when running remote.