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