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.
- checksums.yaml +7 -0
- data/.gitattributes +2 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/.rubocop.yml +31 -0
- data/.travis.yml +25 -0
- data/CODEOWNERS +2 -0
- data/DESIGN.md +70 -0
- data/Gemfile +51 -0
- data/LICENSE.txt +21 -0
- data/README.md +69 -0
- data/Rakefile +44 -0
- data/design-comms.png +0 -0
- data/lib/pwsh.rb +653 -0
- data/lib/pwsh/util.rb +54 -0
- data/lib/pwsh/version.rb +6 -0
- data/lib/pwsh/windows_powershell.rb +108 -0
- data/lib/ruby-pwsh.rb +3 -0
- data/lib/templates/init.ps1 +824 -0
- data/ruby-pwsh.gemspec +39 -0
- metadata +66 -0
data/lib/pwsh/util.rb
ADDED
@@ -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
|
data/lib/pwsh/version.rb
ADDED
@@ -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
|
data/lib/ruby-pwsh.rb
ADDED
@@ -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$_"
|