CLI: Fix UTF-8 encoding in non-tty relay use (#41235)

David Bennett committed Aug 4, 2026 at 12:29 UTC d229aad496389d03e8606909bcbe3f06209c1533
5 files changed +73 -1
src/windows/common/ConsoleState.cpp
+20 -1
@@ -109,7 +109,11 @@ void ConsoleState::SetInteractiveMode()
109
110 if (m_OutputHandle)
111 {
112 - m_SavedOutputCodePage = GetConsoleOutputCP();
112 + if (!m_SavedOutputCodePage.has_value())
113 + {
114 + m_SavedOutputCodePage = GetConsoleOutputCP();
115 + }
116 +
117 LOG_IF_WIN32_BOOL_FALSE(SetConsoleOutputCP(CP_UTF8));
118
119 // Configure for VT output.
@@ -126,6 +130,21 @@ void ConsoleState::SetInteractiveMode()
130 cleanup.release();
131 }
132
133 +void ConsoleState::SetOutputCodePageUtf8()
134 +{
135 + if (!m_OutputHandle)
136 + {
137 + return;
138 + }
139 +
140 + if (!m_SavedOutputCodePage.has_value())
141 + {
142 + m_SavedOutputCodePage = GetConsoleOutputCP();
143 + }
144 +
145 + LOG_IF_WIN32_BOOL_FALSE(SetConsoleOutputCP(CP_UTF8));
146 +}
147 +
148 ConsoleState::~ConsoleState()
149 {
150 RestoreConsoleState();
src/windows/common/ConsoleState.h
+6
@@ -34,6 +34,12 @@ public:
34 COORD GetWindowSize() const;
35 void SetInteractiveMode();
36
37 + // Sets the console output code page to UTF-8 for the lifetime of this object, restoring the
38 + // saved code page on destruction, without altering any console modes. Use for non-interactive
39 + // relays that stream raw UTF-8 bytes (e.g. container logs, non-TTY process output) so
40 + // multi-byte characters render correctly under any console output code page.
41 + void SetOutputCodePageUtf8();
42 +
43 private:
44 void RestoreConsoleState();
45
src/windows/wslc/services/ConsoleService.cpp
+4
@@ -132,6 +132,10 @@ bool ConsoleService::RelayInteractiveTty(wsl::windows::common::ConsoleState& Con
132
133 void ConsoleService::RelayNonTtyProcess(wil::unique_handle&& Stdin, wil::unique_handle&& Stdout, wil::unique_handle&& Stderr)
134 {
135 + // Process output is UTF-8.
136 + wsl::windows::common::ConsoleState console;
137 + console.SetOutputCodePageUtf8();
138 +
139 windows::common::io::MultiHandleWait io;
140
141 wil::unique_event exitEvent;
src/windows/wslc/services/ContainerService.cpp
+4
@@ -672,6 +672,10 @@ void ContainerService::Logs(Session& session, const std::string& id, bool follow
672
673 THROW_IF_FAILED(container->Logs(flags, &stdoutHandle, &stderrHandle, since, until, tail));
674
675 + // Container output is UTF-8.
676 + wsl::windows::common::ConsoleState console;
677 + console.SetOutputCodePageUtf8();
678 +
679 wsl::windows::common::io::MultiHandleWait io;
680 io.AddHandle(std::make_unique<wsl::windows::common::io::RelayHandle<wsl::windows::common::io::ReadHandle>>(
681 stdoutHandle.Release(), GetStdHandle(STD_OUTPUT_HANDLE)));
test/windows/UnitTests.cpp
+39
@@ -24,6 +24,7 @@ Abstract:
24 #include "registry.hpp"
25 #include "helpers.hpp"
26 #include "svccomm.hpp"
27 +#include "ConsoleState.h"
28 #include "lxfsshares.h"
29 #include <userenv.h>
30 #include <nlohmann/json.hpp>
@@ -7767,5 +7768,43 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n",
7768 VERIFY_ARE_EQUAL(exists, std::wstring(L"no"));
7769 }
7770
7771 + TEST_METHOD(ConsoleState_SetOutputCodePageUtf8)
7772 + {
7773 + // 437 (OEM-US) and 850 (OEM Multilingual) are built-in Windows code pages that are always
7774 + // available. 437 is the baseline the helper must restore; 850 stands in for another
7775 + // component changing the code page after the helper first ran.
7776 + constexpr UINT baselineCodePage = 437;
7777 + constexpr UINT intermediateCodePage = 850;
7778 +
7779 + const UINT originalCodePage = GetConsoleOutputCP();
7780 + auto restore = wil::scope_exit([originalCodePage]() { SetConsoleOutputCP(originalCodePage); });
7781 +
7782 + // A settable console output code page requires an attached console, which CI and service
7783 + // contexts often lack. Skip the test when the code page cannot be set so the suite stays stable.
7784 + if (!SetConsoleOutputCP(baselineCodePage))
7785 + {
7786 + LogSkipped("Skipping test: no attached console with a settable output code page");
7787 + return;
7788 + }
7789 + VERIFY_ARE_EQUAL(baselineCodePage, GetConsoleOutputCP());
7790 +
7791 + {
7792 + wsl::windows::common::ConsoleState console;
7793 + console.SetOutputCodePageUtf8();
7794 + VERIFY_ARE_EQUAL(
7795 + static_cast<UINT>(CP_UTF8),
7796 + GetConsoleOutputCP(),
7797 + L"SetOutputCodePageUtf8 sets the console output code page to UTF-8");
7798 +
7799 + // Another component changes the code page; a repeated call must re-assert UTF-8.
7800 + VERIFY_IS_TRUE(static_cast<bool>(SetConsoleOutputCP(intermediateCodePage)));
7801 + console.SetOutputCodePageUtf8();
7802 + VERIFY_ARE_EQUAL(
7803 + static_cast<UINT>(CP_UTF8), GetConsoleOutputCP(), L"A repeated call re-asserts UTF-8 after the code page changed");
7804 + }
7805 +
7806 + VERIFY_ARE_EQUAL(baselineCodePage, GetConsoleOutputCP(), L"Destruction restores the code page saved on the first call");
7807 + }
7808 +
7809 }; // namespace UnitTests
7810 } // namespace UnitTests