From aad9fbd4f5145ad17e66fc49df63096ee90fcb51 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Tue, 9 Sep 2025 04:30:38 +1000 Subject: [PATCH] Windows async - handle trailing junk output (#85820) Add handling for when a PowerShell module emits more than just the module result JSON. The behaviour reflects the Python async wrapper where trailing data after the module result will emit a warning. --- .../fragments/win_async-junk-output.yml | 3 ++ .../executor/powershell/async_watchdog.ps1 | 28 ++++++++++++++++--- .../library/trailing_output.ps1 | 6 ++++ .../targets/win_async_wrapper/tasks/main.yml | 15 ++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 changelogs/fragments/win_async-junk-output.yml create mode 100644 test/integration/targets/win_async_wrapper/library/trailing_output.ps1 diff --git a/changelogs/fragments/win_async-junk-output.yml b/changelogs/fragments/win_async-junk-output.yml new file mode 100644 index 00000000000..d6616e4548a --- /dev/null +++ b/changelogs/fragments/win_async-junk-output.yml @@ -0,0 +1,3 @@ +bugfixes: + - >- + Windows async - Handle running PowerShell modules with trailing data after the module result diff --git a/lib/ansible/executor/powershell/async_watchdog.ps1 b/lib/ansible/executor/powershell/async_watchdog.ps1 index 391016de563..aec771efe61 100644 --- a/lib/ansible/executor/powershell/async_watchdog.ps1 +++ b/lib/ansible/executor/powershell/async_watchdog.ps1 @@ -67,14 +67,34 @@ try { $result.finished = $true if ($jobAsyncResult.IsCompleted) { - $jobOutput = $ps.EndInvoke($jobAsyncResult) + $jobOutput = @($ps.EndInvoke($jobAsyncResult) | Out-String) -join "`n" $jobError = $ps.Streams.Error # write success/output/error to result object - # TODO: cleanse leading/trailing junk - $moduleResult = $jobOutput | ConvertFrom-Json | Convert-JsonObject + $moduleResultJson = $jobOutput + $startJsonChar = $moduleResultJson.IndexOf([char]'{') + if ($startJsonChar -eq -1) { + throw "No start of json char found in module result" + } + $moduleResultJson = $moduleResultJson.Substring($startJsonChar) + + $endJsonChar = $moduleResultJson.LastIndexOf([char]'}') + if ($endJsonChar -eq -1) { + throw "No end of json char found in module result" + } + + $trailingJunk = $moduleResultJson.Substring($endJsonChar + 1).Trim() + $moduleResultJson = $moduleResultJson.Substring(0, $endJsonChar + 1) + $moduleResult = $moduleResultJson | ConvertFrom-Json | Convert-JsonObject # TODO: check for conflicting keys $result = $result + $moduleResult + + if ($trailingJunk) { + if (-not $result.warnings) { + $result.warnings = @() + } + $result.warnings += "Module invocation had junk after the JSON data: $trailingJunk" + } } else { # We can't call Stop() as pwsh won't respond if it is busy calling a .NET @@ -103,7 +123,7 @@ catch { $result.failed = $true $result.msg = "failure during async watchdog: $_" # return output back, if available, to Ansible to help with debugging errors - $result.stdout = $jobOutput | Out-String + $result.stdout = $jobOutput $result.stderr = $jobError | Out-String } finally { diff --git a/test/integration/targets/win_async_wrapper/library/trailing_output.ps1 b/test/integration/targets/win_async_wrapper/library/trailing_output.ps1 new file mode 100644 index 00000000000..af7e1ad587b --- /dev/null +++ b/test/integration/targets/win_async_wrapper/library/trailing_output.ps1 @@ -0,0 +1,6 @@ +#!powershell + +#AnsibleRequires -Wrapper + +[Console]::Out.WriteLine('{"changed": false, "test": 123}') +'trailing junk after module result' diff --git a/test/integration/targets/win_async_wrapper/tasks/main.yml b/test/integration/targets/win_async_wrapper/tasks/main.yml index 0fc64d8c5c5..6bfcdf0d61f 100644 --- a/test/integration/targets/win_async_wrapper/tasks/main.yml +++ b/test/integration/targets/win_async_wrapper/tasks/main.yml @@ -206,6 +206,21 @@ - not success_async_custom_dir_poll.failed - success_async_custom_dir_poll.results_file == win_output_dir + '\\' + async_custom_dir_poll.ansible_job_id +- name: test async with trailing output + trailing_output: + async: 10 + poll: 1 + register: async_trailing_output + +- name: assert test async with trailing output + assert: + that: + - async_trailing_output is not changed + - async_trailing_output.test == 123 + - async_trailing_output.warnings | count == 1 + - >- + async_trailing_output.warnings[0] is search('Module invocation had junk after the JSON data: trailing junk after module result') + # FUTURE: figure out why the last iteration of this test often fails on shippable #- name: loop async success # async_test: