Fix various issues around initial tty sizes + add test coverage (#40722)
* Save state * Save state * Prepare for PR * Cleanup diff * Cleanup diff * Save state * Rethink tests * Apply PR feedback
Blue committed
Jun 9, 2026 at 11:37 UTC
313313718dad1c934c0ba02ec4643c7e486f746e
30 files changed
+443
-93
src/windows/common/ConsoleState.cpp
+29
-14
@@ -64,12 +64,33 @@ namespace wsl::windows::common {
64
65
ConsoleState::ConsoleState()
66
{
67
- // Ensure console state is restored if the constructor throws.
68
- auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { RestoreConsoleState(); });
69
-
67
m_InputHandle.reset(
68
CreateFileW(L"CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr));
69
70
+ if (!m_InputHandle)
71
+ {
72
+ LOG_LAST_ERROR_MSG("CreateFileW(CONIN$) failed");
73
+ }
74
+
75
+ m_OutputHandle.reset(
76
+ CreateFileW(L"CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr));
77
+
78
+ if (!m_OutputHandle)
79
+ {
80
+ LOG_LAST_ERROR_MSG("CreateFileW(CONOUT$) failed");
81
+ }
82
+}
83
+
84
+void ConsoleState::SetInteractiveMode()
85
+{
86
+ if (m_interactiveModeConfigured)
87
+ {
88
+ return;
89
+ }
90
+
91
+ // Ensure console state is restored if this method throws.
92
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { RestoreConsoleState(); });
93
+
94
if (m_InputHandle)
95
{
96
m_SavedInputCodePage = GetConsoleCP();
@@ -85,13 +106,6 @@ ConsoleState::ConsoleState()
106
ChangeConsoleMode(m_InputHandle.get(), NewMode);
107
m_SavedInputMode = mode;
108
}
88
- else
89
- {
90
- LOG_LAST_ERROR_MSG("CreateFileW(CONIN$) failed");
91
- }
92
-
93
- m_OutputHandle.reset(
94
- CreateFileW(L"CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr));
109
110
if (m_OutputHandle)
111
{
@@ -107,11 +121,8 @@ ConsoleState::ConsoleState()
121
ChangeConsoleMode(m_OutputHandle.get(), NewMode);
122
m_SavedOutputMode = mode;
123
}
110
- else
111
- {
112
- LOG_LAST_ERROR_MSG("CreateFileW(CONOUT$) failed");
113
- }
124
125
+ m_interactiveModeConfigured = true;
126
cleanup.release();
127
}
128
@@ -127,11 +138,13 @@ void ConsoleState::RestoreConsoleState()
138
if (m_SavedInputCodePage.has_value())
139
{
140
LOG_IF_WIN32_BOOL_FALSE(SetConsoleCP(m_SavedInputCodePage.value()));
141
+ m_SavedInputCodePage.reset();
142
}
143
144
if (m_SavedInputMode.has_value())
145
{
146
TrySetConsoleMode(m_InputHandle.get(), m_SavedInputMode.value());
147
+ m_SavedInputMode.reset();
148
}
149
}
150
@@ -140,11 +153,13 @@ void ConsoleState::RestoreConsoleState()
153
if (m_SavedOutputCodePage.has_value())
154
{
155
LOG_IF_WIN32_BOOL_FALSE(SetConsoleOutputCP(m_SavedOutputCodePage.value()));
156
+ m_SavedOutputCodePage.reset();
157
}
158
159
if (m_SavedOutputMode.has_value())
160
{
161
TrySetConsoleMode(m_OutputHandle.get(), m_SavedOutputMode.value());
162
+ m_SavedOutputMode.reset();
163
}
164
}
165
}
src/windows/common/ConsoleState.h
+2
@@ -32,12 +32,14 @@ public:
32
ConsoleState& operator=(ConsoleState&&) = delete;
33
34
COORD GetWindowSize() const;
35
+ void SetInteractiveMode();
36
37
private:
38
void RestoreConsoleState();
39
40
wil::unique_hfile m_InputHandle;
41
wil::unique_hfile m_OutputHandle;
42
+ bool m_interactiveModeConfigured{false};
43
std::optional<DWORD> m_SavedInputMode{};
44
std::optional<UINT> m_SavedInputCodePage{};
45
std::optional<DWORD> m_SavedOutputMode{};
src/windows/common/WSLCContainerLauncher.cpp
+5
-1
@@ -259,7 +259,11 @@ std::pair<HRESULT, std::optional<RunningWSLCContainer>> WSLCContainerLauncher::L
259
return std::make_pair(result, std::optional<RunningWSLCContainer>{});
260
}
261
262
- result = container.value().Get().Start(Flags, nullptr, WarningCallback);
262
+ WSLCProcessStartOptions startOptions{};
263
+ startOptions.TtyRows = m_rows;
264
+ startOptions.TtyColumns = m_columns;
265
+
266
+ result = container.value().Get().Start(Flags, &startOptions, WarningCallback);
267
268
return std::make_pair(result, std::move(container));
269
}
src/windows/common/WSLCContainerLauncher.h
+1
@@ -86,6 +86,7 @@ public:
86
void AddUlimit(const std::string& Name, std::int64_t Soft, std::int64_t Hard);
87
88
using WSLCProcessLauncher::FormatResult;
89
+ using WSLCProcessLauncher::SetTtySize;
90
using WSLCProcessLauncher::SetUser;
91
using WSLCProcessLauncher::SetWorkingDirectory;
92
src/windows/common/WSLCProcessLauncher.cpp
+7
-4
@@ -57,8 +57,6 @@ std::tuple<WSLCProcessOptions, std::vector<const char*>, std::vector<const char*
57
WSLCProcessOptions options{};
58
options.CommandLine = {.Values = commandLine.data(), .Count = static_cast<DWORD>(commandLine.size())};
59
options.Environment = {.Values = environment.data(), .Count = static_cast<DWORD>(environment.size())};
60
- options.TtyColumns = m_columns;
61
- options.TtyRows = m_rows;
60
options.Flags = m_flags;
61
62
if (!m_workingDirectory.empty())
@@ -182,7 +180,7 @@ std::tuple<HRESULT, std::optional<ClientRunningWSLCProcess>, int> WSLCProcessLau
180
181
wil::com_ptr<IWSLCProcess> process;
182
int error = -1;
185
- auto result = Session.CreateRootNamespaceProcess(m_executable.c_str(), &options, &process, &error);
183
+ auto result = Session.CreateRootNamespaceProcess(m_executable.c_str(), &options, m_rows, m_columns, &process, &error);
184
if (FAILED(result))
185
{
186
return std::make_tuple(result, std::optional<ClientRunningWSLCProcess>(), error);
@@ -198,7 +196,12 @@ std::tuple<HRESULT, std::optional<ClientRunningWSLCProcess>> WSLCProcessLauncher
196
auto [options, commandLine, env] = CreateProcessOptions();
197
198
wil::com_ptr<IWSLCProcess> process;
201
- auto result = Container.Exec(&options, m_detachKeys.has_value() ? m_detachKeys->c_str() : nullptr, &process);
199
+ WSLCProcessStartOptions startOptions{};
200
+ startOptions.TtyRows = m_rows;
201
+ startOptions.TtyColumns = m_columns;
202
+ startOptions.DetachKeys = m_detachKeys.has_value() ? m_detachKeys->c_str() : nullptr;
203
+
204
+ auto result = Container.Exec(&options, &startOptions, &process);
205
if (FAILED(result))
206
{
207
return std::make_pair(result, std::optional<ClientRunningWSLCProcess>());
src/windows/common/WSLCProcessLauncher.h
+3
-3
@@ -109,8 +109,8 @@ protected:
109
std::optional<std::string> m_detachKeys;
110
std::vector<std::string> m_arguments;
111
std::vector<std::string> m_environment;
112
- DWORD m_rows = 0;
113
- DWORD m_columns = 0;
112
+ DWORD m_rows = 24;
113
+ DWORD m_columns = 80;
114
};
115
116
-} // namespace wsl::windows::common
\ No newline at end of file
116
+} // namespace wsl::windows::common
src/windows/common/WslClient.cpp
+1
@@ -1501,6 +1501,7 @@ int RunDebugShell()
1501
1502
// Create a thread to relay stdin to the pipe.
1503
wsl::windows::common::ConsoleState console;
1504
+ console.SetInteractiveMode();
1505
auto exitEvent = wil::unique_event(wil::EventOptions::ManualReset);
1506
std::thread inputThread([&]() {
1507
wsl::windows::common::relay::StandardInputRelay(GetStdHandle(STD_INPUT_HANDLE), pipe.get(), []() {}, exitEvent.get());
src/windows/common/svccomm.cpp
+1
@@ -294,6 +294,7 @@ wsl::windows::common::SvcComm::LaunchProcess(
294
//
295
296
ConsoleState Io;
297
+ Io.SetInteractiveMode();
298
COORD WindowSize = Io.GetWindowSize();
299
ULONG Flags = LXSS_CREATE_INSTANCE_FLAGS_ALLOW_FS_UPGRADE;
300
if (WI_IsFlagSet(LaunchFlags, LXSS_LAUNCH_FLAG_USE_SYSTEM_DISTRO))
src/windows/service/exe/PluginManager.cpp
+1
-1
@@ -197,7 +197,7 @@ try
197
198
wil::com_ptr<IWSLCProcess> process;
199
int errnoValue = 0;
200
- auto result = session->CreateRootNamespaceProcess(Executable, &options, &process, &errnoValue);
200
+ auto result = session->CreateRootNamespaceProcess(Executable, &options, 0, 0, &process, &errnoValue);
201
202
if (Errno != nullptr)
203
{
src/windows/service/inc/wslc.idl
+9
-4
@@ -221,9 +221,14 @@ typedef struct _WSLCProcessOptions
221
WSLCStringArray CommandLine;
222
WSLCStringArray Environment;
223
WSLCProcessFlags Flags;
224
+} WSLCProcessOptions;
225
+
226
+typedef struct _WSLCProcessStartOptions
227
+{
228
ULONG TtyRows; // Only needed when tty fd's are passed.
229
ULONG TtyColumns;
226
-} WSLCProcessOptions;
230
+ [unique, string] LPCSTR DetachKeys;
231
+} WSLCProcessStartOptions;
232
233
typedef struct _WSLCNamedVolume
234
{
@@ -574,12 +579,12 @@ interface IWSLCContainer : IUnknown
579
{
580
HRESULT Attach([in, unique] LPCSTR DetachKeys, [out] WSLCHandle* StdIn, [out] WSLCHandle* StdOut, [out] WSLCHandle* StdErr);
581
HRESULT Stop([in] WSLCSignal Signal, [in] LONG TimeoutSeconds);
577
- HRESULT Start([in] WSLCContainerStartFlags Flags, [in, unique] LPCSTR DetachKeys, [in, unique] IWarningCallback* WarningCallback);
582
+ HRESULT Start([in] WSLCContainerStartFlags Flags, [in, unique] const WSLCProcessStartOptions* StartOptions, [in, unique] IWarningCallback* WarningCallback);
583
HRESULT Delete([in] WSLCDeleteFlags Flags);
584
HRESULT Export([in] WSLCHandle TarHandle);
585
HRESULT GetState([out] WSLCContainerState* State);
586
HRESULT GetInitProcess([out] IWSLCProcess** Process);
582
- HRESULT Exec([in, ref] const WSLCProcessOptions* Options, [in, unique] LPCSTR DetachKeys, [out] IWSLCProcess** Process);
587
+ HRESULT Exec([in, ref] const WSLCProcessOptions* Options, [in, unique] const WSLCProcessStartOptions* StartOptions, [out] IWSLCProcess** Process);
588
HRESULT Inspect([out] LPSTR* Output);
589
HRESULT Logs([in] WSLCLogsFlags Flags, [out] WSLCHandle* Stdout, [out] WSLCHandle* Stderr, [in] ULONGLONG Since, [in] ULONGLONG Until, [in] ULONGLONG Tail);
590
HRESULT GetId([out, string] WSLCContainerId Id);
@@ -763,7 +768,7 @@ interface IWSLCSession : IUnknown
768
HRESULT PruneContainers([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out] WSLCPruneContainersResults* Result);
769
770
// Create a process at the VM level. This is meant for debugging.
766
- HRESULT CreateRootNamespaceProcess([in, ref] LPCSTR Executable, [in, ref] const WSLCProcessOptions* Options, [out] IWSLCProcess** Process, [out] int* Errno);
771
+ HRESULT CreateRootNamespaceProcess([in, ref] LPCSTR Executable, [in, ref] const WSLCProcessOptions* Options, [in] ULONG TtyRows, [in] ULONG TtyColumns, [out] IWSLCProcess** Process, [out] int* Errno);
772
773
// TODO: an OpenProcess() method can be added later if needed.
774
src/windows/wslc/services/ConsoleService.cpp
+5
-5
@@ -21,10 +21,10 @@ using wsl::windows::common::ClientRunningWSLCProcess;
21
using wsl::windows::common::io::ReadHandle;
22
using wsl::windows::common::io::RelayHandle;
23
24
-bool ConsoleService::RelayInteractiveTty(ClientRunningWSLCProcess& Process, HANDLE Tty, bool triggerRefresh)
24
+bool ConsoleService::RelayInteractiveTty(wsl::windows::common::ConsoleState& console, ClientRunningWSLCProcess& Process, HANDLE Tty, bool triggerRefresh)
25
{
26
- // Configure console for interactive usage.
27
- wsl::windows::common::ConsoleState console;
26
+ // Configure the console for interactive usage.
27
+ console.SetInteractiveMode();
28
29
if (triggerRefresh)
30
{
@@ -108,11 +108,11 @@ void ConsoleService::RelayNonTtyProcess(wil::unique_handle&& Stdin, wil::unique_
108
io.Run({});
109
}
110
111
-int ConsoleService::AttachToCurrentConsole(wsl::windows::common::ClientRunningWSLCProcess&& process)
111
+int ConsoleService::AttachToCurrentConsole(wsl::windows::common::ConsoleState& console, wsl::windows::common::ClientRunningWSLCProcess&& process, bool triggerRefresh)
112
{
113
if (WI_IsFlagSet(process.Flags(), WSLCProcessFlagsTty))
114
{
115
- if (!RelayInteractiveTty(process, process.GetStdHandle(WSLCFDTty).get()))
115
+ if (!RelayInteractiveTty(console, process, process.GetStdHandle(WSLCFDTty).get(), triggerRefresh))
116
{
117
wsl::windows::common::wslutil::PrintMessage(L"[detached]", stderr);
118
return 0;
src/windows/wslc/services/ConsoleService.h
+5
-2
@@ -15,13 +15,16 @@ Abstract:
15
16
#include <wslc.h>
17
#include <WSLCContainerLauncher.h>
18
+#include <ConsoleState.h>
19
20
namespace wsl::windows::wslc::services {
21
class ConsoleService
22
{
23
public:
23
- static int AttachToCurrentConsole(wsl::windows::common::ClientRunningWSLCProcess&& process);
24
- static bool RelayInteractiveTty(wsl::windows::common::ClientRunningWSLCProcess& process, HANDLE tty, bool triggerRefresh = false);
24
+ static int AttachToCurrentConsole(
25
+ wsl::windows::common::ConsoleState& console, wsl::windows::common::ClientRunningWSLCProcess&& process, bool triggerRefresh = false);
26
+ static bool RelayInteractiveTty(
27
+ wsl::windows::common::ConsoleState& console, wsl::windows::common::ClientRunningWSLCProcess& process, HANDLE tty, bool triggerRefresh = false);
28
static void RelayNonTtyProcess(wil::unique_handle&& Stdin, wil::unique_handle&& Stdout, wil::unique_handle&& Stderr);
29
};
30
} // namespace wsl::windows::wslc::services
src/windows/wslc/services/ContainerService.cpp
+37
-9
@@ -20,6 +20,7 @@ Abstract:
20
#include "WarningCallback.h"
21
#include <wslutil.h>
22
#include <WSLCProcessLauncher.h>
23
+#include <ConsoleState.h>
24
#include <CommandLine.h>
25
#include <filesystem>
26
#include <unordered_map>
@@ -292,7 +293,8 @@ int ContainerService::Attach(Session& session, const std::string& id)
293
{
294
// TTY process - relay using interactive TTY handling
295
WI_ASSERT(stderrLogs.Empty());
295
- if (!ConsoleService::RelayInteractiveTty(runningProcess, stdinLogs.Release().get(), true))
296
+ wsl::windows::common::ConsoleState console;
297
+ if (!ConsoleService::RelayInteractiveTty(console, runningProcess, stdinLogs.Release().get(), true))
298
{
299
wsl::windows::common::wslutil::PrintMessage(L"[detached]", stderr);
300
return 0; // Exit early if user detached
@@ -382,17 +384,29 @@ int ContainerService::Run(Session& session, const std::string& image, ContainerO
384
// Start the created container
385
WSLCContainerStartFlags startFlags{};
386
WI_SetFlagIf(startFlags, WSLCContainerStartFlagsAttach, !runOptions.Detach);
385
- THROW_IF_FAILED(container.Start(startFlags, nullptr, warningCallback.Get())); // TODO: Error message, detach keys
387
+
388
+ const bool attach = WI_IsFlagSet(startFlags, WSLCContainerStartFlagsAttach);
389
+
390
+ wsl::windows::common::ConsoleState console;
391
+ WSLCProcessStartOptions startOptions{};
392
+ if (runOptions.TTY)
393
+ {
394
+
395
+ const auto size = console.GetWindowSize();
396
+ startOptions.TtyRows = size.Y;
397
+ startOptions.TtyColumns = size.X;
398
+ }
399
+
400
+ THROW_IF_FAILED(container.Start(startFlags, &startOptions, warningCallback.Get())); // TODO: detach keys
401
402
// Disable auto-delete only after successful start
403
runningContainer.SetDeleteOnClose(false);
404
cidFile.Commit(containerId);
405
406
// Handle attach if requested
392
- if (WI_IsFlagSet(startFlags, WSLCContainerStartFlagsAttach))
407
+ if (attach)
408
{
394
- ConsoleService consoleService;
395
- return consoleService.AttachToCurrentConsole(runningContainer.GetInitProcess());
409
+ return ConsoleService::AttachToCurrentConsole(console, runningContainer.GetInitProcess());
410
}
411
412
PrintMessage(L"%hs", stdout, containerId);
@@ -418,7 +432,14 @@ int ContainerService::Start(Session& session, const std::string& id, bool attach
432
THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
433
WSLCContainerStartFlags flags = attach ? WSLCContainerStartFlagsAttach : WSLCContainerStartFlagsNone;
434
auto warningCallback = Microsoft::WRL::Make<WarningCallback>();
421
- THROW_IF_FAILED_EXCEPT(container->Start(flags, nullptr, warningCallback.Get()), WSLC_E_CONTAINER_IS_RUNNING);
435
+
436
+ wsl::windows::common::ConsoleState console;
437
+ WSLCProcessStartOptions startOptions{};
438
+ const auto size = console.GetWindowSize();
439
+ startOptions.TtyRows = size.Y;
440
+ startOptions.TtyColumns = size.X;
441
+
442
+ THROW_IF_FAILED_EXCEPT(container->Start(flags, &startOptions, warningCallback.Get()), WSLC_E_CONTAINER_IS_RUNNING);
443
444
if (!attach)
445
{
@@ -432,8 +453,7 @@ int ContainerService::Start(Session& session, const std::string& id, bool attach
453
THROW_IF_FAILED(process->GetFlags(&processFlags));
454
ClientRunningWSLCProcess runningProcess(std::move(process), processFlags);
455
435
- ConsoleService consoleService;
436
- return consoleService.AttachToCurrentConsole(std::move(runningProcess));
456
+ return ConsoleService::AttachToCurrentConsole(console, std::move(runningProcess), true);
457
}
458
459
void ContainerService::Stop(Session& session, const std::string& id, StopContainerOptions options)
@@ -514,6 +534,14 @@ int ContainerService::Exec(Session& session, const std::string& id, ContainerOpt
534
WI_SetFlagIf(execFlags, WSLCProcessFlagsTty, options.TTY);
535
536
auto processLauncher = wsl::windows::common::WSLCProcessLauncher({}, options.Arguments, options.EnvironmentVariables, execFlags);
537
+
538
+ wsl::windows::common::ConsoleState console;
539
+ if (options.TTY)
540
+ {
541
+ const auto size = console.GetWindowSize();
542
+ processLauncher.SetTtySize(size.Y, size.X);
543
+ }
544
+
545
if (options.User.has_value())
546
{
547
auto user = options.User.value();
@@ -524,7 +552,7 @@ int ContainerService::Exec(Session& session, const std::string& id, ContainerOpt
552
processLauncher.SetWorkingDirectory(std::move(options.WorkingDirectory));
553
}
554
527
- return ConsoleService::AttachToCurrentConsole(processLauncher.Launch(*container));
555
+ return ConsoleService::AttachToCurrentConsole(console, processLauncher.Launch(*container));
556
}
557
558
InspectContainer ContainerService::Inspect(Session& session, const std::string& id)
src/windows/wslc/services/SessionService.cpp
+2
-1
@@ -57,6 +57,7 @@ int SessionService::Attach(const std::wstring& sessionName)
57
58
// Configure console for interactive usage.
59
wsl::windows::common::ConsoleState console{};
60
+ console.SetInteractiveMode();
61
const auto windowSize = console.GetWindowSize();
62
63
const std::string shell = "/bin/sh";
@@ -142,7 +143,7 @@ int SessionService::Enter(const std::wstring& storagePath, const std::wstring& d
143
const auto windowSize = console.GetWindowSize();
144
launcher.SetTtySize(windowSize.Y, windowSize.X);
145
145
- return ConsoleService::AttachToCurrentConsole(launcher.Launch(*session.get()));
146
+ return ConsoleService::AttachToCurrentConsole(console, launcher.Launch(*session.get()));
147
}
148
149
std::vector<SessionInformation> SessionService::List()
src/windows/wslcsession/ServiceProcessLauncher.cpp
+3
-2
@@ -57,8 +57,9 @@ std::tuple<HRESULT, int, std::optional<ServiceRunningProcess>> ServiceProcessLau
57
int error = -1;
58
59
std::optional<ServiceRunningProcess> process;
60
- auto result = wil::ResultFromException(
61
- [&]() { process.emplace(virtualMachine.CreateLinuxProcess(m_executable.c_str(), options, &error), m_flags); });
60
+ auto result = wil::ResultFromException([&]() {
61
+ process.emplace(virtualMachine.CreateLinuxProcess(m_executable.c_str(), options, m_rows, m_columns, &error), m_flags);
62
+ });
63
64
return {result, error, std::move(process)};
65
}
src/windows/wslcsession/WSLCContainer.cpp
+47
-11
@@ -690,7 +690,7 @@ void WSLCContainerImpl::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle*
690
*Stderr = common::wslutil::ToCOMOutputHandle(reinterpret_cast<HANDLE>(stderrRead.get()), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe);
691
}
692
693
-void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, LPCSTR DetachKeys)
693
+void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions)
694
{
695
// Acquire an exclusive lock since this method modifies m_initProcessControl, m_initProcess and m_state.
696
auto lock = m_lock.lock_exclusive();
@@ -704,6 +704,20 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, LPCSTR DetachKeys)
704
m_id.c_str(),
705
m_state);
706
707
+ std::optional<std::string> detachKeys;
708
+
709
+ if (StartOptions != nullptr)
710
+ {
711
+ detachKeys = StartOptions->DetachKeys != nullptr ? std::optional<std::string>(StartOptions->DetachKeys) : std::nullopt;
712
+
713
+ THROW_HR_IF_MSG(
714
+ E_INVALIDARG,
715
+ WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty) && (StartOptions->TtyColumns == 0 || StartOptions->TtyRows == 0),
716
+ "Invalid tty size: %lu:%lu",
717
+ StartOptions->TtyRows,
718
+ StartOptions->TtyColumns);
719
+ }
720
+
721
// Attach to the container's init process so no IO is lost.
722
std::unique_ptr<WSLCProcessIO> io;
723
@@ -711,8 +725,6 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, LPCSTR DetachKeys)
725
{
726
if (WI_IsFlagSet(Flags, WSLCContainerStartFlagsAttach))
727
{
714
- auto detachKeys = DetachKeys == nullptr ? std::nullopt : std::optional<std::string>(DetachKeys);
715
-
728
if (WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty))
729
{
730
io = std::make_unique<TTYProcessIO>(TypedHandle{
@@ -753,10 +765,19 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, LPCSTR DetachKeys)
765
766
try
767
{
756
- m_dockerClient.StartContainer(m_id, DetachKeys == nullptr ? std::nullopt : std::optional<std::string>(DetachKeys));
768
+ m_dockerClient.StartContainer(m_id, detachKeys);
769
}
770
CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to start container '%hs'", m_id.c_str());
771
772
+ if (WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty) && StartOptions != nullptr)
773
+ {
774
+ try
775
+ {
776
+ m_dockerClient.ResizeContainerTty(m_id, StartOptions->TtyRows, StartOptions->TtyColumns);
777
+ }
778
+ CATCH_LOG();
779
+ }
780
+
781
auto inspectJson = InspectLockHeld();
782
const auto pluginResult = m_pluginNotifier->OnContainerStarted(inspectJson.c_str());
783
if (FAILED(pluginResult))
@@ -1072,7 +1093,7 @@ void WSLCContainerImpl::GetInitProcess(IWSLCProcess** Process) const
1093
THROW_IF_FAILED(m_initProcess.CopyTo(__uuidof(IWSLCProcess), (void**)Process));
1094
}
1095
1075
-void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, LPCSTR DetachKeys, IWSLCProcess** Process)
1096
+void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, const WSLCProcessStartOptions* StartOptions, IWSLCProcess** Process)
1097
{
1098
THROW_HR_IF_MSG(E_INVALIDARG, Options->CommandLine.Count == 0, "Exec command line cannot be empty");
1099
@@ -1080,6 +1101,16 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, LPCSTR DetachKey
1101
1102
THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_NOT_RUNNING, Localization::MessageWslcContainerNotRunning(m_id), m_state != WslcContainerStateRunning);
1103
1104
+ if (StartOptions != nullptr)
1105
+ {
1106
+ THROW_HR_IF_MSG(
1107
+ E_INVALIDARG,
1108
+ WI_IsFlagSet(Options->Flags, WSLCProcessFlagsTty) && (StartOptions->TtyRows == 0 || StartOptions->TtyColumns == 0),
1109
+ "Invalid tty size: %lu:%lu",
1110
+ StartOptions->TtyRows,
1111
+ StartOptions->TtyColumns);
1112
+ }
1113
+
1114
common::docker_schema::CreateExec request{};
1115
request.AttachStdout = true;
1116
request.AttachStderr = true;
@@ -1100,6 +1131,11 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, LPCSTR DetachKey
1131
if (WI_IsFlagSet(Options->Flags, WSLCProcessFlagsTty))
1132
{
1133
request.Tty = true;
1134
+
1135
+ if (StartOptions != nullptr)
1136
+ {
1137
+ request.ConsoleSize = {StartOptions->TtyRows, StartOptions->TtyColumns};
1138
+ }
1139
}
1140
1141
if (WI_IsFlagSet(Options->Flags, WSLCProcessFlagsStdin))
@@ -1107,9 +1143,9 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, LPCSTR DetachKey
1143
request.AttachStdin = true;
1144
}
1145
1110
- if (DetachKeys != nullptr)
1146
+ if (StartOptions != nullptr && StartOptions->DetachKeys != nullptr)
1147
{
1112
- request.DetachKeys = DetachKeys;
1148
+ request.DetachKeys = StartOptions->DetachKeys;
1149
}
1150
1151
try
@@ -2120,7 +2156,7 @@ HRESULT WSLCContainer::GetInitProcess(IWSLCProcess** Process)
2156
return hr;
2157
}
2158
2123
-HRESULT WSLCContainer::Exec(const WSLCProcessOptions* Options, LPCSTR DetachKeys, IWSLCProcess** Process)
2159
+HRESULT WSLCContainer::Exec(const WSLCProcessOptions* Options, const WSLCProcessStartOptions* StartOptions, IWSLCProcess** Process)
2160
{
2161
WSLCExecutionContext context(&m_session);
2162
@@ -2129,7 +2165,7 @@ HRESULT WSLCContainer::Exec(const WSLCProcessOptions* Options, LPCSTR DetachKeys
2165
RETURN_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Options->Flags, ~WSLCProcessFlagsValid), "Invalid flags: 0x%x", Options->Flags);
2166
2167
*Process = nullptr;
2132
- return CallImpl(&WSLCContainerImpl::Exec, Options, DetachKeys, Process);
2168
+ return CallImpl(&WSLCContainerImpl::Exec, Options, StartOptions, Process);
2169
}
2170
2171
HRESULT WSLCContainer::Stop(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds)
@@ -2146,14 +2182,14 @@ HRESULT WSLCContainer::Kill(_In_ WSLCSignal Signal)
2182
return CallImpl(&WSLCContainerImpl::Stop, Signal, {}, true);
2183
}
2184
2149
-HRESULT WSLCContainer::Start(WSLCContainerStartFlags Flags, LPCSTR DetachKeys, IWarningCallback* WarningCallback)
2185
+HRESULT WSLCContainer::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, IWarningCallback* WarningCallback)
2186
try
2187
{
2188
WSLCExecutionContext context(&m_session, WarningCallback);
2189
2190
THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCContainerStartFlagsValid), "Invalid flags: 0x%x", Flags);
2191
2156
- return CallImpl(&WSLCContainerImpl::Start, Flags, DetachKeys);
2192
+ return CallImpl(&WSLCContainerImpl::Start, Flags, StartOptions);
2193
}
2194
CATCH_RETURN();
2195
src/windows/wslcsession/WSLCContainer.h
+4
-4
@@ -91,7 +91,7 @@ public:
91
92
~WSLCContainerImpl();
93
94
- void Start(WSLCContainerStartFlags Flags, LPCSTR DetachKeys);
94
+ void Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions);
95
void Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* Stdout, WSLCHandle* Stderr) const;
96
void Stop(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds, bool Kill);
97
void Delete(WSLCDeleteFlags Flags);
@@ -100,7 +100,7 @@ public:
100
void GetCreatedAt(_Out_ ULONGLONG* CreatedAt);
101
void GetState(_Out_ WSLCContainerState* State);
102
void GetInitProcess(_Out_ IWSLCProcess** process) const;
103
- void Exec(_In_ const WSLCProcessOptions* Options, LPCSTR DetachKeys, _Out_ IWSLCProcess** Process);
103
+ void Exec(_In_ const WSLCProcessOptions* Options, const WSLCProcessStartOptions* StartOptions, _Out_ IWSLCProcess** Process);
104
void Inspect(LPSTR* Output) const;
105
void Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) const;
106
void Stats(LPSTR* Output) const;
@@ -231,8 +231,8 @@ public:
231
IFACEMETHOD(Export)(_In_ WSLCHandle TarHandle) override;
232
IFACEMETHOD(GetState)(_Out_ WSLCContainerState* State) override;
233
IFACEMETHOD(GetInitProcess)(_Out_ IWSLCProcess** process) override;
234
- IFACEMETHOD(Exec)(_In_ const WSLCProcessOptions* Options, _In_opt_ LPCSTR DetachKeys, _Out_ IWSLCProcess** Process) override;
235
- IFACEMETHOD(Start)(WSLCContainerStartFlags Flags, _In_opt_ LPCSTR DetachKeys, _In_opt_ IWarningCallback* WarningCallback) override;
234
+ IFACEMETHOD(Exec)(_In_ const WSLCProcessOptions* Options, _In_opt_ const WSLCProcessStartOptions* StartOptions, _Out_ IWSLCProcess** Process) override;
235
+ IFACEMETHOD(Start)(WSLCContainerStartFlags Flags, _In_opt_ const WSLCProcessStartOptions* StartOptions, _In_opt_ IWarningCallback* WarningCallback) override;
236
IFACEMETHOD(Inspect)(_Out_ LPSTR* Output) override;
237
IFACEMETHOD(Logs)(_In_ WSLCLogsFlags Flags, _Out_ WSLCHandle* Stdout, _Out_ WSLCHandle* Stderr, _In_ ULONGLONG Since, _In_ ULONGLONG Until, _In_ ULONGLONG Tail) override;
238
IFACEMETHOD(GetId)(_Out_ WSLCContainerId Id) override;
src/windows/wslcsession/WSLCSession.cpp
+3
-2
@@ -2009,7 +2009,8 @@ try
2009
}
2010
CATCH_RETURN();
2011
2012
-HRESULT WSLCSession::CreateRootNamespaceProcess(LPCSTR Executable, const WSLCProcessOptions* Options, IWSLCProcess** Process, int* Errno)
2012
+HRESULT WSLCSession::CreateRootNamespaceProcess(
2013
+ LPCSTR Executable, const WSLCProcessOptions* Options, ULONG TtyRows, ULONG TtyColumns, IWSLCProcess** Process, int* Errno)
2014
try
2015
{
2016
WSLCExecutionContext context(this);
@@ -2027,7 +2028,7 @@ try
2028
auto lock = m_lock.lock_shared();
2029
THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
2030
2030
- auto process = m_virtualMachine->CreateLinuxProcess(Executable, *Options, Errno);
2031
+ auto process = m_virtualMachine->CreateLinuxProcess(Executable, *Options, TtyRows, TtyColumns, Errno);
2032
THROW_IF_FAILED(process.CopyTo(Process));
2033
2034
return S_OK;
src/windows/wslcsession/WSLCSession.h
+6
-1
@@ -140,7 +140,12 @@ public:
140
141
// VM management.
142
IFACEMETHOD(CreateRootNamespaceProcess)(
143
- _In_ LPCSTR Executable, _In_ const WSLCProcessOptions* Options, _Out_ IWSLCProcess** VirtualMachine, _Out_ int* Errno) override;
143
+ _In_ LPCSTR Executable,
144
+ _In_ const WSLCProcessOptions* Options,
145
+ _In_ ULONG TtyRows,
146
+ _In_ ULONG TtyColumns,
147
+ _Out_ IWSLCProcess** VirtualMachine,
148
+ _Out_ int* Errno) override;
149
150
// Disk management.
151
IFACEMETHOD(FormatVirtualDisk)(_In_ LPCWSTR Path) override;
src/windows/wslcsession/WSLCVirtualMachine.cpp
+5
-5
@@ -386,7 +386,7 @@ void WSLCVirtualMachine::ConfigureNetworking()
386
options.CommandLine = {.Values = cmd.data(), .Count = static_cast<ULONG>(cmd.size())};
387
};
388
389
- auto process = CreateLinuxProcessImpl("/init", options, fds, nullptr, prepareCommandLine);
389
+ auto process = CreateLinuxProcessImpl("/init", options, fds, 0, 0, nullptr, prepareCommandLine);
390
391
// Call back to the service to configure the networking engine.
392
auto gnsHandle = process->GetStdHandle(gnsChannelFd);
@@ -639,7 +639,7 @@ std::string WSLCVirtualMachine::GetVhdDevicePath(ULONG Lun)
639
}
640
641
Microsoft::WRL::ComPtr<WSLCProcess> WSLCVirtualMachine::CreateLinuxProcess(
642
- _In_ LPCSTR Executable, _In_ const WSLCProcessOptions& Options, int* Errno, const TPrepareCommandLine& PrepareCommandLine)
642
+ _In_ LPCSTR Executable, _In_ const WSLCProcessOptions& Options, ULONG TtyRows, ULONG TtyColumns, int* Errno, const TPrepareCommandLine& PrepareCommandLine)
643
{
644
// Check if this is a tty or not
645
std::vector<WSLCProcessFd> fds;
@@ -659,11 +659,11 @@ Microsoft::WRL::ComPtr<WSLCProcess> WSLCVirtualMachine::CreateLinuxProcess(
659
fds.emplace_back(WSLCProcessFd{.Fd = WSLCFDStderr, .Type = WSLCFdType::WSLCFdTypeDefault});
660
}
661
662
- return CreateLinuxProcessImpl(Executable, Options, fds, Errno, PrepareCommandLine);
662
+ return CreateLinuxProcessImpl(Executable, Options, fds, TtyRows, TtyColumns, Errno, PrepareCommandLine);
663
}
664
665
Microsoft::WRL::ComPtr<WSLCProcess> WSLCVirtualMachine::CreateLinuxProcessImpl(
666
- LPCSTR Executable, const WSLCProcessOptions& Options, const std::vector<WSLCProcessFd>& Fds, int* Errno, const TPrepareCommandLine& PrepareCommandLine)
666
+ LPCSTR Executable, const WSLCProcessOptions& Options, const std::vector<WSLCProcessFd>& Fds, ULONG TtyRows, ULONG TtyColumns, int* Errno, const TPrepareCommandLine& PrepareCommandLine)
667
{
668
// N.B This check is there to prevent processes from being started before the VM is done initializing.
669
// to avoid potential deadlocks, since the processExitThread is required to signal the process exit events.
@@ -729,7 +729,7 @@ Microsoft::WRL::ComPtr<WSLCProcess> WSLCVirtualMachine::CreateLinuxProcessImpl(
729
// If this is an interactive tty, we need a relay process
730
if (tty != nullptr)
731
{
732
- auto [grandChildPid, ptyMaster, grandChildChannel] = Fork(childChannel, WSLC_FORK::Pty, Options.TtyRows, Options.TtyColumns);
732
+ auto [grandChildPid, ptyMaster, grandChildChannel] = Fork(childChannel, WSLC_FORK::Pty, TtyRows, TtyColumns);
733
WSLC_TTY_RELAY relayMessage{};
734
relayMessage.TtyMaster = ptyMaster;
735
relayMessage.Socket = tty->Fd;
src/windows/wslcsession/WSLCVirtualMachine.h
+4
@@ -144,6 +144,8 @@ public:
144
Microsoft::WRL::ComPtr<WSLCProcess> CreateLinuxProcess(
145
_In_ LPCSTR Executable,
146
_In_ const WSLCProcessOptions& Options,
147
+ _In_ ULONG TtyRows = 0,
148
+ _In_ ULONG TtyColumns = 0,
149
int* Errno = nullptr,
150
const TPrepareCommandLine& PrepareCommandLine = [](const auto&) {});
151
@@ -187,6 +189,8 @@ private:
189
_In_ LPCSTR Executable,
190
_In_ const WSLCProcessOptions& Options,
191
_In_ const std::vector<WSLCProcessFd>& Fds = {},
192
+ _In_ ULONG TtyRows = 0,
193
+ _In_ ULONG TtyColumns = 0,
194
int* Errno = nullptr,
195
const TPrepareCommandLine& PrepareCommandLine = [](const auto&) {});
196
test/windows/WSLCTests.cpp
+70
-2
@@ -5522,6 +5522,15 @@ class WSLCTests
5522
VERIFY_SUCCEEDED(m_defaultSession->CreateContainer(&options, nullptr, &container));
5523
VERIFY_SUCCEEDED(container->Delete(WSLCDeleteFlagsNone));
5524
}
5525
+
5526
+ // Validate that invalid tty sizes are rejected.
5527
+ {
5528
+ WSLCContainerLauncher launcher("debian:latest", "invalid-tty-size-init", {"/bin/sh"}, {}, {}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin);
5529
+ launcher.SetTtySize(0, 0);
5530
+
5531
+ auto [result, container] = launcher.LaunchNoThrow(*m_defaultSession);
5532
+ VERIFY_ARE_EQUAL(result, E_INVALIDARG);
5533
+ }
5534
}
5535
5536
WSLC_TEST_METHOD(ContainerStartAfterStop)
@@ -7182,6 +7191,18 @@ class WSLCTests
7191
VERIFY_ARE_EQUAL(result, WSLC_E_CONTAINER_NOT_RUNNING);
7192
ValidateCOMErrorMessage(std::format(L"Container '{}' is not running.", id));
7193
}
7194
+
7195
+ // Validate that invalid tty sizes are rejected.
7196
+ {
7197
+ WSLCContainerLauncher launcher("debian:latest", "invalid-tty-size-exec", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
7198
+ auto container = launcher.Launch(*m_defaultSession);
7199
+
7200
+ WSLCProcessLauncher execLauncher({}, {"/bin/sh", "-c", "stty size"}, {}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin);
7201
+ execLauncher.SetTtySize(0, 0);
7202
+
7203
+ auto [result, process] = execLauncher.LaunchNoThrow(container.Get());
7204
+ VERIFY_ARE_EQUAL(result, E_INVALIDARG);
7205
+ }
7206
}
7207
7208
WSLC_TEST_METHOD(ExecContainerDelete)
@@ -8981,6 +9002,44 @@ class WSLCTests
9002
}
9003
}
9004
9005
+ WSLC_TEST_METHOD(TtySize)
9006
+ {
9007
+ constexpr ULONG c_rows = 43;
9008
+ constexpr ULONG c_columns = 42;
9009
+ const std::string expectedSize = "43 42";
9010
+
9011
+ // Container init process.
9012
+ {
9013
+ WSLCContainerLauncher launcher(
9014
+ "debian:latest", "tty-size-init", {"/bin/sh", "-c", "while true; do stty size; sleep 1; done"}, {}, {}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin);
9015
+ launcher.SetTtySize(c_rows, c_columns);
9016
+
9017
+ auto container = launcher.Launch(*m_defaultSession);
9018
+ auto process = container.GetInitProcess();
9019
+ auto tty = process.GetStdHandle(WSLCFDTty);
9020
+
9021
+ // Wait for the size to be reflected in a loop, since the tty size is applied asynchronously.
9022
+ PartialHandleRead reader(tty.get());
9023
+ wsl::shared::retry::RetryWithTimeout<void>(
9024
+ [&]() { THROW_HR_IF(E_ABORT, reader.GetData().find(expectedSize) == std::string::npos); },
9025
+ std::chrono::milliseconds(100),
9026
+ std::chrono::seconds(60));
9027
+ }
9028
+
9029
+ // Exec process.
9030
+ {
9031
+ WSLCContainerLauncher launcher("debian:latest", "tty-size-exec", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
9032
+ auto container = launcher.Launch(*m_defaultSession);
9033
+
9034
+ WSLCProcessLauncher execLauncher({}, {"/usr/bin/stty", "size"}, {}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin);
9035
+ execLauncher.SetTtySize(c_rows, c_columns);
9036
+
9037
+ auto process = execLauncher.Launch(container.Get());
9038
+
9039
+ ValidateProcessOutput(process, {{WSLCFDTty, expectedSize + "\r\n"}});
9040
+ }
9041
+ }
9042
+
9043
WSLC_TEST_METHOD(ContainerStats_RunningContainer)
9044
{
9045
// Start a long-lived detached container on a bridged network so network stats are populated.
@@ -9574,7 +9633,12 @@ class WSLCTests
9633
WSLCContainerLauncher launcher("debian:latest", "test-detach", {"sleep", "9999999"}, {}, {}, WSLCProcessFlagsStdin | WSLCProcessFlagsTty);
9634
9635
auto container = launcher.Create(*m_defaultSession);
9577
- VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsAttach, DetachKeys, nullptr));
9636
+
9637
+ WSLCProcessStartOptions startOptions{};
9638
+ startOptions.TtyRows = 24;
9639
+ startOptions.TtyColumns = 80;
9640
+ startOptions.DetachKeys = DetachKeys;
9641
+ VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsAttach, &startOptions, nullptr));
9642
9643
auto initProcess = container.GetInitProcess();
9644
@@ -9625,7 +9689,11 @@ class WSLCTests
9689
WSLCContainerLauncher launcher("debian:latest", "test-detach", {"cat"}, {}, {}, WSLCProcessFlagsStdin | WSLCProcessFlagsTty);
9690
auto container = launcher.Create(*m_defaultSession);
9691
9628
- VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsAttach, "invalid", nullptr), E_INVALIDARG);
9692
+ WSLCProcessStartOptions invalidDetachOptions{};
9693
+ invalidDetachOptions.TtyRows = 24;
9694
+ invalidDetachOptions.TtyColumns = 80;
9695
+ invalidDetachOptions.DetachKeys = "invalid";
9696
+ VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsAttach, &invalidDetachOptions, nullptr), E_INVALIDARG);
9697
9698
VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsNone, nullptr, nullptr));
9699
test/windows/wslc/e2e/WSLCE2EContainerAttachTests.cpp
+3
-5
@@ -57,15 +57,13 @@ class WSLCE2EContainerAttachTests
57
result.Verify({.Stderr = L"", .ExitCode = 0});
58
auto containerId = result.GetStdoutOneLine();
59
60
- const auto& expectedAttachPrompt = VT::BuildContainerAttachPrompt(prompt);
60
const auto& expectedPrompt = VT::BuildContainerPrompt(prompt);
61
62
auto session = RunWslcInteractive(std::format(L"container attach {}", containerId));
63
VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running");
64
66
- // The container attach prompt appears twice.
67
- session.ExpectStdout(expectedAttachPrompt);
68
- session.ExpectStdout(expectedAttachPrompt);
65
+ // Ignore resize-repaint messages. Those are emitted when the the tty initial size is set, which can happen before or after we start running commands.
66
+ session.IgnoreSequence(VT::BuildContainerAttachPrompt(prompt));
67
68
session.WriteLine("echo hello");
69
session.ExpectCommandEcho("echo hello");
@@ -164,4 +162,4 @@ private:
162
return options.str();
163
}
164
};
167
-} // namespace WSLCE2ETests
\ No newline at end of file
165
+} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+4
-1
@@ -469,11 +469,14 @@ class WSLCE2EContainerCreateTests
469
result.Verify({.Stderr = L"", .ExitCode = 0});
470
auto containerId = result.GetStdoutOneLine();
471
472
- const auto& expectedPrompt = VT::BuildContainerPrompt(prompt);
472
+ const auto& expectedPrompt = VT::BuildContainerPrompt(prompt, true);
473
474
auto session = RunWslcInteractive(std::format(L"container start --attach {}", containerId));
475
VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running");
476
477
+ // Ignore resize-repaint messages. Those are emitted when the the tty initial size is set, which can happen before or after we start running commands.
478
+ session.IgnoreSequence(VT::BuildContainerAttachPrompt(prompt));
479
+
480
session.ExpectStdout(expectedPrompt);
481
482
session.WriteLine("echo hello");
test/windows/wslc/e2e/WSLCE2EContainerExecTests.cpp
+16
@@ -154,6 +154,22 @@ class WSLCE2EContainerExecTests
154
session.VerifyNoErrors();
155
}
156
157
+ WSLC_TEST_METHOD(WSLCE2E_Container_Exec_PseudoConsole_TerminalSize)
158
+ {
159
+ VerifyContainerIsNotListed(WslcContainerName);
160
+
161
+ auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
162
+ result.Verify({.Stderr = L"", .ExitCode = 0});
163
+
164
+ constexpr SHORT columns = 42;
165
+ constexpr SHORT rows = 43;
166
+ const auto commandLine =
167
+ std::format(L"container exec -it {} /bin/sh -c -- \"while true; do stty size; sleep 1; done\"", WslcContainerName);
168
+
169
+ auto session = RunWslcInteractive(commandLine, ElevationType::Elevated, PseudoConsole{columns, rows});
170
+ VerifyPseudoConsoleTtySize(session, columns, rows);
171
+ }
172
+
173
WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvOption)
174
{
175
auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+19
-1
@@ -529,7 +529,10 @@ class WSLCE2EContainerRunTests
529
std::format(L"container run -it -e PS1={} --name {} {} bash --norc", prompt, WslcContainerName, DebianImage.NameAndTag()));
530
VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running");
531
532
- const auto& expectedPrompt = VT::BuildContainerPrompt(prompt);
532
+ // Ignore resize-repaint messages. Those are emitted when the the tty initial size is set, which can happen before or after we start running commands.
533
+ session.IgnoreSequence(VT::BuildContainerAttachPrompt(prompt));
534
+
535
+ const auto& expectedPrompt = VT::BuildContainerPrompt(prompt, true);
536
session.ExpectStdout(expectedPrompt);
537
538
session.WriteLine("echo hello");
@@ -567,6 +570,21 @@ class WSLCE2EContainerRunTests
570
session.VerifyNoErrors();
571
}
572
573
+ WSLC_TEST_METHOD(WSLCE2E_Container_Run_PseudoConsole_TerminalSize)
574
+ {
575
+ VerifyContainerIsNotListed(WslcContainerName);
576
+
577
+ constexpr SHORT columns = 42;
578
+ constexpr SHORT rows = 43;
579
+ const auto commandLine = std::format(
580
+ L"container run --rm -it --name {} {} /bin/sh -c \"while true; do stty size; sleep 1; done\"",
581
+ WslcContainerName,
582
+ DebianImage.NameAndTag());
583
+
584
+ auto session = RunWslcInteractive(commandLine, ElevationType::Elevated, PseudoConsole{columns, rows});
585
+ VerifyPseudoConsoleTtySize(session, columns, rows);
586
+ }
587
+
588
WSLC_TEST_METHOD(WSLCE2E_Container_Run_Tmpfs)
589
{
590
auto result = RunWslc(std::format(
test/windows/wslc/e2e/WSLCE2EHelpers.cpp
+40
@@ -553,4 +553,44 @@ std::wstring GetPythonHttpServerScript(uint16_t port)
553
{
554
return std::format(L"python3 -m http.server {}", port);
555
}
556
+
557
+namespace {
558
+
559
+ void WaitForTtySize(const WSLCInteractiveSession& session, SHORT columns, SHORT rows)
560
+ {
561
+ try
562
+ {
563
+ wsl::shared::retry::RetryWithTimeout<void>(
564
+ [&]() {
565
+ const std::string data = session.GetStdoutData();
566
+ THROW_HR_IF(E_ABORT, data.find(std::format("{} {}\r\n", rows, columns)) == std::string::npos);
567
+ },
568
+ std::chrono::milliseconds(200),
569
+ std::chrono::seconds(60));
570
+ }
571
+ catch (...)
572
+ {
573
+ const std::string data = session.GetStdoutData();
574
+ VERIFY_FAIL(std::format(
575
+ L"Timed out waiting for tty resize. Captured pseudoconsole output: \"{}\"",
576
+ wsl::shared::string::MultiByteToWide(EscapeString(data)))
577
+ .c_str());
578
+ }
579
+ }
580
+
581
+} // namespace
582
+
583
+void VerifyPseudoConsoleTtySize(WSLCInteractiveSession& session, SHORT columns, SHORT rows)
584
+{
585
+ constexpr SHORT resizedColumns = 100;
586
+ constexpr SHORT resizedRows = 37;
587
+ VERIFY_IS_TRUE(columns != resizedColumns || rows != resizedRows, L"Resized tty size must differ from the initial size");
588
+
589
+ WaitForTtySize(session, columns, rows);
590
+
591
+ session.ResizePseudoConsole(resizedColumns, resizedRows);
592
+ WaitForTtySize(session, resizedColumns, resizedRows);
593
+
594
+ session.Terminate();
595
+}
596
} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EHelpers.h
+2
@@ -192,6 +192,8 @@ inline void VerifyContainerIsNotListed(const std::wstring& containerNameOrId)
192
193
wil::com_ptr<IWSLCSession> OpenDefaultElevatedSession();
194
195
+void VerifyPseudoConsoleTtySize(WSLCInteractiveSession& session, SHORT columns, SHORT rows);
196
+
197
// Starts a local registry container with host networking using the COM API.
198
// Returns the running container (holds it alive) and the registry address (e.g. "127.0.0.1:PORT").
199
std::pair<wsl::windows::common::RunningWSLCContainer, std::string> StartLocalRegistry(
test/windows/wslc/e2e/WSLCExecutor.cpp
+85
-13
@@ -235,20 +235,40 @@ std::wstring GetWslcHeader()
235
return header.str();
236
}
237
238
-WSLCInteractiveSession RunWslcInteractive(const std::wstring& commandLine, ElevationType elevationType)
238
+WSLCInteractiveSession RunWslcInteractive(const std::wstring& commandLine, ElevationType elevationType, std::optional<PseudoConsole> pseudoConsole)
239
{
240
auto cmd = L"\"" + GetWslcPath() + L"\" " + commandLine;
241
242
- auto [childStdinRead, parentStdinWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(65536, false, true);
243
- auto [parentStdoutRead, childStdoutWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(65536, true, false);
244
- auto [parentStderrRead, childStderrWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(65536, true, false);
242
+ wsl::windows::common::SubProcess process(nullptr, cmd.c_str());
243
246
- THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStdinRead.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
247
- THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStdoutWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
248
- THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStderrWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
244
+ wil::unique_hfile parentStdinWrite;
245
+ wil::unique_hfile parentStdoutRead;
246
+ wil::unique_hfile parentStderrRead;
247
+ wsl::windows::common::helpers::unique_pseudo_console console;
248
250
- wsl::windows::common::SubProcess process(nullptr, cmd.c_str());
251
- process.SetStdHandles(childStdinRead.get(), childStdoutWrite.get(), childStderrWrite.get());
249
+ wil::unique_hfile childStdinRead;
250
+ wil::unique_hfile childStdoutWrite;
251
+ wil::unique_hfile childStderrWrite;
252
+
253
+ if (pseudoConsole.has_value())
254
+ {
255
+ process.SetPseudoConsole(pseudoConsole->Handle.get());
256
+ parentStdinWrite = std::move(pseudoConsole->InputWrite);
257
+ parentStdoutRead = std::move(pseudoConsole->OutputRead);
258
+ console = std::move(pseudoConsole->Handle);
259
+ }
260
+ else
261
+ {
262
+ std::tie(childStdinRead, parentStdinWrite) = wsl::windows::common::wslutil::OpenAnonymousPipe(65536, false, true);
263
+ std::tie(parentStdoutRead, childStdoutWrite) = wsl::windows::common::wslutil::OpenAnonymousPipe(65536, true, false);
264
+ std::tie(parentStderrRead, childStderrWrite) = wsl::windows::common::wslutil::OpenAnonymousPipe(65536, true, false);
265
+
266
+ THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStdinRead.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
267
+ THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStdoutWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
268
+ THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStderrWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
269
+
270
+ process.SetStdHandles(childStdinRead.get(), childStdoutWrite.get(), childStderrWrite.get());
271
+ }
272
273
wil::unique_handle nonElevatedToken;
274
if (elevationType == ElevationType::NonElevated)
@@ -269,7 +289,22 @@ WSLCInteractiveSession RunWslcInteractive(const std::wstring& commandLine, Eleva
289
std::move(parentStdoutRead),
290
std::move(parentStderrRead),
291
std::move(processHandle),
272
- std::move(nonElevatedToken)); // Transfer token ownership to the session
292
+ std::move(nonElevatedToken), // Transfer token ownership to the session
293
+ std::move(console));
294
+}
295
+
296
+PseudoConsole::PseudoConsole(SHORT columns, SHORT rows)
297
+{
298
+ auto [inputRead, inputWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(0, false, true);
299
+
300
+ auto [outputRead, outputWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(0, true, false);
301
+
302
+ HPCON rawPseudoConsole{};
303
+ THROW_IF_FAILED(::CreatePseudoConsole(COORD{columns, rows}, inputRead.get(), outputWrite.get(), 0, &rawPseudoConsole));
304
+ Handle.reset(rawPseudoConsole);
305
+
306
+ InputWrite = std::move(inputWrite);
307
+ OutputRead = std::move(outputRead);
308
}
309
310
// WSLCInteractiveSession implementation
@@ -280,16 +315,24 @@ WSLCInteractiveSession::WSLCInteractiveSession(
315
wil::unique_hfile stdoutRead,
316
wil::unique_hfile stderrRead,
317
wil::unique_handle processHandle,
283
- wil::unique_handle nonElevatedToken) :
318
+ wil::unique_handle nonElevatedToken,
319
+ wsl::windows::common::helpers::unique_pseudo_console pseudoConsole) :
320
CommandLine(std::move(commandLine)),
321
m_stdinWrite(std::move(stdinWrite)),
322
m_stdoutRead(std::move(stdoutRead)),
323
m_stderrRead(std::move(stderrRead)),
324
+ m_pseudoConsole(std::move(pseudoConsole)),
325
m_processHandle(std::move(processHandle)),
326
m_nonElevatedToken(std::move(nonElevatedToken))
327
{
328
m_stdoutReader = std::make_unique<PartialHandleRead>(m_stdoutRead.get());
292
- m_stderrReader = std::make_unique<PartialHandleRead>(m_stderrRead.get());
329
+
330
+ // In pseudoconsole mode stderr is multiplexed onto the conpty output, so there is no
331
+ // separate stderr handle to read from.
332
+ if (m_stderrRead.is_valid())
333
+ {
334
+ m_stderrReader = std::make_unique<PartialHandleRead>(m_stderrRead.get());
335
+ }
336
}
337
338
WSLCInteractiveSession::~WSLCInteractiveSession()
@@ -313,12 +356,34 @@ WSLCInteractiveSession::~WSLCInteractiveSession()
356
357
void WSLCInteractiveSession::ExpectStdout(const std::string& expected)
358
{
359
+ if (m_ignoreSequence.has_value())
360
+ {
361
+ while (m_stdoutReader->ReadBytes(m_ignoreSequence->size()) == *m_ignoreSequence)
362
+ {
363
+ Log::Comment(std::format(L"Consuming ignored sequence: \"{}\"", wsl::shared::string::MultiByteToWide(EscapeString(*m_ignoreSequence)))
364
+ .c_str());
365
+ m_stdoutReader->ConsumeBytes(m_ignoreSequence->size());
366
+ }
367
+ }
368
+
369
Log::Comment(std::format(L"Expecting stdout: \"{}\"", wsl::shared::string::MultiByteToWide(EscapeString(expected))).c_str());
370
m_stdoutReader->ExpectConsume(expected);
371
}
372
373
+std::string WSLCInteractiveSession::GetStdoutData() const
374
+{
375
+ return m_stdoutReader->GetData();
376
+}
377
+
378
+void WSLCInteractiveSession::ResizePseudoConsole(SHORT columns, SHORT rows)
379
+{
380
+ VERIFY_IS_TRUE(static_cast<bool>(m_pseudoConsole), L"ResizePseudoConsole requires a pseudoconsole-backed session");
381
+ THROW_IF_FAILED(::ResizePseudoConsole(m_pseudoConsole.get(), COORD{columns, rows}));
382
+}
383
+
384
void WSLCInteractiveSession::ExpectStderr(const std::string& expected)
385
{
386
+ WI_ASSERT(m_stderrReader.get() != nullptr);
387
Log::Comment(std::format(L"Expecting stderr: \"{}\"", wsl::shared::string::MultiByteToWide(EscapeString(expected))).c_str());
388
m_stderrReader->ExpectConsume(expected);
389
}
@@ -329,6 +394,12 @@ void WSLCInteractiveSession::ExpectCommandEcho(const std::string& command)
394
ExpectStdout(std::format("{}\r\n{}\r", command, VT::B_END));
395
}
396
397
+void WSLCInteractiveSession::IgnoreSequence(const std::string& sequence)
398
+{
399
+ VERIFY_IS_FALSE(m_ignoreSequence.has_value());
400
+ m_ignoreSequence = sequence;
401
+}
402
+
403
void WSLCInteractiveSession::Write(const std::string& data)
404
{
405
Log::Comment(std::format(L"Writing to stdin: \"{}\"", wsl::shared::string::MultiByteToWide(EscapeString(data))).c_str());
@@ -434,6 +505,7 @@ bool WSLCInteractiveSession::Terminate(UINT exitCode)
505
506
void WSLCInteractiveSession::VerifyNoErrors()
507
{
508
+ WI_ASSERT(m_stderrReader.get() != nullptr);
509
m_stderrReader->ExpectClosed(DefaultWaitTimeoutMs);
510
511
// Verify that stderr was actually empty - not just closed
@@ -459,4 +531,4 @@ int WSLCInteractiveSession::ExitAndVerifyNoErrors(DWORD timeoutMs)
531
return exitCode;
532
}
533
462
-} // namespace WSLCE2ETests
\ No newline at end of file
534
+} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCExecutor.h
+24
-2
@@ -47,6 +47,18 @@ struct WSLCExecutionResult
47
bool StdoutContainsSubstring(const std::wstring& substring) const;
48
};
49
50
+struct PseudoConsole
51
+{
52
+ NON_COPYABLE(PseudoConsole);
53
+ DEFAULT_MOVABLE(PseudoConsole);
54
+
55
+ PseudoConsole(SHORT columns, SHORT rows);
56
+
57
+ wil::unique_hfile InputWrite;
58
+ wil::unique_hfile OutputRead;
59
+ wsl::windows::common::helpers::unique_pseudo_console Handle;
60
+};
61
+
62
// Interactive session for testing wslc commands that require stdin/stdout interaction.
63
// Uses PartialHandleRead for race-free output validation
64
struct WSLCInteractiveSession
@@ -57,7 +69,8 @@ struct WSLCInteractiveSession
69
wil::unique_hfile stdoutRead,
70
wil::unique_hfile stderrRead,
71
wil::unique_handle processHandle,
60
- wil::unique_handle nonElevatedToken = wil::unique_handle{});
72
+ wil::unique_handle nonElevatedToken = wil::unique_handle{},
73
+ wsl::windows::common::helpers::unique_pseudo_console pseudoConsole = {});
74
~WSLCInteractiveSession();
75
76
// Non-copyable, non-movable
@@ -74,6 +87,12 @@ struct WSLCInteractiveSession
87
void ExpectStderr(const std::string& expected);
88
void ExpectCommandEcho(const std::string& command);
89
90
+ void IgnoreSequence(const std::string& sequence);
91
+
92
+ std::string GetStdoutData() const;
93
+
94
+ void ResizePseudoConsole(SHORT columns, SHORT rows);
95
+
96
bool IsRunning() const;
97
void CloseStdin();
98
std::optional<int> GetExitCode() const;
@@ -88,10 +107,12 @@ private:
107
wil::unique_hfile m_stdinWrite;
108
wil::unique_hfile m_stdoutRead;
109
wil::unique_hfile m_stderrRead;
110
+ wsl::windows::common::helpers::unique_pseudo_console m_pseudoConsole;
111
wil::unique_handle m_processHandle;
112
wil::unique_handle m_nonElevatedToken; // Keep token alive for the lifetime of the session
113
std::unique_ptr<PartialHandleRead> m_stdoutReader;
114
std::unique_ptr<PartialHandleRead> m_stderrReader;
115
+ std::optional<std::string> m_ignoreSequence;
116
};
117
118
WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType elevationType = ElevationType::Elevated);
@@ -102,6 +123,7 @@ WSLCExecutionResult RunWslcAndRedirectToFile(
123
void RunWslcAndVerify(const std::wstring& cmd, const WSLCExecutionResult& expected, ElevationType elevationType = ElevationType::Elevated);
124
125
std::wstring GetWslcHeader();
105
-WSLCInteractiveSession RunWslcInteractive(const std::wstring& commandLine, ElevationType elevationType = ElevationType::Elevated);
126
+WSLCInteractiveSession RunWslcInteractive(
127
+ const std::wstring& commandLine, ElevationType elevationType = ElevationType::Elevated, std::optional<PseudoConsole> pseudoConsole = std::nullopt);
128
129
} // namespace WSLCE2ETests