From f767813a69e819e1188a83d2ed3ed971116723bc Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 21 Mar 2018 13:49:57 -0700 Subject: [PATCH 1/9] Add Start-EditorServices script from vscode-powershell repo --- scripts/Start-EditorServices.ps1 | 353 +++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 scripts/Start-EditorServices.ps1 diff --git a/scripts/Start-EditorServices.ps1 b/scripts/Start-EditorServices.ps1 new file mode 100644 index 000000000..a1f9152ab --- /dev/null +++ b/scripts/Start-EditorServices.ps1 @@ -0,0 +1,353 @@ +# 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("Diagnostic", "Normal", "Verbose", "Error", "Diagnostic")] + $LogLevel, + + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string] + $SessionDetailsPath, + + [switch] + $EnableConsoleRepl, + + [switch] + $DebugServiceOnly, + + [string[]] + $AdditionalModules, + + [string[]] + $FeatureFlags, + + [switch] + $WaitForDebugger, + + [switch] + $ConfirmInstall +) + +$minPortNumber = 10000 +$maxPortNumber = 30000 + +if ($LogLevel -eq "Diagnostic") { + $VerbosePreference = 'Continue' + Start-Transcript (Join-Path (Split-Path $LogPath -Parent) Start-EditorServices.log) -Force +} + +function LogSection([string]$msg) { + Write-Verbose "`n#-- $msg $('-' * ([Math]::Max(0, 73 - $msg.Length)))" +} + +function Log([string[]]$msg) { + $msg | Write-Verbose +} + +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) { + $sessionInfoJson = ConvertTo-Json -InputObject $sessionInfo -Compress + Log "Writing session file with contents:" + Log $sessionInfoJson + $sessionInfoJson | 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) { + LogSection "Removing PSReadLine module" + 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) { + Log "Testing module availability $ModuleName $ModuleVersion" + + $modules = Get-Module -ListAvailable $moduleName + if ($modules -ne $null) { + if ($ModuleVersion -ne $null) { + foreach ($module in $modules) { + if ($module.Version.Equals($moduleVersion)) { + Log "$ModuleName $ModuleVersion found" + return $true; + } + } + } + else { + Log "$ModuleName $ModuleVersion found" + return $true; + } + } + + Log "$ModuleName $ModuleVersion NOT found" + return $false; +} + +function Test-PortAvailability { + param( + [Parameter(Mandatory=$true)] + [int] + $PortNumber + ) + + $portAvailable = $true + + try { + if ($isPS5orLater) { + $ipAddresses = [System.Net.Dns]::GetHostEntryAsync("localhost").Result.AddressList + } + else { + $ipAddresses = [System.Net.Dns]::GetHostEntry("localhost").AddressList + } + + foreach ($ipAddress in $ipAddresses) + { + Log "Testing availability of port ${PortNumber} at address ${ipAddress} / $($ipAddress.AddressFamily)" + + $tcpListener = New-Object System.Net.Sockets.TcpListener @($ipAddress, $PortNumber) + $tcpListener.Start() + $tcpListener.Stop() + } + } + catch [System.Net.Sockets.SocketException] { + $portAvailable = $false + + # Check the SocketErrorCode to see if it's the expected exception + if ($_.Exception.SocketErrorCode -eq [System.Net.Sockets.SocketError]::AddressAlreadyInUse) { + Log "Port $PortNumber is in use." + } + else { + Log "SocketException on port ${PortNumber}: $($_.Exception)" + } + } + + $portAvailable +} + +$portsInUse = @{} +$rand = New-Object System.Random +function Get-AvailablePort() { + $triesRemaining = 10; + + while ($triesRemaining -gt 0) { + do { + $port = $rand.Next($minPortNumber, $maxPortNumber) + } + while ($portsInUse.ContainsKey($port)) + + # Whether we succeed or fail, don't try this port again + $portsInUse[$port] = 1 + + Log "Checking port: $port, attempts remaining $triesRemaining --------------------" + if ((Test-PortAvailability -PortNumber $port) -eq $true) { + Log "Port: $port is available" + return $port + } + + Log "Port: $port is NOT available" + $triesRemaining--; + } + + Log "Did not find any available ports!!" + return $null +} + +# Add BundledModulesPath to $env:PSModulePath +if ($BundledModulesPath) { + $env:PSModulePath = $env:PSModulePath.TrimEnd([System.IO.Path]::PathSeparator) + [System.IO.Path]::PathSeparator + $BundledModulesPath + LogSection "Updated PSModulePath to:" + Log ($env:PSModulePath -split [System.IO.Path]::PathSeparator) +} + +LogSection "Check required modules available" +# Check if PowerShellGet module is available +if ((Test-ModuleAvailable "PowerShellGet") -eq $false) { + Log "Failed to find PowerShellGet module" + # 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" $parsedVersion) -eq $false) { + if ($ConfirmInstall -and $isPS5orLater) { + # TODO: Check for error and return failure if necessary + LogSection "Install PowerShellEditorServices" + Install-Module "PowerShellEditorServices" -RequiredVersion $parsedVersion -Confirm + } + else { + # Indicate to the client that the PowerShellEditorServices module + # needs to be installed + Write-Output "needs_install" + } +} + +try { + LogSection "Start up PowerShellEditorServices" + Log "Importing PowerShellEditorServices" + + if ($isPS5orLater) { + Import-Module PowerShellEditorServices -RequiredVersion $parsedVersion -ErrorAction Stop + } + else { + Import-Module PowerShellEditorServices -Version $parsedVersion -ErrorAction Stop + } + + # Locate available port numbers for services + Log "Searching for available socket port for the language service" + $languageServicePort = Get-AvailablePort + + Log "Searching for available socket port for the debug service" + $debugServicePort = Get-AvailablePort + + if (!$languageServicePort -or !$debugServicePort) { + ExitWithError "Failed to find an open socket port for either the language or debug service." + } + + if ($EnableConsoleRepl) { + Write-Host "PowerShell Integrated Console`n" + } + + # Create the Editor Services host + Log "Invoking Start-EditorServicesHost" + $editorServicesHost = + Start-EditorServicesHost ` + -HostName $HostName ` + -HostProfileId $HostProfileId ` + -HostVersion $HostVersion ` + -LogPath $LogPath ` + -LogLevel $LogLevel ` + -AdditionalModules $AdditionalModules ` + -LanguageServicePort $languageServicePort ` + -DebugServicePort $debugServicePort ` + -BundledModulesPath $BundledModulesPath ` + -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` + -DebugServiceOnly:$DebugServiceOnly.IsPresent ` + -WaitForDebugger:$WaitForDebugger.IsPresent + + # TODO: Verify that the service is started + Log "Start-EditorServicesHost returned $editorServicesHost" + + $resultDetails = @{ + "status" = "started"; + "channel" = "tcp"; + "languageServicePort" = $languageServicePort; + "debugServicePort" = $debugServicePort; + } + + # Notify the client that the services have started + WriteSessionFile $resultDetails + + Log "Wrote out session file" +} +catch [System.Exception] { + $e = $_.Exception; + $errorString = "" + + Log "ERRORS caught starting up EditorServicesHost" + + while ($e -ne $null) { + $errorString = $errorString + ($e.Message + "`r`n" + $e.StackTrace + "`r`n") + $e = $e.InnerException; + Log $errorString + } + + ExitWithError ("An error occurred while starting PowerShell Editor Services:`r`n`r`n" + $errorString) +} + +try { + # Wait for the host to complete execution before exiting + LogSection "Waiting for EditorServicesHost to complete execution" + $editorServicesHost.WaitForCompletion() + Log "EditorServicesHost has completed execution" +} +catch [System.Exception] { + $e = $_.Exception; + $errorString = "" + + Log "ERRORS caught while waiting for EditorServicesHost to complete execution" + + while ($e -ne $null) { + $errorString = $errorString + ($e.Message + "`r`n" + $e.StackTrace + "`r`n") + $e = $e.InnerException; + Log $errorString + } +} From 8e04e18f33619911e620b389236e09dc5add0c66 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 21 Mar 2018 14:00:53 -0700 Subject: [PATCH 2/9] Move Start-EditorServices to module and overwrite the old one --- module/Start-EditorServices.ps1 | 263 ++++++++++++++++++----- scripts/Start-EditorServices.ps1 | 353 ------------------------------- 2 files changed, 211 insertions(+), 405 deletions(-) delete mode 100644 scripts/Start-EditorServices.ps1 diff --git a/module/Start-EditorServices.ps1 b/module/Start-EditorServices.ps1 index 332016352..a1f9152ab 100644 --- a/module/Start-EditorServices.ps1 +++ b/module/Start-EditorServices.ps1 @@ -44,9 +44,26 @@ param( [ValidateNotNullOrEmpty()] $LogPath, - [ValidateSet("Normal", "Verbose", "Error","Diagnostic")] + [ValidateSet("Diagnostic", "Normal", "Verbose", "Error", "Diagnostic")] $LogLevel, + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string] + $SessionDetailsPath, + + [switch] + $EnableConsoleRepl, + + [switch] + $DebugServiceOnly, + + [string[]] + $AdditionalModules, + + [string[]] + $FeatureFlags, + [switch] $WaitForDebugger, @@ -54,89 +71,186 @@ param( $ConfirmInstall ) +$minPortNumber = 10000 +$maxPortNumber = 30000 + +if ($LogLevel -eq "Diagnostic") { + $VerbosePreference = 'Continue' + Start-Transcript (Join-Path (Split-Path $LogPath -Parent) Start-EditorServices.log) -Force +} + +function LogSection([string]$msg) { + Write-Verbose "`n#-- $msg $('-' * ([Math]::Max(0, 73 - $msg.Length)))" +} + +function Log([string[]]$msg) { + $msg | Write-Verbose +} + +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) { + $sessionInfoJson = ConvertTo-Json -InputObject $sessionInfo -Compress + Log "Writing session file with contents:" + Log $sessionInfoJson + $sessionInfoJson | 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) { + LogSection "Removing PSReadLine module" + 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) { + Log "Testing module availability $ModuleName $ModuleVersion" + $modules = Get-Module -ListAvailable $moduleName if ($modules -ne $null) { if ($ModuleVersion -ne $null) { foreach ($module in $modules) { if ($module.Version.Equals($moduleVersion)) { + Log "$ModuleName $ModuleVersion found" return $true; } } } else { + Log "$ModuleName $ModuleVersion found" return $true; } } + Log "$ModuleName $ModuleVersion NOT found" return $false; } -function Test-PortAvailability($PortNumber) { - $portAvailable = $true; +function Test-PortAvailability { + param( + [Parameter(Mandatory=$true)] + [int] + $PortNumber + ) + + $portAvailable = $true try { - $ipAddress = [System.Net.Dns]::GetHostEntryAsync("localhost").Result.AddressList[0]; - $tcpListener = [System.Net.Sockets.TcpListener]::new($ipAddress, $portNumber); - $tcpListener.Start(); - $tcpListener.Stop(); + if ($isPS5orLater) { + $ipAddresses = [System.Net.Dns]::GetHostEntryAsync("localhost").Result.AddressList + } + else { + $ipAddresses = [System.Net.Dns]::GetHostEntry("localhost").AddressList + } + foreach ($ipAddress in $ipAddresses) + { + Log "Testing availability of port ${PortNumber} at address ${ipAddress} / $($ipAddress.AddressFamily)" + + $tcpListener = New-Object System.Net.Sockets.TcpListener @($ipAddress, $PortNumber) + $tcpListener.Start() + $tcpListener.Stop() + } } catch [System.Net.Sockets.SocketException] { + $portAvailable = $false + # 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; + if ($_.Exception.SocketErrorCode -eq [System.Net.Sockets.SocketError]::AddressAlreadyInUse) { + Log "Port $PortNumber is in use." } else { - Write-Output ("Error code: " + $error[0].SocketErrorCode) + Log "SocketException on port ${PortNumber}: $($_.Exception)" } } - return $portAvailable; + $portAvailable } -$rand = [System.Random]::new() -function Get-AvailablePort { +$portsInUse = @{} +$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) { + do { + $port = $rand.Next($minPortNumber, $maxPortNumber) + } + while ($portsInUse.ContainsKey($port)) + + # Whether we succeed or fail, don't try this port again + $portsInUse[$port] = 1 + + Log "Checking port: $port, attempts remaining $triesRemaining --------------------" + if ((Test-PortAvailability -PortNumber $port) -eq $true) { + Log "Port: $port is available" return $port } + Log "Port: $port is NOT available" $triesRemaining--; } + Log "Did not find any available ports!!" return $null } -# OUTPUT PROTOCOL -# - "started 29981 39898" - Server(s) are started, language and debug server ports (respectively) -# - "failed Error message describing the failure" - General failure while starting, show error message to user (?) -# - "needs_install" - User should be prompted to install PowerShell Editor Services via the PowerShell Gallery - # Add BundledModulesPath to $env:PSModulePath if ($BundledModulesPath) { - $env:PSMODULEPATH = $BundledModulesPath + [System.IO.Path]::PathSeparator + $env:PSMODULEPATH + $env:PSModulePath = $env:PSModulePath.TrimEnd([System.IO.Path]::PathSeparator) + [System.IO.Path]::PathSeparator + $BundledModulesPath + LogSection "Updated PSModulePath to:" + Log ($env:PSModulePath -split [System.IO.Path]::PathSeparator) } +LogSection "Check required modules available" # Check if PowerShellGet module is available if ((Test-ModuleAvailable "PowerShellGet") -eq $false) { + Log "Failed to find PowerShellGet module" # TODO: WRITE ERROR } # Check if the expected version of the PowerShell Editor Services # module is installed -$parsedVersion = [System.Version]::new($EditorServicesVersion) -if ((Test-ModuleAvailable "PowerShellEditorServices" -RequiredVersion $parsedVersion) -eq $false) { - if ($ConfirmInstall) { +$parsedVersion = New-Object System.Version @($EditorServicesVersion) +if ((Test-ModuleAvailable "PowerShellEditorServices" $parsedVersion) -eq $false) { + if ($ConfirmInstall -and $isPS5orLater) { # TODO: Check for error and return failure if necessary + LogSection "Install PowerShellEditorServices" Install-Module "PowerShellEditorServices" -RequiredVersion $parsedVersion -Confirm } else { @@ -146,49 +260,94 @@ if ((Test-ModuleAvailable "PowerShellEditorServices" -RequiredVersion $parsedVer } } -Import-Module PowerShellEditorServices -RequiredVersion $parsedVersion -ErrorAction Stop +try { + LogSection "Start up PowerShellEditorServices" + Log "Importing PowerShellEditorServices" + + if ($isPS5orLater) { + Import-Module PowerShellEditorServices -RequiredVersion $parsedVersion -ErrorAction Stop + } + else { + Import-Module PowerShellEditorServices -Version $parsedVersion -ErrorAction Stop + } + + # Locate available port numbers for services + Log "Searching for available socket port for the language service" + $languageServicePort = Get-AvailablePort + + Log "Searching for available socket port for the debug service" + $debugServicePort = Get-AvailablePort -# Locate available port numbers for services -$languageServicePort = Get-AvailablePort -$debugServicePort = Get-AvailablePort + if (!$languageServicePort -or !$debugServicePort) { + ExitWithError "Failed to find an open socket port for either the language or debug service." + } + + if ($EnableConsoleRepl) { + Write-Host "PowerShell Integrated Console`n" + } + + # Create the Editor Services host + Log "Invoking Start-EditorServicesHost" + $editorServicesHost = + Start-EditorServicesHost ` + -HostName $HostName ` + -HostProfileId $HostProfileId ` + -HostVersion $HostVersion ` + -LogPath $LogPath ` + -LogLevel $LogLevel ` + -AdditionalModules $AdditionalModules ` + -LanguageServicePort $languageServicePort ` + -DebugServicePort $debugServicePort ` + -BundledModulesPath $BundledModulesPath ` + -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` + -DebugServiceOnly:$DebugServiceOnly.IsPresent ` + -WaitForDebugger:$WaitForDebugger.IsPresent + + # TODO: Verify that the service is started + Log "Start-EditorServicesHost returned $editorServicesHost" + + $resultDetails = @{ + "status" = "started"; + "channel" = "tcp"; + "languageServicePort" = $languageServicePort; + "debugServicePort" = $debugServicePort; + } + + # Notify the client that the services have started + WriteSessionFile $resultDetails -$editorServicesHost = - Start-EditorServicesHost ` - -HostName $HostName ` - -HostProfileId $HostProfileId ` - -HostVersion $HostVersion ` - -LogPath $LogPath ` - -LogLevel $LogLevel ` - -AdditionalModules @() ` - -LanguageServicePort $languageServicePort ` - -DebugServicePort $debugServicePort ` - -BundledModulesPath $BundledModulesPath ` - -WaitForDebugger:$WaitForDebugger.IsPresent + Log "Wrote out session file" +} +catch [System.Exception] { + $e = $_.Exception; + $errorString = "" -# TODO: Verify that the service is started + Log "ERRORS caught starting up EditorServicesHost" -$resultDetails = @{ - "status" = "started"; - "channel" = "tcp"; - "languageServicePort" = $languageServicePort; - "debugServicePort" = $debugServicePort; -}; + while ($e -ne $null) { + $errorString = $errorString + ($e.Message + "`r`n" + $e.StackTrace + "`r`n") + $e = $e.InnerException; + Log $errorString + } -# Notify the client that the services have started -Write-Output (ConvertTo-Json -InputObject $resultDetails -Compress) + ExitWithError ("An error occurred while starting PowerShell Editor Services:`r`n`r`n" + $errorString) +} try { # Wait for the host to complete execution before exiting + LogSection "Waiting for EditorServicesHost to complete execution" $editorServicesHost.WaitForCompletion() + Log "EditorServicesHost has completed execution" } catch [System.Exception] { - $e = $_.Exception; #.InnerException; + $e = $_.Exception; $errorString = "" + Log "ERRORS caught while waiting for EditorServicesHost to complete execution" + while ($e -ne $null) { $errorString = $errorString + ($e.Message + "`r`n" + $e.StackTrace + "`r`n") $e = $e.InnerException; + Log $errorString } - - Write-Error ("`r`nCaught error while waiting for EditorServicesHost to complete:`r`n" + $errorString) } diff --git a/scripts/Start-EditorServices.ps1 b/scripts/Start-EditorServices.ps1 deleted file mode 100644 index a1f9152ab..000000000 --- a/scripts/Start-EditorServices.ps1 +++ /dev/null @@ -1,353 +0,0 @@ -# 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("Diagnostic", "Normal", "Verbose", "Error", "Diagnostic")] - $LogLevel, - - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] - $SessionDetailsPath, - - [switch] - $EnableConsoleRepl, - - [switch] - $DebugServiceOnly, - - [string[]] - $AdditionalModules, - - [string[]] - $FeatureFlags, - - [switch] - $WaitForDebugger, - - [switch] - $ConfirmInstall -) - -$minPortNumber = 10000 -$maxPortNumber = 30000 - -if ($LogLevel -eq "Diagnostic") { - $VerbosePreference = 'Continue' - Start-Transcript (Join-Path (Split-Path $LogPath -Parent) Start-EditorServices.log) -Force -} - -function LogSection([string]$msg) { - Write-Verbose "`n#-- $msg $('-' * ([Math]::Max(0, 73 - $msg.Length)))" -} - -function Log([string[]]$msg) { - $msg | Write-Verbose -} - -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) { - $sessionInfoJson = ConvertTo-Json -InputObject $sessionInfo -Compress - Log "Writing session file with contents:" - Log $sessionInfoJson - $sessionInfoJson | 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) { - LogSection "Removing PSReadLine module" - 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) { - Log "Testing module availability $ModuleName $ModuleVersion" - - $modules = Get-Module -ListAvailable $moduleName - if ($modules -ne $null) { - if ($ModuleVersion -ne $null) { - foreach ($module in $modules) { - if ($module.Version.Equals($moduleVersion)) { - Log "$ModuleName $ModuleVersion found" - return $true; - } - } - } - else { - Log "$ModuleName $ModuleVersion found" - return $true; - } - } - - Log "$ModuleName $ModuleVersion NOT found" - return $false; -} - -function Test-PortAvailability { - param( - [Parameter(Mandatory=$true)] - [int] - $PortNumber - ) - - $portAvailable = $true - - try { - if ($isPS5orLater) { - $ipAddresses = [System.Net.Dns]::GetHostEntryAsync("localhost").Result.AddressList - } - else { - $ipAddresses = [System.Net.Dns]::GetHostEntry("localhost").AddressList - } - - foreach ($ipAddress in $ipAddresses) - { - Log "Testing availability of port ${PortNumber} at address ${ipAddress} / $($ipAddress.AddressFamily)" - - $tcpListener = New-Object System.Net.Sockets.TcpListener @($ipAddress, $PortNumber) - $tcpListener.Start() - $tcpListener.Stop() - } - } - catch [System.Net.Sockets.SocketException] { - $portAvailable = $false - - # Check the SocketErrorCode to see if it's the expected exception - if ($_.Exception.SocketErrorCode -eq [System.Net.Sockets.SocketError]::AddressAlreadyInUse) { - Log "Port $PortNumber is in use." - } - else { - Log "SocketException on port ${PortNumber}: $($_.Exception)" - } - } - - $portAvailable -} - -$portsInUse = @{} -$rand = New-Object System.Random -function Get-AvailablePort() { - $triesRemaining = 10; - - while ($triesRemaining -gt 0) { - do { - $port = $rand.Next($minPortNumber, $maxPortNumber) - } - while ($portsInUse.ContainsKey($port)) - - # Whether we succeed or fail, don't try this port again - $portsInUse[$port] = 1 - - Log "Checking port: $port, attempts remaining $triesRemaining --------------------" - if ((Test-PortAvailability -PortNumber $port) -eq $true) { - Log "Port: $port is available" - return $port - } - - Log "Port: $port is NOT available" - $triesRemaining--; - } - - Log "Did not find any available ports!!" - return $null -} - -# Add BundledModulesPath to $env:PSModulePath -if ($BundledModulesPath) { - $env:PSModulePath = $env:PSModulePath.TrimEnd([System.IO.Path]::PathSeparator) + [System.IO.Path]::PathSeparator + $BundledModulesPath - LogSection "Updated PSModulePath to:" - Log ($env:PSModulePath -split [System.IO.Path]::PathSeparator) -} - -LogSection "Check required modules available" -# Check if PowerShellGet module is available -if ((Test-ModuleAvailable "PowerShellGet") -eq $false) { - Log "Failed to find PowerShellGet module" - # 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" $parsedVersion) -eq $false) { - if ($ConfirmInstall -and $isPS5orLater) { - # TODO: Check for error and return failure if necessary - LogSection "Install PowerShellEditorServices" - Install-Module "PowerShellEditorServices" -RequiredVersion $parsedVersion -Confirm - } - else { - # Indicate to the client that the PowerShellEditorServices module - # needs to be installed - Write-Output "needs_install" - } -} - -try { - LogSection "Start up PowerShellEditorServices" - Log "Importing PowerShellEditorServices" - - if ($isPS5orLater) { - Import-Module PowerShellEditorServices -RequiredVersion $parsedVersion -ErrorAction Stop - } - else { - Import-Module PowerShellEditorServices -Version $parsedVersion -ErrorAction Stop - } - - # Locate available port numbers for services - Log "Searching for available socket port for the language service" - $languageServicePort = Get-AvailablePort - - Log "Searching for available socket port for the debug service" - $debugServicePort = Get-AvailablePort - - if (!$languageServicePort -or !$debugServicePort) { - ExitWithError "Failed to find an open socket port for either the language or debug service." - } - - if ($EnableConsoleRepl) { - Write-Host "PowerShell Integrated Console`n" - } - - # Create the Editor Services host - Log "Invoking Start-EditorServicesHost" - $editorServicesHost = - Start-EditorServicesHost ` - -HostName $HostName ` - -HostProfileId $HostProfileId ` - -HostVersion $HostVersion ` - -LogPath $LogPath ` - -LogLevel $LogLevel ` - -AdditionalModules $AdditionalModules ` - -LanguageServicePort $languageServicePort ` - -DebugServicePort $debugServicePort ` - -BundledModulesPath $BundledModulesPath ` - -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` - -DebugServiceOnly:$DebugServiceOnly.IsPresent ` - -WaitForDebugger:$WaitForDebugger.IsPresent - - # TODO: Verify that the service is started - Log "Start-EditorServicesHost returned $editorServicesHost" - - $resultDetails = @{ - "status" = "started"; - "channel" = "tcp"; - "languageServicePort" = $languageServicePort; - "debugServicePort" = $debugServicePort; - } - - # Notify the client that the services have started - WriteSessionFile $resultDetails - - Log "Wrote out session file" -} -catch [System.Exception] { - $e = $_.Exception; - $errorString = "" - - Log "ERRORS caught starting up EditorServicesHost" - - while ($e -ne $null) { - $errorString = $errorString + ($e.Message + "`r`n" + $e.StackTrace + "`r`n") - $e = $e.InnerException; - Log $errorString - } - - ExitWithError ("An error occurred while starting PowerShell Editor Services:`r`n`r`n" + $errorString) -} - -try { - # Wait for the host to complete execution before exiting - LogSection "Waiting for EditorServicesHost to complete execution" - $editorServicesHost.WaitForCompletion() - Log "EditorServicesHost has completed execution" -} -catch [System.Exception] { - $e = $_.Exception; - $errorString = "" - - Log "ERRORS caught while waiting for EditorServicesHost to complete execution" - - while ($e -ne $null) { - $errorString = $errorString + ($e.Message + "`r`n" + $e.StackTrace + "`r`n") - $e = $e.InnerException; - Log $errorString - } -} From f176acf2935c5501bd14eb13709832b5625d860f Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 21 Mar 2018 16:00:49 -0700 Subject: [PATCH 3/9] blah --- test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs index 6cfb1f6dc..e85b7991c 100644 --- a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs +++ b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs @@ -58,7 +58,10 @@ protected async Task> LaunchService( "-HostVersion \"1.0.0\" " + "-BundledModulesPath \\\"" + modulePath + "\\\" " + "-LogLevel \"Verbose\" " + - "-LogPath \"" + logPath + "\" ", + "-LogPath \"" + logPath + "\" " + + "-SessionDetailsPath \".\\sessionDetails\" " + + "-FeatureFlags @() " + + "-AdditionalModules @() ", editorServicesModuleVersion); if (waitForDebugger) From 7225e9899e035f5b7c87808bd923d63f5114e48d Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 21 Mar 2018 17:16:55 -0700 Subject: [PATCH 4/9] Add EnableConsoleRepl flag --- test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs index e85b7991c..d3e4c18de 100644 --- a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs +++ b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs @@ -61,6 +61,7 @@ protected async Task> LaunchService( "-LogPath \"" + logPath + "\" " + "-SessionDetailsPath \".\\sessionDetails\" " + "-FeatureFlags @() " + + "-EnableConsoleRepl " + "-AdditionalModules @() ", editorServicesModuleVersion); From b876e30dc80e19a5aa313957cc2f3907a15fc441 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 21 Mar 2018 17:50:58 -0700 Subject: [PATCH 5/9] Revert EnableConsoleRepl --- test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs index d3e4c18de..e85b7991c 100644 --- a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs +++ b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs @@ -61,7 +61,6 @@ protected async Task> LaunchService( "-LogPath \"" + logPath + "\" " + "-SessionDetailsPath \".\\sessionDetails\" " + "-FeatureFlags @() " + - "-EnableConsoleRepl " + "-AdditionalModules @() ", editorServicesModuleVersion); From be10c364d2915476221b9a907d05f27969dd5253 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 21 Mar 2018 20:19:43 -0700 Subject: [PATCH 6/9] read session data from file --- .../ServerTestsBase.cs | 50 +++++++------------ 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs index e85b7991c..4b5147107 100644 --- a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs +++ b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs @@ -11,6 +11,7 @@ using System.Diagnostics; using System.IO; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Test.Host @@ -35,12 +36,15 @@ protected async Task> LaunchService( string scriptPath = Path.Combine(modulePath, "Start-EditorServices.ps1"); #if CoreCLR + string assemblyPath = this.GetType().GetTypeInfo().Assembly.Location; FileVersionInfo fileVersionInfo = - FileVersionInfo.GetVersionInfo(this.GetType().GetTypeInfo().Assembly.Location); + FileVersionInfo.GetVersionInfo(assemblyPath); #else + string assemblyPath = this.GetType().Assembly.Location; FileVersionInfo fileVersionInfo = - FileVersionInfo.GetVersionInfo(this.GetType().Assembly.Location); + FileVersionInfo.GetVersionInfo(assemblyPath); #endif + string sessionPath = Path.Combine(Path.GetDirectoryName(assemblyPath), "session.json"); string editorServicesModuleVersion = string.Format( @@ -59,7 +63,7 @@ protected async Task> LaunchService( "-BundledModulesPath \\\"" + modulePath + "\\\" " + "-LogLevel \"Verbose\" " + "-LogPath \"" + logPath + "\" " + - "-SessionDetailsPath \".\\sessionDetails\" " + + "-SessionDetailsPath \"" + sessionPath + "\" " + "-FeatureFlags @() " + "-AdditionalModules @() ", editorServicesModuleVersion); @@ -98,40 +102,20 @@ protected async Task> LaunchService( this.serviceProcess.Start(); // Wait for the server to finish initializing - Task stdoutTask = this.serviceProcess.StandardOutput.ReadLineAsync(); - Task stderrTask = this.serviceProcess.StandardError.ReadLineAsync(); - Task completedRead = await Task.WhenAny(stdoutTask, stderrTask); - - if (completedRead == stdoutTask) + while(!File.Exists(sessionPath)) { - JObject result = JObject.Parse(completedRead.Result); - if (result["status"].Value() == "started") - { - return new Tuple( - result["languageServicePort"].Value(), - result["debugServicePort"].Value()); - } - - return null; + Thread.Sleep(100); } - else - { - // Must have read an error? Keep reading from error stream - string errorString = completedRead.Result; - Task errorRead = this.serviceProcess.StandardError.ReadToEndAsync(); - // Lets give the read operation 5 seconds to complete. Ideally, it shouldn't - // take that long at all, but just in case... - if (errorRead.Wait(5000)) - { - if (!string.IsNullOrEmpty(errorRead.Result)) - { - errorString += errorRead.Result + Environment.NewLine; - } - } - - throw new Exception("Could not launch powershell.exe:\r\n\r\n" + errorString); + JObject result = JObject.Parse(File.ReadAllText(sessionPath)); + if (result["status"].Value() == "started") + { + return new Tuple( + result["languageServicePort"].Value(), + result["debugServicePort"].Value()); } + + return null; } protected void KillService() From 33e350e1750eade89ff1ca869f0eb4e1a61f1d59 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 21 Mar 2018 20:46:55 -0700 Subject: [PATCH 7/9] add file length check as well --- test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs index 4b5147107..ac0e468f8 100644 --- a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs +++ b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs @@ -102,7 +102,7 @@ protected async Task> LaunchService( this.serviceProcess.Start(); // Wait for the server to finish initializing - while(!File.Exists(sessionPath)) + while(!File.Exists(sessionPath) && new FileInfo(assemblyPath).Length > 0) { Thread.Sleep(100); } From 9921019e6df3d7683a2a01e305c74f2e0462e5b3 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 21 Mar 2018 20:48:16 -0700 Subject: [PATCH 8/9] add CoreCLR System.Reflection --- test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs index ac0e468f8..4296dfc7c 100644 --- a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs +++ b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs @@ -10,6 +10,9 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.IO; +#if CoreCLR +using System.Reflection; +#endif using System.Text; using System.Threading; using System.Threading.Tasks; From 3d8a32216d232d2dfb64c2a3dbb0b4cfccceaf40 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Wed, 21 Mar 2018 23:28:54 -0600 Subject: [PATCH 9/9] Fix issue with checking if session file available Change to check length of sessionPath file and only if file exists test passes. Update to use CodeBase which is the original location of the file under bin dir. Location is in the temp dir when run under xUnit. --- .../ServerTestsBase.cs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs index 4296dfc7c..0d89acf51 100644 --- a/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs +++ b/test/PowerShellEditorServices.Test.Host/ServerTestsBase.cs @@ -10,9 +10,7 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.IO; -#if CoreCLR using System.Reflection; -#endif using System.Text; using System.Threading; using System.Threading.Tasks; @@ -21,6 +19,7 @@ namespace Microsoft.PowerShell.EditorServices.Test.Host { public class ServerTestsBase { + private static int sessionCounter; private Process serviceProcess; protected IMessageSender messageSender; protected IMessageHandlers messageHandlers; @@ -39,15 +38,18 @@ protected async Task> LaunchService( string scriptPath = Path.Combine(modulePath, "Start-EditorServices.ps1"); #if CoreCLR - string assemblyPath = this.GetType().GetTypeInfo().Assembly.Location; - FileVersionInfo fileVersionInfo = - FileVersionInfo.GetVersionInfo(assemblyPath); + Assembly assembly = this.GetType().GetTypeInfo().Assembly; #else - string assemblyPath = this.GetType().Assembly.Location; + Assembly assembly = this.GetType().Assembly; +#endif + + string assemblyPath = new Uri(assembly.CodeBase).LocalPath; FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(assemblyPath); -#endif - string sessionPath = Path.Combine(Path.GetDirectoryName(assemblyPath), "session.json"); + + string sessionPath = + Path.Combine( + Path.GetDirectoryName(assemblyPath), $"session-{++sessionCounter}.json"); string editorServicesModuleVersion = string.Format( @@ -105,7 +107,7 @@ protected async Task> LaunchService( this.serviceProcess.Start(); // Wait for the server to finish initializing - while(!File.Exists(sessionPath) && new FileInfo(assemblyPath).Length > 0) + while (!File.Exists(sessionPath) || (new FileInfo(sessionPath).Length == 0)) { Thread.Sleep(100); }