powershell: display non-ascii characters in command outputs (#37229)

pull/38366/head
Jordan Borean 7 years ago committed by Matt Davis
parent fd4d264253
commit 71e8527d7c

@ -2,6 +2,7 @@
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) # Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
$process_util = @" $process_util = @"
using Microsoft.Win32.SafeHandles;
using System; using System;
using System.Collections; using System.Collections;
using System.IO; using System.IO;
@ -42,9 +43,9 @@ namespace Ansible
public Int16 wShowWindow; public Int16 wShowWindow;
public Int16 cbReserved2; public Int16 cbReserved2;
public IntPtr lpReserved2; public IntPtr lpReserved2;
public IntPtr hStdInput; public SafeFileHandle hStdInput;
public IntPtr hStdOutput; public SafeFileHandle hStdOutput;
public IntPtr hStdError; public SafeFileHandle hStdError;
public STARTUPINFO() public STARTUPINFO()
{ {
cb = Marshal.SizeOf(this); cb = Marshal.SizeOf(this);
@ -88,7 +89,7 @@ namespace Ansible
{ {
public NativeWaitHandle(IntPtr handle) public NativeWaitHandle(IntPtr handle)
{ {
this.Handle = handle; this.SafeWaitHandle = new SafeWaitHandle(handle, false);
} }
} }
@ -110,7 +111,6 @@ namespace Ansible
public class CommandUtil public class CommandUtil
{ {
private static UInt32 CREATE_UNICODE_ENVIRONMENT = 0x000000400; private static UInt32 CREATE_UNICODE_ENVIRONMENT = 0x000000400;
private static UInt32 CREATE_NEW_CONSOLE = 0x00000010;
private static UInt32 EXTENDED_STARTUPINFO_PRESENT = 0x00080000; private static UInt32 EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)] [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
@ -130,43 +130,22 @@ namespace Ansible
[DllImport("kernel32.dll")] [DllImport("kernel32.dll")]
public static extern bool CreatePipe( public static extern bool CreatePipe(
out IntPtr hReadPipe, out SafeFileHandle hReadPipe,
out IntPtr hWritePipe, out SafeFileHandle hWritePipe,
SECURITY_ATTRIBUTES lpPipeAttributes, SECURITY_ATTRIBUTES lpPipeAttributes,
uint nSize); uint nSize);
[DllImport("kernel32.dll", SetLastError = true)] [DllImport("kernel32.dll", SetLastError = true)]
public static extern bool SetHandleInformation( public static extern bool SetHandleInformation(
IntPtr hObject, SafeFileHandle hObject,
HandleFlags dwMask, HandleFlags dwMask,
int dwFlags); 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)] [DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetExitCodeProcess( private static extern bool GetExitCodeProcess(
IntPtr hProcess, IntPtr hProcess,
out uint lpExitCode); out uint lpExitCode);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(
IntPtr hObject);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern uint SearchPath( public static extern uint SearchPath(
string lpPath, string lpPath,
@ -220,7 +199,7 @@ namespace Ansible
public static CommandResult RunCommand(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, string stdinInput, IDictionary environment) 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(); STARTUPINFOEX si = new STARTUPINFOEX();
si.startupInfo.dwFlags = (int)StartupInfoFlags.USESTDHANDLES; si.startupInfo.dwFlags = (int)StartupInfoFlags.USESTDHANDLES;
@ -228,7 +207,7 @@ namespace Ansible
pipesec.bInheritHandle = true; pipesec.bInheritHandle = true;
// Create the stdout, stderr and stdin pipes used in the process and add to the startupInfo // 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)) if (!CreatePipe(out stdout_read, out stdout_write, pipesec, 0))
throw new Win32Exception("STDOUT pipe setup failed"); throw new Win32Exception("STDOUT pipe setup failed");
if (!SetHandleInformation(stdout_read, HandleFlags.INHERIT, 0)) if (!SetHandleInformation(stdout_read, HandleFlags.INHERIT, 0))
@ -248,37 +227,9 @@ namespace Ansible
si.startupInfo.hStdError = stderr_write; si.startupInfo.hStdError = stderr_write;
si.startupInfo.hStdInput = stdin_read; 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 // Setup the stdin buffer
UTF8Encoding utf8_encoding = new UTF8Encoding(false); 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); StreamWriter stdin = new StreamWriter(stdin_fs, utf8_encoding, 32768);
// If lpCurrentDirectory is set to null in PS it will be an empty // If lpCurrentDirectory is set to null in PS it will be an empty
@ -320,12 +271,12 @@ namespace Ansible
} }
// Setup the output buffers and get stdout/stderr // 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); 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); StreamReader stderr = new StreamReader(stderr_fs, utf8_encoding, true, 4096);
CloseHandle(stdout_write); stderr_write.Close();
CloseHandle(stderr_write);
stdin.WriteLine(stdinInput); stdin.WriteLine(stdinInput);
stdin.Close(); stdin.Close();
@ -384,7 +335,7 @@ Function Load-CommandUtils {
# [Ansible.CommandUtil]::RunCommand(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, string stdinInput, string environmentBlock) # [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 # 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) { Function Get-ExecutablePath($executable, $directory) {

@ -1096,6 +1096,27 @@ $exec_wrapper = {
$DebugPreference = "Continue" $DebugPreference = "Continue"
$ErrorActionPreference = "Stop" $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) { Function ConvertTo-HashtableFromPsCustomObject($myPsObject) {
$output = @{} $output = @{}
$myPsObject | Get-Member -MemberType *Property | % { $myPsObject | Get-Member -MemberType *Property | % {
@ -1142,8 +1163,8 @@ $exec_wrapper = {
} }
$output = $entrypoint.Run($payload) $output = $entrypoint.Run($payload)
# base64 encode the output so the non-ascii characters are preserved
Write-Output $output Write-Output ([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((Write-Output $output))))
} # end exec_wrapper } # end exec_wrapper
Function Dump-Error ($excep) { 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) $result = [Ansible.BecomeUtil]::RunAsUser($username, $password, $lp_command_line, $lp_current_directory, $payload_string, $logon_flags, $logon_type)
$stdout = $result.StandardOut $stdout = $result.StandardOut
$stdout = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($stdout.Trim()))
$stderr = $result.StandardError $stderr = $result.StandardError
$rc = $result.ExitCode $rc = $result.ExitCode
[Console]::Out.WriteLine($stdout.Trim()) [Console]::Out.WriteLine($stdout)
[Console]::Error.WriteLine($stderr.Trim()) [Console]::Error.WriteLine($stderr.Trim())
} Catch { } Catch {
$excep = $_ $excep = $_

@ -144,6 +144,20 @@
# TODO: re-enable after catastrophic failure behavior is cleaned up # TODO: re-enable after catastrophic failure behavior is cleaned up
# - asyncresult.msg is search('failing via exception') # - 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 # FUTURE: figure out why the last iteration of this test often fails on shippable
#- name: loop async success #- name: loop async success

@ -266,6 +266,20 @@
- become_netcredentials.label.account_name == 'High Mandatory Level' - become_netcredentials.label.account_name == 'High Mandatory Level'
- become_netcredentials.label.sid == 'S-1-16-12288' - 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: 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 # FUTURE: add standalone playbook tests to include password prompting and play become keywords

@ -222,3 +222,16 @@
- cmdout.stdout_lines|count == 1 - cmdout.stdout_lines|count == 1
- cmdout.stdout_lines[0] == "some input" - cmdout.stdout_lines[0] == "some input"
- cmdout.stderr == "" - 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 == ''

@ -244,3 +244,16 @@
- shellout.rc == 0 - shellout.rc == 0
- shellout.stderr == "" - shellout.stderr == ""
- shellout.stdout == "some input\r\n" - 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 == ''

Loading…
Cancel
Save