powershell: 2.5 backport display non-ascii characters in command outputs (#38365)
* powershell: display non-ascii characters in command outputs (#37229)
(cherry picked from commit 71e8527d7c
)
* Added changelog fragment
This commit is contained in:
parent
2e881f2929
commit
38a13f41cd
7 changed files with 99 additions and 69 deletions
3
changelogs/fragments/win_utf8_character_output.yaml
Normal file
3
changelogs/fragments/win_utf8_character_output.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
bugfixes:
|
||||||
|
- windows - display UTF-8 characters correctly in Windows return json
|
||||||
|
https://github.com/ansible/ansible/pull/37229
|
|
@ -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
|
||||||
|
@ -288,7 +239,7 @@ namespace Ansible
|
||||||
|
|
||||||
StringBuilder environmentString = null;
|
StringBuilder environmentString = null;
|
||||||
|
|
||||||
if(environment != null && environment.Count > 0)
|
if (environment != null && environment.Count > 0)
|
||||||
{
|
{
|
||||||
environmentString = new StringBuilder();
|
environmentString = new StringBuilder();
|
||||||
foreach (DictionaryEntry kv in environment)
|
foreach (DictionaryEntry kv in environment)
|
||||||
|
@ -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…
Reference in a new issue