ruby-pwsh 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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$_"