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