windows command changed to use CreateProcess (#30253)

* windows command changed to use CreateProcess

* change to get become to work
This commit is contained in:
Jordan Borean 2017-09-14 02:58:49 +10:00 committed by Matt Davis
parent ea8af15dfe
commit 6d196eaa98
6 changed files with 669 additions and 221 deletions

View file

@ -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 *

View file

@ -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")

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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