From 71e8527d7cc5ac9110ac6b440e6c389de4a09763 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Fri, 6 Apr 2018 07:59:51 +1000 Subject: [PATCH] powershell: display non-ascii characters in command outputs (#37229) --- .../Ansible.ModuleUtils.CommandUtil.psm1 | 83 ++++--------------- lib/ansible/plugins/shell/powershell.py | 28 ++++++- .../targets/win_async_wrapper/tasks/main.yml | 14 ++++ .../targets/win_become/tasks/main.yml | 14 ++++ .../targets/win_command/tasks/main.yml | 13 +++ .../targets/win_shell/tasks/main.yml | 13 +++ 6 files changed, 96 insertions(+), 69 deletions(-) diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 index 9b85f4934f4..ce94827dbce 100644 --- a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 +++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 @@ -2,6 +2,7 @@ # Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) $process_util = @" +using Microsoft.Win32.SafeHandles; using System; using System.Collections; using System.IO; @@ -42,9 +43,9 @@ namespace Ansible public Int16 wShowWindow; public Int16 cbReserved2; public IntPtr lpReserved2; - public IntPtr hStdInput; - public IntPtr hStdOutput; - public IntPtr hStdError; + public SafeFileHandle hStdInput; + public SafeFileHandle hStdOutput; + public SafeFileHandle hStdError; public STARTUPINFO() { cb = Marshal.SizeOf(this); @@ -88,7 +89,7 @@ namespace Ansible { public NativeWaitHandle(IntPtr handle) { - this.Handle = handle; + this.SafeWaitHandle = new SafeWaitHandle(handle, false); } } @@ -110,7 +111,6 @@ namespace Ansible 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)] @@ -130,43 +130,22 @@ namespace Ansible [DllImport("kernel32.dll")] public static extern bool CreatePipe( - out IntPtr hReadPipe, - out IntPtr hWritePipe, + out SafeFileHandle hReadPipe, + out SafeFileHandle hWritePipe, SECURITY_ATTRIBUTES lpPipeAttributes, uint nSize); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool SetHandleInformation( - IntPtr hObject, + SafeFileHandle 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, @@ -220,7 +199,7 @@ namespace Ansible public static CommandResult RunCommand(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, string stdinInput, IDictionary environment) { - UInt32 startup_flags = CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | EXTENDED_STARTUPINFO_PRESENT; + UInt32 startup_flags = CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT; STARTUPINFOEX si = new STARTUPINFOEX(); si.startupInfo.dwFlags = (int)StartupInfoFlags.USESTDHANDLES; @@ -228,7 +207,7 @@ namespace Ansible 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; + SafeFileHandle stdout_read, stdout_write, stderr_read, stderr_write, stdin_read, stdin_write; if (!CreatePipe(out stdout_read, out stdout_write, pipesec, 0)) throw new Win32Exception("STDOUT pipe setup failed"); if (!SetHandleInformation(stdout_read, HandleFlags.INHERIT, 0)) @@ -248,37 +227,9 @@ namespace Ansible 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); + FileStream stdin_fs = new FileStream(stdin_write, FileAccess.Write, 32768); StreamWriter stdin = new StreamWriter(stdin_fs, utf8_encoding, 32768); // If lpCurrentDirectory is set to null in PS it will be an empty @@ -288,7 +239,7 @@ namespace Ansible StringBuilder environmentString = null; - if(environment != null && environment.Count > 0) + if (environment != null && environment.Count > 0) { environmentString = new StringBuilder(); foreach (DictionaryEntry kv in environment) @@ -320,12 +271,12 @@ namespace Ansible } // Setup the output buffers and get stdout/stderr - FileStream stdout_fs = new FileStream(stdout_read, FileAccess.Read, true, 4096); + FileStream stdout_fs = new FileStream(stdout_read, FileAccess.Read, 4096); StreamReader stdout = new StreamReader(stdout_fs, utf8_encoding, true, 4096); - FileStream stderr_fs = new FileStream(stderr_read, FileAccess.Read, true, 4096); + stdout_write.Close(); + FileStream stderr_fs = new FileStream(stderr_read, FileAccess.Read, 4096); StreamReader stderr = new StreamReader(stderr_fs, utf8_encoding, true, 4096); - CloseHandle(stdout_write); - CloseHandle(stderr_write); + stderr_write.Close(); stdin.WriteLine(stdinInput); stdin.Close(); @@ -384,7 +335,7 @@ Function Load-CommandUtils { # [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 -Debug:$false + Add-Type -TypeDefinition $process_util } Function Get-ExecutablePath($executable, $directory) { diff --git a/lib/ansible/plugins/shell/powershell.py b/lib/ansible/plugins/shell/powershell.py index 0708abbe59c..2b43bc9ecfb 100644 --- a/lib/ansible/plugins/shell/powershell.py +++ b/lib/ansible/plugins/shell/powershell.py @@ -1096,6 +1096,27 @@ $exec_wrapper = { $DebugPreference = "Continue" $ErrorActionPreference = "Stop" + # become process is run under a different console to the WinRM one so we + # need to set the UTF-8 codepage again + Add-Type -Debug:$false -TypeDefinition @' +using System; +using System.Runtime.InteropServices; + +namespace Ansible +{ + public class ConsoleCP + { + [DllImport("kernel32.dll")] + public static extern bool SetConsoleCP(UInt32 wCodePageID); + + [DllImport("kernel32.dll")] + public static extern bool SetConsoleOutputCP(UInt32 wCodePageID); + } +} +'@ + [Ansible.ConsoleCP]::SetConsoleCP(65001) > $null + [Ansible.ConsoleCP]::SetConsoleOutputCP(65001) > $null + Function ConvertTo-HashtableFromPsCustomObject($myPsObject) { $output = @{} $myPsObject | Get-Member -MemberType *Property | % { @@ -1142,8 +1163,8 @@ $exec_wrapper = { } $output = $entrypoint.Run($payload) - - Write-Output $output + # base64 encode the output so the non-ascii characters are preserved + Write-Output ([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((Write-Output $output)))) } # end exec_wrapper Function Dump-Error ($excep) { @@ -1262,10 +1283,11 @@ Function Run($payload) { $result = [Ansible.BecomeUtil]::RunAsUser($username, $password, $lp_command_line, $lp_current_directory, $payload_string, $logon_flags, $logon_type) $stdout = $result.StandardOut + $stdout = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($stdout.Trim())) $stderr = $result.StandardError $rc = $result.ExitCode - [Console]::Out.WriteLine($stdout.Trim()) + [Console]::Out.WriteLine($stdout) [Console]::Error.WriteLine($stderr.Trim()) } Catch { $excep = $_ diff --git a/test/integration/targets/win_async_wrapper/tasks/main.yml b/test/integration/targets/win_async_wrapper/tasks/main.yml index fbeaad31eaf..38b4d528ca9 100644 --- a/test/integration/targets/win_async_wrapper/tasks/main.yml +++ b/test/integration/targets/win_async_wrapper/tasks/main.yml @@ -144,6 +144,20 @@ # TODO: re-enable after catastrophic failure behavior is cleaned up # - asyncresult.msg is search('failing via exception') +- name: echo some non ascii characters + win_command: cmd.exe /c echo über den Fußgängerübergang gehen + async: 10 + poll: 1 + register: nonascii_output + +- name: assert echo some non ascii characters + assert: + that: + - nonascii_output is changed + - nonascii_output.rc == 0 + - nonascii_output.stdout_lines|count == 1 + - nonascii_output.stdout_lines[0] == 'über den Fußgängerübergang gehen' + - nonascii_output.stderr == '' # FUTURE: figure out why the last iteration of this test often fails on shippable #- name: loop async success diff --git a/test/integration/targets/win_become/tasks/main.yml b/test/integration/targets/win_become/tasks/main.yml index a2ec24b6040..a71e2bb9c9a 100644 --- a/test/integration/targets/win_become/tasks/main.yml +++ b/test/integration/targets/win_become/tasks/main.yml @@ -266,6 +266,20 @@ - become_netcredentials.label.account_name == 'High Mandatory Level' - become_netcredentials.label.sid == 'S-1-16-12288' + - name: echo some non ascii characters + win_command: cmd.exe /c echo über den Fußgängerübergang gehen + vars: *become_vars + register: nonascii_output + + - name: assert echo some non ascii characters + assert: + that: + - nonascii_output is changed + - nonascii_output.rc == 0 + - nonascii_output.stdout_lines|count == 1 + - nonascii_output.stdout_lines[0] == 'über den Fußgängerübergang gehen' + - nonascii_output.stderr == '' + # FUTURE: test raw + script become behavior once they're running under the exec wrapper again # FUTURE: add standalone playbook tests to include password prompting and play become keywords diff --git a/test/integration/targets/win_command/tasks/main.yml b/test/integration/targets/win_command/tasks/main.yml index d99c22e36e8..f1fc8b7e4ad 100644 --- a/test/integration/targets/win_command/tasks/main.yml +++ b/test/integration/targets/win_command/tasks/main.yml @@ -222,3 +222,16 @@ - cmdout.stdout_lines|count == 1 - cmdout.stdout_lines[0] == "some input" - cmdout.stderr == "" + +- name: echo some non ascii characters + win_command: cmd.exe /c echo über den Fußgängerübergang gehen + register: nonascii_output + +- name: assert echo some non ascii characters + assert: + that: + - nonascii_output is changed + - nonascii_output.rc == 0 + - nonascii_output.stdout_lines|count == 1 + - nonascii_output.stdout_lines[0] == 'über den Fußgängerübergang gehen' + - nonascii_output.stderr == '' diff --git a/test/integration/targets/win_shell/tasks/main.yml b/test/integration/targets/win_shell/tasks/main.yml index 4680e417eda..867fe237dd0 100644 --- a/test/integration/targets/win_shell/tasks/main.yml +++ b/test/integration/targets/win_shell/tasks/main.yml @@ -244,3 +244,16 @@ - shellout.rc == 0 - shellout.stderr == "" - shellout.stdout == "some input\r\n" + +- name: echo some non ascii characters + win_shell: Write-Host über den Fußgängerübergang gehen + register: nonascii_output + +- name: assert echo some non ascii characters + assert: + that: + - nonascii_output is changed + - nonascii_output.rc == 0 + - nonascii_output.stdout_lines|count == 1 + - nonascii_output.stdout_lines[0] == 'über den Fußgängerübergang gehen' + - nonascii_output.stderr == ''