ruby-pwsh 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pwsh
4
+ # Various helper methods
5
+ module Util
6
+ module_function
7
+
8
+ # Verifies whether or not the current context is running on a Windows node.
9
+ #
10
+ # @return [Bool] true if on windows
11
+ def on_windows?
12
+ # Ruby only sets File::ALT_SEPARATOR on Windows and the Ruby standard
13
+ # library uses that to test what platform it's on.
14
+ !!File::ALT_SEPARATOR # rubocop:disable Style/DoubleNegation
15
+ end
16
+
17
+ # Verify paths specified are valid directories which exist.
18
+ #
19
+ # @return [Bool] true if any directories specified do not exist
20
+ def invalid_directories?(path_collection)
21
+ invalid_paths = false
22
+
23
+ return invalid_paths if path_collection.nil? || path_collection.empty?
24
+
25
+ paths = on_windows? ? path_collection.split(';') : path_collection.split(':')
26
+ paths.each do |path|
27
+ invalid_paths = true unless File.directory?(path) || path.empty?
28
+ end
29
+
30
+ invalid_paths
31
+ end
32
+ end
33
+ end
34
+
35
+ # POWERSHELL_MODULE_UPGRADE_MSG ||= <<-UPGRADE
36
+ # Currently, the PowerShell module has reduced v1 functionality on this machine
37
+ # due to the following condition:
38
+
39
+ # - PowerShell v2 with .NET Framework 2.0
40
+
41
+ # PowerShell v2 works with both .NET Framework 2.0 and .NET Framework 3.5.
42
+ # To be able to use the enhancements, we require .NET Framework 3.5.
43
+ # Typically you will only see this on a base Windows Server 2008 (and R2)
44
+ # install.
45
+
46
+ # To enable these improvements, it is suggested to ensure you have .NET Framework
47
+ # 3.5 installed.
48
+ # UPGRADE
49
+
50
+ # TODO: Generalize this upgrade message to be independent of Puppet
51
+ # def upgrade_message
52
+ # # Puppet.warning POWERSHELL_MODULE_UPGRADE_MSG if !@upgrade_warning_issued
53
+ # @upgrade_warning_issued = true
54
+ # end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pwsh
4
+ # The version of the ruby-pwsh gem
5
+ VERSION = '0.1.0'
6
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.join(File.dirname(__FILE__), 'util')
4
+
5
+ module Pwsh
6
+ # Returns information about the available versions of Windows PowerShell on the node, if any.
7
+ class WindowsPowerShell
8
+ # Return whether or not the latest version of PowerShell available on the machine
9
+ # is compatible with the implementation of the Manager.
10
+ def self.compatible_version?
11
+ # If this method isn't defined, we're not on Windows!
12
+ return false if defined?(Pwsh::WindowsPowerShell.version).nil?
13
+
14
+ powershell_version = defined?(Pwsh::WindowsPowerShell.version) ? Pwsh::WindowsPowerShell.version : nil
15
+
16
+ # If we get nil, something's gone wrong and we're not compatible.
17
+ return false if powershell_version.nil?
18
+
19
+ # PowerShell v1 - definitely not good to go. Really the whole library
20
+ # may not even work but I digress
21
+ return false if Gem::Version.new(powershell_version) < Gem::Version.new(2)
22
+
23
+ # PowerShell v3+, we are good to go b/c .NET 4+
24
+ # https://msdn.microsoft.com/en-us/powershell/scripting/setup/windows-powershell-system-requirements
25
+ # Look at Microsoft .NET Framwork Requirements section.
26
+ return true if Gem::Version.new(powershell_version) >= Gem::Version.new(3)
27
+
28
+ # If we are using PowerShell v2, we need to see what the latest
29
+ # version of .NET is that we have
30
+ # https://msdn.microsoft.com/en-us/library/hh925568.aspx
31
+ value = false
32
+ if Pwsh::Util.on_windows?
33
+ require 'win32/registry'
34
+ begin
35
+ # At this point in the check, PowerShell is using .NET Framework
36
+ # 2.x family, so we only need to verify v3.5 key exists.
37
+ # If we were verifying all compatible types we would look for
38
+ # any of these keys: v3.5, v4.0, v4
39
+ Win32::Registry::HKEY_LOCAL_MACHINE.open('SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5', Win32::Registry::KEY_READ | 0x100) do
40
+ value = true
41
+ end
42
+ rescue Win32::Registry::Error
43
+ value = false
44
+ end
45
+ end
46
+
47
+ value
48
+ end
49
+ end
50
+ end
51
+
52
+ if Pwsh::Util.on_windows?
53
+ require 'win32/registry'
54
+ module Pwsh
55
+ # Returns information about the available versions of Windows PowerShell on the node, if any.
56
+ class WindowsPowerShell
57
+ # Shorthand constant to reference the registry key access type
58
+ ACCESS_TYPE = Win32::Registry::KEY_READ | 0x100
59
+ # Shorthand constant to reference the local machine hive
60
+ HKLM = Win32::Registry::HKEY_LOCAL_MACHINE
61
+ # The path to the original version of the Windows PowerShell Engine's data in registry
62
+ PS_ONE_REG_PATH = 'SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine'
63
+ # The path to the newer version of the Windows PowerShell Engine's data in registry
64
+ PS_THREE_REG_PATH = 'SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine'
65
+ # The name of the registry key for looking up the latest version of Windows PowerShell for a given engine.
66
+ REG_KEY = 'PowerShellVersion'
67
+
68
+ # Returns the latest available version of Windows PowerShell on the machine
69
+ #
70
+ # @return [String] a version string representing the latest version of Windows PowerShell available
71
+ def self.version
72
+ powershell_three_version || powershell_one_version
73
+ end
74
+
75
+ # Returns the latest available version of Windows PowerShell using the older
76
+ # engine as determined by checking the registry.
77
+ #
78
+ # @return [String] a version string representing the latest version of Windows PowerShell using the original engine
79
+ def self.powershell_one_version
80
+ version = nil
81
+ begin
82
+ HKLM.open(PS_ONE_REG_PATH, ACCESS_TYPE) do |reg|
83
+ version = reg[REG_KEY]
84
+ end
85
+ rescue
86
+ version = nil
87
+ end
88
+ version
89
+ end
90
+
91
+ # Returns the latest available version of Windows PowerShell as determined by
92
+ # checking the registry.
93
+ #
94
+ # @return [String] a version string representing the latest version of Windows PowerShell using the newer engine
95
+ def self.powershell_three_version
96
+ version = nil
97
+ begin
98
+ HKLM.open(PS_THREE_REG_PATH, ACCESS_TYPE) do |reg|
99
+ version = reg[REG_KEY]
100
+ end
101
+ rescue
102
+ version = nil
103
+ end
104
+ version
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pwsh'
@@ -0,0 +1,824 @@
1
+ [CmdletBinding()]
2
+ param (
3
+ [Parameter(Mandatory = $true)]
4
+ [String]
5
+ $NamedPipeName,
6
+
7
+ [Parameter(Mandatory = $false)]
8
+ [Switch]
9
+ $EmitDebugOutput = $False,
10
+
11
+ [Parameter(Mandatory = $false)]
12
+ [System.Text.Encoding]
13
+ $Encoding = [System.Text.Encoding]::UTF8
14
+ )
15
+
16
+ $script:EmitDebugOutput = $EmitDebugOutput
17
+ # Necessary for [System.Console]::Error.WriteLine to roundtrip with UTF-8
18
+ [System.Console]::OutputEncoding = $Encoding
19
+
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
+ "@
324
+
325
+ Add-Type -TypeDefinition $hostSource -Language CSharp
326
+ $global:DefaultWorkingDirectory = (Get-Location -PSProvider FileSystem).Path
327
+
328
+ #this is a string so we can import into our dynamic PS instance
329
+ $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 }
363
+
364
+ Remove-Item -Path Env:\* -ErrorAction SilentlyContinue -WarningAction SilentlyContinue -Recurse
365
+
366
+ $vars.GetEnumerator() | % { Set-Item -Path "Env:\$($_.Name)" -Value $_.Value }
367
+ }
368
+
369
+ function Reset-ProcessPowerShellVariables
370
+ {
371
+ 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
376
+ }
377
+ }
378
+ }
379
+ '@
380
+
381
+ function Invoke-PowerShellUserCode
382
+ {
383
+ [CmdletBinding()]
384
+ param(
385
+ [String]
386
+ $Code,
387
+
388
+ [Int]
389
+ $TimeoutMilliseconds,
390
+
391
+ [String]
392
+ $WorkingDirectory,
393
+
394
+ [Hashtable]
395
+ $ExecEnvironmentVariables
396
+ )
397
+
398
+ if ($global:runspace -eq $null){
399
+ # CreateDefault2 requires PS3
400
+ if ([System.Management.Automation.Runspaces.InitialSessionState].GetMethod('CreateDefault2')){
401
+ $sessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault2()
402
+ }else{
403
+ $sessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
404
+ }
405
+
406
+ $global:puppetPSHost = New-Object Puppet.PuppetPSHost
407
+ $global:runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($global:puppetPSHost, $sessionState)
408
+ $global:runspace.Open()
409
+ }
410
+
411
+ try
412
+ {
413
+ $ps = $null
414
+ $global:puppetPSHost.ResetExitStatus()
415
+ $global:puppetPSHost.ResetConsoleStreams()
416
+
417
+ if ($PSVersionTable.PSVersion -ge [Version]'3.0') {
418
+ $global:runspace.ResetRunspaceState()
419
+ }
420
+
421
+ $ps = [System.Management.Automation.PowerShell]::Create()
422
+ $ps.Runspace = $global:runspace
423
+ [Void]$ps.AddScript($global:ourFunctions)
424
+ $ps.Invoke()
425
+
426
+ if ([string]::IsNullOrEmpty($WorkingDirectory)) {
427
+ [Void]$ps.Runspace.SessionStateProxy.Path.SetLocation($global:DefaultWorkingDirectory)
428
+ } else {
429
+ if (-not (Test-Path -Path $WorkingDirectory)) { Throw "Working directory `"$WorkingDirectory`" does not exist" }
430
+ [Void]$ps.Runspace.SessionStateProxy.Path.SetLocation($WorkingDirectory)
431
+ }
432
+
433
+ if(!$global:environmentVariables){
434
+ $ps.Commands.Clear()
435
+ $global:environmentVariables = $ps.AddCommand('Get-ProcessEnvironmentVariables').Invoke()
436
+ }
437
+
438
+ if($PSVersionTable.PSVersion -le [Version]'2.0'){
439
+ if(!$global:psVariables){
440
+ $global:psVariables = $ps.AddScript('Get-Variable').Invoke()
441
+ }
442
+
443
+ $ps.Commands.Clear()
444
+ [void]$ps.AddScript('Get-Variable -Scope Global | Remove-Variable -Force -ErrorAction SilentlyContinue -WarningAction SilentlyContinue')
445
+ $ps.Invoke()
446
+
447
+ $ps.Commands.Clear()
448
+ [void]$ps.AddCommand('Reset-ProcessPowerShellVariables').AddParameter('psVariables', $global:psVariables)
449
+ $ps.Invoke()
450
+ }
451
+
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 }
459
+ }
460
+
461
+ # we clear the commands before each new command
462
+ # to avoid command pollution
463
+ $ps.Commands.Clear()
464
+ [Void]$ps.AddScript($Code)
465
+
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");
470
+
471
+ # if the call operator & established an exit code, exit with it
472
+ [Void]$ps.AddScript('if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }')
473
+
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);
478
+ }
479
+ $asyncResult = $ps.BeginInvoke()
480
+
481
+ if (!$asyncResult.AsyncWaitHandle.WaitOne($TimeoutMilliseconds)){
482
+ # forcibly terminate execution of pipeline
483
+ $ps.Stop()
484
+ throw "Catastrophic failure: PowerShell module timeout ($TimeoutMilliseconds ms) exceeded while executing"
485
+ }
486
+
487
+ try
488
+ {
489
+ $ps.EndInvoke($asyncResult)
490
+ } catch [System.Management.Automation.IncompleteParseException] {
491
+ # https://msdn.microsoft.com/en-us/library/system.management.automation.incompleteparseexception%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396
492
+ throw $_.Exception.Message
493
+ } catch {
494
+ if ($_.Exception.InnerException -ne $null)
495
+ {
496
+ throw $_.Exception.InnerException
497
+ } else {
498
+ throw $_.Exception
499
+ }
500
+ }
501
+
502
+ [Puppet.PuppetPSHostUserInterface]$ui = $global:puppetPSHost.UI
503
+ return @{
504
+ exitcode = $global:puppetPSHost.Exitcode;
505
+ stdout = $ui.Output;
506
+ stderr = $ui.StdErr;
507
+ errormessage = $null;
508
+ }
509
+ }
510
+ catch
511
+ {
512
+ try
513
+ {
514
+ if ($global:runspace) { $global:runspace.Dispose() }
515
+ }
516
+ finally
517
+ {
518
+ $global:runspace = $null
519
+ }
520
+ if(($global:puppetPSHost -ne $null) -and $global:puppetPSHost.ExitCode){
521
+ $ec = $global:puppetPSHost.ExitCode
522
+ }else{
523
+ # This is technically not true at this point as we do not
524
+ # know what exitcode we should return as an unexpected exception
525
+ # 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.
527
+ $ec = 1
528
+ }
529
+
530
+ if ($_.Exception.ErrorRecord.InvocationInfo -ne $null)
531
+ {
532
+ $output = $_.Exception.Message + "`n`r" + $_.Exception.ErrorRecord.InvocationInfo.PositionMessage
533
+ } else {
534
+ $output = $_.Exception.Message | Out-String
535
+ }
536
+
537
+ # 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 }
540
+
541
+ return @{
542
+ exitcode = $ec;
543
+ stdout = $out;
544
+ stderr = $err;
545
+ errormessage = $output;
546
+ }
547
+ }
548
+ finally
549
+ {
550
+ if ($ps -ne $null) { [Void]$ps.Dispose() }
551
+ }
552
+ }
553
+
554
+ function Write-SystemDebugMessage
555
+ {
556
+ [CmdletBinding()]
557
+ param(
558
+ [Parameter(Mandatory = $true)]
559
+ [String]
560
+ $Message
561
+ )
562
+
563
+ if ($script:EmitDebugOutput -or ($DebugPreference -ne 'SilentlyContinue'))
564
+ {
565
+ [System.Diagnostics.Debug]::WriteLine($Message)
566
+ }
567
+ }
568
+
569
+ function Signal-Event
570
+ {
571
+ [CmdletBinding()]
572
+ param(
573
+ [String]
574
+ $EventName
575
+ )
576
+
577
+ $event = [System.Threading.EventWaitHandle]::OpenExisting($EventName)
578
+
579
+ [Void]$event.Set()
580
+ [Void]$event.Close()
581
+ if ($PSVersionTable.CLRVersion.Major -ge 3) {
582
+ [Void]$event.Dispose()
583
+ }
584
+
585
+ Write-SystemDebugMessage -Message "Signaled event $EventName"
586
+ }
587
+
588
+ function ConvertTo-LittleEndianBytes
589
+ {
590
+ [CmdletBinding()]
591
+ param (
592
+ [Parameter(Mandatory = $true)]
593
+ [Int32]
594
+ $Value
595
+ )
596
+
597
+ $bytes = [BitConverter]::GetBytes($Value)
598
+ if (![BitConverter]::IsLittleEndian) { [Array]::Reverse($bytes) }
599
+
600
+ return $bytes
601
+ }
602
+
603
+ function ConvertTo-ByteArray
604
+ {
605
+ [CmdletBinding()]
606
+ param (
607
+ [Parameter(Mandatory = $true)]
608
+ [Hashtable]
609
+ $Hash,
610
+
611
+ [Parameter(Mandatory = $true)]
612
+ [System.Text.Encoding]
613
+ $Encoding
614
+ )
615
+
616
+ # Initialize empty byte array that can be appended to
617
+ $result = [Byte[]]@()
618
+ # and add length / name / length / value from Hashtable
619
+ $Hash.GetEnumerator() |
620
+ % {
621
+ $name = $Encoding.GetBytes($_.Name)
622
+ $result += (ConvertTo-LittleEndianBytes $name.Length) + $name
623
+
624
+ $value = @()
625
+ if ($_.Value -ne $null) { $value = $Encoding.GetBytes($_.Value.ToString()) }
626
+ $result += (ConvertTo-LittleEndianBytes $value.Length) + $value
627
+ }
628
+
629
+ return $result
630
+ }
631
+
632
+ function Write-StreamResponse
633
+ {
634
+ [CmdletBinding()]
635
+ param (
636
+ [Parameter(Mandatory = $true)]
637
+ [System.IO.Pipes.PipeStream]
638
+ $Stream,
639
+
640
+ [Parameter(Mandatory = $true)]
641
+ [Byte[]]
642
+ $Bytes
643
+ )
644
+
645
+ $length = ConvertTo-LittleEndianBytes -Value $Bytes.Length
646
+ $Stream.Write($length, 0, 4)
647
+ $Stream.Flush()
648
+
649
+ Write-SystemDebugMessage -Message "Wrote Int32 $($bytes.Length) as Byte[] $length to Stream"
650
+
651
+ $Stream.Write($bytes, 0, $bytes.Length)
652
+ $Stream.Flush()
653
+
654
+ Write-SystemDebugMessage -Message "Wrote $($bytes.Length) bytes of data to Stream"
655
+ }
656
+
657
+ function Read-Int32FromStream
658
+ {
659
+ [CmdletBinding()]
660
+ param (
661
+ [Parameter(Mandatory = $true)]
662
+ [System.IO.Pipes.PipeStream]
663
+ $Stream
664
+ )
665
+
666
+ $length = New-Object Byte[] 4
667
+ # Read blocks until all 4 bytes available
668
+ $Stream.Read($length, 0, 4) | Out-Null
669
+ # value is sent in Little Endian, but if the CPU is not, in-place reverse the array
670
+ if (![BitConverter]::IsLittleEndian) { [Array]::Reverse($length) }
671
+ $value = [BitConverter]::ToInt32($length, 0)
672
+
673
+ Write-SystemDebugMessage -Message "Read Byte[] $length from stream as Int32 $value"
674
+
675
+ return $value
676
+ }
677
+
678
+ # Message format is:
679
+ # 1 byte - command identifier
680
+ # 0 - Exit
681
+ # 1 - Execute
682
+ # -1 - Exit - automatically returned when ReadByte encounters a closed pipe
683
+ # [optional] 4 bytes - Little Endian encoded 32-bit code block length for execute
684
+ # Intel CPUs are little endian, hence the .NET Framework typically is
685
+ # [optional] variable length - code block
686
+ function ConvertTo-PipeCommand
687
+ {
688
+ [CmdletBinding()]
689
+ param (
690
+ [Parameter(Mandatory = $true)]
691
+ [System.IO.Pipes.PipeStream]
692
+ $Stream,
693
+
694
+ [Parameter(Mandatory = $true)]
695
+ [System.Text.Encoding]
696
+ $Encoding,
697
+
698
+ [Parameter(Mandatory = $false)]
699
+ [Int32]
700
+ $BufferChunkSize = 4096
701
+ )
702
+
703
+ # command identifier is a single value - ReadByte blocks until byte is ready / pipe closes
704
+ $command = $Stream.ReadByte()
705
+
706
+ Write-SystemDebugMessage -Message "Command id $command read from pipe"
707
+
708
+ switch ($command)
709
+ {
710
+ # Exit
711
+ # ReadByte returns a -1 when the pipe is closed on the other end
712
+ { @(0, -1) -contains $_ } { return @{ Command = 'Exit' }}
713
+
714
+ # Execute
715
+ 1 { $parsed = @{ Command = 'Execute' } }
716
+
717
+ default { throw "Catastrophic failure: Unexpected Command $command received" }
718
+ }
719
+
720
+ # read size of incoming byte buffer
721
+ $parsed.Length = Read-Int32FromStream -Stream $Stream
722
+ Write-SystemDebugMessage -Message "Expecting $($parsed.Length) raw bytes of $($Encoding.EncodingName) characters"
723
+
724
+ # Read blocks until all bytes are read or EOF / broken pipe hit - tested with 5MB and worked fine
725
+ $parsed.RawData = New-Object Byte[] $parsed.Length
726
+ $readBytes = 0
727
+ do {
728
+ $attempt = $attempt + 1
729
+ # This will block if there's not enough data in the pipe
730
+ $read = $Stream.Read($parsed.RawData, $readBytes, $parsed.Length - $readBytes)
731
+ if ($read -eq 0)
732
+ {
733
+ throw "Catastrophic failure: Expected $($parsed.Length - $readBytesh) raw bytes, but the pipe reached an end of stream"
734
+ }
735
+
736
+ $readBytes = $readBytes + $read
737
+ Write-SystemDebugMessage -Message "Read $($read) bytes from the pipe"
738
+ } while ($readBytes -lt $parsed.Length)
739
+
740
+ if ($readBytes -lt $parsed.Length)
741
+ {
742
+ throw "Catastrophic failure: Expected $($parsed.Length) raw bytes, only received $readBytes"
743
+ }
744
+
745
+ # turn the raw bytes into the expected encoded string!
746
+ $parsed.Code = $Encoding.GetString($parsed.RawData)
747
+
748
+ return $parsed
749
+ }
750
+
751
+ function Start-PipeServer
752
+ {
753
+ [CmdletBinding()]
754
+ param (
755
+ [Parameter(Mandatory = $true)]
756
+ [String]
757
+ $CommandChannelPipeName,
758
+
759
+ [Parameter(Mandatory = $true)]
760
+ [System.Text.Encoding]
761
+ $Encoding
762
+ )
763
+
764
+ Add-Type -AssemblyName System.Core
765
+
766
+ # this does not require versioning in the payload as client / server are tightly coupled
767
+ $server = New-Object System.IO.Pipes.NamedPipeServerStream($CommandChannelPipeName,
768
+ [System.IO.Pipes.PipeDirection]::InOut)
769
+
770
+ try
771
+ {
772
+ # block until Ruby process connects
773
+ $server.WaitForConnection()
774
+
775
+ Write-SystemDebugMessage -Message "Incoming Connection to $CommandChannelPipeName Received - Expecting Strings as $($Encoding.EncodingName)"
776
+
777
+ # Infinite Loop to process commands until EXIT received
778
+ $running = $true
779
+ while ($running)
780
+ {
781
+ # throws if an unxpected command id is read from pipe
782
+ $response = ConvertTo-PipeCommand -Stream $server -Encoding $Encoding
783
+
784
+ Write-SystemDebugMessage -Message "Received $($response.Command) command from client"
785
+
786
+ switch ($response.Command)
787
+ {
788
+ 'Execute' {
789
+ Write-SystemDebugMessage -Message "[Execute] Invoking user code:`n`n $($response.Code)"
790
+
791
+ # assuming that the Ruby code always calls Invoked-PowerShellUserCode,
792
+ # result should already be returned as a hash
793
+ $result = Invoke-Expression $response.Code
794
+
795
+ $bytes = ConvertTo-ByteArray -Hash $result -Encoding $Encoding
796
+
797
+ Write-StreamResponse -Stream $server -Bytes $bytes
798
+ }
799
+ 'Exit' { $running = $false }
800
+ }
801
+ }
802
+ }
803
+ catch [Exception]
804
+ {
805
+ Write-SystemDebugMessage -Message "PowerShell Pipe Server Failed!`n`n$_"
806
+ throw
807
+ }
808
+ finally
809
+ {
810
+ if ($global:runspace -ne $null)
811
+ {
812
+ $global:runspace.Dispose()
813
+ Write-SystemDebugMessage -Message "PowerShell Runspace Disposed`n`n$_"
814
+ }
815
+ if ($server -ne $null)
816
+ {
817
+ $server.Dispose()
818
+ Write-SystemDebugMessage -Message "NamedPipeServerStream Disposed`n`n$_"
819
+ }
820
+ }
821
+ }
822
+
823
+ Start-PipeServer -CommandChannelPipeName $NamedPipeName -Encoding $Encoding
824
+ Write-SystemDebugMessage -Message "Start-PipeServer Finished`n`n$_"