ruby-pwsh 0.7.2 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -15,371 +15,48 @@ param (
15
15
 
16
16
  $script:EmitDebugOutput = $EmitDebugOutput
17
17
  # Necessary for [System.Console]::Error.WriteLine to roundtrip with UTF-8
18
+ # Need to ensure we ignore encoding from other places and are consistent internally
18
19
  [System.Console]::OutputEncoding = $Encoding
19
20
 
20
- $hostSource = @"
21
- using System;
22
- using System.Collections.Generic;
23
- using System.Collections.ObjectModel;
24
- using System.Globalization;
25
- using System.IO;
26
- using System.Management.Automation;
27
- using System.Management.Automation.Host;
28
- using System.Security;
29
- using System.Text;
30
- using System.Threading;
31
-
32
- namespace Puppet
33
- {
34
- public class PuppetPSHostRawUserInterface : PSHostRawUserInterface
35
- {
36
- public PuppetPSHostRawUserInterface()
37
- {
38
- buffersize = new Size(120, 120);
39
- backgroundcolor = ConsoleColor.Black;
40
- foregroundcolor = ConsoleColor.White;
41
- cursorposition = new Coordinates(0, 0);
42
- cursorsize = 1;
43
- }
44
-
45
- private ConsoleColor backgroundcolor;
46
- public override ConsoleColor BackgroundColor
47
- {
48
- get { return backgroundcolor; }
49
- set { backgroundcolor = value; }
50
- }
51
-
52
- private Size buffersize;
53
- public override Size BufferSize
54
- {
55
- get { return buffersize; }
56
- set { buffersize = value; }
57
- }
58
-
59
- private Coordinates cursorposition;
60
- public override Coordinates CursorPosition
61
- {
62
- get { return cursorposition; }
63
- set { cursorposition = value; }
64
- }
65
-
66
- private int cursorsize;
67
- public override int CursorSize
68
- {
69
- get { return cursorsize; }
70
- set { cursorsize = value; }
71
- }
72
-
73
- private ConsoleColor foregroundcolor;
74
- public override ConsoleColor ForegroundColor
75
- {
76
- get { return foregroundcolor; }
77
- set { foregroundcolor = value; }
78
- }
79
-
80
- private Coordinates windowposition;
81
- public override Coordinates WindowPosition
82
- {
83
- get { return windowposition; }
84
- set { windowposition = value; }
85
- }
86
-
87
- private Size windowsize;
88
- public override Size WindowSize
89
- {
90
- get { return windowsize; }
91
- set { windowsize = value; }
92
- }
93
-
94
- private string windowtitle;
95
- public override string WindowTitle
96
- {
97
- get { return windowtitle; }
98
- set { windowtitle = value; }
99
- }
100
-
101
- public override bool KeyAvailable
102
- {
103
- get { return false; }
104
- }
105
-
106
- public override Size MaxPhysicalWindowSize
107
- {
108
- get { return new Size(165, 66); }
109
- }
110
-
111
- public override Size MaxWindowSize
112
- {
113
- get { return new Size(165, 66); }
114
- }
115
-
116
- public override void FlushInputBuffer()
117
- {
118
- throw new NotImplementedException();
119
- }
120
-
121
- public override BufferCell[,] GetBufferContents(Rectangle rectangle)
122
- {
123
- throw new NotImplementedException();
124
- }
125
-
126
- public override KeyInfo ReadKey(ReadKeyOptions options)
127
- {
128
- throw new NotImplementedException();
129
- }
130
-
131
- public override void ScrollBufferContents(Rectangle source, Coordinates destination, Rectangle clip, BufferCell fill)
132
- {
133
- throw new NotImplementedException();
134
- }
135
-
136
- public override void SetBufferContents(Rectangle rectangle, BufferCell fill)
137
- {
138
- throw new NotImplementedException();
139
- }
140
-
141
- public override void SetBufferContents(Coordinates origin, BufferCell[,] contents)
142
- {
143
- throw new NotImplementedException();
144
- }
145
- }
146
-
147
- public class PuppetPSHostUserInterface : PSHostUserInterface
148
- {
149
- private PuppetPSHostRawUserInterface _rawui;
150
- private StringBuilder _sb;
151
- private StringWriter _errWriter;
152
- private StringWriter _outWriter;
153
-
154
- public PuppetPSHostUserInterface()
155
- {
156
- _sb = new StringBuilder();
157
- _errWriter = new StringWriter(new StringBuilder());
158
- // NOTE: StringWriter / StringBuilder are not technically thread-safe
159
- // but PowerShell Write-XXX cmdlets and System.Console.Out.WriteXXX
160
- // should not be executed concurrently within PowerShell, so should be safe
161
- _outWriter = new StringWriter(_sb);
162
- }
163
-
164
- public override PSHostRawUserInterface RawUI
165
- {
166
- get
167
- {
168
- if ( _rawui == null){
169
- _rawui = new PuppetPSHostRawUserInterface();
170
- }
171
- return _rawui;
172
- }
173
- }
174
-
175
- public void ResetConsoleStreams()
176
- {
177
- System.Console.SetError(_errWriter);
178
- System.Console.SetOut(_outWriter);
179
- }
180
-
181
- public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value)
182
- {
183
- _sb.Append(value);
184
- }
185
-
186
- public override void Write(string value)
187
- {
188
- _sb.Append(value);
189
- }
190
-
191
- public override void WriteDebugLine(string message)
192
- {
193
- _sb.AppendLine("DEBUG: " + message);
194
- }
195
-
196
- public override void WriteErrorLine(string value)
197
- {
198
- _sb.AppendLine(value);
199
- }
200
-
201
- public override void WriteLine(string value)
202
- {
203
- _sb.AppendLine(value);
204
- }
205
-
206
- public override void WriteVerboseLine(string message)
207
- {
208
- _sb.AppendLine("VERBOSE: " + message);
209
- }
210
-
211
- public override void WriteWarningLine(string message)
212
- {
213
- _sb.AppendLine("WARNING: " + message);
214
- }
215
-
216
- public override void WriteProgress(long sourceId, ProgressRecord record)
217
- {
218
- }
219
-
220
- public string Output
221
- {
222
- get
223
- {
224
- _outWriter.Flush();
225
- string text = _outWriter.GetStringBuilder().ToString();
226
- _outWriter.GetStringBuilder().Length = 0; // Only .NET 4+ has .Clear()
227
- return text;
228
- }
229
- }
230
-
231
- public string StdErr
232
- {
233
- get
234
- {
235
- _errWriter.Flush();
236
- string text = _errWriter.GetStringBuilder().ToString();
237
- _errWriter.GetStringBuilder().Length = 0; // Only .NET 4+ has .Clear()
238
- return text;
239
- }
240
- }
241
-
242
- public override Dictionary<string, PSObject> Prompt(string caption, string message, Collection<FieldDescription> descriptions)
243
- {
244
- throw new NotImplementedException();
245
- }
246
-
247
- public override int PromptForChoice(string caption, string message, Collection<ChoiceDescription> choices, int defaultChoice)
248
- {
249
- throw new NotImplementedException();
250
- }
251
-
252
- public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName)
253
- {
254
- throw new NotImplementedException();
255
- }
256
-
257
- public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options)
258
- {
259
- throw new NotImplementedException();
260
- }
261
-
262
- public override string ReadLine()
263
- {
264
- throw new NotImplementedException();
265
- }
266
-
267
- public override SecureString ReadLineAsSecureString()
268
- {
269
- throw new NotImplementedException();
270
- }
271
- }
272
-
273
- public class PuppetPSHost : PSHost
274
- {
275
- private Guid _hostId = Guid.NewGuid();
276
- private bool shouldExit;
277
- private int exitCode;
278
-
279
- private readonly PuppetPSHostUserInterface _ui = new PuppetPSHostUserInterface();
280
-
281
- public PuppetPSHost () {}
282
-
283
- public bool ShouldExit { get { return this.shouldExit; } }
284
- public int ExitCode { get { return this.exitCode; } }
285
- public void ResetExitStatus()
286
- {
287
- this.exitCode = 0;
288
- this.shouldExit = false;
289
- }
290
- public void ResetConsoleStreams()
291
- {
292
- _ui.ResetConsoleStreams();
293
- }
294
-
295
- public override Guid InstanceId { get { return _hostId; } }
296
- public override string Name { get { return "PuppetPSHost"; } }
297
- public override Version Version { get { return new Version(1, 1); } }
298
- public override PSHostUserInterface UI
299
- {
300
- get { return _ui; }
301
- }
302
- public override CultureInfo CurrentCulture
303
- {
304
- get { return Thread.CurrentThread.CurrentCulture; }
305
- }
306
- public override CultureInfo CurrentUICulture
307
- {
308
- get { return Thread.CurrentThread.CurrentUICulture; }
309
- }
310
-
311
- public override void EnterNestedPrompt() { throw new NotImplementedException(); }
312
- public override void ExitNestedPrompt() { throw new NotImplementedException(); }
313
- public override void NotifyBeginApplication() { return; }
314
- public override void NotifyEndApplication() { return; }
315
-
316
- public override void SetShouldExit(int exitCode)
317
- {
318
- this.shouldExit = true;
319
- this.exitCode = exitCode;
320
- }
321
- }
322
- }
323
- "@
21
+ $TemplateFolderPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Path
22
+ $hostSource = Get-Content -Path "$TemplateFolderPath/RubyPwsh.cs" -Raw
324
23
 
24
+ # Load the Custom PowerShell Host CSharp code
325
25
  Add-Type -TypeDefinition $hostSource -Language CSharp
26
+
27
+ # Cache the current directory as the working directory for the Dynamic PowerShell session
326
28
  $global:DefaultWorkingDirectory = (Get-Location -PSProvider FileSystem).Path
327
29
 
30
+ # Cache initial Environment Variables and values prior to any munging:
31
+ $global:CachedEnvironmentVariables = Get-ChildItem -Path Env:\
32
+
328
33
  #this is a string so we can import into our dynamic PS instance
329
34
  $global:ourFunctions = @'
330
- function Get-ProcessEnvironmentVariables
331
- {
332
- $processVars = [Environment]::GetEnvironmentVariables('Process').Keys |
333
- % -Begin { $h = @{} } -Process { $h.$_ = (Get-Item Env:\$_).Value } -End { $h }
334
-
335
- # eliminate Machine / User vars so that we have only process vars
336
- 'Machine', 'User' |
337
- % { [Environment]::GetEnvironmentVariables($_).GetEnumerator() } |
338
- ? { $processVars.ContainsKey($_.Name) -and ($processVars[$_.Name] -eq $_.Value) } |
339
- % { $processVars.Remove($_.Name) }
340
-
341
- $processVars.GetEnumerator() | Sort-Object Name
342
- }
343
-
344
- function Reset-ProcessEnvironmentVariables
345
- {
346
- param($processVars)
347
-
348
- # query Machine vars from registry, ensuring expansion EXCEPT for PATH
349
- $vars = [Environment]::GetEnvironmentVariables('Machine').GetEnumerator() |
350
- % -Begin { $h = @{} } -Process { $v = if ($_.Name -eq 'Path') { $_.Value } else { [Environment]::GetEnvironmentVariable($_.Name, 'Machine') }; $h."$($_.Name)" = $v } -End { $h }
351
-
352
- # query User vars from registry, ensuring expansion EXCEPT for PATH
353
- [Environment]::GetEnvironmentVariables('User').GetEnumerator() | % {
354
- if ($_.Name -eq 'Path') { $vars[$_.Name] += ';' + $_.Value }
355
- else
356
- {
357
- $value = [Environment]::GetEnvironmentVariable($_.Name, 'User')
358
- $vars[$_.Name] = $value
359
- }
360
- }
361
-
362
- $processVars.GetEnumerator() | % { $vars[$_.Name] = $_.Value }
35
+ function Reset-ProcessEnvironmentVariables {
36
+ param($CachedEnvironmentVariables)
363
37
 
38
+ # Delete existing environment variables
364
39
  Remove-Item -Path Env:\* -ErrorAction SilentlyContinue -WarningAction SilentlyContinue -Recurse
365
40
 
366
- $vars.GetEnumerator() | % { Set-Item -Path "Env:\$($_.Name)" -Value $_.Value }
41
+ # Re-add the cached environment variables
42
+ $CachedEnvironmentVariables |
43
+ ForEach-Object -Process { Set-Item -Path "Env:\$($_.Name)" -Value $_.Value }
367
44
  }
368
45
 
369
- function Reset-ProcessPowerShellVariables
370
- {
46
+ function Reset-ProcessPowerShellVariables {
371
47
  param($psVariables)
372
- $psVariables | %{
373
- $tempVar = $_
374
- if(-not(Get-Variable -Name $_.Name -ErrorAction SilentlyContinue)){
375
- New-Variable -Name $_.Name -Value $_.Value -Description $_.Description -Option $_.Options -Visibility $_.Visibility
48
+
49
+ $psVariables |
50
+ ForEach-Object -Process {
51
+ $tempVar = $_
52
+ if (-not(Get-Variable -Name $_.Name -ErrorAction SilentlyContinue)) {
53
+ New-Variable -Name $_.Name -Value $_.Value -Description $_.Description -Option $_.Options -Visibility $_.Visibility
54
+ }
376
55
  }
377
- }
378
56
  }
379
57
  '@
380
58
 
381
- function Invoke-PowerShellUserCode
382
- {
59
+ function Invoke-PowerShellUserCode {
383
60
  [CmdletBinding()]
384
61
  param(
385
62
  [String]
@@ -392,37 +69,47 @@ function Invoke-PowerShellUserCode
392
69
  $WorkingDirectory,
393
70
 
394
71
  [Hashtable]
395
- $ExecEnvironmentVariables
72
+ $AdditionalEnvironmentVariables
396
73
  )
397
74
 
75
+ # Instantiate the PowerShell Host and a new runspace to use if one is not already defined.
398
76
  if ($global:runspace -eq $null){
399
77
  # CreateDefault2 requires PS3
78
+ # Setup Initial Session State - can be modified later, defaults to only core PowerShell
79
+ # commands loaded/available. Everything else will dynamically load when needed.
400
80
  if ([System.Management.Automation.Runspaces.InitialSessionState].GetMethod('CreateDefault2')){
401
81
  $sessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault2()
402
- }else{
82
+ } else {
403
83
  $sessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
404
84
  }
405
85
 
406
- $global:puppetPSHost = New-Object Puppet.PuppetPSHost
407
- $global:runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($global:puppetPSHost, $sessionState)
86
+ $global:RubyPwshPSHost = New-Object RubyPwsh.RubyPwshPSHost
87
+ $global:runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($global:RubyPwshPSHost, $sessionState)
408
88
  $global:runspace.Open()
409
89
  }
410
90
 
411
- try
412
- {
91
+ try {
92
+ # Reset the PowerShell handle, exit status, and streams.
413
93
  $ps = $null
414
- $global:puppetPSHost.ResetExitStatus()
415
- $global:puppetPSHost.ResetConsoleStreams()
94
+ $global:RubyPwshPSHost.ResetExitStatus()
95
+ $global:RubyPwshPSHost.ResetConsoleStreams()
416
96
 
97
+ # This resets the variables from prior runs, clearing them from memory.
417
98
  if ($PSVersionTable.PSVersion -ge [Version]'3.0') {
418
99
  $global:runspace.ResetRunspaceState()
419
100
  }
420
101
 
102
+ # Create a new instance of the PowerShell handle and drop into our reused runspace.
421
103
  $ps = [System.Management.Automation.PowerShell]::Create()
422
104
  $ps.Runspace = $global:runspace
105
+
106
+ # Preload our own functions; this could be moved into the array of startup scripts in the
107
+ # InitialSessionState, once implemented.
423
108
  [Void]$ps.AddScript($global:ourFunctions)
424
109
  $ps.Invoke()
425
110
 
111
+ # Set the working directory for the runspace; if not specified, use default; If it doesn't
112
+ # exist, terminate the execution and report the error back.
426
113
  if ([string]::IsNullOrEmpty($WorkingDirectory)) {
427
114
  [Void]$ps.Runspace.SessionStateProxy.Path.SetLocation($global:DefaultWorkingDirectory)
428
115
  } else {
@@ -430,13 +117,16 @@ function Invoke-PowerShellUserCode
430
117
  [Void]$ps.Runspace.SessionStateProxy.Path.SetLocation($WorkingDirectory)
431
118
  }
432
119
 
433
- if(!$global:environmentVariables){
434
- $ps.Commands.Clear()
435
- $global:environmentVariables = $ps.AddCommand('Get-ProcessEnvironmentVariables').Invoke()
436
- }
120
+ # Reset the environment variables to those cached at the instantiation of the PowerShell Host.
121
+ $ps.Commands.Clear()
122
+ [Void]$ps.AddCommand('Reset-ProcessEnvironmentVariables').AddParameter('CachedEnvironmentVariables', $global:CachedEnvironmentVariables)
123
+ $ps.Invoke()
437
124
 
438
- if($PSVersionTable.PSVersion -le [Version]'2.0'){
439
- if(!$global:psVariables){
125
+ # This code is the companion to the code at L403-405 and clears variables from prior runs.
126
+ # Because ResetRunspaceState does not work on v2 and earlier, it must be called here, after
127
+ # a new handle to PowerShell is created in prior steps.
128
+ if ($PSVersionTable.PSVersion -le [Version]'2.0'){
129
+ if (-not $global:psVariables){
440
130
  $global:psVariables = $ps.AddScript('Get-Variable').Invoke()
441
131
  }
442
132
 
@@ -449,110 +139,125 @@ function Invoke-PowerShellUserCode
449
139
  $ps.Invoke()
450
140
  }
451
141
 
452
- $ps.Commands.Clear()
453
- [Void]$ps.AddCommand('Reset-ProcessEnvironmentVariables').AddParameter('processVars', $global:environmentVariables)
454
- $ps.Invoke()
455
-
456
- # Set any exec level environment variables
457
- if ($ExecEnvironmentVariables -ne $null) {
458
- $ExecEnvironmentVariables.GetEnumerator() | % { Set-Item -Path "Env:\$($_.Name)" -Value $_.Value }
142
+ # Set any provided environment variables
143
+ if ($AdditionalEnvironmentVariables -ne $null) {
144
+ $AdditionalEnvironmentVariables.GetEnumerator() |
145
+ ForEach-Object -Process { Set-Item -Path "Env:\$($_.Name)" -Value $_.Value }
459
146
  }
460
147
 
461
- # we clear the commands before each new command
462
- # to avoid command pollution
148
+ # We clear the commands before each new command to avoid command pollution This does not need
149
+ # to be a single command, it works the same if you pass a string with multiple lines of
150
+ # PowerShell code. The user supplies a string and this gives it to the Host to execute.
463
151
  $ps.Commands.Clear()
464
152
  [Void]$ps.AddScript($Code)
465
153
 
466
- # out-default and MergeMyResults takes all output streams
467
- # and writes it to the PSHost we create
468
- # this needs to be the last thing executed
469
- [void]$ps.AddCommand("out-default");
154
+ # Out-Default and MergeMyResults takes all output streams and writes it to the PowerShell Host
155
+ # we create this needs to be the last thing executed.
156
+ [void]$ps.AddCommand("out-default")
470
157
 
471
- # if the call operator & established an exit code, exit with it
158
+ # if the call operator & established an exit code, exit with it; if this is NOT included, exit
159
+ # codes for scripts will not work; anything that does not throw will exit 0.
472
160
  [Void]$ps.AddScript('if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }')
473
161
 
474
- if($PSVersionTable.PSVersion -le [Version]'2.0'){
475
- $ps.Commands.Commands[0].MergeMyResults([System.Management.Automation.Runspaces.PipelineResultTypes]::Error, [System.Management.Automation.Runspaces.PipelineResultTypes]::Output);
476
- }else{
477
- $ps.Commands.Commands[0].MergeMyResults([System.Management.Automation.Runspaces.PipelineResultTypes]::All, [System.Management.Automation.Runspaces.PipelineResultTypes]::Output);
162
+ # This is the code that ensures the output from the Host is interleaved; this ensures
163
+ # everything written to the streams in the Host is returned.
164
+ if ($PSVersionTable.PSVersion -le [Version]'2.0') {
165
+ $ps.Commands.Commands[0].MergeMyResults([System.Management.Automation.Runspaces.PipelineResultTypes]::Error,
166
+ [System.Management.Automation.Runspaces.PipelineResultTypes]::Output)
167
+ } else {
168
+ $ps.Commands.Commands[0].MergeMyResults([System.Management.Automation.Runspaces.PipelineResultTypes]::All,
169
+ [System.Management.Automation.Runspaces.PipelineResultTypes]::Output)
478
170
  }
171
+
172
+ # The asynchronous execution enables a user to set a timeout for the execution of their
173
+ # provided code; this keeps the process from hanging eternally until an external caller
174
+ # times out or the user kills the process.
479
175
  $asyncResult = $ps.BeginInvoke()
480
176
 
481
- if (!$asyncResult.AsyncWaitHandle.WaitOne($TimeoutMilliseconds)){
177
+ if (-not $asyncResult.AsyncWaitHandle.WaitOne($TimeoutMilliseconds)) {
482
178
  # forcibly terminate execution of pipeline
483
179
  $ps.Stop()
484
180
  throw "Catastrophic failure: PowerShell module timeout ($TimeoutMilliseconds ms) exceeded while executing"
485
181
  }
486
182
 
487
- try
488
- {
183
+ try {
489
184
  $ps.EndInvoke($asyncResult)
490
185
  } catch [System.Management.Automation.IncompleteParseException] {
186
+ # This surfaces an error for when syntactically incorrect code is passed
491
187
  # https://msdn.microsoft.com/en-us/library/system.management.automation.incompleteparseexception%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396
492
188
  throw $_.Exception.Message
493
189
  } catch {
494
- if ($_.Exception.InnerException -ne $null)
495
- {
190
+ # This catches any execution errors from the passed code, drops out of execution here and
191
+ # throws the most specific exception available.
192
+ if ($null -ne $_.Exception.InnerException) {
496
193
  throw $_.Exception.InnerException
497
194
  } else {
498
195
  throw $_.Exception
499
196
  }
500
197
  }
501
198
 
502
- [Puppet.PuppetPSHostUserInterface]$ui = $global:puppetPSHost.UI
199
+ [RubyPwsh.RubyPwshPSHostUserInterface]$ui = $global:RubyPwshPSHost.UI
503
200
  return @{
504
- exitcode = $global:puppetPSHost.Exitcode;
201
+ exitcode = $global:RubyPwshPSHost.Exitcode;
505
202
  stdout = $ui.Output;
506
203
  stderr = $ui.StdErr;
507
204
  errormessage = $null;
508
205
  }
509
206
  }
510
- catch
511
- {
512
- try
513
- {
207
+ catch {
208
+ # if an execution or parse error is surfaced, dispose of the runspace and clear the global
209
+ # runspace; it will be rebuilt on the next execution.
210
+ try {
514
211
  if ($global:runspace) { $global:runspace.Dispose() }
515
- }
516
- finally
517
- {
212
+ } finally {
518
213
  $global:runspace = $null
519
214
  }
520
- if(($global:puppetPSHost -ne $null) -and $global:puppetPSHost.ExitCode){
521
- $ec = $global:puppetPSHost.ExitCode
522
- }else{
215
+ if (($global:RubyPwshPSHost -ne $null) -and $global:RubyPwshPSHost.ExitCode) {
216
+ $ec = $global:RubyPwshPSHost.ExitCode
217
+ } else {
523
218
  # This is technically not true at this point as we do not
524
219
  # know what exitcode we should return as an unexpected exception
525
220
  # happened and the user did not set an exitcode. Our best guess
526
- # is to return 1 so that we ensure Puppet reports this run as an error.
221
+ # is to return 1 so that we ensure ruby treats this run as an error.
527
222
  $ec = 1
528
223
  }
529
224
 
530
- if ($_.Exception.ErrorRecord.InvocationInfo -ne $null)
531
- {
225
+ # Format the exception message; this could be improved to surface more functional messaging
226
+ # to the user; right now it dumps the exception message as a string.
227
+ if ($_.Exception.ErrorRecord.InvocationInfo -ne $null) {
532
228
  $output = $_.Exception.Message + "`n`r" + $_.Exception.ErrorRecord.InvocationInfo.PositionMessage
533
229
  } else {
534
230
  $output = $_.Exception.Message | Out-String
535
231
  }
536
232
 
537
233
  # make an attempt to read Output / StdErr as it may contain partial output / info about failures
538
- try { $out = $global:puppetPSHost.UI.Output } catch { $out = $null }
539
- try { $err = $global:puppetPSHost.UI.StdErr } catch { $err = $null }
234
+ # The PowerShell Host could be entirely dead and broken at this stage.
235
+ try {
236
+ $out = $global:RubyPwshPSHost.UI.Output
237
+ } catch {
238
+ $out = $null
239
+ }
240
+ try {
241
+ $err = $global:RubyPwshPSHost.UI.StdErr
242
+ } catch {
243
+ $err = $null
244
+ }
540
245
 
246
+ # Make sure we return the expected data structure for what happened.
541
247
  return @{
542
248
  exitcode = $ec;
543
249
  stdout = $out;
544
250
  stderr = $err;
545
251
  errormessage = $output;
546
252
  }
547
- }
548
- finally
549
- {
253
+ } finally {
254
+ # Dispose of the shell regardless of success/failure. This clears state and memory both.
255
+ # To enable conditional keeping of state, this would need an additional condition.
550
256
  if ($ps -ne $null) { [Void]$ps.Dispose() }
551
257
  }
552
258
  }
553
259
 
554
- function Write-SystemDebugMessage
555
- {
260
+ function Write-SystemDebugMessage {
556
261
  [CmdletBinding()]
557
262
  param(
558
263
  [Parameter(Mandatory = $true)]
@@ -560,14 +265,16 @@ function Write-SystemDebugMessage
560
265
  $Message
561
266
  )
562
267
 
563
- if ($script:EmitDebugOutput -or ($DebugPreference -ne 'SilentlyContinue'))
564
- {
268
+ if ($script:EmitDebugOutput -or ($DebugPreference -ne 'SilentlyContinue')) {
269
+ # This writes to the console, not to the PowerShell streams.
270
+ # This is captured for communications with the pipe server.
565
271
  [System.Diagnostics.Debug]::WriteLine($Message)
566
272
  }
567
273
  }
568
274
 
569
- function Signal-Event
570
- {
275
+ # This is not called anywhere else in the project. It may be dead code for
276
+ # event handling used in an earlier implementation. Or magic?
277
+ function Signal-Event {
571
278
  [CmdletBinding()]
572
279
  param(
573
280
  [String]
@@ -585,8 +292,7 @@ function Signal-Event
585
292
  Write-SystemDebugMessage -Message "Signaled event $EventName"
586
293
  }
587
294
 
588
- function ConvertTo-LittleEndianBytes
589
- {
295
+ function ConvertTo-LittleEndianBytes {
590
296
  [CmdletBinding()]
591
297
  param (
592
298
  [Parameter(Mandatory = $true)]
@@ -595,13 +301,12 @@ function ConvertTo-LittleEndianBytes
595
301
  )
596
302
 
597
303
  $bytes = [BitConverter]::GetBytes($Value)
598
- if (![BitConverter]::IsLittleEndian) { [Array]::Reverse($bytes) }
304
+ if (-not [BitConverter]::IsLittleEndian) { [Array]::Reverse($bytes) }
599
305
 
600
306
  return $bytes
601
307
  }
602
308
 
603
- function ConvertTo-ByteArray
604
- {
309
+ function ConvertTo-ByteArray {
605
310
  [CmdletBinding()]
606
311
  param (
607
312
  [Parameter(Mandatory = $true)]
@@ -617,7 +322,7 @@ function ConvertTo-ByteArray
617
322
  $result = [Byte[]]@()
618
323
  # and add length / name / length / value from Hashtable
619
324
  $Hash.GetEnumerator() |
620
- % {
325
+ ForEach-Object -Process {
621
326
  $name = $Encoding.GetBytes($_.Name)
622
327
  $result += (ConvertTo-LittleEndianBytes $name.Length) + $name
623
328
 
@@ -629,8 +334,7 @@ function ConvertTo-ByteArray
629
334
  return $result
630
335
  }
631
336
 
632
- function Write-StreamResponse
633
- {
337
+ function Write-StreamResponse {
634
338
  [CmdletBinding()]
635
339
  param (
636
340
  [Parameter(Mandatory = $true)]
@@ -654,8 +358,7 @@ function Write-StreamResponse
654
358
  Write-SystemDebugMessage -Message "Wrote $($bytes.Length) bytes of data to Stream"
655
359
  }
656
360
 
657
- function Read-Int32FromStream
658
- {
361
+ function Read-Int32FromStream {
659
362
  [CmdletBinding()]
660
363
  param (
661
364
  [Parameter(Mandatory = $true)]
@@ -667,7 +370,7 @@ function Read-Int32FromStream
667
370
  # Read blocks until all 4 bytes available
668
371
  $Stream.Read($length, 0, 4) | Out-Null
669
372
  # value is sent in Little Endian, but if the CPU is not, in-place reverse the array
670
- if (![BitConverter]::IsLittleEndian) { [Array]::Reverse($length) }
373
+ if (-not [BitConverter]::IsLittleEndian) { [Array]::Reverse($length) }
671
374
  $value = [BitConverter]::ToInt32($length, 0)
672
375
 
673
376
  Write-SystemDebugMessage -Message "Read Byte[] $length from stream as Int32 $value"
@@ -683,8 +386,7 @@ function Read-Int32FromStream
683
386
  # [optional] 4 bytes - Little Endian encoded 32-bit code block length for execute
684
387
  # Intel CPUs are little endian, hence the .NET Framework typically is
685
388
  # [optional] variable length - code block
686
- function ConvertTo-PipeCommand
687
- {
389
+ function ConvertTo-PipeCommand {
688
390
  [CmdletBinding()]
689
391
  param (
690
392
  [Parameter(Mandatory = $true)]
@@ -705,8 +407,7 @@ function ConvertTo-PipeCommand
705
407
 
706
408
  Write-SystemDebugMessage -Message "Command id $command read from pipe"
707
409
 
708
- switch ($command)
709
- {
410
+ switch ($command) {
710
411
  # Exit
711
412
  # ReadByte returns a -1 when the pipe is closed on the other end
712
413
  { @(0, -1) -contains $_ } { return @{ Command = 'Exit' }}
@@ -728,8 +429,7 @@ function ConvertTo-PipeCommand
728
429
  $attempt = $attempt + 1
729
430
  # This will block if there's not enough data in the pipe
730
431
  $read = $Stream.Read($parsed.RawData, $readBytes, $parsed.Length - $readBytes)
731
- if ($read -eq 0)
732
- {
432
+ if ($read -eq 0) {
733
433
  throw "Catastrophic failure: Expected $($parsed.Length - $readBytesh) raw bytes, but the pipe reached an end of stream"
734
434
  }
735
435
 
@@ -737,8 +437,7 @@ function ConvertTo-PipeCommand
737
437
  Write-SystemDebugMessage -Message "Read $($read) bytes from the pipe"
738
438
  } while ($readBytes -lt $parsed.Length)
739
439
 
740
- if ($readBytes -lt $parsed.Length)
741
- {
440
+ if ($readBytes -lt $parsed.Length) {
742
441
  throw "Catastrophic failure: Expected $($parsed.Length) raw bytes, only received $readBytes"
743
442
  }
744
443
 
@@ -748,8 +447,7 @@ function ConvertTo-PipeCommand
748
447
  return $parsed
749
448
  }
750
449
 
751
- function Start-PipeServer
752
- {
450
+ function Start-PipeServer {
753
451
  [CmdletBinding()]
754
452
  param (
755
453
  [Parameter(Mandatory = $true)]
@@ -767,8 +465,7 @@ function Start-PipeServer
767
465
  $server = New-Object System.IO.Pipes.NamedPipeServerStream($CommandChannelPipeName,
768
466
  [System.IO.Pipes.PipeDirection]::InOut)
769
467
 
770
- try
771
- {
468
+ try {
772
469
  # block until Ruby process connects
773
470
  $server.WaitForConnection()
774
471
 
@@ -776,15 +473,13 @@ function Start-PipeServer
776
473
 
777
474
  # Infinite Loop to process commands until EXIT received
778
475
  $running = $true
779
- while ($running)
780
- {
476
+ while ($running) {
781
477
  # throws if an unxpected command id is read from pipe
782
478
  $response = ConvertTo-PipeCommand -Stream $server -Encoding $Encoding
783
479
 
784
480
  Write-SystemDebugMessage -Message "Received $($response.Command) command from client"
785
481
 
786
- switch ($response.Command)
787
- {
482
+ switch ($response.Command) {
788
483
  'Execute' {
789
484
  Write-SystemDebugMessage -Message "[Execute] Invoking user code:`n`n $($response.Code)"
790
485
 
@@ -799,26 +494,21 @@ function Start-PipeServer
799
494
  'Exit' { $running = $false }
800
495
  }
801
496
  }
802
- }
803
- catch [Exception]
804
- {
497
+ } catch [Exception] {
805
498
  Write-SystemDebugMessage -Message "PowerShell Pipe Server Failed!`n`n$_"
806
499
  throw
807
- }
808
- finally
809
- {
810
- if ($global:runspace -ne $null)
811
- {
500
+ } finally {
501
+ if ($global:runspace -ne $null) {
812
502
  $global:runspace.Dispose()
813
503
  Write-SystemDebugMessage -Message "PowerShell Runspace Disposed`n`n$_"
814
504
  }
815
- if ($server -ne $null)
816
- {
505
+ if ($server -ne $null) {
817
506
  $server.Dispose()
818
507
  Write-SystemDebugMessage -Message "NamedPipeServerStream Disposed`n`n$_"
819
508
  }
820
509
  }
821
510
  }
822
511
 
512
+ # Start the pipe server and wait for it to close.
823
513
  Start-PipeServer -CommandChannelPipeName $NamedPipeName -Encoding $Encoding
824
514
  Write-SystemDebugMessage -Message "Start-PipeServer Finished`n`n$_"