# PowerShell Editor Services Bootstrapper Script # ---------------------------------------------- # This script contains startup logic for the PowerShell Editor Services # module when launched by an editor. It handles the following tasks: # # - Verifying the existence of dependencies like PowerShellGet # - Verifying that the expected version of the PowerShellEditorServices module is installed # - Installing the PowerShellEditorServices module if confirmed by the user # - Finding unused TCP port numbers for the language and debug services to use # - Starting the language and debug services from the PowerShellEditorServices module # # NOTE: If editor integration authors make modifications to this # script, please consider contributing changes back to the # canonical version of this script at the PowerShell Editor # Services GitHub repository: # # https://github.com/PowerShell/PowerShellEditorServices/blob/master/module/Start-EditorServices.ps1 param( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $EditorServicesVersion, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $HostName, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $HostProfileId, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $HostVersion, [ValidateNotNullOrEmpty()] [string] $BundledModulesPath, [ValidateNotNullOrEmpty()] $LogPath, [ValidateSet("Normal", "Verbose", "Error")] $LogLevel, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $SessionDetailsPath, [switch] $EnableConsoleRepl, [string] $DebugServiceOnly, [string[]] $FeatureFlags, [switch] $WaitForDebugger, [switch] $ConfirmInstall ) function ExitWithError($errorString) { Write-Host -ForegroundColor Red "`n`n$errorString" # Sleep for a while to make sure the user has time to see and copy the # error message Start-Sleep -Seconds 300 exit 1; } # Are we running in PowerShell 2 or earlier? if ($PSVersionTable.PSVersion.Major -le 2) { # No ConvertTo-Json on PSv2 and below, so write out the JSON manually "{`"status`": `"failed`", `"reason`": `"unsupported`", `"powerShellVersion`": `"$($PSVersionTable.PSVersion.ToString())`"}" | Set-Content -Force -Path "$SessionDetailsPath" -ErrorAction Stop ExitWithError "Unsupported PowerShell version $($PSVersionTable.PSVersion), language features are disabled." } function WriteSessionFile($sessionInfo) { ConvertTo-Json -InputObject $sessionInfo -Compress | Set-Content -Force -Path "$SessionDetailsPath" -ErrorAction Stop } if ($host.Runspace.LanguageMode -eq 'ConstrainedLanguage') { WriteSessionFile @{ "status" = "failed" "reason" = "languageMode" "detail" = $host.Runspace.LanguageMode.ToString() } ExitWithError "PowerShell is configured with an unsupported LanguageMode (ConstrainedLanguage), language features are disabled." } # Are we running in PowerShell 5 or later? $isPS5orLater = $PSVersionTable.PSVersion.Major -ge 5 # If PSReadline is present in the session, remove it so that runspace # management is easier if ((Get-Module PSReadline).Count -gt 0) { Remove-Module PSReadline -ErrorAction SilentlyContinue } # This variable will be assigned later to contain information about # what happened while attempting to launch the PowerShell Editor # Services host $resultDetails = $null; function Test-ModuleAvailable($ModuleName, $ModuleVersion) { $modules = Get-Module -ListAvailable $moduleName if ($modules -ne $null) { if ($ModuleVersion -ne $null) { foreach ($module in $modules) { if ($module.Version.Equals($moduleVersion)) { return $true; } } } else { return $true; } } return $false; } function Test-PortAvailability($PortNumber) { $portAvailable = $true; try { if ($isPS5orLater) { $ipAddress = [System.Net.Dns]::GetHostEntryAsync("localhost").Result.AddressList[0]; } else { $ipAddress = [System.Net.Dns]::GetHostEntry("localhost").AddressList[0]; } $tcpListener = New-Object System.Net.Sockets.TcpListener @($ipAddress, $portNumber) $tcpListener.Start(); $tcpListener.Stop(); } catch [System.Net.Sockets.SocketException] { # Check the SocketErrorCode to see if it's the expected exception if ($error[0].Exception.InnerException.SocketErrorCode -eq [System.Net.Sockets.SocketError]::AddressAlreadyInUse) { $portAvailable = $false; } else { Write-Output ("Error code: " + $error[0].SocketErrorCode) } } return $portAvailable; } $rand = New-Object System.Random function Get-AvailablePort { $triesRemaining = 10; while ($triesRemaining -gt 0) { $port = $rand.Next(10000, 30000) if ((Test-PortAvailability -PortAvailability $port) -eq $true) { return $port } $triesRemaining--; } return $null } # Add BundledModulesPath to $env:PSModulePath if ($BundledModulesPath) { $env:PSModulePath = $env:PSModulePath + [System.IO.Path]::PathSeparator + $BundledModulesPath } # Check if PowerShellGet module is available if ((Test-ModuleAvailable "PowerShellGet") -eq $false) { # TODO: WRITE ERROR } # Check if the expected version of the PowerShell Editor Services # module is installed $parsedVersion = New-Object System.Version @($EditorServicesVersion) if ((Test-ModuleAvailable "PowerShellEditorServices" -RequiredVersion $parsedVersion) -eq $false) { if ($ConfirmInstall -and $isPS5orLater) { # TODO: Check for error and return failure if necessary Install-Module "PowerShellEditorServices" -RequiredVersion $parsedVersion -Confirm } else { # Indicate to the client that the PowerShellEditorServices module # needs to be installed Write-Output "needs_install" } } try { if ($isPS5orLater) { Import-Module PowerShellEditorServices -RequiredVersion $parsedVersion -ErrorAction Stop } else { Import-Module PowerShellEditorServices -Version $parsedVersion -ErrorAction Stop } # Locate available port numbers for services $languageServicePort = Get-AvailablePort $debugServicePort = Get-AvailablePort Write-Host "Starting PowerShell...`n" -ForegroundColor Blue # Create the Editor Services host $editorServicesHost = Start-EditorServicesHost ` -HostName $HostName ` -HostProfileId $HostProfileId ` -HostVersion $HostVersion ` -LogPath $LogPath ` -LogLevel $LogLevel ` -LanguageServicePort $languageServicePort ` -DebugServicePort $debugServicePort ` -BundledModulesPath $BundledModulesPath ` -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` -DebugServiceOnly:$DebugServiceOnly.IsPresent ` -WaitForDebugger:$WaitForDebugger.IsPresent # TODO: Verify that the service is started $resultDetails = @{ "status" = "started"; "channel" = "tcp"; "languageServicePort" = $languageServicePort; "debugServicePort" = $debugServicePort; }; # Notify the client that the services have started WriteSessionFile $resultDetails } catch [System.Exception] { $e = $_.Exception; $errorString = "" while ($e -ne $null) { $errorString = $errorString + ($e.Message + "`r`n" + $e.StackTrace + "`r`n") $e = $e.InnerException; } ExitWithError ("An error occurred while starting PowerShell Editor Services:`r`n`r`n" + $errorString) } try { # Wait for the host to complete execution before exiting $editorServicesHost.WaitForCompletion() } catch [System.Exception] { $e = $_.Exception; $errorString = "" while ($e -ne $null) { $errorString = $errorString + ($e.Message + "`r`n" + $e.StackTrace + "`r`n") $e = $e.InnerException; } ExitWithError ("A terminating error occurred in PowerShell Editor Services:`r`n`r`n" + $errorString) }