summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1442
-rw-r--r--lib/ansible/modules/windows/win_command.ps1147
-rw-r--r--lib/ansible/modules/windows/win_shell.ps1119
-rw-r--r--test/integration/targets/win_command/tasks/main.yml46
-rw-r--r--test/integration/targets/win_module_utils/library/command_util_test.ps1114
-rw-r--r--test/integration/targets/win_module_utils/tasks/main.yml24
6 files changed, 670 insertions, 222 deletions
diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1
new file mode 100644
index 0000000000..c2cb669dd9
--- /dev/null
+++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1
@@ -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 *
diff --git a/lib/ansible/modules/windows/win_command.ps1 b/lib/ansible/modules/windows/win_command.ps1
index f2763d0295..4570ffc31f 100644
--- a/lib/ansible/modules/windows/win_command.ps1
+++ b/lib/ansible/modules/windows/win_command.ps1
@@ -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")
diff --git a/lib/ansible/modules/windows/win_shell.ps1 b/lib/ansible/modules/windows/win_shell.ps1
index 3e4f4dc441..e25678d2dc 100644
--- a/lib/ansible/modules/windows/win_shell.ps1
+++ b/lib/ansible/modules/windows/win_shell.ps1
@@ -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
-}
-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 $command -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.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")
diff --git a/test/integration/targets/win_command/tasks/main.yml b/test/integration/targets/win_command/tasks/main.yml
index afd02510f8..b49d19301d 100644
--- a/test/integration/targets/win_command/tasks/main.yml
+++ b/test/integration/targets/win_command/tasks/main.yml
@@ -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
diff --git a/test/integration/targets/win_module_utils/library/command_util_test.ps1 b/test/integration/targets/win_module_utils/library/command_util_test.ps1
new file mode 100644
index 0000000000..0a0826cd54
--- /dev/null
+++ b/test/integration/targets/win_module_utils/library/command_util_test.ps1
@@ -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
diff --git a/test/integration/targets/win_module_utils/tasks/main.yml b/test/integration/targets/win_module_utils/tasks/main.yml
index 3c1b9d7fa6..baf43177fc 100644
--- a/test/integration/targets/win_module_utils/tasks/main.yml
+++ b/test/integration/targets/win_module_utils/tasks/main.yml
@@ -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