@samitouri / QOSAMI-WSL / commits / 31331371

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