829 lines
29 KiB
PowerShell
829 lines
29 KiB
PowerShell
#!powershell
|
|
|
|
# Copyright: (c) 2021, Ansible Project
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
#AnsibleRequires -PowerShell Ansible.ModuleUtils.AddType
|
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
|
|
|
#AnsibleRequires -PowerShell ..module_utils.Process
|
|
|
|
$spec = @{
|
|
options = @{
|
|
arguments = @{ type = 'list'; elements = 'str' }
|
|
chdir = @{ type = 'str' }
|
|
creates = @{ type = 'str' }
|
|
depth = @{ type = 'int'; default = 2 }
|
|
error_action = @{ type = 'str'; choices = 'silently_continue', 'continue', 'stop'; default = 'continue' }
|
|
executable = @{ type = 'str' }
|
|
parameters = @{ type = 'dict' }
|
|
removes = @{ type = 'str' }
|
|
script = @{ type = 'str'; required = $true }
|
|
}
|
|
supports_check_mode = $true
|
|
}
|
|
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
|
|
|
$module.Result.result = @{}
|
|
$module.Result.host_out = ''
|
|
$module.Result.host_err = ''
|
|
$module.Result.output = @()
|
|
$module.Result.error = @()
|
|
$module.Result.warning = @()
|
|
$module.Result.verbose = @()
|
|
$module.Result.debug = @()
|
|
$module.Result.information = @()
|
|
|
|
$stdPinvoke = @'
|
|
using Microsoft.Win32.SafeHandles;
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace Ansible.Windows.WinPowerShell
|
|
{
|
|
public class NativeMethods
|
|
{
|
|
[DllImport("Kernel32.dll", SetLastError = true)]
|
|
public static extern bool AllocConsole();
|
|
|
|
[DllImport("Kernel32.dll", SetLastError = true)]
|
|
public static extern IntPtr GetConsoleWindow();
|
|
|
|
[DllImport("Kernel32.dll")]
|
|
public static extern IntPtr GetStdHandle(
|
|
int nStdHandle);
|
|
|
|
[DllImport("Kernel32.dll", SetLastError = true)]
|
|
public static extern bool FreeConsole();
|
|
|
|
[DllImport("Kernel32.dll")]
|
|
public static extern bool SetStdHandle(
|
|
int nStdHandle,
|
|
IntPtr hHandle);
|
|
}
|
|
}
|
|
'@
|
|
|
|
Add-CSharpType -AnsibleModule $module -References $stdPinvoke, @'
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Management.Automation;
|
|
using System.Management.Automation.Host;
|
|
using System.Security;
|
|
|
|
namespace Ansible.Windows.WinPowerShell
|
|
{
|
|
public class Host : PSHost
|
|
{
|
|
private readonly PSHost PSHost;
|
|
private readonly HostUI HostUI;
|
|
|
|
public Host(PSHost host, StreamWriter stdout, StreamWriter stderr)
|
|
{
|
|
PSHost = host;
|
|
HostUI = new HostUI(stdout, stderr);
|
|
}
|
|
|
|
public override CultureInfo CurrentCulture { get { return PSHost.CurrentCulture; } }
|
|
|
|
public override CultureInfo CurrentUICulture { get { return PSHost.CurrentUICulture; } }
|
|
|
|
public override Guid InstanceId { get { return PSHost.InstanceId; } }
|
|
|
|
public override string Name { get { return PSHost.Name; } }
|
|
|
|
public override PSHostUserInterface UI { get { return HostUI; } }
|
|
|
|
public override Version Version { get { return PSHost.Version; } }
|
|
|
|
public override void EnterNestedPrompt()
|
|
{
|
|
PSHost.EnterNestedPrompt();
|
|
}
|
|
|
|
public override void ExitNestedPrompt()
|
|
{
|
|
PSHost.ExitNestedPrompt();
|
|
}
|
|
|
|
public override void NotifyBeginApplication()
|
|
{
|
|
PSHost.NotifyBeginApplication();
|
|
}
|
|
|
|
public override void NotifyEndApplication()
|
|
{
|
|
PSHost.NotifyEndApplication();
|
|
}
|
|
|
|
public override void SetShouldExit(int exitCode)
|
|
{
|
|
PSHost.SetShouldExit(exitCode);
|
|
}
|
|
}
|
|
|
|
public class HostUI : PSHostUserInterface
|
|
{
|
|
private StreamWriter _stdout;
|
|
private StreamWriter _stderr;
|
|
|
|
public HostUI(StreamWriter stdout, StreamWriter stderr)
|
|
{
|
|
_stdout = stdout;
|
|
_stderr = stderr;
|
|
}
|
|
|
|
public override PSHostRawUserInterface RawUI { get { return null; } }
|
|
|
|
public override Dictionary<string, PSObject> Prompt(string caption, string message, Collection<FieldDescription> descriptions)
|
|
{
|
|
throw new MethodInvocationException("PowerShell is in NonInteractive mode. Read and Prompt functionality is not available.");
|
|
}
|
|
|
|
public override int PromptForChoice(string caption, string message, Collection<ChoiceDescription> choices, int defaultChoice)
|
|
{
|
|
throw new MethodInvocationException("PowerShell is in NonInteractive mode. Read and Prompt functionality is not available.");
|
|
}
|
|
|
|
public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName,
|
|
PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options)
|
|
{
|
|
throw new MethodInvocationException("PowerShell is in NonInteractive mode. Read and Prompt functionality is not available.");
|
|
}
|
|
|
|
public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName)
|
|
{
|
|
throw new MethodInvocationException("PowerShell is in NonInteractive mode. Read and Prompt functionality is not available.");
|
|
}
|
|
|
|
public override string ReadLine()
|
|
{
|
|
throw new MethodInvocationException("PowerShell is in NonInteractive mode. Read and Prompt functionality is not available.");
|
|
}
|
|
|
|
public override SecureString ReadLineAsSecureString()
|
|
{
|
|
throw new MethodInvocationException("PowerShell is in NonInteractive mode. Read and Prompt functionality is not available.");
|
|
}
|
|
|
|
public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value)
|
|
{
|
|
_stdout.Write(value);
|
|
}
|
|
|
|
public override void Write(string value)
|
|
{
|
|
_stdout.Write(value);
|
|
}
|
|
|
|
public override void WriteDebugLine(string message)
|
|
{
|
|
_stdout.WriteLine(String.Format("DEBUG: {0}", message));
|
|
}
|
|
|
|
public override void WriteErrorLine(string value)
|
|
{
|
|
_stderr.WriteLine(value);
|
|
}
|
|
|
|
public override void WriteLine(string value)
|
|
{
|
|
_stdout.WriteLine(value);
|
|
}
|
|
|
|
public override void WriteProgress(long sourceId, ProgressRecord record) {}
|
|
|
|
public override void WriteVerboseLine(string message)
|
|
{
|
|
_stdout.WriteLine(String.Format("VERBOSE: {0}", message));
|
|
}
|
|
|
|
public override void WriteWarningLine(string message)
|
|
{
|
|
_stdout.WriteLine(String.Format("WARNING: {0}", message));
|
|
}
|
|
}
|
|
}
|
|
'@
|
|
|
|
Function Get-StdHandle {
|
|
[CmdletBinding()]
|
|
param (
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateSet('Stdout', 'Stderr')]
|
|
[string]
|
|
$Stream
|
|
)
|
|
|
|
$id, $dotnet = switch ($Stream) {
|
|
Stdout { -11, [Console]::Out }
|
|
Stderr { -12, [Console]::Error }
|
|
}
|
|
$handle = [Ansible.Windows.WinPowerShell.NativeMethods]::GetStdHandle($id)
|
|
|
|
[PSCustomObject]@{
|
|
Stream = $Stream
|
|
NET = $dotnet
|
|
Raw = $handle
|
|
}
|
|
}
|
|
|
|
Function Set-StdHandle {
|
|
[CmdletBinding()]
|
|
param (
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateSet('Stdout', 'Stderr')]
|
|
[string]
|
|
$Stream,
|
|
|
|
[IO.TextWriter]
|
|
$NET,
|
|
|
|
[IntPtr]
|
|
$Raw
|
|
)
|
|
|
|
$id, $meth = switch ($Stream) {
|
|
Stdout { -11; [Console]::SetOut($NET) }
|
|
Stderr { -12; [Console]::SetError($NET) }
|
|
}
|
|
|
|
# .NET does not actually affect the std handle on the process, we need to call SetStdHandle so any child processes
|
|
# spawned with Start-Process -NoNewWindow will use our custom pipe.
|
|
[void][Ansible.Windows.WinPowerShell.NativeMethods]::SetStdHandle($id, $Raw)
|
|
}
|
|
|
|
Function Convert-OutputObject {
|
|
[CmdletBinding()]
|
|
param (
|
|
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
|
|
[AllowNull()]
|
|
[object]
|
|
$InputObject,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[int]
|
|
$Depth
|
|
)
|
|
|
|
begin {
|
|
$childDepth = $Depth - 1
|
|
|
|
$isType = {
|
|
[CmdletBinding()]
|
|
param (
|
|
[Object]
|
|
$InputObject,
|
|
|
|
[Type]
|
|
$Type
|
|
)
|
|
|
|
if ($InputObject -is $Type) {
|
|
return $true
|
|
}
|
|
|
|
$psTypes = @($InputObject.PSTypeNames | ForEach-Object -Process {
|
|
$_ -replace '^Deserialized.'
|
|
})
|
|
|
|
$Type.FullName -in $psTypes
|
|
}
|
|
}
|
|
|
|
process {
|
|
if ($null -eq $InputObject) {
|
|
$null
|
|
}
|
|
elseif ((&$isType -InputObject $InputObject -Type ([Enum])) -and $Depth -ge 0) {
|
|
# ToString() gives the human readable value but I thought it better to give some more context behind
|
|
# these types.
|
|
@{
|
|
Type = ($InputObject.PSTypeNames[0] -replace '^Deserialized.')
|
|
String = $InputObject.ToString()
|
|
Value = [int]$InputObject
|
|
}
|
|
}
|
|
elseif ($InputObject -is [DateTime]) {
|
|
# The offset is based on the Kind value
|
|
# Unspecified leaves it off
|
|
# UTC set it to Z
|
|
# Local sets it to the local timezone
|
|
$InputObject.ToString('o')
|
|
}
|
|
elseif (&$isType -InputObject $InputObject -Type ([DateTimeOffset])) {
|
|
# If this is a deserialized object (from an executable) we need recreate a live DateTimeOffset
|
|
if ($InputObject -isnot [DateTimeOffset]) {
|
|
$InputObject = New-Object -TypeName DateTimeOffset $InputObject.DateTime, $InputObject.Offset
|
|
}
|
|
$InputObject.ToString('o')
|
|
}
|
|
elseif (&$isType -InputObject $InputObject -Type ([Type])) {
|
|
if ($Depth -lt 0) {
|
|
$InputObject.FullName
|
|
}
|
|
else {
|
|
# This type is very complex with circular properties, only return somewhat useful properties.
|
|
# BaseType might be a string (serialized output), try and convert it back to a Type if possible.
|
|
$baseType = $InputObject.BaseType -as [Type]
|
|
if ($baseType) {
|
|
$baseType = Convert-OutputObject -InputObject $baseType -Depth $childDepth
|
|
}
|
|
|
|
@{
|
|
Name = $InputObject.Name
|
|
FullName = $InputObject.FullName
|
|
AssemblyQualifiedName = $InputObject.AssemblyQualifiedName
|
|
BaseType = $baseType
|
|
}
|
|
}
|
|
}
|
|
elseif ($InputObject -is [string]) {
|
|
$InputObject
|
|
}
|
|
elseif (&$isType -InputObject $InputObject -Type ([switch])) {
|
|
$InputObject.IsPresent
|
|
}
|
|
elseif ($InputObject.GetType().IsValueType) {
|
|
# We want to display just this value and not any properties it has (if any).
|
|
$InputObject
|
|
}
|
|
elseif ($Depth -lt 0) {
|
|
# This must occur after the above to ensure ints and other ValueTypes are preserved as is.
|
|
[string]$InputObject
|
|
}
|
|
elseif ($InputObject -is [Collections.IList]) {
|
|
, @(foreach ($obj in $InputObject) {
|
|
Convert-OutputObject -InputObject $obj -Depth $childDepth
|
|
})
|
|
}
|
|
elseif ($InputObject -is [Collections.IDictionary]) {
|
|
$newObj = @{}
|
|
|
|
# Replicate ConvertTo-Json, props are replaced by keys if they share the same name. We only want ETS
|
|
# properties as well.
|
|
foreach ($prop in $InputObject.PSObject.Properties) {
|
|
if ($prop.MemberType -notin @('AliasProperty', 'ScriptProperty', 'NoteProperty')) {
|
|
continue
|
|
}
|
|
$newObj[$prop.Name] = Convert-OutputObject -InputObject $prop.Value -Depth $childDepth
|
|
}
|
|
foreach ($kvp in $InputObject.GetEnumerator()) {
|
|
$newObj[$kvp.Key] = Convert-OutputObject -InputObject $kvp.Value -Depth $childDepth
|
|
}
|
|
$newObj
|
|
}
|
|
else {
|
|
$newObj = @{}
|
|
foreach ($prop in $InputObject.PSObject.Properties) {
|
|
$newObj[$prop.Name] = Convert-OutputObject -InputObject $prop.Value -Depth $childDepth
|
|
}
|
|
$newObj
|
|
}
|
|
}
|
|
}
|
|
|
|
Function Format-Exception {
|
|
[CmdletBinding()]
|
|
param (
|
|
[Parameter(Mandatory = $true)]
|
|
[AllowNull()]
|
|
$Exception
|
|
)
|
|
|
|
if (-not $Exception) {
|
|
return $null
|
|
}
|
|
elseif ($Exception -is [Management.Automation.RemoteException]) {
|
|
# When using a separate process the exceptions are a RemoteException, we want to get the info on the actual
|
|
# exception.
|
|
return Format-Exception -Exception $Exception.SerializedRemoteException
|
|
}
|
|
|
|
$type = if ($Exception -is [Exception]) {
|
|
$Exception.GetType().FullName
|
|
}
|
|
else {
|
|
# This is a RemoteException, we want to report the original non-serialized type.
|
|
$Exception.PSTypeNames[0] -replace '^Deserialized.'
|
|
}
|
|
|
|
@{
|
|
message = $Exception.Message
|
|
type = $type
|
|
help_link = $Exception.HelpLink
|
|
source = $Exception.Source
|
|
hresult = $Exception.HResult
|
|
inner_exception = Format-Exception -Exception $Exception.InnerException
|
|
}
|
|
}
|
|
|
|
Function Test-AnsiblePath {
|
|
[CmdletBinding()]
|
|
param (
|
|
[Parameter(Mandatory = $true)]
|
|
[String]
|
|
$Path
|
|
)
|
|
|
|
# Certain system files fail on Test-Path due to it being locked, we can still get the attributes though.
|
|
try {
|
|
[void][System.IO.File]::GetAttributes($Path)
|
|
return $true
|
|
}
|
|
catch [System.IO.FileNotFoundException], [System.IO.DirectoryNotFoundException] {
|
|
return $false
|
|
}
|
|
catch {
|
|
# When testing a path like Cert:\LocalMachine\My, System.IO.File will
|
|
# not work, we just revert back to using Test-Path for this
|
|
return Test-Path -Path $Path
|
|
}
|
|
}
|
|
|
|
Function New-AnonymousPipe {
|
|
[CmdletBinding()]
|
|
param ()
|
|
|
|
$utf8NoBom = New-Object -TypeName Text.UTF8Encoding -ArgumentList $false
|
|
|
|
$server = New-Object -TypeName IO.Pipes.AnonymousPipeServerStream -ArgumentList 'In', 'Inheritable'
|
|
$client = New-Object -TypeName IO.Pipes.AnonymousPipeClientStream -ArgumentList 'Out', $server.ClientSafePipeHandle
|
|
$clientWriter = New-Object -TypeName IO.StreamWriter -ArgumentList $client, $utf8NoBom
|
|
$clientWriter.AutoFlush = $true # Ensures the data stays in sync when dealing with subprocesses.
|
|
|
|
# Create the background task that will constantly read from the pipe and append to our StringBuilder until the pipe
|
|
# is closed. It also closes the pipe once finished so we don't have to. Without this the pipe buffer can become
|
|
# full and hang the script.
|
|
$sb = New-Object -TypeName Text.StringBuilder
|
|
$ps = [PowerShell]::Create()
|
|
|
|
[void]$ps.AddScript(@'
|
|
[CmdletBinding()]
|
|
param (
|
|
[Text.StringBuilder]
|
|
$StringBuilder,
|
|
|
|
[IO.Pipes.AnonymousPipeServerStream]
|
|
$Server,
|
|
|
|
[Text.Encoding]
|
|
$Encoding
|
|
)
|
|
|
|
$sr = New-Object -TypeName IO.StreamReader -ArgumentList $Server, $Encoding
|
|
try {
|
|
$buffer = New-Object -TypeName char[] -ArgumentList $Server.InBufferSize
|
|
while ($read = $sr.Read($buffer, 0, $buffer.Length)) {
|
|
[void]$StringBuilder.Append($buffer, 0, $read)
|
|
}
|
|
}
|
|
finally {
|
|
$sr.Dispose()
|
|
}
|
|
'@).AddParameters(@{
|
|
StringBuilder = $sb
|
|
Server = $server
|
|
Encoding = $utf8NoBom
|
|
})
|
|
$task = $ps.BeginInvoke()
|
|
|
|
[PSCustomObject]@{
|
|
PowerShell = $ps
|
|
Task = $task
|
|
Output = $sb
|
|
Client = $clientWriter
|
|
ClientString = $server.GetClientHandleAsString()
|
|
}
|
|
}
|
|
|
|
$creates = $module.Params.creates
|
|
if ($creates -and (Test-AnsiblePath -Path $creates)) {
|
|
$module.Result.msg = "skipped, since $creates exists"
|
|
$module.ExitJson()
|
|
}
|
|
|
|
$removes = $module.Params.removes
|
|
if ($removes -and -not (Test-AnsiblePath -Path $removes)) {
|
|
$module.Result.msg = "skipped, since $removes does not exist"
|
|
$module.ExitJson()
|
|
}
|
|
|
|
# Check if the script has [CmdletBinding(SupportsShouldProcess)] on it
|
|
$scriptAst = [ScriptBlock]::Create($module.Params.script).Ast
|
|
$supportsShouldProcess = $false
|
|
if ($scriptAst -is [Management.Automation.Language.ScriptBlockAst] -and $scriptAst.ParamBlock.Attributes) {
|
|
$supportsShouldProcess = [bool]($scriptAst.ParamBlock.Attributes |
|
|
Where-Object { $_.TypeName.Name -eq 'CmdletBinding' } |
|
|
Select-Object -First 1 |
|
|
ForEach-Object -Process {
|
|
$_.NamedArguments | Where-Object {
|
|
$_.ArgumentName -eq 'SupportsShouldProcess' -and ($_.ExpressionOmitted -or $_.Argument.ToString() -eq '$true')
|
|
}
|
|
})
|
|
}
|
|
|
|
if ($module.CheckMode -and -not $supportsShouldProcess) {
|
|
$module.Result.changed = $true
|
|
$module.Result.msg = "skipped, running in check mode"
|
|
$module.ExitJson()
|
|
}
|
|
|
|
$runspace = $null
|
|
$processId = $null
|
|
$newStdout = New-AnonymousPipe
|
|
$newStderr = New-AnonymousPipe
|
|
$freeConsole = $false
|
|
|
|
try {
|
|
$oldStdout = Get-StdHandle -Stream Stdout
|
|
$oldStderr = Get-StdHandle -Stream Stderr
|
|
|
|
if ($module.Params.executable) {
|
|
if ($PSVersionTable.PSVersion -lt [version]'5.0') {
|
|
$module.FailJson("executable requires PowerShell 5.0 or newer")
|
|
}
|
|
|
|
# Neither Start-Process or Diagnostics.Process give us the ability to create a process with a new console and
|
|
# the ability to inherit handles so we use own home grown CreateProcess wrapper.
|
|
$applicationName = Resolve-ExecutablePath -FilePath $module.Params.executable
|
|
$commandLine = ConvertTo-EscapedArgument -InputObject $module.Params.executable
|
|
if ($module.Params.arguments) {
|
|
$escapedArguments = @($module.Params.arguments | ConvertTo-EscapedArgument)
|
|
$commandLine += " $($escapedArguments -join ' ')"
|
|
}
|
|
|
|
# While we could attach the stdout/stderr pipes here we would capture the startup info and prompt that
|
|
# powershell will output. Instead we set the console as part of the pipeline we run.
|
|
$si = [Ansible.Windows.Process.StartupInfo]@{
|
|
WindowStyle = 'Hidden' # Useful when debugging locally, doesn't really matter in normal Ansible.
|
|
}
|
|
$pi = [Ansible.Windows.Process.ProcessUtil]::NativeCreateProcess(
|
|
$applicationName,
|
|
$commandLIne,
|
|
$null,
|
|
$null,
|
|
$true, # Required so the child process can inherit our anon pipes.
|
|
'CreateNewConsole', # Ensures we don't mess with the current console output.
|
|
$null,
|
|
$null,
|
|
$si
|
|
)
|
|
$processId = $pi.ProcessId
|
|
$pi.Dispose()
|
|
}
|
|
|
|
# Using a custom host allows us to capture any host UI calls through our own output.
|
|
$runspaceHost = New-Object -TypeName Ansible.Windows.WinPowerShell.Host -ArgumentList $Host, $newStdout.Client, $newStderr.Client
|
|
if ($processId) {
|
|
$connInfo = [System.Management.Automation.Runspaces.NamedPipeConnectionInfo]$processId
|
|
|
|
# In case a user specified an executable that does not support the PSHost named pipe that PowerShell uses we
|
|
# specify a timeout so the module does not hang.
|
|
$connInfo.OpenTimeout = 5000
|
|
$runspace = [RunspaceFactory]::CreateRunspace($runspaceHost, $connInfo)
|
|
}
|
|
else {
|
|
$runspace = [RunspaceFactory]::CreateRunspace($runspaceHost)
|
|
}
|
|
|
|
$runspace.Open()
|
|
|
|
$ps = [PowerShell]::Create()
|
|
$ps.Runspace = $runspace
|
|
|
|
$ps.Runspace.SessionStateProxy.SetVariable('Ansible', [PSCustomObject]@{
|
|
PSTypeName = 'Ansible.Windows.WinPowerShell.Module'
|
|
CheckMode = $module.CheckMode
|
|
Verbosity = $module.Verbosity
|
|
Result = @{}
|
|
Changed = $true
|
|
Failed = $false
|
|
Tmpdir = $module.Tmpdir
|
|
})
|
|
|
|
$eap = switch ($module.Params.error_action) {
|
|
'stop' { 'Stop' }
|
|
'continue' { 'Continue' }
|
|
'silently_continue' { 'SilentlyContinue' }
|
|
}
|
|
$ps.Runspace.SessionStateProxy.SetVariable('ErrorActionPreference', [Management.Automation.ActionPreference]$eap)
|
|
|
|
if ($processId) {
|
|
# If we are running in a new process we need to set the various console encoding values to UTF-8 to ensure a
|
|
# consistent encoding experience when PowerShell is running native commands and getting the output back. We
|
|
# also need to redirect the stdout/stderr pipes to our anonymous pipe so we can capture any native console
|
|
# output from .NET or calling a native application with 'Start-Process -NoNewWindow'.
|
|
|
|
[void]$ps.AddScript(@'
|
|
[CmdletBinding()]
|
|
param (
|
|
[Parameter(Mandatory=$true)]
|
|
[String]
|
|
$StdoutHandle,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[String]
|
|
$StderrHandle,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[String]
|
|
$SetStdPInvoke,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[String]
|
|
$SetScriptBlock,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[String]
|
|
$AddTypeCode,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[String]
|
|
$Tmpdir
|
|
)
|
|
|
|
# Using Add-Type here leaves an empty folder for some reason, our code does not and also allows us to control the
|
|
# temp directory used.
|
|
&([ScriptBlock]::Create($AddTypeCode)) -References $SetStdPInvoke -TempPath $Tmpdir
|
|
|
|
$setHandle = [ScriptBlock]::Create($SetScriptBlock)
|
|
$utf8NoBom = New-Object -TypeName Text.UTF8Encoding -ArgumentList $false
|
|
|
|
# Make sure our console encoding values are all set to UTF-8 for a consistent experience.
|
|
$OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = $utf8NoBom
|
|
|
|
# Set the stdout/stderr for both .NET and natively to our anonymous pipe.
|
|
@{Name = 'Stdout'; Handle = $StdoutHandle}, @{Name = 'Stderr'; Handle = $StderrHandle} | ForEach-Object -Process {
|
|
$pipe = New-Object -TypeName IO.Pipes.AnonymousPipeClientStream -ArgumentList 'Out', $_.Handle
|
|
$writer = New-Object -TypeName IO.StreamWriter -ArgumentList $pipe, $utf8NoBom
|
|
$writer.AutoFlush = $true # Ensures we data in the correct order.
|
|
|
|
&$setHandle -Stream $_.Name -NET $writer -Raw $pipe.SafePipeHandle.DangerousGetHandle()
|
|
}
|
|
'@, $true).AddParameters(@{
|
|
StdoutHandle = $newStdout.ClientString
|
|
StderrHandle = $newStderr.ClientString
|
|
SetStdPInvoke = $stdPinvoke
|
|
SetScriptBlock = ${function:Set-StdHandle}
|
|
AddTypeCode = ${function:Add-CSharpType}
|
|
TmpDir = $module.Tmpdir
|
|
}).AddStatement()
|
|
}
|
|
else {
|
|
# The psrp connection plugin doesn't have a console so we need to create one ourselves.
|
|
if ([Ansible.Windows.WinPowerShell.NativeMethods]::GetConsoleWindow() -eq [IntPtr]::Zero) {
|
|
$freeConsole = [Ansible.Windows.WinPowerShell.NativeMethods]::AllocConsole()
|
|
}
|
|
|
|
# Else we are running in the same process, we need to redirect the console and .NET output pipes to our
|
|
# anonymous pipe. We shouldn't have to set the encoding, the module wrapper already does this.
|
|
Set-StdHandle -Stream Stdout -NET $newStdout.Client -Raw $newStdout.Client.BaseStream.SafePipeHandle.DangerousGetHandle()
|
|
Set-StdHandle -Stream Stderr -NET $newStderr.Client -Raw $newStderr.Client.BaseStream.SafePipeHandle.DangerousGetHandle()
|
|
|
|
$utf8NoBom = New-Object -TypeName Text.UTF8Encoding -ArgumentList $false
|
|
$OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = $utf8NoBom
|
|
}
|
|
|
|
if ($module.Params.chdir) {
|
|
[void]$ps.AddCommand('Set-Location').AddParameter('LiteralPath', $module.Params.chdir).AddStatement()
|
|
}
|
|
|
|
[void]$ps.AddScript($module.Params.script)
|
|
|
|
# We copy the existing parameter dictionary and add/modify the Confirm/WhatIf parameters if the script supports
|
|
# processing. We do a copy to avoid modifying the original Params dictionary just for safety.
|
|
$parameters = @{}
|
|
if ($module.Params.parameters) {
|
|
foreach ($kvp in $module.Params.parameters.GetEnumerator()) {
|
|
$parameters[$kvp.Key] = $kvp.Value
|
|
}
|
|
}
|
|
if ($supportsShouldProcess) {
|
|
# We do this last to ensure we take precedence over any user inputted settings.
|
|
$parameters.Confirm = $false # Ensure we don't block on any confirmation prompts
|
|
$parameters.WhatIf = $module.CheckMode
|
|
}
|
|
|
|
if ($parameters) {
|
|
[void]$ps.AddParameters($parameters)
|
|
}
|
|
|
|
# We cannot natively call a generic function so need to resort to reflection to get the method we know is there
|
|
# and turn it into an invocable method. We do this so we can call the overload that takes in an IList for the
|
|
# output which means we don't loose anything from before a terminating exception was raised.
|
|
$psOutput = [Collections.Generic.List[Object]]@()
|
|
$invokeMethod = $ps.GetType().GetMethods('Public, Instance, InvokeMethod') | Where-Object {
|
|
if ($_.Name -ne 'Invoke' -or $_.ReturnType -ne [void] -or -not $_.ContainsGenericParameters) {
|
|
return $false
|
|
}
|
|
|
|
$parameters = $_.GetParameters()
|
|
(
|
|
$parameters.Count -eq 2 -and
|
|
$parameters[0].ParameterType -eq [Collections.IEnumerable] -and
|
|
$parameters[1].ParameterType.Namespace -eq 'System.Collections.Generic' -and
|
|
$parameters[1].ParameterType.Name -eq 'IList`1'
|
|
)
|
|
}
|
|
$invoke = $invokeMethod.MakeGenericMethod([object])
|
|
|
|
try {
|
|
$invoke.Invoke($ps, @(@(), $psOutput))
|
|
}
|
|
catch [Management.Automation.RuntimeException] {
|
|
# $ErrorActionPrefrence = 'Stop' and an error was raised
|
|
# OR
|
|
# General exception was raised in the script like 'throw "error"'.
|
|
# We treat these as failures in the script and return them back to the user.
|
|
$module.Result.failed = $true
|
|
$ps.Streams.Error.Add($_.Exception.ErrorRecord)
|
|
}
|
|
|
|
# Get the internal Ansible variable that can contain code specific information.
|
|
$result = $runspace.SessionStateProxy.GetVariable('Ansible')
|
|
}
|
|
finally {
|
|
if (-not $processId) {
|
|
$oldStdout, $oldStderr | ForEach-Object -Process {
|
|
Set-StdHandle -Stream $_.Stream -NET $_.NET -Raw $_.Raw
|
|
}
|
|
}
|
|
|
|
if ($runspace) {
|
|
$runspace.Dispose()
|
|
}
|
|
if ($processId) {
|
|
Stop-Process -Id $processId -Force
|
|
}
|
|
|
|
$newStdout, $newStderr | ForEach-Object -Process {
|
|
$_.Client.Dispose()
|
|
[void]$_.PowerShell.EndInvoke($_.Task)
|
|
}
|
|
|
|
if ($freeConsole) {
|
|
[void][Ansible.Windows.WinPowerShell.NativeMethods]::FreeConsole()
|
|
}
|
|
}
|
|
|
|
$module.Result.host_out = $newStdout.Output.ToString()
|
|
$module.Result.host_err = $newStderr.Output.ToString()
|
|
$module.Result.result = Convert-OutputObject -InputObject $result.Result -Depth $module.Params.depth
|
|
$module.Result.changed = $result.Changed
|
|
$module.Result.failed = $module.Result.failed -or $result.Failed
|
|
|
|
# We process the output outselves to flatten anything beyond the depth and deal with certain problematic types with
|
|
# json serialization.
|
|
$module.Result.output = Convert-OutputObject -InputObject $psOutput -Depth $module.Params.depth
|
|
|
|
$module.Result.error = @($ps.Streams.Error | ForEach-Object -Process {
|
|
$err = @{
|
|
output = ($_ | Out-String)
|
|
error_details = $null
|
|
exception = Format-Exception -Exception $_.Exception
|
|
target_object = $_.TargetObject
|
|
category_info = @{
|
|
category = [string]$_.CategoryInfo.Category
|
|
category_id = [int]$_.CategoryInfo.Category
|
|
activity = $_.CategoryInfo.Activity
|
|
reason = $_.CategoryInfo.Reason
|
|
target_name = $_.CategoryInfo.TargetName
|
|
target_type = $_.CategoryInfo.TargetType
|
|
}
|
|
fully_qualified_error_id = $_.FullyQualifiedErrorId
|
|
script_stack_trace = $_.ScriptStackTrace
|
|
pipeline_iteration_info = $_.PipelineIterationInfo
|
|
}
|
|
if ($_.ErrorDetails) {
|
|
$err.error_details = @{
|
|
message = $_.ErrorDetails.Message
|
|
recommended_action = $_.ErrorDetails.RecommendedAction
|
|
}
|
|
}
|
|
|
|
$err
|
|
})
|
|
|
|
'debug', 'verbose', 'warning' | ForEach-Object -Process {
|
|
$module.Result.$_ = @($ps.Streams.$_ | Select-Object -ExpandProperty Message)
|
|
}
|
|
|
|
# Use Select-Object as Information may not be present on earlier pwsh version (<v5).
|
|
$module.Result.information = @($ps.Streams |
|
|
Select-Object -ExpandProperty Information -ErrorAction SilentlyContinue |
|
|
ForEach-Object -Process {
|
|
@{
|
|
message_data = Convert-OutputObject -InputObject $_.MessageData -Depth $module.Params.depth
|
|
source = $_.Source
|
|
time_generated = $_.TimeGenerated.ToUniversalTime().ToString('o')
|
|
tags = @($_.Tags)
|
|
}
|
|
}
|
|
)
|
|
|
|
$module.ExitJson()
|