* Add output_encoding_override params to win_command/win_shell (#54896) This enhancement enables Ansible to parse the output of localized commands that ignore the prompt code page. * Added changelog and minor nits
This commit is contained in:
parent
c11d73575b
commit
c0331053db
11 changed files with 114 additions and 12 deletions
2
changelogs/fragments/win_command-encoding.yaml
Normal file
2
changelogs/fragments/win_command-encoding.yaml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
minor_changes:
|
||||||
|
- win_command, win_shell - Add the ability to override the console output encoding with ``output_encoding_override`` - https://github.com/ansible/ansible/issues/54896
|
|
@ -264,6 +264,18 @@ namespace Ansible.Process
|
||||||
|
|
||||||
public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
|
public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
|
||||||
IDictionary environment, string stdin)
|
IDictionary environment, string stdin)
|
||||||
|
{
|
||||||
|
return CreateProcess(lpApplicationName, lpCommandLine, lpCurrentDirectory, environment, stdin, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
|
||||||
|
IDictionary environment, byte[] stdin)
|
||||||
|
{
|
||||||
|
return CreateProcess(lpApplicationName, lpCommandLine, lpCurrentDirectory, environment, stdin, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
|
||||||
|
IDictionary environment, string stdin, string outputEncoding)
|
||||||
{
|
{
|
||||||
byte[] stdinBytes;
|
byte[] stdinBytes;
|
||||||
if (String.IsNullOrEmpty(stdin))
|
if (String.IsNullOrEmpty(stdin))
|
||||||
|
@ -274,7 +286,7 @@ namespace Ansible.Process
|
||||||
stdin += Environment.NewLine;
|
stdin += Environment.NewLine;
|
||||||
stdinBytes = new UTF8Encoding(false).GetBytes(stdin);
|
stdinBytes = new UTF8Encoding(false).GetBytes(stdin);
|
||||||
}
|
}
|
||||||
return CreateProcess(lpApplicationName, lpCommandLine, lpCurrentDirectory, environment, stdinBytes);
|
return CreateProcess(lpApplicationName, lpCommandLine, lpCurrentDirectory, environment, stdinBytes, outputEncoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -285,9 +297,10 @@ namespace Ansible.Process
|
||||||
/// <param name="lpCurrentDirectory">The full path to the current directory for the process, null will have the same cwd as the calling process</param>
|
/// <param name="lpCurrentDirectory">The full path to the current directory for the process, null will have the same cwd as the calling process</param>
|
||||||
/// <param name="environment">A dictionary of key/value pairs to define the new process environment</param>
|
/// <param name="environment">A dictionary of key/value pairs to define the new process environment</param>
|
||||||
/// <param name="stdin">A byte array to send over the stdin pipe</param>
|
/// <param name="stdin">A byte array to send over the stdin pipe</param>
|
||||||
|
/// <param name="outputEncoding">The character encoding for decoding stdout/stderr output of the process.</param>
|
||||||
/// <returns>Result object that contains the command output and return code</returns>
|
/// <returns>Result object that contains the command output and return code</returns>
|
||||||
public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
|
public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
|
||||||
IDictionary environment, byte[] stdin)
|
IDictionary environment, byte[] stdin, string outputEncoding)
|
||||||
{
|
{
|
||||||
NativeHelpers.ProcessCreationFlags creationFlags = NativeHelpers.ProcessCreationFlags.CREATE_UNICODE_ENVIRONMENT |
|
NativeHelpers.ProcessCreationFlags creationFlags = NativeHelpers.ProcessCreationFlags.CREATE_UNICODE_ENVIRONMENT |
|
||||||
NativeHelpers.ProcessCreationFlags.EXTENDED_STARTUPINFO_PRESENT;
|
NativeHelpers.ProcessCreationFlags.EXTENDED_STARTUPINFO_PRESENT;
|
||||||
|
@ -337,7 +350,8 @@ namespace Ansible.Process
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return WaitProcess(stdoutRead, stdoutWrite, stderrRead, stderrWrite, stdinStream, stdin, pi.hProcess);
|
return WaitProcess(stdoutRead, stdoutWrite, stderrRead, stderrWrite, stdinStream, stdin, pi.hProcess,
|
||||||
|
outputEncoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void CreateStdioPipes(NativeHelpers.STARTUPINFOEX si, out SafeFileHandle stdoutRead,
|
internal static void CreateStdioPipes(NativeHelpers.STARTUPINFOEX si, out SafeFileHandle stdoutRead,
|
||||||
|
@ -383,16 +397,18 @@ namespace Ansible.Process
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static Result WaitProcess(SafeFileHandle stdoutRead, SafeFileHandle stdoutWrite, SafeFileHandle stderrRead,
|
internal static Result WaitProcess(SafeFileHandle stdoutRead, SafeFileHandle stdoutWrite, SafeFileHandle stderrRead,
|
||||||
SafeFileHandle stderrWrite, FileStream stdinStream, byte[] stdin, IntPtr hProcess)
|
SafeFileHandle stderrWrite, FileStream stdinStream, byte[] stdin, IntPtr hProcess, string outputEncoding = null)
|
||||||
{
|
{
|
||||||
// Setup the output buffers and get stdout/stderr
|
// Default to using UTF-8 as the output encoding, this should be a sane default for most scenarios.
|
||||||
UTF8Encoding utf8Encoding = new UTF8Encoding(false);
|
outputEncoding = String.IsNullOrEmpty(outputEncoding) ? "utf-8" : outputEncoding;
|
||||||
|
Encoding encodingInstance = Encoding.GetEncoding(outputEncoding);
|
||||||
|
|
||||||
FileStream stdoutFS = new FileStream(stdoutRead, FileAccess.Read, 4096);
|
FileStream stdoutFS = new FileStream(stdoutRead, FileAccess.Read, 4096);
|
||||||
StreamReader stdout = new StreamReader(stdoutFS, utf8Encoding, true, 4096);
|
StreamReader stdout = new StreamReader(stdoutFS, encodingInstance, true, 4096);
|
||||||
stdoutWrite.Close();
|
stdoutWrite.Close();
|
||||||
|
|
||||||
FileStream stderrFS = new FileStream(stderrRead, FileAccess.Read, 4096);
|
FileStream stderrFS = new FileStream(stderrRead, FileAccess.Read, 4096);
|
||||||
StreamReader stderr = new StreamReader(stderrFS, utf8Encoding, true, 4096);
|
StreamReader stderr = new StreamReader(stderrFS, encodingInstance, true, 4096);
|
||||||
stderrWrite.Close();
|
stderrWrite.Close();
|
||||||
|
|
||||||
stdinStream.Write(stdin, 0, stdin.Length);
|
stdinStream.Write(stdin, 0, stdin.Length);
|
||||||
|
|
|
@ -76,6 +76,9 @@ Function Run-Command {
|
||||||
.PARAMETER environment
|
.PARAMETER environment
|
||||||
A hashtable of key/value pairs to run with the command. If set, it will replace all other env vars.
|
A hashtable of key/value pairs to run with the command. If set, it will replace all other env vars.
|
||||||
|
|
||||||
|
.PARAMETER output_encoding_override
|
||||||
|
The character encoding name for decoding stdout/stderr output of the process.
|
||||||
|
|
||||||
.OUTPUT
|
.OUTPUT
|
||||||
[Hashtable]
|
[Hashtable]
|
||||||
[String]executable - The full path to the executable that was run
|
[String]executable - The full path to the executable that was run
|
||||||
|
@ -87,7 +90,8 @@ Function Run-Command {
|
||||||
[string]$command,
|
[string]$command,
|
||||||
[string]$working_directory = $null,
|
[string]$working_directory = $null,
|
||||||
[string]$stdin = "",
|
[string]$stdin = "",
|
||||||
[hashtable]$environment = @{}
|
[hashtable]$environment = @{},
|
||||||
|
[string]$output_encoding_override = $null
|
||||||
)
|
)
|
||||||
|
|
||||||
# need to validate the working directory if it is set
|
# need to validate the working directory if it is set
|
||||||
|
@ -104,7 +108,7 @@ Function Run-Command {
|
||||||
$executable = Get-ExecutablePath -executable $arguments[0] -directory $working_directory
|
$executable = Get-ExecutablePath -executable $arguments[0] -directory $working_directory
|
||||||
|
|
||||||
# run the command and get the results
|
# run the command and get the results
|
||||||
$command_result = [Ansible.Process.ProcessUtil]::CreateProcess($executable, $command, $working_directory, $environment, $stdin)
|
$command_result = [Ansible.Process.ProcessUtil]::CreateProcess($executable, $command, $working_directory, $environment, $stdin, $output_encoding_override)
|
||||||
|
|
||||||
return ,@{
|
return ,@{
|
||||||
executable = $executable
|
executable = $executable
|
||||||
|
|
|
@ -18,7 +18,8 @@ $raw_command_line = Get-AnsibleParam -obj $params -name "_raw_params" -type "str
|
||||||
$chdir = Get-AnsibleParam -obj $params -name "chdir" -type "path"
|
$chdir = Get-AnsibleParam -obj $params -name "chdir" -type "path"
|
||||||
$creates = Get-AnsibleParam -obj $params -name "creates" -type "path"
|
$creates = Get-AnsibleParam -obj $params -name "creates" -type "path"
|
||||||
$removes = Get-AnsibleParam -obj $params -name "removes" -type "path"
|
$removes = Get-AnsibleParam -obj $params -name "removes" -type "path"
|
||||||
$stdin = Get-AnsibleParam -obj $params -name "stdin" -type 'str"'
|
$stdin = Get-AnsibleParam -obj $params -name "stdin" -type "str"
|
||||||
|
$output_encoding_override = Get-AnsibleParam -obj $params -name "output_encoding_override" -type "str"
|
||||||
|
|
||||||
$raw_command_line = $raw_command_line.Trim()
|
$raw_command_line = $raw_command_line.Trim()
|
||||||
|
|
||||||
|
@ -44,6 +45,9 @@ if ($chdir) {
|
||||||
if ($stdin) {
|
if ($stdin) {
|
||||||
$command_args['stdin'] = $stdin
|
$command_args['stdin'] = $stdin
|
||||||
}
|
}
|
||||||
|
if ($output_encoding_override) {
|
||||||
|
$command_args['output_encoding_override'] = $output_encoding_override
|
||||||
|
}
|
||||||
|
|
||||||
$start_datetime = [DateTime]::UtcNow
|
$start_datetime = [DateTime]::UtcNow
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -44,6 +44,15 @@ options:
|
||||||
- Set the stdin of the command directly to the specified value.
|
- Set the stdin of the command directly to the specified value.
|
||||||
type: str
|
type: str
|
||||||
version_added: '2.5'
|
version_added: '2.5'
|
||||||
|
output_encoding_override:
|
||||||
|
description:
|
||||||
|
- This option overrides the encoding of stdout/stderr output.
|
||||||
|
- You can use this option when you need to run a command which ignore the console's codepage.
|
||||||
|
- You should only need to use this option in very rare circumstances.
|
||||||
|
- This value can be any valid encoding C(Name) based on the output of C([System.Text.Encoding]::GetEncodings()).
|
||||||
|
See U(https://docs.microsoft.com/dotnet/api/system.text.encoding.getencodings).
|
||||||
|
type: str
|
||||||
|
version_added: '2.10'
|
||||||
notes:
|
notes:
|
||||||
- If you want to run a command through a shell (say you are using C(<),
|
- If you want to run a command through a shell (say you are using C(<),
|
||||||
C(>), C(|), etc), you actually want the M(win_shell) module instead. The
|
C(>), C(|), etc), you actually want the M(win_shell) module instead. The
|
||||||
|
|
|
@ -48,6 +48,7 @@ $creates = Get-AnsibleParam -obj $params -name "creates" -type "path"
|
||||||
$removes = Get-AnsibleParam -obj $params -name "removes" -type "path"
|
$removes = Get-AnsibleParam -obj $params -name "removes" -type "path"
|
||||||
$stdin = Get-AnsibleParam -obj $params -name "stdin" -type "str"
|
$stdin = Get-AnsibleParam -obj $params -name "stdin" -type "str"
|
||||||
$no_profile = Get-AnsibleParam -obj $params -name "no_profile" -type "bool" -default $false
|
$no_profile = Get-AnsibleParam -obj $params -name "no_profile" -type "bool" -default $false
|
||||||
|
$output_encoding_override = Get-AnsibleParam -obj $params -name "output_encoding_override" -type "str"
|
||||||
|
|
||||||
$raw_command_line = $raw_command_line.Trim()
|
$raw_command_line = $raw_command_line.Trim()
|
||||||
|
|
||||||
|
@ -103,6 +104,9 @@ if ($chdir) {
|
||||||
if ($stdin) {
|
if ($stdin) {
|
||||||
$run_command_arg['stdin'] = $stdin
|
$run_command_arg['stdin'] = $stdin
|
||||||
}
|
}
|
||||||
|
if ($output_encoding_override) {
|
||||||
|
$run_command_arg['output_encoding_override'] = $output_encoding_override
|
||||||
|
}
|
||||||
|
|
||||||
$start_datetime = [DateTime]::UtcNow
|
$start_datetime = [DateTime]::UtcNow
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -54,6 +54,15 @@ options:
|
||||||
type: bool
|
type: bool
|
||||||
default: no
|
default: no
|
||||||
version_added: '2.8'
|
version_added: '2.8'
|
||||||
|
output_encoding_override:
|
||||||
|
description:
|
||||||
|
- This option overrides the encoding of stdout/stderr output.
|
||||||
|
- You can use this option when you need to run a command which ignore the console's codepage.
|
||||||
|
- You should only need to use this option in very rare circumstances.
|
||||||
|
- This value can be any valid encoding C(Name) based on the output of C([System.Text.Encoding]::GetEncodings()).
|
||||||
|
See U(https://docs.microsoft.com/dotnet/api/system.text.encoding.getencodings).
|
||||||
|
type: str
|
||||||
|
version_added: '2.10'
|
||||||
notes:
|
notes:
|
||||||
- If you want to run an executable securely and predictably, it may be
|
- If you want to run an executable securely and predictably, it may be
|
||||||
better to use the M(win_command) module instead. Best practices when writing
|
better to use the M(win_command) module instead. Best practices when writing
|
||||||
|
|
15
test/integration/targets/win_command/files/crt_setmode.c
Normal file
15
test/integration/targets/win_command/files/crt_setmode.c
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// crt_setmode.c
|
||||||
|
// This program uses _setmode to change
|
||||||
|
// stdout from text mode to binary mode.
|
||||||
|
// Used to test output_encoding_override for win_command.
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <io.h>
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
_setmode(_fileno(stdout), _O_BINARY);
|
||||||
|
// Translates to 日本 in shift_jis
|
||||||
|
printf("\x93\xFa\x96\x7B - Japan");
|
||||||
|
}
|
|
@ -203,6 +203,24 @@
|
||||||
- cmdout.stdout_lines[1] == 'ADDLOCAL=msi,example'
|
- cmdout.stdout_lines[1] == 'ADDLOCAL=msi,example'
|
||||||
- cmdout.stdout_lines[2] == 'two\\\\slashes'
|
- cmdout.stdout_lines[2] == 'two\\\\slashes'
|
||||||
|
|
||||||
|
- name: download binary that output shift_jis chars to console
|
||||||
|
win_get_url:
|
||||||
|
url: https://ansible-ci-files.s3.amazonaws.com/test/integration/targets/win_command/OutputEncodingOverride.exe
|
||||||
|
dest: C:\ansible testing\OutputEncodingOverride.exe
|
||||||
|
|
||||||
|
- name: call binary with shift_jis output encoding override
|
||||||
|
win_command: '"C:\ansible testing\OutputEncodingOverride.exe"'
|
||||||
|
args:
|
||||||
|
output_encoding_override: shift_jis
|
||||||
|
register: cmdout
|
||||||
|
|
||||||
|
- name: assert call to binary with shift_jis output
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- cmdout is changed
|
||||||
|
- cmdout.rc == 0
|
||||||
|
- cmdout.stdout_lines[0] == '日本 - Japan'
|
||||||
|
|
||||||
- name: remove testing folder
|
- name: remove testing folder
|
||||||
win_file:
|
win_file:
|
||||||
path: C:\ansible testing
|
path: C:\ansible testing
|
||||||
|
|
|
@ -217,6 +217,13 @@ $tests = @{
|
||||||
$actual.StandardError | Assert-Equals -Expected ""
|
$actual.StandardError | Assert-Equals -Expected ""
|
||||||
$actual.ExitCode | Assert-Equals -Expected 0
|
$actual.ExitCode | Assert-Equals -Expected 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"CreateProcess with unicode and us-ascii encoding" = {
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, "cmd.exe /c echo 💩 café", $null, $null, '', 'us-ascii')
|
||||||
|
$actual.StandardOut | Assert-Equals -Expected "???? caf??`r`n"
|
||||||
|
$actual.StandardError | Assert-Equals -Expected ""
|
||||||
|
$actual.ExitCode | Assert-Equals -Expected 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($test_impl in $tests.GetEnumerator()) {
|
foreach ($test_impl in $tests.GetEnumerator()) {
|
||||||
|
@ -226,4 +233,3 @@ foreach ($test_impl in $tests.GetEnumerator()) {
|
||||||
|
|
||||||
$module.Result.data = "success"
|
$module.Result.data = "success"
|
||||||
$module.ExitJson()
|
$module.ExitJson()
|
||||||
|
|
||||||
|
|
|
@ -258,6 +258,21 @@
|
||||||
- nonascii_output.stdout_lines[0] == 'über den Fußgängerübergang gehen'
|
- nonascii_output.stdout_lines[0] == 'über den Fußgängerübergang gehen'
|
||||||
- nonascii_output.stderr == ''
|
- nonascii_output.stderr == ''
|
||||||
|
|
||||||
|
- name: echo some non ascii characters with us-ascii output encoding
|
||||||
|
win_shell: Write-Host über den Fußgängerübergang gehen
|
||||||
|
args:
|
||||||
|
output_encoding_override: us-ascii
|
||||||
|
register: nonascii_output_us_ascii_encoding
|
||||||
|
|
||||||
|
- name: assert echo some non ascii characters with us-ascii output encoding
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- nonascii_output_us_ascii_encoding is changed
|
||||||
|
- nonascii_output_us_ascii_encoding.rc == 0
|
||||||
|
- nonascii_output_us_ascii_encoding.stdout_lines|count == 1
|
||||||
|
- nonascii_output_us_ascii_encoding.stdout_lines[0] == '??ber den Fu??g??nger??bergang gehen'
|
||||||
|
- nonascii_output_us_ascii_encoding.stderr == ''
|
||||||
|
|
||||||
- name: execute powershell without no_profile
|
- name: execute powershell without no_profile
|
||||||
win_shell: '[System.Environment]::CommandLine'
|
win_shell: '[System.Environment]::CommandLine'
|
||||||
register: no_profile
|
register: no_profile
|
||||||
|
|
Loading…
Add table
Reference in a new issue