Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from Apr 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -88,7 +89,7 @@ namespace Ansible
{
public NativeWaitHandle(IntPtr handle)
{
this.Handle = handle;
this.SafeWaitHandle = new SafeWaitHandle(handle, false);
}
}

Expand All @@ -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)]
Expand All @@ -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,
Expand Down Expand Up @@ -220,15 +199,15 @@ 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;

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;
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))
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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) {
Expand Down
28 changes: 25 additions & 3 deletions lib/ansible/plugins/shell/powershell.py
Expand Up @@ -1096,6 +1096,27 @@ class NativeWaitHandle : WaitHandle
$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 | % {
Expand Down Expand Up @@ -1142,8 +1163,8 @@ class NativeWaitHandle : WaitHandle
}

$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) {
Expand Down Expand Up @@ -1262,10 +1283,11 @@ class NativeWaitHandle : WaitHandle

$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 = $_
Expand Down
14 changes: 14 additions & 0 deletions test/integration/targets/win_async_wrapper/tasks/main.yml
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions test/integration/targets/win_become/tasks/main.yml
Expand Up @@ -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

Expand Down
13 changes: 13 additions & 0 deletions test/integration/targets/win_command/tasks/main.yml
Expand Up @@ -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 == ''
13 changes: 13 additions & 0 deletions test/integration/targets/win_shell/tasks/main.yml
Expand Up @@ -231,3 +231,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 == ''