@@ -44,99 +44,213 @@ param(
44
44
[ValidateNotNullOrEmpty ()]
45
45
$LogPath ,
46
46
47
- [ValidateSet (" Normal" , " Verbose" , " Error" , " Diagnostic" )]
47
+ [ValidateSet (" Diagnostic " , " Normal" , " Verbose" , " Error" , " Diagnostic" )]
48
48
$LogLevel ,
49
49
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
+
50
67
[switch ]
51
68
$WaitForDebugger ,
52
69
53
70
[switch ]
54
71
$ConfirmInstall
55
72
)
56
73
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
+
57
136
# This variable will be assigned later to contain information about
58
137
# what happened while attempting to launch the PowerShell Editor
59
138
# Services host
60
139
$resultDetails = $null ;
61
140
62
141
function Test-ModuleAvailable ($ModuleName , $ModuleVersion ) {
142
+ Log " Testing module availability $ModuleName $ModuleVersion "
143
+
63
144
$modules = Get-Module - ListAvailable $moduleName
64
145
if ($modules -ne $null ) {
65
146
if ($ModuleVersion -ne $null ) {
66
147
foreach ($module in $modules ) {
67
148
if ($module.Version.Equals ($moduleVersion )) {
149
+ Log " $ModuleName $ModuleVersion found"
68
150
return $true ;
69
151
}
70
152
}
71
153
}
72
154
else {
155
+ Log " $ModuleName $ModuleVersion found"
73
156
return $true ;
74
157
}
75
158
}
76
159
160
+ Log " $ModuleName $ModuleVersion NOT found"
77
161
return $false ;
78
162
}
79
163
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
82
172
83
173
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
+ }
88
180
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
+ }
89
189
}
90
190
catch [System.Net.Sockets.SocketException ] {
191
+ $portAvailable = $false
192
+
91
193
# 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. "
94
196
}
95
197
else {
96
- Write-Output ( " Error code: " + $error [ 0 ].SocketErrorCode)
198
+ Log " SocketException on port ${PortNumber} : $ ( $_ .Exception ) "
97
199
}
98
200
}
99
201
100
- return $portAvailable ;
202
+ $portAvailable
101
203
}
102
204
103
- $rand = [System.Random ]::new()
104
- function Get-AvailablePort {
205
+ $portsInUse = @ {}
206
+ $rand = New-Object System.Random
207
+ function Get-AvailablePort () {
105
208
$triesRemaining = 10 ;
106
209
107
210
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"
110
222
return $port
111
223
}
112
224
225
+ Log " Port: $port is NOT available"
113
226
$triesRemaining -- ;
114
227
}
115
228
229
+ Log " Did not find any available ports!!"
116
230
return $null
117
231
}
118
232
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
-
124
233
# Add BundledModulesPath to $env:PSModulePath
125
234
if ($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)
127
238
}
128
239
240
+ LogSection " Check required modules available"
129
241
# Check if PowerShellGet module is available
130
242
if ((Test-ModuleAvailable " PowerShellGet" ) -eq $false ) {
243
+ Log " Failed to find PowerShellGet module"
131
244
# TODO: WRITE ERROR
132
245
}
133
246
134
247
# Check if the expected version of the PowerShell Editor Services
135
248
# 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 ) {
139
252
# TODO: Check for error and return failure if necessary
253
+ LogSection " Install PowerShellEditorServices"
140
254
Install-Module " PowerShellEditorServices" - RequiredVersion $parsedVersion - Confirm
141
255
}
142
256
else {
@@ -146,49 +260,94 @@ if ((Test-ModuleAvailable "PowerShellEditorServices" -RequiredVersion $parsedVer
146
260
}
147
261
}
148
262
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
150
280
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
154
318
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 = " "
167
324
168
- # TODO: Verify that the service is started
325
+ Log " ERRORS caught starting up EditorServicesHost "
169
326
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
+ }
176
332
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
+ }
179
335
180
336
try {
181
337
# Wait for the host to complete execution before exiting
338
+ LogSection " Waiting for EditorServicesHost to complete execution"
182
339
$editorServicesHost.WaitForCompletion ()
340
+ Log " EditorServicesHost has completed execution"
183
341
}
184
342
catch [System.Exception ] {
185
- $e = $_.Exception ; # .InnerException;
343
+ $e = $_.Exception ;
186
344
$errorString = " "
187
345
346
+ Log " ERRORS caught while waiting for EditorServicesHost to complete execution"
347
+
188
348
while ($e -ne $null ) {
189
349
$errorString = $errorString + ($e.Message + " `r`n " + $e.StackTrace + " `r`n " )
190
350
$e = $e.InnerException ;
351
+ Log $errorString
191
352
}
192
-
193
- Write-Error (" `r`n Caught error while waiting for EditorServicesHost to complete:`r`n " + $errorString )
194
353
}
0 commit comments