windows command changed to use CreateProcess (#30253)
* windows command changed to use CreateProcess * change to get become to work
This commit is contained in:
parent
ea8af15dfe
commit
6d196eaa98
6 changed files with 669 additions and 221 deletions
lib/ansible
module_utils/powershell
modules/windows
test/integration/targets
|
@ -0,0 +1,442 @@
|
|||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
$process_util = @"
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ansible
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public class SECURITY_ATTRIBUTES
|
||||
{
|
||||
public int nLength;
|
||||
public IntPtr lpSecurityDescriptor;
|
||||
public bool bInheritHandle = false;
|
||||
public SECURITY_ATTRIBUTES()
|
||||
{
|
||||
nLength = Marshal.SizeOf(this);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public class STARTUPINFO
|
||||
{
|
||||
public Int32 cb;
|
||||
public IntPtr lpReserved;
|
||||
public IntPtr lpDesktop;
|
||||
public IntPtr lpTitle;
|
||||
public Int32 dwX;
|
||||
public Int32 dwY;
|
||||
public Int32 dwXSize;
|
||||
public Int32 dwYSize;
|
||||
public Int32 dwXCountChars;
|
||||
public Int32 dwYCountChars;
|
||||
public Int32 dwFillAttribute;
|
||||
public Int32 dwFlags;
|
||||
public Int16 wShowWindow;
|
||||
public Int16 cbReserved2;
|
||||
public IntPtr lpReserved2;
|
||||
public IntPtr hStdInput;
|
||||
public IntPtr hStdOutput;
|
||||
public IntPtr hStdError;
|
||||
public STARTUPINFO()
|
||||
{
|
||||
cb = Marshal.SizeOf(this);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public class STARTUPINFOEX
|
||||
{
|
||||
public STARTUPINFO startupInfo;
|
||||
public IntPtr lpAttributeList;
|
||||
public STARTUPINFOEX()
|
||||
{
|
||||
startupInfo = new STARTUPINFO();
|
||||
startupInfo.cb = Marshal.SizeOf(this);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct PROCESS_INFORMATION
|
||||
{
|
||||
public IntPtr hProcess;
|
||||
public IntPtr hThread;
|
||||
public int dwProcessId;
|
||||
public int dwThreadId;
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum StartupInfoFlags : uint
|
||||
{
|
||||
USESTDHANDLES = 0x00000100
|
||||
}
|
||||
|
||||
public enum HandleFlags : uint
|
||||
{
|
||||
None = 0,
|
||||
INHERIT = 1
|
||||
}
|
||||
|
||||
class NativeWaitHandle : WaitHandle
|
||||
{
|
||||
public NativeWaitHandle(IntPtr handle)
|
||||
{
|
||||
this.Handle = handle;
|
||||
}
|
||||
}
|
||||
|
||||
public class Win32Exception : System.ComponentModel.Win32Exception
|
||||
{
|
||||
private string _msg;
|
||||
|
||||
public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
|
||||
|
||||
public Win32Exception(int errorCode, string message) : base(errorCode)
|
||||
{
|
||||
_msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
|
||||
}
|
||||
|
||||
public override string Message { get { return _msg; } }
|
||||
public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
|
||||
}
|
||||
|
||||
public class CommandUtil
|
||||
{
|
||||
private static UInt32 CREATE_UNICODE_ENVIRONMENT = 0x000000400;
|
||||
private static UInt32 CREATE_NEW_CONSOLE = 0x00000010;
|
||||
private static UInt32 EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
|
||||
public static extern bool CreateProcess(
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
string lpApplicationName,
|
||||
StringBuilder lpCommandLine,
|
||||
IntPtr lpProcessAttributes,
|
||||
IntPtr lpThreadAttributes,
|
||||
bool bInheritHandles,
|
||||
uint dwCreationFlags,
|
||||
IntPtr lpEnvironment,
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
string lpCurrentDirectory,
|
||||
STARTUPINFOEX lpStartupInfo,
|
||||
out PROCESS_INFORMATION lpProcessInformation);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
public static extern bool CreatePipe(
|
||||
out IntPtr hReadPipe,
|
||||
out IntPtr hWritePipe,
|
||||
SECURITY_ATTRIBUTES lpPipeAttributes,
|
||||
uint nSize);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool SetHandleInformation(
|
||||
IntPtr hObject,
|
||||
HandleFlags dwMask,
|
||||
int dwFlags);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool InitializeProcThreadAttributeList(
|
||||
IntPtr lpAttributeList,
|
||||
int dwAttributeCount,
|
||||
int dwFlags,
|
||||
ref int lpSize);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool UpdateProcThreadAttribute(
|
||||
IntPtr lpAttributeList,
|
||||
uint dwFlags,
|
||||
IntPtr Attribute,
|
||||
IntPtr lpValue,
|
||||
IntPtr cbSize,
|
||||
IntPtr lpPreviousValue,
|
||||
IntPtr lpReturnSize);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool GetExitCodeProcess(
|
||||
IntPtr hProcess,
|
||||
out uint lpExitCode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool CloseHandle(
|
||||
IntPtr hObject);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
public static extern uint SearchPath(
|
||||
string lpPath,
|
||||
string lpFileName,
|
||||
string lpExtension,
|
||||
int nBufferLength,
|
||||
[MarshalAs (UnmanagedType.LPTStr)]
|
||||
StringBuilder lpBuffer,
|
||||
out IntPtr lpFilePart);
|
||||
|
||||
[DllImport("shell32.dll", SetLastError = true)]
|
||||
static extern IntPtr CommandLineToArgvW(
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
string lpCmdLine,
|
||||
out int pNumArgs);
|
||||
|
||||
public static string[] ParseCommandLine(string lpCommandLine)
|
||||
{
|
||||
int numArgs;
|
||||
IntPtr ret = CommandLineToArgvW(lpCommandLine, out numArgs);
|
||||
|
||||
if (ret == IntPtr.Zero)
|
||||
throw new Win32Exception("Error parsing command line");
|
||||
|
||||
IntPtr[] strptrs = new IntPtr[numArgs];
|
||||
Marshal.Copy(ret, strptrs, 0, numArgs);
|
||||
string[] cmdlineParts = strptrs.Select(s => Marshal.PtrToStringUni(s)).ToArray();
|
||||
|
||||
Marshal.FreeHGlobal(ret);
|
||||
|
||||
return cmdlineParts;
|
||||
}
|
||||
|
||||
public static string SearchPath(string lpFileName)
|
||||
{
|
||||
StringBuilder sbOut = new StringBuilder(1024);
|
||||
IntPtr filePartOut;
|
||||
|
||||
if (SearchPath(null, lpFileName, null, sbOut.Capacity, sbOut, out filePartOut) == 0)
|
||||
throw new FileNotFoundException(String.Format("Could not locate the following executable {0}", lpFileName));
|
||||
|
||||
return sbOut.ToString();
|
||||
}
|
||||
|
||||
public static Tuple<string, string, uint> RunCommand(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, string stdinInput, string environmentBlock)
|
||||
{
|
||||
UInt32 startup_flags = CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | EXTENDED_STARTUPINFO_PRESENT;
|
||||
STARTUPINFOEX si = new STARTUPINFOEX();
|
||||
si.startupInfo.dwFlags = (int)StartupInfoFlags.USESTDHANDLES;
|
||||
|
||||
SECURITY_ATTRIBUTES pipesec = new SECURITY_ATTRIBUTES();
|
||||
pipesec.bInheritHandle = true;
|
||||
|
||||
// Create the stdout, stderr and stdin pipes used in the process and add to the startupInfo
|
||||
IntPtr stdout_read, stdout_write, stderr_read, stderr_write, stdin_read, stdin_write = IntPtr.Zero;
|
||||
if (!CreatePipe(out stdout_read, out stdout_write, pipesec, 0))
|
||||
throw new Win32Exception("STDOUT pipe setup failed");
|
||||
if (!SetHandleInformation(stdout_read, HandleFlags.INHERIT, 0))
|
||||
throw new Win32Exception("STDOUT pipe handle setup failed");
|
||||
|
||||
if (!CreatePipe(out stderr_read, out stderr_write, pipesec, 0))
|
||||
throw new Win32Exception("STDERR pipe setup failed");
|
||||
if (!SetHandleInformation(stderr_read, HandleFlags.INHERIT, 0))
|
||||
throw new Win32Exception("STDERR pipe handle setup failed");
|
||||
|
||||
if (!CreatePipe(out stdin_read, out stdin_write, pipesec, 0))
|
||||
throw new Win32Exception("STDIN pipe setup failed");
|
||||
if (!SetHandleInformation(stdin_write, HandleFlags.INHERIT, 0))
|
||||
throw new Win32Exception("STDIN pipe handle setup failed");
|
||||
|
||||
si.startupInfo.hStdOutput = stdout_write;
|
||||
si.startupInfo.hStdError = stderr_write;
|
||||
si.startupInfo.hStdInput = stdin_read;
|
||||
|
||||
// Handle the inheritance for the pipes so the process can access them
|
||||
Int32 buf_sz = 0;
|
||||
if (!InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0, ref buf_sz))
|
||||
{
|
||||
int last_err = Marshal.GetLastWin32Error();
|
||||
if (last_err != 122) // ERROR_INSUFFICIENT_BUFFER
|
||||
throw new Win32Exception(last_err, "Attribute list size query failed");
|
||||
}
|
||||
si.lpAttributeList = Marshal.AllocHGlobal(buf_sz);
|
||||
if (!InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, ref buf_sz))
|
||||
throw new Win32Exception("Attribute list init failed");
|
||||
|
||||
|
||||
IntPtr[] handles_to_inherit = new IntPtr[3];
|
||||
handles_to_inherit[0] = stdin_read;
|
||||
handles_to_inherit[1] = stdout_write;
|
||||
handles_to_inherit[2] = stderr_write;
|
||||
GCHandle pinned_handles = GCHandle.Alloc(handles_to_inherit, GCHandleType.Pinned);
|
||||
|
||||
if (!UpdateProcThreadAttribute(si.lpAttributeList, 0,
|
||||
(IntPtr)0x20002, // PROC_THREAD_ATTRIBUTE_HANDLE_LIST
|
||||
pinned_handles.AddrOfPinnedObject(),
|
||||
(IntPtr)(Marshal.SizeOf(typeof(IntPtr)) * handles_to_inherit.Length),
|
||||
IntPtr.Zero, IntPtr.Zero))
|
||||
{
|
||||
throw new Win32Exception("Attribute list update failed");
|
||||
}
|
||||
|
||||
// Setup the stdin buffer
|
||||
UTF8Encoding utf8_encoding = new UTF8Encoding(false);
|
||||
FileStream stdin_fs = new FileStream(stdin_write, FileAccess.Write, true, 32768);
|
||||
StreamWriter stdin = new StreamWriter(stdin_fs, utf8_encoding, 32768);
|
||||
|
||||
// If lpCurrentDirectory is set to null in PS it will be an empty
|
||||
// string here, we need to convert it
|
||||
if (lpCurrentDirectory == "")
|
||||
lpCurrentDirectory = null;
|
||||
|
||||
// Create the environment block if set
|
||||
IntPtr lpEnvironment = IntPtr.Zero;
|
||||
if (environmentBlock != "")
|
||||
lpEnvironment = Marshal.StringToHGlobalUni(environmentBlock);
|
||||
|
||||
// Create new process and run
|
||||
StringBuilder argument_string = new StringBuilder(lpCommandLine);
|
||||
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
|
||||
if (!CreateProcess(
|
||||
lpApplicationName,
|
||||
argument_string,
|
||||
IntPtr.Zero,
|
||||
IntPtr.Zero,
|
||||
true,
|
||||
startup_flags,
|
||||
lpEnvironment,
|
||||
lpCurrentDirectory,
|
||||
si,
|
||||
out pi))
|
||||
{
|
||||
throw new Win32Exception("Failed to create new process");
|
||||
}
|
||||
|
||||
// Setup the output buffers and get stdout/stderr
|
||||
FileStream stdout_fs = new FileStream(stdout_read, FileAccess.Read, true, 4096);
|
||||
StreamReader stdout = new StreamReader(stdout_fs, utf8_encoding, true, 4096);
|
||||
FileStream stderr_fs = new FileStream(stderr_read, FileAccess.Read, true, 4096);
|
||||
StreamReader stderr = new StreamReader(stderr_fs, utf8_encoding, true, 4096);
|
||||
CloseHandle(stdout_write);
|
||||
CloseHandle(stderr_write);
|
||||
|
||||
stdin.WriteLine(stdinInput);
|
||||
stdin.Close();
|
||||
|
||||
string stdout_str, stderr_str = null;
|
||||
GetProcessOutput(stdout, stderr, out stdout_str, out stderr_str);
|
||||
uint rc = GetProcessExitCode(pi.hProcess);
|
||||
|
||||
return Tuple.Create(stdout_str, stderr_str, rc);
|
||||
}
|
||||
|
||||
private static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr)
|
||||
{
|
||||
var sowait = new EventWaitHandle(false, EventResetMode.ManualReset);
|
||||
var sewait = new EventWaitHandle(false, EventResetMode.ManualReset);
|
||||
string so = null, se = null;
|
||||
ThreadPool.QueueUserWorkItem((s) =>
|
||||
{
|
||||
so = stdoutStream.ReadToEnd();
|
||||
sowait.Set();
|
||||
});
|
||||
ThreadPool.QueueUserWorkItem((s) =>
|
||||
{
|
||||
se = stderrStream.ReadToEnd();
|
||||
sewait.Set();
|
||||
});
|
||||
foreach (var wh in new WaitHandle[] { sowait, sewait })
|
||||
wh.WaitOne();
|
||||
stdout = so;
|
||||
stderr = se;
|
||||
}
|
||||
|
||||
private static uint GetProcessExitCode(IntPtr processHandle)
|
||||
{
|
||||
new NativeWaitHandle(processHandle).WaitOne();
|
||||
uint exitCode;
|
||||
if (!GetExitCodeProcess(processHandle, out exitCode))
|
||||
throw new Win32Exception("Error getting process exit code");
|
||||
return exitCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
Function Load-CommandUtils {
|
||||
# makes the following static functions available
|
||||
# [Ansible.CommandUtil]::ParseCommandLine(string lpCommandLine)
|
||||
# [Ansible.CommandUtil]::SearchPath(string lpFileName)
|
||||
# [Ansible.CommandUtil]::RunCommand(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, string stdinInput, string environmentBlock)
|
||||
#
|
||||
# there are also numerous P/Invoke methods that can be called if you are feeling adventurous
|
||||
Add-Type -TypeDefinition $process_util -IgnoreWarnings
|
||||
}
|
||||
|
||||
Function Get-ExecutablePath($executable, $directory) {
|
||||
# lpApplicationName requires the full path to a file, we need to find it
|
||||
# ourselves.
|
||||
|
||||
# we need to add .exe if it doesn't have an extension already
|
||||
if (-not [System.IO.Path]::HasExtension($executable)) {
|
||||
$executable = "$($executable).exe"
|
||||
}
|
||||
$full_path = [System.IO.Path]::GetFullPath($executable)
|
||||
|
||||
if ($full_path -ne $executable -and $directory -ne $null) {
|
||||
$file = Get-Item -Path "$directory\$executable" -Force -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
$file = Get-Item -Path $executable -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if ($file -ne $null) {
|
||||
$executable_path = $file.FullName
|
||||
} else {
|
||||
$executable_path = [Ansible.CommandUtil]::SearchPath($executable)
|
||||
}
|
||||
return $executable_path
|
||||
}
|
||||
|
||||
Function Run-Command {
|
||||
Param(
|
||||
[string]$command, # the full command to run including the executable
|
||||
[string]$working_directory = $null, # the working directory to run under, will default to the current dir
|
||||
[string]$stdin = $null, # a string to send to the stdin pipe when executing the command
|
||||
[hashtable]$environment = @{} # a hashtable of environment values to run the command under, this will replace all the other environment variables with these
|
||||
)
|
||||
|
||||
# load the C# code we call in this function
|
||||
Load-CommandUtils
|
||||
|
||||
# need to validate the working directory if it is set
|
||||
if ($working_directory) {
|
||||
# validate working directory is a valid path
|
||||
if (-not (Test-Path -Path $working_directory)) {
|
||||
throw "invalid working directory path '$working_directory'"
|
||||
}
|
||||
}
|
||||
|
||||
# lpApplicationName needs to be the full path to an executable, we do this
|
||||
# by getting the executable as the first arg and then getting the full path
|
||||
$arguments = [Ansible.CommandUtil]::ParseCommandLine($command)
|
||||
$executable = Get-ExecutablePath -executable $arguments[0] -directory $working_directory
|
||||
|
||||
# set the extra environment variables
|
||||
$environment_string = $null
|
||||
if ($environment.Count -gt 0) {
|
||||
$environment_string = ""
|
||||
}
|
||||
foreach ($environment_entry in $environment.GetEnumerator()){
|
||||
$environment_key = $environment_entry.Name
|
||||
$environment_value = $environment_entry.Value
|
||||
$environment_string += "$environment_key=$environment_value`0"
|
||||
}
|
||||
if ($environment_string) {
|
||||
$environment_string += "`0"
|
||||
}
|
||||
|
||||
# run the command and get the results
|
||||
$command_result = [Ansible.CommandUtil]::RunCommand($executable, $command, $working_directory, $stdin, $environment_string)
|
||||
|
||||
# RunCommand returns a tuple, we will convert to a hashtable
|
||||
return ,@{
|
||||
executable = $executable
|
||||
stdout = $command_result.Item1
|
||||
stderr = $command_result.Item2
|
||||
rc = $command_result.Item3
|
||||
}
|
||||
}
|
||||
|
||||
# this line must stay at the bottom to ensure all defined module parts are exported
|
||||
Export-ModuleMember -Alias * -Function * -Cmdlet *
|
|
@ -1,88 +1,16 @@
|
|||
#!powershell
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# WANT_JSON
|
||||
# POWERSHELL_COMMON
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy.psm1
|
||||
#Requires -Module Ansible.ModuleUtils.CommandUtil.psm1
|
||||
|
||||
# TODO: add check mode support
|
||||
|
||||
Set-StrictMode -Version 2
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$helper_def = @'
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ansible.Command
|
||||
{
|
||||
public static class NativeUtil
|
||||
{
|
||||
[DllImport("shell32.dll", SetLastError = true)]
|
||||
static extern IntPtr CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, out int pNumArgs);
|
||||
|
||||
public static string[] ParseCommandLine(string cmdline)
|
||||
{
|
||||
int numArgs;
|
||||
IntPtr ret = CommandLineToArgvW(cmdline, out numArgs);
|
||||
|
||||
if (ret == IntPtr.Zero)
|
||||
throw new Exception(String.Format("Error parsing command line: {0}", new Win32Exception(Marshal.GetLastWin32Error()).Message));
|
||||
|
||||
IntPtr[] strptrs = new IntPtr[numArgs];
|
||||
Marshal.Copy(ret, strptrs, 0, numArgs);
|
||||
string[] cmdlineParts = strptrs.Select(s=>Marshal.PtrToStringUni(s)).ToArray();
|
||||
|
||||
Marshal.FreeHGlobal(ret);
|
||||
|
||||
return cmdlineParts;
|
||||
}
|
||||
|
||||
public static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr)
|
||||
{
|
||||
var sowait = new EventWaitHandle(false, EventResetMode.ManualReset);
|
||||
var sewait = new EventWaitHandle(false, EventResetMode.ManualReset);
|
||||
|
||||
string so = null, se = null;
|
||||
|
||||
ThreadPool.QueueUserWorkItem((s)=>
|
||||
{
|
||||
so = stdoutStream.ReadToEnd();
|
||||
sowait.Set();
|
||||
});
|
||||
|
||||
ThreadPool.QueueUserWorkItem((s) =>
|
||||
{
|
||||
se = stderrStream.ReadToEnd();
|
||||
sewait.Set();
|
||||
});
|
||||
|
||||
foreach(var wh in new WaitHandle[] { sowait, sewait })
|
||||
wh.WaitOne();
|
||||
|
||||
stdout = so;
|
||||
stderr = se;
|
||||
}
|
||||
}
|
||||
}
|
||||
'@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$params = Parse-Args $args -supports_check_mode $false
|
||||
|
||||
|
@ -106,61 +34,24 @@ If($removes -and -not $(Test-Path -Path $removes)) {
|
|||
Exit-Json @{msg="skipped, since $removes does not exist";cmd=$raw_command_line;changed=$false;skipped=$true;rc=0}
|
||||
}
|
||||
|
||||
Add-Type -TypeDefinition $helper_def
|
||||
|
||||
$exec_args = $null
|
||||
|
||||
# FUTURE: extract this code to separate module_utils as Windows module API version of run_command
|
||||
|
||||
# Parse the command-line with the Win32 parser to get the application name to run. The Win32 parser
|
||||
# will deal with quoting/escaping for us...
|
||||
# FUTURE: no longer necessary once we switch to raw Win32 CreateProcess
|
||||
$parsed_command_line = [Ansible.Command.NativeUtil]::ParseCommandLine($raw_command_line);
|
||||
$exec_application = $parsed_command_line[0]
|
||||
If($parsed_command_line.Length -gt 1) {
|
||||
# lop the application off, then rejoin the args as a single string
|
||||
$exec_args = $parsed_command_line[1..$($parsed_command_line.Length-1)] -join " "
|
||||
}
|
||||
|
||||
$proc = New-Object System.Diagnostics.Process
|
||||
$psi = $proc.StartInfo
|
||||
$psi.FileName = $exec_application
|
||||
$psi.Arguments = $exec_args
|
||||
$psi.RedirectStandardOutput = $true
|
||||
$psi.RedirectStandardError = $true
|
||||
$psi.UseShellExecute = $false
|
||||
|
||||
If ($chdir) {
|
||||
$psi.WorkingDirectory = $chdir
|
||||
}
|
||||
|
||||
$start_datetime = [DateTime]::UtcNow
|
||||
|
||||
Try {
|
||||
$proc.Start() | Out-Null # will always return $true for non shell-exec cases
|
||||
}
|
||||
Catch [System.ComponentModel.Win32Exception] {
|
||||
# fail nicely for "normal" error conditions
|
||||
# FUTURE: this probably won't work on Nano Server
|
||||
$excep = $_
|
||||
Exit-Json @{msg = $excep.Exception.Message; cmd = $raw_command_line; changed = $false; rc = $excep.Exception.NativeErrorCode}
|
||||
try {
|
||||
$command_result = Run-Command -command $raw_command_line -working_directory $chdir
|
||||
} catch {
|
||||
$result.changed = $false
|
||||
try {
|
||||
$result.rc = $_.Exception.NativeErrorCode
|
||||
} catch {
|
||||
$result.rc = 2
|
||||
}
|
||||
Fail-Json -obj $result -message $_.Exception.Message
|
||||
}
|
||||
|
||||
$stdout = $stderr = [string] $null
|
||||
|
||||
[Ansible.Command.NativeUtil]::GetProcessOutput($proc.StandardOutput, $proc.StandardError, [ref] $stdout, [ref] $stderr) | Out-Null
|
||||
|
||||
$result.stdout = $stdout
|
||||
$result.stderr = $stderr
|
||||
|
||||
# TODO: decode CLIXML stderr output (and other streams?)
|
||||
|
||||
$proc.WaitForExit() | Out-Null
|
||||
|
||||
$result.rc = $proc.ExitCode
|
||||
$result.stdout = $command_result.stdout
|
||||
$result.stderr = $command_result.stderr
|
||||
$result.rc = $command_result.rc
|
||||
|
||||
$end_datetime = [DateTime]::UtcNow
|
||||
|
||||
$result.start = $start_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
|
||||
$result.end = $end_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
|
||||
$result.delta = $($end_datetime - $start_datetime).ToString("h\:mm\:ss\.ffffff")
|
||||
|
|
|
@ -1,65 +1,17 @@
|
|||
#!powershell
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# WANT_JSON
|
||||
# POWERSHELL_COMMON
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy.psm1
|
||||
#Requires -Module Ansible.ModuleUtils.CommandUtil.psm1
|
||||
|
||||
# TODO: add check mode support
|
||||
|
||||
Set-StrictMode -Version 2
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$helper_def = @"
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ansible.Shell
|
||||
{
|
||||
public class ProcessUtil
|
||||
{
|
||||
public static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr)
|
||||
{
|
||||
var sowait = new EventWaitHandle(false, EventResetMode.ManualReset);
|
||||
var sewait = new EventWaitHandle(false, EventResetMode.ManualReset);
|
||||
|
||||
string so = null, se = null;
|
||||
|
||||
ThreadPool.QueueUserWorkItem((s)=>
|
||||
{
|
||||
so = stdoutStream.ReadToEnd();
|
||||
sowait.Set();
|
||||
});
|
||||
|
||||
ThreadPool.QueueUserWorkItem((s) =>
|
||||
{
|
||||
se = stderrStream.ReadToEnd();
|
||||
sewait.Set();
|
||||
});
|
||||
|
||||
foreach(var wh in new WaitHandle[] { sowait, sewait })
|
||||
wh.WaitOne();
|
||||
|
||||
stdout = so;
|
||||
stderr = se;
|
||||
}
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
# Cleanse CLIXML from stderr (sift out error stream data, discard others for now)
|
||||
Function Cleanse-Stderr($raw_stderr) {
|
||||
Try {
|
||||
|
@ -110,12 +62,9 @@ If($removes -and -not $(Test-Path $removes)) {
|
|||
Exit-Json @{msg="skipped, since $removes does not exist";cmd=$raw_command_line;changed=$false;skipped=$true;rc=0}
|
||||
}
|
||||
|
||||
Add-Type -TypeDefinition $helper_def
|
||||
|
||||
$exec_args = $null
|
||||
|
||||
If(-not $executable -or $executable -eq "powershell") {
|
||||
$exec_application = "powershell"
|
||||
$exec_application = "powershell.exe"
|
||||
|
||||
# force input encoding to preamble-free UTF8 so PS sub-processes (eg, Start-Job) don't blow up
|
||||
$raw_command_line = "[Console]::InputEncoding = New-Object Text.UTF8Encoding `$false; " + $raw_command_line
|
||||
|
@ -123,53 +72,37 @@ If(-not $executable -or $executable -eq "powershell") {
|
|||
# Base64 encode the command so we don't have to worry about the various levels of escaping
|
||||
$encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($raw_command_line))
|
||||
|
||||
$exec_args = @("-noninteractive", "-encodedcommand", $encoded_command)
|
||||
$exec_args = "-noninteractive -encodedcommand $encoded_command"
|
||||
}
|
||||
Else {
|
||||
# FUTURE: support arg translation from executable (or executable_args?) to process arguments for arbitrary interpreter?
|
||||
$exec_application = $executable
|
||||
$exec_args = @("/c", $raw_command_line)
|
||||
}
|
||||
|
||||
$proc = New-Object System.Diagnostics.Process
|
||||
$psi = $proc.StartInfo
|
||||
$psi.FileName = $exec_application
|
||||
$psi.Arguments = $exec_args
|
||||
$psi.RedirectStandardOutput = $true
|
||||
$psi.RedirectStandardError = $true
|
||||
$psi.UseShellExecute = $false
|
||||
|
||||
If ($chdir) {
|
||||
$psi.WorkingDirectory = $chdir
|
||||
if (-not ($exec_application.EndsWith(".exe"))) {
|
||||
$exec_application = "$($exec_application).exe"
|
||||
}
|
||||
$exec_args = "/c $raw_command_line"
|
||||
}
|
||||
|
||||
$command = "$exec_application $exec_args"
|
||||
$start_datetime = [DateTime]::UtcNow
|
||||
|
||||
Try {
|
||||
$proc.Start() | Out-Null # will always return $true for non shell-exec cases
|
||||
try {
|
||||
$command_result = Run-Command -command $command -working_directory $chdir
|
||||
} catch {
|
||||
$result.changed = $false
|
||||
try {
|
||||
$result.rc = $_.Exception.NativeErrorCode
|
||||
} catch {
|
||||
$result.rc = 2
|
||||
}
|
||||
Fail-Json -obj $result -message $_.Exception.Message
|
||||
}
|
||||
Catch [System.ComponentModel.Win32Exception] {
|
||||
# fail nicely for "normal" error conditions
|
||||
# FUTURE: this probably won't work on Nano Server
|
||||
$excep = $_
|
||||
Exit-Json @{msg = $excep.Exception.Message; cmd = $raw_command_line; changed = $false; rc = $excep.Exception.NativeErrorCode}
|
||||
}
|
||||
|
||||
$stdout = $stderr = [string] $null
|
||||
|
||||
[Ansible.Shell.ProcessUtil]::GetProcessOutput($proc.StandardOutput, $proc.StandardError, [ref] $stdout, [ref] $stderr) | Out-Null
|
||||
|
||||
$result.stdout = $stdout
|
||||
$result.stderr = Cleanse-Stderr $stderr
|
||||
|
||||
# TODO: decode CLIXML stderr output (and other streams?)
|
||||
|
||||
$proc.WaitForExit() | Out-Null
|
||||
|
||||
$result.rc = $proc.ExitCode
|
||||
$result.stdout = $command_result.stdout
|
||||
$result.stderr = Cleanse-Stderr $command_result.stderr
|
||||
$result.rc = $command_result.rc
|
||||
|
||||
$end_datetime = [DateTime]::UtcNow
|
||||
|
||||
$result.start = $start_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
|
||||
$result.end = $end_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
|
||||
$result.delta = $($end_datetime - $start_datetime).ToString("h\:mm\:ss\.ffffff")
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
- not cmdout|changed
|
||||
- cmdout.cmd == 'bogus_command1234'
|
||||
- cmdout.rc == 2
|
||||
- cmdout.msg is search('cannot find the file specified')
|
||||
- "'Could not locate the following executable bogus_command1234' in cmdout.msg"
|
||||
|
||||
- name: execute something with error output
|
||||
win_command: cmd /c "echo some output & echo some error 1>&2"
|
||||
|
@ -134,3 +134,47 @@
|
|||
- cmdout.stdout is search("doneout")
|
||||
- cmdout.stderr is search("starterror")
|
||||
- cmdout.stderr is search("doneerror")
|
||||
|
||||
- name: create testing folder for argv binary
|
||||
win_file:
|
||||
path: C:\ansible testing
|
||||
state: directory
|
||||
|
||||
- name: download binary the outputs argv to stdout
|
||||
win_get_url:
|
||||
url: https://s3.amazonaws.com/ansible-ci-files/test/integration/roles/test_win_module_utils/PrintArgv.exe
|
||||
dest: C:\ansible testing\PrintArgv.exe
|
||||
|
||||
- name: call argv binary with absolute path
|
||||
win_command: '"C:\ansible testing\PrintArgv.exe" arg1 "arg 2" C:\path\arg "\"quoted arg\""'
|
||||
register: cmdout
|
||||
|
||||
- name: assert call to argv binary with absolute path
|
||||
assert:
|
||||
that:
|
||||
- cmdout|changed
|
||||
- cmdout.rc == 0
|
||||
- cmdout.stdout_lines[0] == 'arg1'
|
||||
- cmdout.stdout_lines[1] == 'arg 2'
|
||||
- cmdout.stdout_lines[2] == 'C:\\path\\arg'
|
||||
- cmdout.stdout_lines[3] == '"quoted arg"'
|
||||
|
||||
- name: call argv binary with relative path
|
||||
win_command: 'PrintArgv.exe C:\path\end\slash\ ADDLOCAL="msi,example" two\\slashes'
|
||||
args:
|
||||
chdir: C:\ansible testing
|
||||
register: cmdout
|
||||
|
||||
- name: assert call to argv binary with relative path
|
||||
assert:
|
||||
that:
|
||||
- cmdout|changed
|
||||
- cmdout.rc == 0
|
||||
- cmdout.stdout_lines[0] == 'C:\\path\\end\\slash\\'
|
||||
- cmdout.stdout_lines[1] == 'ADDLOCAL=msi,example'
|
||||
- cmdout.stdout_lines[2] == 'two\\\\slashes'
|
||||
|
||||
- name: remove testing folder
|
||||
win_file:
|
||||
path: C:\ansible testing
|
||||
state: absent
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
#!powershell
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
#Requires -Module Ansible.ModuleUtils.CommandUtil
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$params = Parse-Args $args
|
||||
$exe = Get-AnsibleParam -obj $params -name "exe" -type "path" -failifempty $true
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
}
|
||||
|
||||
$exe_directory = Split-Path -Path $exe -Parent
|
||||
$exe_filename = Split-Path -Path $exe -Leaf
|
||||
$test_name = $null
|
||||
|
||||
Function Assert-Equals($actual, $expected) {
|
||||
if ($actual -cne $expected) {
|
||||
Fail-Json -obj $result -message "Test $test_name failed`nActual: '$actual' != Expected: '$expected'"
|
||||
}
|
||||
}
|
||||
|
||||
$test_name = "full exe path"
|
||||
$actual = Run-Command -command "`"$exe`" arg1 arg2 `"arg 3`""
|
||||
Assert-Equals -actual $actual.rc -expected 0
|
||||
Assert-Equals -actual $actual.stdout -expected "arg1`r`narg2`r`narg 3`r`n"
|
||||
Assert-Equals -actual $actual.stderr -expected ""
|
||||
Assert-Equals -actual $actual.executable -expected $exe
|
||||
|
||||
$test_name = "invalid exe path"
|
||||
try {
|
||||
$actual = Run-Command -command "C:\fakepath\$exe_filename arg1"
|
||||
Fail-Json -obj $result -message "Test $test_name failed`nCommand should have thrown an exception"
|
||||
} catch {
|
||||
Assert-Equals -actual $_.Exception.Message -expected "Exception calling `"SearchPath`" with `"1`" argument(s): `"Could not locate the following executable C:\fakepath\$exe_filename`""
|
||||
}
|
||||
|
||||
$test_name = "exe in current folder"
|
||||
$actual = Run-Command -command "$exe_filename arg1" -working_directory $exe_directory
|
||||
Assert-Equals -actual $actual.rc -expected 0
|
||||
Assert-Equals -actual $actual.stdout -expected "arg1`r`n"
|
||||
Assert-Equals -actual $actual.stderr -expected ""
|
||||
Assert-Equals -actual $actual.executable -expected $exe
|
||||
|
||||
$test_name = "no working directory set"
|
||||
$actual = Run-Command -command "cmd.exe /c cd"
|
||||
Assert-Equals -actual $actual.rc -expected 0
|
||||
Assert-Equals -actual $actual.stdout -expected "$($pwd.Path)`r`n"
|
||||
Assert-Equals -actual $actual.stderr -expected ""
|
||||
Assert-Equals -actual $actual.executable.ToUpper() -expected "$env:SystemRoot\System32\cmd.exe".ToUpper()
|
||||
|
||||
$test_name = "working directory override"
|
||||
$actual = Run-Command -command "cmd.exe /c cd" -working_directory $env:SystemRoot
|
||||
Assert-Equals -actual $actual.rc -expected 0
|
||||
Assert-Equals -actual $actual.stdout -expected "$env:SystemRoot`r`n"
|
||||
Assert-Equals -actual $actual.stderr -expected ""
|
||||
Assert-Equals -actual $actual.executable.ToUpper() -expected "$env:SystemRoot\System32\cmd.exe".ToUpper()
|
||||
|
||||
$test_name = "working directory invalid path"
|
||||
try {
|
||||
$actual = Run-Command -command "doesn't matter" -working_directory "invalid path here"
|
||||
Fail-Json -obj $result -message "Test $test_name failed`nCommand should have thrown an exception"
|
||||
} catch {
|
||||
Assert-Equals -actual $_.Exception.Message -expected "invalid working directory path 'invalid path here'"
|
||||
}
|
||||
|
||||
$test_name = "invalid arguments"
|
||||
$actual = Run-Command -command "ipconfig.exe /asdf"
|
||||
Assert-Equals -actual $actual.rc -expected 1
|
||||
|
||||
$test_name = "test stdout and stderr streams"
|
||||
$actual = Run-Command -command "cmd.exe /c echo stdout && echo stderr 1>&2"
|
||||
Assert-Equals -actual $actual.rc -expected 0
|
||||
Assert-Equals -actual $actual.stdout -expected "stdout `r`n"
|
||||
Assert-Equals -actual $actual.stderr -expected "stderr `r`n"
|
||||
|
||||
$test_name = "test default environment variable"
|
||||
Set-Item -Path env:TESTENV -Value "test"
|
||||
$actual = Run-Command -command "cmd.exe /c set"
|
||||
$env_present = $actual.stdout -split "`r`n" | Where-Object { $_ -eq "TESTENV=test" }
|
||||
if ($env_present -eq $null) {
|
||||
Fail-Json -obj $result -message "Test $test_name failed`nenvironment variable TESTENV not found in stdout`n$($actual.stdout)"
|
||||
}
|
||||
|
||||
$test_name = "test custom environment variable1"
|
||||
$actual = Run-Command -command "cmd.exe /c set" -environment @{ TESTENV2 = "testing" }
|
||||
$env_not_present = $actual.stdout -split "`r`n" | Where-Object { $_ -eq "TESTENV=test" }
|
||||
$env_present = $actual.stdout -split "`r`n" | Where-Object { $_ -eq "TESTENV2=testing" }
|
||||
if ($env_not_present -ne $null) {
|
||||
Fail-Json -obj $result -message "Test $test_name failed`nenvironment variabel TESTENV found in stdout when it should be`n$($actual.stdout)"
|
||||
}
|
||||
if ($env_present -eq $null) {
|
||||
Fail-json -obj $result -message "Test $test_name failed`nenvironment variable TESTENV2 not found in stdout`n$($actual.stdout)"
|
||||
}
|
||||
|
||||
$test_name = "input test"
|
||||
$wrapper = @"
|
||||
begin {
|
||||
`$string = ""
|
||||
} process {
|
||||
`$current_input = [string]`$input
|
||||
`$string += `$current_input
|
||||
} end {
|
||||
Write-Host `$string
|
||||
}
|
||||
"@
|
||||
$encoded_wrapper = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($wrapper))
|
||||
$actual = Run-Command -command "powershell.exe -ExecutionPolicy ByPass -EncodedCommand $encoded_wrapper" -stdin "Ansible"
|
||||
Assert-Equals -actual $actual.stdout -expected "Ansible`n"
|
||||
|
||||
$result.data = "success"
|
||||
Exit-Json -obj $result
|
|
@ -47,3 +47,27 @@
|
|||
- assert:
|
||||
that:
|
||||
- sid_test.data == 'success'
|
||||
|
||||
- name: create testing folder for argv binary
|
||||
win_file:
|
||||
path: C:\ansible testing
|
||||
state: directory
|
||||
|
||||
- name: download binary the outputs argv to stdout
|
||||
win_get_url:
|
||||
url: https://s3.amazonaws.com/ansible-ci-files/test/integration/roles/test_win_module_utils/PrintArgv.exe
|
||||
dest: C:\ansible testing\PrintArgv.exe
|
||||
|
||||
- name: call module with CommandUtil tests
|
||||
command_util_test:
|
||||
exe: C:\ansible testing\PrintArgv.exe
|
||||
register: command_util
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- command_util.data == 'success'
|
||||
|
||||
- name: remove testing folder
|
||||
win_file:
|
||||
path: C:\ansible testing
|
||||
state: absent
|
||||
|
|
Loading…
Reference in a new issue