@@ -44,99 +44,213 @@ param(
4444 [ValidateNotNullOrEmpty ()]
4545 $LogPath ,
4646
47- [ValidateSet (" Normal" , " Verbose" , " Error" , " Diagnostic" )]
47+ [ValidateSet (" Diagnostic " , " Normal" , " Verbose" , " Error" , " Diagnostic" )]
4848 $LogLevel ,
4949
50+ [Parameter (Mandatory = $true )]
51+ [ValidateNotNullOrEmpty ()]
52+ [string ]
53+ $SessionDetailsPath ,
54+
55+ [switch ]
56+ $EnableConsoleRepl ,
57+
58+ [switch ]
59+ $DebugServiceOnly ,
60+
61+ [string []]
62+ $AdditionalModules ,
63+
64+ [string []]
65+ $FeatureFlags ,
66+
5067 [switch ]
5168 $WaitForDebugger ,
5269
5370 [switch ]
5471 $ConfirmInstall
5572)
5673
74+ $minPortNumber = 10000
75+ $maxPortNumber = 30000
76+
77+ if ($LogLevel -eq " Diagnostic" ) {
78+ $VerbosePreference = ' Continue'
79+ Start-Transcript (Join-Path (Split-Path $LogPath - Parent) Start-EditorServices .log) - Force
80+ }
81+
82+ function LogSection ([string ]$msg ) {
83+ Write-Verbose " `n #-- $msg $ ( ' -' * ([Math ]::Max(0 , 73 - $msg.Length ))) "
84+ }
85+
86+ function Log ([string []]$msg ) {
87+ $msg | Write-Verbose
88+ }
89+
90+ function ExitWithError ($errorString ) {
91+ Write-Host - ForegroundColor Red " `n`n $errorString "
92+
93+ # Sleep for a while to make sure the user has time to see and copy the
94+ # error message
95+ Start-Sleep - Seconds 300
96+
97+ exit 1 ;
98+ }
99+
100+ # Are we running in PowerShell 2 or earlier?
101+ if ($PSVersionTable.PSVersion.Major -le 2 ) {
102+ # No ConvertTo-Json on PSv2 and below, so write out the JSON manually
103+ " {`" status`" : `" failed`" , `" reason`" : `" unsupported`" , `" powerShellVersion`" : `" $ ( $PSVersionTable.PSVersion.ToString ()) `" }" |
104+ Set-Content - Force - Path " $SessionDetailsPath " - ErrorAction Stop
105+
106+ ExitWithError " Unsupported PowerShell version $ ( $PSVersionTable.PSVersion ) , language features are disabled."
107+ }
108+
109+ function WriteSessionFile ($sessionInfo ) {
110+ $sessionInfoJson = ConvertTo-Json - InputObject $sessionInfo - Compress
111+ Log " Writing session file with contents:"
112+ Log $sessionInfoJson
113+ $sessionInfoJson | Set-Content - Force - Path " $SessionDetailsPath " - ErrorAction Stop
114+ }
115+
116+ if ($host.Runspace.LanguageMode -eq ' ConstrainedLanguage' ) {
117+ WriteSessionFile @ {
118+ " status" = " failed"
119+ " reason" = " languageMode"
120+ " detail" = $host.Runspace.LanguageMode.ToString ()
121+ }
122+
123+ ExitWithError " PowerShell is configured with an unsupported LanguageMode (ConstrainedLanguage), language features are disabled."
124+ }
125+
126+ # Are we running in PowerShell 5 or later?
127+ $isPS5orLater = $PSVersionTable.PSVersion.Major -ge 5
128+
129+ # If PSReadline is present in the session, remove it so that runspace
130+ # management is easier
131+ if ((Get-Module PSReadline).Count -gt 0 ) {
132+ LogSection " Removing PSReadLine module"
133+ Remove-Module PSReadline - ErrorAction SilentlyContinue
134+ }
135+
57136# This variable will be assigned later to contain information about
58137# what happened while attempting to launch the PowerShell Editor
59138# Services host
60139$resultDetails = $null ;
61140
62141function Test-ModuleAvailable ($ModuleName , $ModuleVersion ) {
142+ Log " Testing module availability $ModuleName $ModuleVersion "
143+
63144 $modules = Get-Module - ListAvailable $moduleName
64145 if ($modules -ne $null ) {
65146 if ($ModuleVersion -ne $null ) {
66147 foreach ($module in $modules ) {
67148 if ($module.Version.Equals ($moduleVersion )) {
149+ Log " $ModuleName $ModuleVersion found"
68150 return $true ;
69151 }
70152 }
71153 }
72154 else {
155+ Log " $ModuleName $ModuleVersion found"
73156 return $true ;
74157 }
75158 }
76159
160+ Log " $ModuleName $ModuleVersion NOT found"
77161 return $false ;
78162}
79163
80- function Test-PortAvailability ($PortNumber ) {
81- $portAvailable = $true ;
164+ function Test-PortAvailability {
165+ param (
166+ [Parameter (Mandatory = $true )]
167+ [int ]
168+ $PortNumber
169+ )
170+
171+ $portAvailable = $true
82172
83173 try {
84- $ipAddress = [System.Net.Dns ]::GetHostEntryAsync(" localhost" ).Result.AddressList[0 ];
85- $tcpListener = [System.Net.Sockets.TcpListener ]::new($ipAddress , $portNumber );
86- $tcpListener.Start ();
87- $tcpListener.Stop ();
174+ if ($isPS5orLater ) {
175+ $ipAddresses = [System.Net.Dns ]::GetHostEntryAsync(" localhost" ).Result.AddressList
176+ }
177+ else {
178+ $ipAddresses = [System.Net.Dns ]::GetHostEntry(" localhost" ).AddressList
179+ }
88180
181+ foreach ($ipAddress in $ipAddresses )
182+ {
183+ Log " Testing availability of port ${PortNumber} at address ${ipAddress} / $ ( $ipAddress.AddressFamily ) "
184+
185+ $tcpListener = New-Object System.Net.Sockets.TcpListener @ ($ipAddress , $PortNumber )
186+ $tcpListener.Start ()
187+ $tcpListener.Stop ()
188+ }
89189 }
90190 catch [System.Net.Sockets.SocketException ] {
191+ $portAvailable = $false
192+
91193 # Check the SocketErrorCode to see if it's the expected exception
92- if ($error [ 0 ] .Exception.InnerException .SocketErrorCode -eq [System.Net.Sockets.SocketError ]::AddressAlreadyInUse) {
93- $portAvailable = $false ;
194+ if ($_ .Exception.SocketErrorCode -eq [System.Net.Sockets.SocketError ]::AddressAlreadyInUse) {
195+ Log " Port $PortNumber is in use. "
94196 }
95197 else {
96- Write-Output ( " Error code: " + $error [ 0 ].SocketErrorCode)
198+ Log " SocketException on port ${PortNumber} : $ ( $_ .Exception ) "
97199 }
98200 }
99201
100- return $portAvailable ;
202+ $portAvailable
101203}
102204
103- $rand = [System.Random ]::new()
104- function Get-AvailablePort {
205+ $portsInUse = @ {}
206+ $rand = New-Object System.Random
207+ function Get-AvailablePort () {
105208 $triesRemaining = 10 ;
106209
107210 while ($triesRemaining -gt 0 ) {
108- $port = $rand.Next (10000 , 30000 )
109- if ((Test-PortAvailability - PortAvailability $port ) -eq $true ) {
211+ do {
212+ $port = $rand.Next ($minPortNumber , $maxPortNumber )
213+ }
214+ while ($portsInUse.ContainsKey ($port ))
215+
216+ # Whether we succeed or fail, don't try this port again
217+ $portsInUse [$port ] = 1
218+
219+ Log " Checking port: $port , attempts remaining $triesRemaining --------------------"
220+ if ((Test-PortAvailability - PortNumber $port ) -eq $true ) {
221+ Log " Port: $port is available"
110222 return $port
111223 }
112224
225+ Log " Port: $port is NOT available"
113226 $triesRemaining -- ;
114227 }
115228
229+ Log " Did not find any available ports!!"
116230 return $null
117231}
118232
119- # OUTPUT PROTOCOL
120- # - "started 29981 39898" - Server(s) are started, language and debug server ports (respectively)
121- # - "failed Error message describing the failure" - General failure while starting, show error message to user (?)
122- # - "needs_install" - User should be prompted to install PowerShell Editor Services via the PowerShell Gallery
123-
124233# Add BundledModulesPath to $env:PSModulePath
125234if ($BundledModulesPath ) {
126- $env: PSMODULEPATH = $BundledModulesPath + [System.IO.Path ]::PathSeparator + $env: PSMODULEPATH
235+ $env: PSModulePath = $env: PSModulePath.TrimEnd ([System.IO.Path ]::PathSeparator) + [System.IO.Path ]::PathSeparator + $BundledModulesPath
236+ LogSection " Updated PSModulePath to:"
237+ Log ($env: PSModulePath -split [System.IO.Path ]::PathSeparator)
127238}
128239
240+ LogSection " Check required modules available"
129241# Check if PowerShellGet module is available
130242if ((Test-ModuleAvailable " PowerShellGet" ) -eq $false ) {
243+ Log " Failed to find PowerShellGet module"
131244 # TODO: WRITE ERROR
132245}
133246
134247# Check if the expected version of the PowerShell Editor Services
135248# module is installed
136- $parsedVersion = [ System.Version ]::new ($EditorServicesVersion )
137- if ((Test-ModuleAvailable " PowerShellEditorServices" - RequiredVersion $parsedVersion ) -eq $false ) {
138- if ($ConfirmInstall ) {
249+ $parsedVersion = New-Object System.Version @ ($EditorServicesVersion )
250+ if ((Test-ModuleAvailable " PowerShellEditorServices" $parsedVersion ) -eq $false ) {
251+ if ($ConfirmInstall -and $isPS5orLater ) {
139252 # TODO: Check for error and return failure if necessary
253+ LogSection " Install PowerShellEditorServices"
140254 Install-Module " PowerShellEditorServices" - RequiredVersion $parsedVersion - Confirm
141255 }
142256 else {
@@ -146,49 +260,94 @@ if ((Test-ModuleAvailable "PowerShellEditorServices" -RequiredVersion $parsedVer
146260 }
147261}
148262
149- Import-Module PowerShellEditorServices - RequiredVersion $parsedVersion - ErrorAction Stop
263+ try {
264+ LogSection " Start up PowerShellEditorServices"
265+ Log " Importing PowerShellEditorServices"
266+
267+ if ($isPS5orLater ) {
268+ Import-Module PowerShellEditorServices - RequiredVersion $parsedVersion - ErrorAction Stop
269+ }
270+ else {
271+ Import-Module PowerShellEditorServices - Version $parsedVersion - ErrorAction Stop
272+ }
273+
274+ # Locate available port numbers for services
275+ Log " Searching for available socket port for the language service"
276+ $languageServicePort = Get-AvailablePort
277+
278+ Log " Searching for available socket port for the debug service"
279+ $debugServicePort = Get-AvailablePort
150280
151- # Locate available port numbers for services
152- $languageServicePort = Get-AvailablePort
153- $debugServicePort = Get-AvailablePort
281+ if (! $languageServicePort -or ! $debugServicePort ) {
282+ ExitWithError " Failed to find an open socket port for either the language or debug service."
283+ }
284+
285+ if ($EnableConsoleRepl ) {
286+ Write-Host " PowerShell Integrated Console`n "
287+ }
288+
289+ # Create the Editor Services host
290+ Log " Invoking Start-EditorServicesHost"
291+ $editorServicesHost =
292+ Start-EditorServicesHost `
293+ - HostName $HostName `
294+ - HostProfileId $HostProfileId `
295+ - HostVersion $HostVersion `
296+ - LogPath $LogPath `
297+ - LogLevel $LogLevel `
298+ - AdditionalModules $AdditionalModules `
299+ - LanguageServicePort $languageServicePort `
300+ - DebugServicePort $debugServicePort `
301+ - BundledModulesPath $BundledModulesPath `
302+ - EnableConsoleRepl:$EnableConsoleRepl.IsPresent `
303+ - DebugServiceOnly:$DebugServiceOnly.IsPresent `
304+ - WaitForDebugger:$WaitForDebugger.IsPresent
305+
306+ # TODO: Verify that the service is started
307+ Log " Start-EditorServicesHost returned $editorServicesHost "
308+
309+ $resultDetails = @ {
310+ " status" = " started" ;
311+ " channel" = " tcp" ;
312+ " languageServicePort" = $languageServicePort ;
313+ " debugServicePort" = $debugServicePort ;
314+ }
315+
316+ # Notify the client that the services have started
317+ WriteSessionFile $resultDetails
154318
155- $editorServicesHost =
156- Start-EditorServicesHost `
157- - HostName $HostName `
158- - HostProfileId $HostProfileId `
159- - HostVersion $HostVersion `
160- - LogPath $LogPath `
161- - LogLevel $LogLevel `
162- - AdditionalModules @ () `
163- - LanguageServicePort $languageServicePort `
164- - DebugServicePort $debugServicePort `
165- - BundledModulesPath $BundledModulesPath `
166- - WaitForDebugger:$WaitForDebugger.IsPresent
319+ Log " Wrote out session file"
320+ }
321+ catch [System.Exception ] {
322+ $e = $_.Exception ;
323+ $errorString = " "
167324
168- # TODO: Verify that the service is started
325+ Log " ERRORS caught starting up EditorServicesHost "
169326
170- $resultDetails = @ {
171- " status" = " started" ;
172- " channel" = " tcp" ;
173- " languageServicePort" = $languageServicePort ;
174- " debugServicePort" = $debugServicePort ;
175- };
327+ while ($e -ne $null ) {
328+ $errorString = $errorString + ($e.Message + " `r`n " + $e.StackTrace + " `r`n " )
329+ $e = $e.InnerException ;
330+ Log $errorString
331+ }
176332
177- # Notify the client that the services have started
178- Write-Output ( ConvertTo-Json - InputObject $resultDetails - Compress)
333+ ExitWithError ( " An error occurred while starting PowerShell Editor Services: `r`n`r`n " + $errorString )
334+ }
179335
180336try {
181337 # Wait for the host to complete execution before exiting
338+ LogSection " Waiting for EditorServicesHost to complete execution"
182339 $editorServicesHost.WaitForCompletion ()
340+ Log " EditorServicesHost has completed execution"
183341}
184342catch [System.Exception ] {
185- $e = $_.Exception ; # .InnerException;
343+ $e = $_.Exception ;
186344 $errorString = " "
187345
346+ Log " ERRORS caught while waiting for EditorServicesHost to complete execution"
347+
188348 while ($e -ne $null ) {
189349 $errorString = $errorString + ($e.Message + " `r`n " + $e.StackTrace + " `r`n " )
190350 $e = $e.InnerException ;
351+ Log $errorString
191352 }
192-
193- Write-Error (" `r`n Caught error while waiting for EditorServicesHost to complete:`r`n " + $errorString )
194353}
0 commit comments