From 0e35ba06437554949a14003e7ef0bdc63b8636c5 Mon Sep 17 00:00:00 2001 From: Blue Date: Fri, 5 Jun 2026 13:50:03 -0700 Subject: [PATCH 1/5] Save state --- src/windows/common/ConsoleState.cpp | 44 ++++++---- src/windows/common/ConsoleState.h | 2 + src/windows/common/WSLCContainerLauncher.cpp | 6 +- src/windows/common/WSLCContainerLauncher.h | 1 + src/windows/common/WSLCProcessLauncher.cpp | 11 ++- src/windows/common/WSLCProcessLauncher.h | 6 +- src/windows/common/WslClient.cpp | 1 + src/windows/common/svccomm.cpp | 1 + src/windows/service/exe/PluginManager.cpp | 2 +- src/windows/service/inc/wslc.idl | 13 ++- src/windows/wslc/services/ConsoleService.cpp | 12 +-- src/windows/wslc/services/ConsoleService.h | 6 +- .../wslc/services/ContainerService.cpp | 53 ++++++++++-- src/windows/wslc/services/SessionService.cpp | 3 +- .../wslcsession/ServiceProcessLauncher.cpp | 5 +- src/windows/wslcsession/WSLCContainer.cpp | 60 +++++++++++--- src/windows/wslcsession/WSLCContainer.h | 8 +- src/windows/wslcsession/WSLCSession.cpp | 5 +- src/windows/wslcsession/WSLCSession.h | 7 +- .../wslcsession/WSLCVirtualMachine.cpp | 10 +-- src/windows/wslcsession/WSLCVirtualMachine.h | 4 + test/windows/WSLCTests.cpp | 82 ++++++++++++++++++- 22 files changed, 271 insertions(+), 71 deletions(-) diff --git a/src/windows/common/ConsoleState.cpp b/src/windows/common/ConsoleState.cpp index 777c218b8..4bf7b655f 100644 --- a/src/windows/common/ConsoleState.cpp +++ b/src/windows/common/ConsoleState.cpp @@ -64,12 +64,34 @@ namespace wsl::windows::common { ConsoleState::ConsoleState() { - // Ensure console state is restored if the constructor throws. - auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { RestoreConsoleState(); }); - m_InputHandle.reset( CreateFileW(L"CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)); + if (!m_InputHandle) + { + LOG_LAST_ERROR_MSG("CreateFileW(CONIN$) failed"); + } + + m_OutputHandle.reset( + CreateFileW(L"CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)); + + if (!m_OutputHandle) + { + LOG_LAST_ERROR_MSG("CreateFileW(CONOUT$) failed"); + } +} + +void ConsoleState::SetInteractiveMode() +{ + // Configuring twice would overwrite the saved original state with the already-modified state. + if (m_interactiveModeConfigured) + { + return; + } + + // Ensure console state is restored if this method throws. + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { RestoreConsoleState(); }); + if (m_InputHandle) { m_SavedInputCodePage = GetConsoleCP(); @@ -85,13 +107,6 @@ ConsoleState::ConsoleState() ChangeConsoleMode(m_InputHandle.get(), NewMode); m_SavedInputMode = mode; } - else - { - LOG_LAST_ERROR_MSG("CreateFileW(CONIN$) failed"); - } - - m_OutputHandle.reset( - CreateFileW(L"CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)); if (m_OutputHandle) { @@ -107,11 +122,8 @@ ConsoleState::ConsoleState() ChangeConsoleMode(m_OutputHandle.get(), NewMode); m_SavedOutputMode = mode; } - else - { - LOG_LAST_ERROR_MSG("CreateFileW(CONOUT$) failed"); - } + m_interactiveModeConfigured = true; cleanup.release(); } @@ -127,11 +139,13 @@ void ConsoleState::RestoreConsoleState() if (m_SavedInputCodePage.has_value()) { LOG_IF_WIN32_BOOL_FALSE(SetConsoleCP(m_SavedInputCodePage.value())); + m_SavedInputCodePage.reset(); } if (m_SavedInputMode.has_value()) { TrySetConsoleMode(m_InputHandle.get(), m_SavedInputMode.value()); + m_SavedInputMode.reset(); } } @@ -140,11 +154,13 @@ void ConsoleState::RestoreConsoleState() if (m_SavedOutputCodePage.has_value()) { LOG_IF_WIN32_BOOL_FALSE(SetConsoleOutputCP(m_SavedOutputCodePage.value())); + m_SavedOutputCodePage.reset(); } if (m_SavedOutputMode.has_value()) { TrySetConsoleMode(m_OutputHandle.get(), m_SavedOutputMode.value()); + m_SavedOutputMode.reset(); } } } diff --git a/src/windows/common/ConsoleState.h b/src/windows/common/ConsoleState.h index 774f4a14b..03be483c6 100644 --- a/src/windows/common/ConsoleState.h +++ b/src/windows/common/ConsoleState.h @@ -32,12 +32,14 @@ class ConsoleState ConsoleState& operator=(ConsoleState&&) = delete; COORD GetWindowSize() const; + void SetInteractiveMode(); private: void RestoreConsoleState(); wil::unique_hfile m_InputHandle; wil::unique_hfile m_OutputHandle; + bool m_interactiveModeConfigured{false}; std::optional m_SavedInputMode{}; std::optional m_SavedInputCodePage{}; std::optional m_SavedOutputMode{}; diff --git a/src/windows/common/WSLCContainerLauncher.cpp b/src/windows/common/WSLCContainerLauncher.cpp index b2fb10026..a3618eabf 100644 --- a/src/windows/common/WSLCContainerLauncher.cpp +++ b/src/windows/common/WSLCContainerLauncher.cpp @@ -259,7 +259,11 @@ std::pair> WSLCContainerLauncher::L return std::make_pair(result, std::optional{}); } - result = container.value().Get().Start(Flags, nullptr, WarningCallback); + WSLCProcessStartOptions startOptions{}; + startOptions.TtyRows = m_rows; + startOptions.TtyColumns = m_columns; + + result = container.value().Get().Start(Flags, &startOptions, WarningCallback); return std::make_pair(result, std::move(container)); } diff --git a/src/windows/common/WSLCContainerLauncher.h b/src/windows/common/WSLCContainerLauncher.h index 048b68aef..7c655f2ff 100644 --- a/src/windows/common/WSLCContainerLauncher.h +++ b/src/windows/common/WSLCContainerLauncher.h @@ -86,6 +86,7 @@ class WSLCContainerLauncher : private WSLCProcessLauncher void AddUlimit(const std::string& Name, std::int64_t Soft, std::int64_t Hard); using WSLCProcessLauncher::FormatResult; + using WSLCProcessLauncher::SetTtySize; using WSLCProcessLauncher::SetUser; using WSLCProcessLauncher::SetWorkingDirectory; diff --git a/src/windows/common/WSLCProcessLauncher.cpp b/src/windows/common/WSLCProcessLauncher.cpp index 894384dfc..1caa34fe5 100644 --- a/src/windows/common/WSLCProcessLauncher.cpp +++ b/src/windows/common/WSLCProcessLauncher.cpp @@ -57,8 +57,6 @@ std::tuple, std::vector(commandLine.size())}; options.Environment = {.Values = environment.data(), .Count = static_cast(environment.size())}; - options.TtyColumns = m_columns; - options.TtyRows = m_rows; options.Flags = m_flags; if (!m_workingDirectory.empty()) @@ -182,7 +180,7 @@ std::tuple, int> WSLCProcessLau wil::com_ptr process; int error = -1; - auto result = Session.CreateRootNamespaceProcess(m_executable.c_str(), &options, &process, &error); + auto result = Session.CreateRootNamespaceProcess(m_executable.c_str(), &options, m_rows, m_columns, &process, &error); if (FAILED(result)) { return std::make_tuple(result, std::optional(), error); @@ -198,7 +196,12 @@ std::tuple> WSLCProcessLauncher auto [options, commandLine, env] = CreateProcessOptions(); wil::com_ptr process; - auto result = Container.Exec(&options, m_detachKeys.has_value() ? m_detachKeys->c_str() : nullptr, &process); + WSLCProcessStartOptions startOptions{}; + startOptions.TtyRows = m_rows; + startOptions.TtyColumns = m_columns; + startOptions.DetachKeys = m_detachKeys.has_value() ? m_detachKeys->c_str() : nullptr; + + auto result = Container.Exec(&options, &startOptions, &process); if (FAILED(result)) { return std::make_pair(result, std::optional()); diff --git a/src/windows/common/WSLCProcessLauncher.h b/src/windows/common/WSLCProcessLauncher.h index a904f9123..67defd445 100644 --- a/src/windows/common/WSLCProcessLauncher.h +++ b/src/windows/common/WSLCProcessLauncher.h @@ -109,8 +109,8 @@ class WSLCProcessLauncher std::optional m_detachKeys; std::vector m_arguments; std::vector m_environment; - DWORD m_rows = 0; - DWORD m_columns = 0; + DWORD m_rows = 24; + DWORD m_columns = 80; }; -} // namespace wsl::windows::common \ No newline at end of file +} // namespace wsl::windows::common diff --git a/src/windows/common/WslClient.cpp b/src/windows/common/WslClient.cpp index 2dc05b57e..5cad5ef5e 100644 --- a/src/windows/common/WslClient.cpp +++ b/src/windows/common/WslClient.cpp @@ -1501,6 +1501,7 @@ int RunDebugShell() // Create a thread to relay stdin to the pipe. wsl::windows::common::ConsoleState console; + console.SetInteractiveMode(); auto exitEvent = wil::unique_event(wil::EventOptions::ManualReset); std::thread inputThread([&]() { wsl::windows::common::relay::StandardInputRelay(GetStdHandle(STD_INPUT_HANDLE), pipe.get(), []() {}, exitEvent.get()); diff --git a/src/windows/common/svccomm.cpp b/src/windows/common/svccomm.cpp index d3ae7f803..1e75011d6 100644 --- a/src/windows/common/svccomm.cpp +++ b/src/windows/common/svccomm.cpp @@ -294,6 +294,7 @@ wsl::windows::common::SvcComm::LaunchProcess( // ConsoleState Io; + Io.SetInteractiveMode(); COORD WindowSize = Io.GetWindowSize(); ULONG Flags = LXSS_CREATE_INSTANCE_FLAGS_ALLOW_FS_UPGRADE; if (WI_IsFlagSet(LaunchFlags, LXSS_LAUNCH_FLAG_USE_SYSTEM_DISTRO)) diff --git a/src/windows/service/exe/PluginManager.cpp b/src/windows/service/exe/PluginManager.cpp index 38c1bfbdf..cdef52f48 100644 --- a/src/windows/service/exe/PluginManager.cpp +++ b/src/windows/service/exe/PluginManager.cpp @@ -215,7 +215,7 @@ try wil::com_ptr process; int errnoValue = 0; - auto result = session->CreateRootNamespaceProcess(Executable, &options, &process, &errnoValue); + auto result = session->CreateRootNamespaceProcess(Executable, &options, 0, 0, &process, &errnoValue); if (Errno != nullptr) { diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 4880fa92b..19e5d5ead 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -219,9 +219,14 @@ typedef struct _WSLCProcessOptions WSLCStringArray CommandLine; WSLCStringArray Environment; WSLCProcessFlags Flags; +} WSLCProcessOptions; + +typedef struct _WSLCProcessStartOptions +{ ULONG TtyRows; // Only needed when tty fd's are passed. ULONG TtyColumns; -} WSLCProcessOptions; + [unique, string] LPCSTR DetachKeys; +} WSLCProcessStartOptions; typedef struct _WSLCNamedVolume { @@ -566,12 +571,12 @@ interface IWSLCContainer : IUnknown { HRESULT Attach([in, unique] LPCSTR DetachKeys, [out] WSLCHandle* StdIn, [out] WSLCHandle* StdOut, [out] WSLCHandle* StdErr); HRESULT Stop([in] WSLCSignal Signal, [in] LONG TimeoutSeconds); - HRESULT Start([in] WSLCContainerStartFlags Flags, [in, unique] LPCSTR DetachKeys, [in, unique] IWarningCallback* WarningCallback); + HRESULT Start([in] WSLCContainerStartFlags Flags, [in, unique] const WSLCProcessStartOptions* StartOptions, [in, unique] IWarningCallback* WarningCallback); HRESULT Delete([in] WSLCDeleteFlags Flags); HRESULT Export([in] WSLCHandle TarHandle); HRESULT GetState([out] WSLCContainerState* State); HRESULT GetInitProcess([out] IWSLCProcess** Process); - HRESULT Exec([in, ref] const WSLCProcessOptions* Options, [in, unique] LPCSTR DetachKeys, [out] IWSLCProcess** Process); + HRESULT Exec([in, ref] const WSLCProcessOptions* Options, [in, unique] const WSLCProcessStartOptions* StartOptions, [out] IWSLCProcess** Process); HRESULT Inspect([out] LPSTR* Output); HRESULT Logs([in] WSLCLogsFlags Flags, [out] WSLCHandle* Stdout, [out] WSLCHandle* Stderr, [in] ULONGLONG Since, [in] ULONGLONG Until, [in] ULONGLONG Tail); HRESULT GetId([out, string] WSLCContainerId Id); @@ -752,7 +757,7 @@ interface IWSLCSession : IUnknown HRESULT PruneContainers([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out] WSLCPruneContainersResults* Result); // Create a process at the VM level. This is meant for debugging. - HRESULT CreateRootNamespaceProcess([in, ref] LPCSTR Executable, [in, ref] const WSLCProcessOptions* Options, [out] IWSLCProcess** Process, [out] int* Errno); + HRESULT CreateRootNamespaceProcess([in, ref] LPCSTR Executable, [in, ref] const WSLCProcessOptions* Options, [in] ULONG TtyRows, [in] ULONG TtyColumns, [out] IWSLCProcess** Process, [out] int* Errno); // TODO: an OpenProcess() method can be added later if needed. diff --git a/src/windows/wslc/services/ConsoleService.cpp b/src/windows/wslc/services/ConsoleService.cpp index ffacbdd99..e8e3e2023 100644 --- a/src/windows/wslc/services/ConsoleService.cpp +++ b/src/windows/wslc/services/ConsoleService.cpp @@ -21,10 +21,11 @@ using wsl::windows::common::ClientRunningWSLCProcess; using wsl::windows::common::io::ReadHandle; using wsl::windows::common::io::RelayHandle; -bool ConsoleService::RelayInteractiveTty(ClientRunningWSLCProcess& Process, HANDLE Tty, bool triggerRefresh) +bool ConsoleService::RelayInteractiveTty( + wsl::windows::common::ConsoleState& console, ClientRunningWSLCProcess& Process, HANDLE Tty, bool triggerRefresh) { - // Configure console for interactive usage. - wsl::windows::common::ConsoleState console; + // Configure the console for interactive usage. + console.SetInteractiveMode(); if (triggerRefresh) { @@ -108,11 +109,12 @@ void ConsoleService::RelayNonTtyProcess(wil::unique_handle&& Stdin, wil::unique_ io.Run({}); } -int ConsoleService::AttachToCurrentConsole(wsl::windows::common::ClientRunningWSLCProcess&& process) +int ConsoleService::AttachToCurrentConsole( + wsl::windows::common::ConsoleState& console, wsl::windows::common::ClientRunningWSLCProcess&& process) { if (WI_IsFlagSet(process.Flags(), WSLCProcessFlagsTty)) { - if (!RelayInteractiveTty(process, process.GetStdHandle(WSLCFDTty).get())) + if (!RelayInteractiveTty(console, process, process.GetStdHandle(WSLCFDTty).get())) { wsl::windows::common::wslutil::PrintMessage(L"[detached]", stderr); return 0; diff --git a/src/windows/wslc/services/ConsoleService.h b/src/windows/wslc/services/ConsoleService.h index 89ba244de..c49c90d18 100644 --- a/src/windows/wslc/services/ConsoleService.h +++ b/src/windows/wslc/services/ConsoleService.h @@ -15,13 +15,15 @@ Module Name: #include #include +#include namespace wsl::windows::wslc::services { class ConsoleService { public: - static int AttachToCurrentConsole(wsl::windows::common::ClientRunningWSLCProcess&& process); - static bool RelayInteractiveTty(wsl::windows::common::ClientRunningWSLCProcess& process, HANDLE tty, bool triggerRefresh = false); + static int AttachToCurrentConsole(wsl::windows::common::ConsoleState& console, wsl::windows::common::ClientRunningWSLCProcess&& process); + static bool RelayInteractiveTty( + wsl::windows::common::ConsoleState& console, wsl::windows::common::ClientRunningWSLCProcess& process, HANDLE tty, bool triggerRefresh = false); static void RelayNonTtyProcess(wil::unique_handle&& Stdin, wil::unique_handle&& Stdout, wil::unique_handle&& Stderr); }; } // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index 0f2b2b776..f91685238 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -20,6 +20,7 @@ Module Name: #include "WarningCallback.h" #include #include +#include #include #include #include @@ -270,7 +271,8 @@ int ContainerService::Attach(Session& session, const std::string& id) { // TTY process - relay using interactive TTY handling WI_ASSERT(stderrLogs.Empty()); - if (!ConsoleService::RelayInteractiveTty(runningProcess, stdinLogs.Release().get(), true)) + wsl::windows::common::ConsoleState console; + if (!ConsoleService::RelayInteractiveTty(console, runningProcess, stdinLogs.Release().get(), true)) { wsl::windows::common::wslutil::PrintMessage(L"[detached]", stderr); return 0; // Exit early if user detached @@ -360,17 +362,32 @@ int ContainerService::Run(Session& session, const std::string& image, ContainerO // Start the created container WSLCContainerStartFlags startFlags{}; WI_SetFlagIf(startFlags, WSLCContainerStartFlagsAttach, !runOptions.Detach); - THROW_IF_FAILED(container.Start(startFlags, nullptr, warningCallback.Get())); // TODO: Error message, detach keys + + const bool attach = WI_IsFlagSet(startFlags, WSLCContainerStartFlagsAttach); + + std::optional console; + WSLCProcessStartOptions startOptions{}; + if (attach) + { + console.emplace(); + if (runOptions.TTY) + { + const auto size = console->GetWindowSize(); + startOptions.TtyRows = size.Y; + startOptions.TtyColumns = size.X; + } + } + + THROW_IF_FAILED(container.Start(startFlags, &startOptions, warningCallback.Get())); //TODO: detach keys // Disable auto-delete only after successful start runningContainer.SetDeleteOnClose(false); cidFile.Commit(containerId); // Handle attach if requested - if (WI_IsFlagSet(startFlags, WSLCContainerStartFlagsAttach)) + if (attach) { - ConsoleService consoleService; - return consoleService.AttachToCurrentConsole(runningContainer.GetInitProcess()); + return ConsoleService::AttachToCurrentConsole(*console, runningContainer.GetInitProcess()); } PrintMessage(L"%hs", stdout, containerId); @@ -396,7 +413,18 @@ int ContainerService::Start(Session& session, const std::string& id, bool attach THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); WSLCContainerStartFlags flags = attach ? WSLCContainerStartFlagsAttach : WSLCContainerStartFlagsNone; auto warningCallback = Microsoft::WRL::Make(); - THROW_IF_FAILED_EXCEPT(container->Start(flags, nullptr, warningCallback.Get()), WSLC_E_CONTAINER_IS_RUNNING); + + std::optional console; + WSLCProcessStartOptions startOptions{}; + if (attach) + { + console.emplace(); + const auto size = console->GetWindowSize(); + startOptions.TtyRows = size.Y; + startOptions.TtyColumns = size.X; + } + + THROW_IF_FAILED_EXCEPT(container->Start(flags, &startOptions, warningCallback.Get()), WSLC_E_CONTAINER_IS_RUNNING); if (!attach) { @@ -410,8 +438,7 @@ int ContainerService::Start(Session& session, const std::string& id, bool attach THROW_IF_FAILED(process->GetFlags(&processFlags)); ClientRunningWSLCProcess runningProcess(std::move(process), processFlags); - ConsoleService consoleService; - return consoleService.AttachToCurrentConsole(std::move(runningProcess)); + return ConsoleService::AttachToCurrentConsole(*console, std::move(runningProcess)); } void ContainerService::Stop(Session& session, const std::string& id, StopContainerOptions options) @@ -492,6 +519,14 @@ int ContainerService::Exec(Session& session, const std::string& id, ContainerOpt WI_SetFlagIf(execFlags, WSLCProcessFlagsTty, options.TTY); auto processLauncher = wsl::windows::common::WSLCProcessLauncher({}, options.Arguments, options.EnvironmentVariables, execFlags); + + wsl::windows::common::ConsoleState console; + if (options.TTY) + { + const auto size = console.GetWindowSize(); + processLauncher.SetTtySize(size.Y, size.X); + } + if (options.User.has_value()) { auto user = options.User.value(); @@ -502,7 +537,7 @@ int ContainerService::Exec(Session& session, const std::string& id, ContainerOpt processLauncher.SetWorkingDirectory(std::move(options.WorkingDirectory)); } - return ConsoleService::AttachToCurrentConsole(processLauncher.Launch(*container)); + return ConsoleService::AttachToCurrentConsole(console, processLauncher.Launch(*container)); } InspectContainer ContainerService::Inspect(Session& session, const std::string& id) diff --git a/src/windows/wslc/services/SessionService.cpp b/src/windows/wslc/services/SessionService.cpp index 15371f1de..d382cfea6 100644 --- a/src/windows/wslc/services/SessionService.cpp +++ b/src/windows/wslc/services/SessionService.cpp @@ -57,6 +57,7 @@ int SessionService::Attach(const std::wstring& sessionName) // Configure console for interactive usage. wsl::windows::common::ConsoleState console{}; + console.SetInteractiveMode(); const auto windowSize = console.GetWindowSize(); const std::string shell = "/bin/sh"; @@ -142,7 +143,7 @@ int SessionService::Enter(const std::wstring& storagePath, const std::wstring& d const auto windowSize = console.GetWindowSize(); launcher.SetTtySize(windowSize.Y, windowSize.X); - return ConsoleService::AttachToCurrentConsole(launcher.Launch(*session.get())); + return ConsoleService::AttachToCurrentConsole(console, launcher.Launch(*session.get())); } std::vector SessionService::List() diff --git a/src/windows/wslcsession/ServiceProcessLauncher.cpp b/src/windows/wslcsession/ServiceProcessLauncher.cpp index 636a8622d..8798428c6 100644 --- a/src/windows/wslcsession/ServiceProcessLauncher.cpp +++ b/src/windows/wslcsession/ServiceProcessLauncher.cpp @@ -57,8 +57,9 @@ std::tuple> ServiceProcessLau int error = -1; std::optional process; - auto result = wil::ResultFromException( - [&]() { process.emplace(virtualMachine.CreateLinuxProcess(m_executable.c_str(), options, &error), m_flags); }); + auto result = wil::ResultFromException([&]() { + process.emplace(virtualMachine.CreateLinuxProcess(m_executable.c_str(), options, m_rows, m_columns, &error), m_flags); + }); return {result, error, std::move(process)}; } diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 4e6f96706..12ff92527 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -690,7 +690,7 @@ void WSLCContainerImpl::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* *Stderr = common::wslutil::ToCOMOutputHandle(reinterpret_cast(stderrRead.get()), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe); } -void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, LPCSTR DetachKeys) +void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions) { // Acquire an exclusive lock since this method modifies m_initProcessControl, m_initProcess and m_state. auto lock = m_lock.lock_exclusive(); @@ -704,6 +704,23 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, LPCSTR DetachKeys) m_id.c_str(), m_state); + std::optional detachKeys; + + if (StartOptions != nullptr) + { + detachKeys = StartOptions->DetachKeys != nullptr ? std::optional(StartOptions->DetachKeys) : std::nullopt; + + // A valid (non-zero) tty size is only required when attaching to a tty init process; detached starts (e.g. + // 'container run -d -t' or 'container start' without attach) legitimately have no host terminal to size from. + THROW_HR_IF_MSG( + E_INVALIDARG, + WI_IsFlagSet(Flags, WSLCContainerStartFlagsAttach) && WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty) && + (StartOptions->TtyColumns == 0 || StartOptions->TtyRows == 0), + "Invalid tty size: %lu:%lu", + StartOptions->TtyRows, + StartOptions->TtyColumns); + } + // Attach to the container's init process so no IO is lost. std::unique_ptr io; @@ -711,8 +728,6 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, LPCSTR DetachKeys) { if (WI_IsFlagSet(Flags, WSLCContainerStartFlagsAttach)) { - auto detachKeys = DetachKeys == nullptr ? std::nullopt : std::optional(DetachKeys); - if (WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty)) { io = std::make_unique(TypedHandle{ @@ -753,10 +768,20 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, LPCSTR DetachKeys) try { - m_dockerClient.StartContainer(m_id, DetachKeys == nullptr ? std::nullopt : std::optional(DetachKeys)); + m_dockerClient.StartContainer(m_id, detachKeys); } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to start container '%hs'", m_id.c_str()); + if (WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty) && StartOptions != nullptr && StartOptions->TtyRows != 0 && + StartOptions->TtyColumns != 0) + { + try + { + m_dockerClient.ResizeContainerTty(m_id, StartOptions->TtyRows, StartOptions->TtyColumns); + } + CATCH_LOG(); + } + auto inspectJson = InspectLockHeld(); const auto pluginResult = m_pluginNotifier->OnContainerStarted(inspectJson.c_str()); if (FAILED(pluginResult)) @@ -1072,7 +1097,7 @@ void WSLCContainerImpl::GetInitProcess(IWSLCProcess** Process) const THROW_IF_FAILED(m_initProcess.CopyTo(__uuidof(IWSLCProcess), (void**)Process)); } -void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, LPCSTR DetachKeys, IWSLCProcess** Process) +void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, const WSLCProcessStartOptions* StartOptions, IWSLCProcess** Process) { THROW_HR_IF_MSG(E_INVALIDARG, Options->CommandLine.Count == 0, "Exec command line cannot be empty"); @@ -1080,6 +1105,14 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, LPCSTR DetachKey THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_NOT_RUNNING, Localization::MessageWslcContainerNotRunning(m_id), m_state != WslcContainerStateRunning); + THROW_HR_IF_MSG( + E_INVALIDARG, + WI_IsFlagSet(Options->Flags, WSLCProcessFlagsTty) && StartOptions != nullptr && + (StartOptions->TtyRows == 0 || StartOptions->TtyColumns == 0), + "Invalid tty size: %lu:%lu", + StartOptions->TtyRows, + StartOptions->TtyColumns); + common::docker_schema::CreateExec request{}; request.AttachStdout = true; request.AttachStderr = true; @@ -1100,6 +1133,11 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, LPCSTR DetachKey if (WI_IsFlagSet(Options->Flags, WSLCProcessFlagsTty)) { request.Tty = true; + + if (StartOptions != nullptr) + { + request.ConsoleSize = {StartOptions->TtyRows, StartOptions->TtyColumns}; + } } if (WI_IsFlagSet(Options->Flags, WSLCProcessFlagsStdin)) @@ -1107,9 +1145,9 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, LPCSTR DetachKey request.AttachStdin = true; } - if (DetachKeys != nullptr) + if (StartOptions != nullptr && StartOptions->DetachKeys != nullptr) { - request.DetachKeys = DetachKeys; + request.DetachKeys = StartOptions->DetachKeys; } try @@ -2114,12 +2152,12 @@ HRESULT WSLCContainer::GetInitProcess(IWSLCProcess** Process) return hr; } -HRESULT WSLCContainer::Exec(const WSLCProcessOptions* Options, LPCSTR DetachKeys, IWSLCProcess** Process) +HRESULT WSLCContainer::Exec(const WSLCProcessOptions* Options, const WSLCProcessStartOptions* StartOptions, IWSLCProcess** Process) { WSLCExecutionContext context(&m_session); *Process = nullptr; - return CallImpl(&WSLCContainerImpl::Exec, Options, DetachKeys, Process); + return CallImpl(&WSLCContainerImpl::Exec, Options, StartOptions, Process); } HRESULT WSLCContainer::Stop(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds) @@ -2136,14 +2174,14 @@ HRESULT WSLCContainer::Kill(_In_ WSLCSignal Signal) return CallImpl(&WSLCContainerImpl::Stop, Signal, {}, true); } -HRESULT WSLCContainer::Start(WSLCContainerStartFlags Flags, LPCSTR DetachKeys, IWarningCallback* WarningCallback) +HRESULT WSLCContainer::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, IWarningCallback* WarningCallback) try { WSLCExecutionContext context(&m_session, WarningCallback); THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCContainerStartFlagsValid), "Invalid flags: 0x%x", Flags); - return CallImpl(&WSLCContainerImpl::Start, Flags, DetachKeys); + return CallImpl(&WSLCContainerImpl::Start, Flags, StartOptions); } CATCH_RETURN(); diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index 37dcc3c5f..0217fc437 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -91,7 +91,7 @@ class WSLCContainerImpl ~WSLCContainerImpl(); - void Start(WSLCContainerStartFlags Flags, LPCSTR DetachKeys); + void Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions); void Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* Stdout, WSLCHandle* Stderr) const; void Stop(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds, bool Kill); void Delete(WSLCDeleteFlags Flags); @@ -100,7 +100,7 @@ class WSLCContainerImpl void GetCreatedAt(_Out_ ULONGLONG* CreatedAt); void GetState(_Out_ WSLCContainerState* State); void GetInitProcess(_Out_ IWSLCProcess** process) const; - void Exec(_In_ const WSLCProcessOptions* Options, LPCSTR DetachKeys, _Out_ IWSLCProcess** Process); + void Exec(_In_ const WSLCProcessOptions* Options, const WSLCProcessStartOptions* StartOptions, _Out_ IWSLCProcess** Process); void Inspect(LPSTR* Output) const; void Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) const; void Stats(LPSTR* Output) const; @@ -229,8 +229,8 @@ class DECLSPEC_UUID("B1F1C4E3-C225-4CAE-AD8A-34C004DE1AE4") WSLCContainer IFACEMETHOD(Export)(_In_ WSLCHandle TarHandle) override; IFACEMETHOD(GetState)(_Out_ WSLCContainerState* State) override; IFACEMETHOD(GetInitProcess)(_Out_ IWSLCProcess** process) override; - IFACEMETHOD(Exec)(_In_ const WSLCProcessOptions* Options, _In_opt_ LPCSTR DetachKeys, _Out_ IWSLCProcess** Process) override; - IFACEMETHOD(Start)(WSLCContainerStartFlags Flags, _In_opt_ LPCSTR DetachKeys, _In_opt_ IWarningCallback* WarningCallback) override; + IFACEMETHOD(Exec)(_In_ const WSLCProcessOptions* Options, _In_opt_ const WSLCProcessStartOptions* StartOptions, _Out_ IWSLCProcess** Process) override; + IFACEMETHOD(Start)(WSLCContainerStartFlags Flags, _In_opt_ const WSLCProcessStartOptions* StartOptions, _In_opt_ IWarningCallback* WarningCallback) override; IFACEMETHOD(Inspect)(_Out_ LPSTR* Output) override; IFACEMETHOD(Logs)(_In_ WSLCLogsFlags Flags, _Out_ WSLCHandle* Stdout, _Out_ WSLCHandle* Stderr, _In_ ULONGLONG Since, _In_ ULONGLONG Until, _In_ ULONGLONG Tail) override; IFACEMETHOD(GetId)(_Out_ WSLCContainerId Id) override; diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 1d7b81210..21eaf2d53 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -1941,7 +1941,8 @@ try } CATCH_RETURN(); -HRESULT WSLCSession::CreateRootNamespaceProcess(LPCSTR Executable, const WSLCProcessOptions* Options, IWSLCProcess** Process, int* Errno) +HRESULT WSLCSession::CreateRootNamespaceProcess( + LPCSTR Executable, const WSLCProcessOptions* Options, ULONG TtyRows, ULONG TtyColumns, IWSLCProcess** Process, int* Errno) try { WSLCExecutionContext context(this); @@ -1954,7 +1955,7 @@ try auto lock = m_lock.lock_shared(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); - auto process = m_virtualMachine->CreateLinuxProcess(Executable, *Options, Errno); + auto process = m_virtualMachine->CreateLinuxProcess(Executable, *Options, TtyRows, TtyColumns, Errno); THROW_IF_FAILED(process.CopyTo(Process)); return S_OK; diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index 5bf6d7029..33e203bf5 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -139,7 +139,12 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession // VM management. IFACEMETHOD(CreateRootNamespaceProcess)( - _In_ LPCSTR Executable, _In_ const WSLCProcessOptions* Options, _Out_ IWSLCProcess** VirtualMachine, _Out_ int* Errno) override; + _In_ LPCSTR Executable, + _In_ const WSLCProcessOptions* Options, + _In_ ULONG TtyRows, + _In_ ULONG TtyColumns, + _Out_ IWSLCProcess** VirtualMachine, + _Out_ int* Errno) override; // Disk management. IFACEMETHOD(FormatVirtualDisk)(_In_ LPCWSTR Path) override; diff --git a/src/windows/wslcsession/WSLCVirtualMachine.cpp b/src/windows/wslcsession/WSLCVirtualMachine.cpp index ba7748de0..fc027d0a8 100644 --- a/src/windows/wslcsession/WSLCVirtualMachine.cpp +++ b/src/windows/wslcsession/WSLCVirtualMachine.cpp @@ -386,7 +386,7 @@ void WSLCVirtualMachine::ConfigureNetworking() options.CommandLine = {.Values = cmd.data(), .Count = static_cast(cmd.size())}; }; - auto process = CreateLinuxProcessImpl("/init", options, fds, nullptr, prepareCommandLine); + auto process = CreateLinuxProcessImpl("/init", options, fds, 0, 0, nullptr, prepareCommandLine); // Call back to the service to configure the networking engine. auto gnsHandle = process->GetStdHandle(gnsChannelFd); @@ -639,7 +639,7 @@ std::string WSLCVirtualMachine::GetVhdDevicePath(ULONG Lun) } Microsoft::WRL::ComPtr WSLCVirtualMachine::CreateLinuxProcess( - _In_ LPCSTR Executable, _In_ const WSLCProcessOptions& Options, int* Errno, const TPrepareCommandLine& PrepareCommandLine) + _In_ LPCSTR Executable, _In_ const WSLCProcessOptions& Options, ULONG TtyRows, ULONG TtyColumns, int* Errno, const TPrepareCommandLine& PrepareCommandLine) { // Check if this is a tty or not std::vector fds; @@ -659,11 +659,11 @@ Microsoft::WRL::ComPtr WSLCVirtualMachine::CreateLinuxProcess( fds.emplace_back(WSLCProcessFd{.Fd = WSLCFDStderr, .Type = WSLCFdType::WSLCFdTypeDefault}); } - return CreateLinuxProcessImpl(Executable, Options, fds, Errno, PrepareCommandLine); + return CreateLinuxProcessImpl(Executable, Options, fds, TtyRows, TtyColumns, Errno, PrepareCommandLine); } Microsoft::WRL::ComPtr WSLCVirtualMachine::CreateLinuxProcessImpl( - LPCSTR Executable, const WSLCProcessOptions& Options, const std::vector& Fds, int* Errno, const TPrepareCommandLine& PrepareCommandLine) + LPCSTR Executable, const WSLCProcessOptions& Options, const std::vector& Fds, ULONG TtyRows, ULONG TtyColumns, int* Errno, const TPrepareCommandLine& PrepareCommandLine) { // N.B This check is there to prevent processes from being started before the VM is done initializing. // to avoid potential deadlocks, since the processExitThread is required to signal the process exit events. @@ -729,7 +729,7 @@ Microsoft::WRL::ComPtr WSLCVirtualMachine::CreateLinuxProcessImpl( // If this is an interactive tty, we need a relay process if (tty != nullptr) { - auto [grandChildPid, ptyMaster, grandChildChannel] = Fork(childChannel, WSLC_FORK::Pty, Options.TtyRows, Options.TtyColumns); + auto [grandChildPid, ptyMaster, grandChildChannel] = Fork(childChannel, WSLC_FORK::Pty, TtyRows, TtyColumns); WSLC_TTY_RELAY relayMessage{}; relayMessage.TtyMaster = ptyMaster; relayMessage.Socket = tty->Fd; diff --git a/src/windows/wslcsession/WSLCVirtualMachine.h b/src/windows/wslcsession/WSLCVirtualMachine.h index a38588a74..1c2aed268 100644 --- a/src/windows/wslcsession/WSLCVirtualMachine.h +++ b/src/windows/wslcsession/WSLCVirtualMachine.h @@ -144,6 +144,8 @@ class WSLCVirtualMachine Microsoft::WRL::ComPtr CreateLinuxProcess( _In_ LPCSTR Executable, _In_ const WSLCProcessOptions& Options, + _In_ ULONG TtyRows = 0, + _In_ ULONG TtyColumns = 0, int* Errno = nullptr, const TPrepareCommandLine& PrepareCommandLine = [](const auto&) {}); @@ -187,6 +189,8 @@ class WSLCVirtualMachine _In_ LPCSTR Executable, _In_ const WSLCProcessOptions& Options, _In_ const std::vector& Fds = {}, + _In_ ULONG TtyRows = 0, + _In_ ULONG TtyColumns = 0, int* Errno = nullptr, const TPrepareCommandLine& PrepareCommandLine = [](const auto&) {}); diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index ac98ce4df..26f8291b2 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -8147,6 +8147,7 @@ class WSLCTests { WSLCContainerLauncher launcher( "debian:latest", "logs-test-5", {"/bin/bash", "-c", "stat -f /dev/stdin | grep -io 'Type:.*$'"}, {}, {}, WSLCProcessFlagsStdin | WSLCProcessFlagsTty); + launcher.SetTtySize(24, 80); auto container = launcher.Launch(*m_defaultSession); auto initProcess = container.GetInitProcess(); @@ -8543,6 +8544,7 @@ class WSLCTests // Validate behavior for tty containers { WSLCContainerLauncher launcher("debian:latest", "attach-test-3", {"/bin/bash"}, {}, {}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin); + launcher.SetTtySize(24, 80); auto container = launcher.Launch(*m_defaultSession); auto process = container.GetInitProcess(); @@ -8603,6 +8605,71 @@ class WSLCTests } } + WSLC_TEST_METHOD(TtySize) + { + constexpr ULONG c_rows = 43; + constexpr ULONG c_columns = 42; + const std::string expectedSize = "43 42"; + + // Container init process. + { + WSLCContainerLauncher launcher( + "debian:latest", "tty-size-init", {"/bin/sh", "-c", "while true; do stty size; sleep 1; done"}, {}, {}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin); + launcher.SetTtySize(c_rows, c_columns); + + auto container = launcher.Launch(*m_defaultSession); + auto process = container.GetInitProcess(); + auto tty = process.GetStdHandle(WSLCFDTty); + + // Wait for the size to be reflected in a loop, since the tty size is applied asynchronously. + PartialHandleRead reader(tty.get()); + wsl::shared::retry::RetryWithTimeout( + [&]() { THROW_HR_IF(E_ABORT, reader.GetData().find(expectedSize) == std::string::npos); }, + std::chrono::milliseconds(100), + std::chrono::seconds(60)); + } + + // Exec process. + { + WSLCContainerLauncher launcher("debian:latest", "tty-size-exec", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin); + auto container = launcher.Launch(*m_defaultSession); + + WSLCProcessLauncher execLauncher({}, {"/usr/bin/stty", "size"}, {}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin); + execLauncher.SetTtySize(c_rows, c_columns); + + auto process = execLauncher.Launch(container.Get()); + + ValidateProcessOutput(process, {{1, expectedSize + "\n"}}); + } + } + + WSLC_TEST_METHOD(ProcessInvalidTtySize) + { + // A tty process created with an explicit zero size (0 rows / 0 columns) is invalid and must be rejected with + // E_INVALIDARG, both for the container init process (attach) and for an exec'd process. + + // Container init process: attaching to a tty container with a (0, 0) size is rejected. + { + WSLCContainerLauncher launcher("debian:latest", "invalid-tty-size-init", {"/bin/sh"}, {}, {}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin); + + // N.B. SetTtySize is intentionally not called, so the launcher passes a (0, 0) size. + auto [result, container] = launcher.LaunchNoThrow(*m_defaultSession); + VERIFY_ARE_EQUAL(result, E_INVALIDARG); + } + + // Exec process: exec'ing a tty process with a (0, 0) size is rejected. + { + WSLCContainerLauncher launcher("debian:latest", "invalid-tty-size-exec", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin); + auto container = launcher.Launch(*m_defaultSession); + + WSLCProcessLauncher execLauncher({}, {"/bin/sh", "-c", "stty size"}, {}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin); + + // N.B. SetTtySize is intentionally not called, so the launcher passes a (0, 0) size. + auto [result, process] = execLauncher.LaunchNoThrow(container.Get()); + VERIFY_ARE_EQUAL(result, E_INVALIDARG); + } + } + WSLC_TEST_METHOD(ContainerStats_RunningContainer) { // Start a long-lived detached container on a bridged network so network stats are populated. @@ -9196,7 +9263,12 @@ class WSLCTests WSLCContainerLauncher launcher("debian:latest", "test-detach", {"sleep", "9999999"}, {}, {}, WSLCProcessFlagsStdin | WSLCProcessFlagsTty); auto container = launcher.Create(*m_defaultSession); - VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsAttach, DetachKeys, nullptr)); + + WSLCProcessStartOptions startOptions{}; + startOptions.TtyRows = 24; + startOptions.TtyColumns = 80; + startOptions.DetachKeys = DetachKeys; + VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsAttach, &startOptions, nullptr)); auto initProcess = container.GetInitProcess(); @@ -9219,6 +9291,7 @@ class WSLCTests // Validate detaching from an exec'd process. { WSLCProcessLauncher processLauncher({}, {"sleep", "9999999"}, {}, WSLCProcessFlagsStdin | WSLCProcessFlagsTty); + processLauncher.SetTtySize(24, 80); if (DetachKeys != nullptr) { @@ -9247,7 +9320,11 @@ class WSLCTests WSLCContainerLauncher launcher("debian:latest", "test-detach", {"cat"}, {}, {}, WSLCProcessFlagsStdin | WSLCProcessFlagsTty); auto container = launcher.Create(*m_defaultSession); - VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsAttach, "invalid", nullptr), E_INVALIDARG); + WSLCProcessStartOptions invalidDetachOptions{}; + invalidDetachOptions.TtyRows = 24; + invalidDetachOptions.TtyColumns = 80; + invalidDetachOptions.DetachKeys = "invalid"; + VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsAttach, &invalidDetachOptions, nullptr), E_INVALIDARG); VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsNone, nullptr, nullptr)); @@ -9255,6 +9332,7 @@ class WSLCTests VERIFY_ARE_EQUAL(container.Get().Attach("invalid", &unusedHandle, &unusedHandle, &unusedHandle), E_INVALIDARG); WSLCProcessLauncher processLauncher({}, {"cat"}, {}, WSLCProcessFlagsStdin | WSLCProcessFlagsTty); + processLauncher.SetTtySize(24, 80); processLauncher.SetDetachKeys("invalid"); // N.B. Docker returns HTTP 500 if the detach keys are invalid, but unlike other cases there's a proper error message. From e9c197c29ce45047d2ea5de2f6d7311689bcfebc Mon Sep 17 00:00:00 2001 From: Blue Date: Fri, 5 Jun 2026 14:04:19 -0700 Subject: [PATCH 2/5] Save state --- test/windows/WSLCTests.cpp | 2 +- test/windows/wslc/e2e/WSLCExecutor.cpp | 89 ++++++++++++++++++++++---- test/windows/wslc/e2e/WSLCExecutor.h | 36 ++++++++++- 3 files changed, 112 insertions(+), 15 deletions(-) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 26f8291b2..568e3f57a 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -8639,7 +8639,7 @@ class WSLCTests auto process = execLauncher.Launch(container.Get()); - ValidateProcessOutput(process, {{1, expectedSize + "\n"}}); + ValidateProcessOutput(process, {{WSLCFDTty, expectedSize + "\r\n"}}); } } diff --git a/test/windows/wslc/e2e/WSLCExecutor.cpp b/test/windows/wslc/e2e/WSLCExecutor.cpp index 4a2166b21..f5e592490 100644 --- a/test/windows/wslc/e2e/WSLCExecutor.cpp +++ b/test/windows/wslc/e2e/WSLCExecutor.cpp @@ -235,20 +235,43 @@ std::wstring GetWslcHeader() return header.str(); } -WSLCInteractiveSession RunWslcInteractive(const std::wstring& commandLine, ElevationType elevationType) +WSLCInteractiveSession RunWslcInteractive(const std::wstring& commandLine, ElevationType elevationType, std::optional pseudoConsole) { auto cmd = L"\"" + GetWslcPath() + L"\" " + commandLine; - auto [childStdinRead, parentStdinWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(65536, false, true); - auto [parentStdoutRead, childStdoutWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(65536, true, false); - auto [parentStderrRead, childStderrWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(65536, true, false); + wsl::windows::common::SubProcess process(nullptr, cmd.c_str()); - THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStdinRead.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); - THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStdoutWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); - THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStderrWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); + wil::unique_hfile parentStdinWrite; + wil::unique_hfile parentStdoutRead; + wil::unique_hfile parentStderrRead; + wsl::windows::common::helpers::unique_pseudo_console console; - wsl::windows::common::SubProcess process(nullptr, cmd.c_str()); - process.SetStdHandles(childStdinRead.get(), childStdoutWrite.get(), childStderrWrite.get()); + // Child handles for the pipe-based path; must stay alive until the process has started. + wil::unique_hfile childStdinRead; + wil::unique_hfile childStdoutWrite; + wil::unique_hfile childStderrWrite; + + if (pseudoConsole.has_value()) + { + // Pseudoconsole mode: wslc.exe is attached to a ConPTY, so stdin/stdout/stderr are + // multiplexed onto the conpty's single output stream and there is no separate stderr. + process.SetPseudoConsole(pseudoConsole->Handle.get()); + parentStdinWrite = std::move(pseudoConsole->InputWrite); + parentStdoutRead = std::move(pseudoConsole->OutputRead); + console = std::move(pseudoConsole->Handle); + } + else + { + std::tie(childStdinRead, parentStdinWrite) = wsl::windows::common::wslutil::OpenAnonymousPipe(65536, false, true); + std::tie(parentStdoutRead, childStdoutWrite) = wsl::windows::common::wslutil::OpenAnonymousPipe(65536, true, false); + std::tie(parentStderrRead, childStderrWrite) = wsl::windows::common::wslutil::OpenAnonymousPipe(65536, true, false); + + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStdinRead.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStdoutWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStderrWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); + + process.SetStdHandles(childStdinRead.get(), childStdoutWrite.get(), childStderrWrite.get()); + } wil::unique_handle nonElevatedToken; if (elevationType == ElevationType::NonElevated) @@ -269,7 +292,28 @@ WSLCInteractiveSession RunWslcInteractive(const std::wstring& commandLine, Eleva std::move(parentStdoutRead), std::move(parentStderrRead), std::move(processHandle), - std::move(nonElevatedToken)); // Transfer token ownership to the session + std::move(nonElevatedToken), // Transfer token ownership to the session + std::move(console)); +} + +PseudoConsole::PseudoConsole(SHORT columns, SHORT rows) +{ + // Read end of the input pipe is handed to the conpty (it reads what would be written to stdin); + // the write end is overlapped so WSLCInteractiveSession::Write (which uses OVERLAPPED I/O) works. + auto [inputRead, inputWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(0, false, true); + + // Read end of the output pipe must be overlapped because PartialHandleRead / InterruptableRead + // use OVERLAPPED I/O with an event-based cancellation pattern. + auto [outputRead, outputWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(0, true, false); + + HPCON rawPseudoConsole{}; + THROW_IF_FAILED(::CreatePseudoConsole(COORD{columns, rows}, inputRead.get(), outputWrite.get(), 0, &rawPseudoConsole)); + Handle.reset(rawPseudoConsole); + + // ConPTY duplicates the handles internally; release the local references now so that EOF + // propagates correctly once ClosePseudoConsole runs. + InputWrite = std::move(inputWrite); + OutputRead = std::move(outputRead); } // WSLCInteractiveSession implementation @@ -280,16 +324,24 @@ WSLCInteractiveSession::WSLCInteractiveSession( wil::unique_hfile stdoutRead, wil::unique_hfile stderrRead, wil::unique_handle processHandle, - wil::unique_handle nonElevatedToken) : + wil::unique_handle nonElevatedToken, + wsl::windows::common::helpers::unique_pseudo_console pseudoConsole) : CommandLine(std::move(commandLine)), m_stdinWrite(std::move(stdinWrite)), m_stdoutRead(std::move(stdoutRead)), m_stderrRead(std::move(stderrRead)), + m_pseudoConsole(std::move(pseudoConsole)), m_processHandle(std::move(processHandle)), m_nonElevatedToken(std::move(nonElevatedToken)) { m_stdoutReader = std::make_unique(m_stdoutRead.get()); - m_stderrReader = std::make_unique(m_stderrRead.get()); + + // In pseudoconsole mode stderr is multiplexed onto the conpty output, so there is no + // separate stderr handle to read from. + if (m_stderrRead.is_valid()) + { + m_stderrReader = std::make_unique(m_stderrRead.get()); + } } WSLCInteractiveSession::~WSLCInteractiveSession() @@ -317,8 +369,20 @@ void WSLCInteractiveSession::ExpectStdout(const std::string& expected) m_stdoutReader->ExpectConsume(expected); } +std::string WSLCInteractiveSession::GetStdoutData() const +{ + return m_stdoutReader->GetData(); +} + +void WSLCInteractiveSession::ResizePseudoConsole(SHORT columns, SHORT rows) +{ + VERIFY_IS_TRUE(static_cast(m_pseudoConsole), L"ResizePseudoConsole requires a pseudoconsole-backed session"); + THROW_IF_FAILED(::ResizePseudoConsole(m_pseudoConsole.get(), COORD{columns, rows})); +} + void WSLCInteractiveSession::ExpectStderr(const std::string& expected) { + VERIFY_IS_NOT_NULL(m_stderrReader.get(), L"ExpectStderr is not supported for pseudoconsole-backed sessions (stderr is merged into stdout)"); Log::Comment(std::format(L"Expecting stderr: \"{}\"", wsl::shared::string::MultiByteToWide(EscapeString(expected))).c_str()); m_stderrReader->ExpectConsume(expected); } @@ -434,6 +498,7 @@ bool WSLCInteractiveSession::Terminate(UINT exitCode) void WSLCInteractiveSession::VerifyNoErrors() { + VERIFY_IS_NOT_NULL(m_stderrReader.get(), L"VerifyNoErrors is not supported for pseudoconsole-backed sessions (stderr is merged into stdout)"); m_stderrReader->ExpectClosed(DefaultWaitTimeoutMs); // Verify that stderr was actually empty - not just closed diff --git a/test/windows/wslc/e2e/WSLCExecutor.h b/test/windows/wslc/e2e/WSLCExecutor.h index 98cc45de9..f83e3d231 100644 --- a/test/windows/wslc/e2e/WSLCExecutor.h +++ b/test/windows/wslc/e2e/WSLCExecutor.h @@ -47,6 +47,24 @@ struct WSLCExecutionResult bool StdoutContainsSubstring(const std::wstring& substring) const; }; +// RAII wrapper around a Windows ConPTY pseudoconsole together with the input-write and +// output-read pipe ends the host uses to drive it. Construct one with the desired initial +// size and hand it to RunWslcInteractive to attach wslc.exe to a real pseudoterminal instead +// of plain pipes. Ownership of the conpty and pipes is transferred into the resulting +// WSLCInteractiveSession, which exposes ResizePseudoConsole() and reads the combined output +// stream via the normal stdout reader. +struct PseudoConsole +{ + PseudoConsole(SHORT columns, SHORT rows); + + NON_COPYABLE(PseudoConsole); + DEFAULT_MOVABLE(PseudoConsole); + + wil::unique_hfile InputWrite; + wil::unique_hfile OutputRead; + wsl::windows::common::helpers::unique_pseudo_console Handle; +}; + // Interactive session for testing wslc commands that require stdin/stdout interaction. // Uses PartialHandleRead for race-free output validation struct WSLCInteractiveSession @@ -57,7 +75,8 @@ struct WSLCInteractiveSession wil::unique_hfile stdoutRead, wil::unique_hfile stderrRead, wil::unique_handle processHandle, - wil::unique_handle nonElevatedToken = wil::unique_handle{}); + wil::unique_handle nonElevatedToken = wil::unique_handle{}, + wsl::windows::common::helpers::unique_pseudo_console pseudoConsole = {}); ~WSLCInteractiveSession(); // Non-copyable, non-movable @@ -74,6 +93,13 @@ struct WSLCInteractiveSession void ExpectStderr(const std::string& expected); void ExpectCommandEcho(const std::string& command); + // Returns a snapshot of everything read from stdout so far (in pseudoconsole mode this is the + // combined output stream). Non-consuming, unlike ExpectStdout. + std::string GetStdoutData() const; + + // Resizes the attached pseudoconsole. Only valid for sessions created with a PseudoConsole. + void ResizePseudoConsole(SHORT columns, SHORT rows); + bool IsRunning() const; void CloseStdin(); std::optional GetExitCode() const; @@ -88,6 +114,9 @@ struct WSLCInteractiveSession wil::unique_hfile m_stdinWrite; wil::unique_hfile m_stdoutRead; wil::unique_hfile m_stderrRead; + // Destroyed (ClosePseudoConsole) after the readers are stopped but before the read pipes are + // closed, so the conpty is torn down in the right order. Null for pipe-based sessions. + wsl::windows::common::helpers::unique_pseudo_console m_pseudoConsole; wil::unique_handle m_processHandle; wil::unique_handle m_nonElevatedToken; // Keep token alive for the lifetime of the session std::unique_ptr m_stdoutReader; @@ -102,6 +131,9 @@ WSLCExecutionResult RunWslcAndRedirectToFile( void RunWslcAndVerify(const std::wstring& cmd, const WSLCExecutionResult& expected, ElevationType elevationType = ElevationType::Elevated); std::wstring GetWslcHeader(); -WSLCInteractiveSession RunWslcInteractive(const std::wstring& commandLine, ElevationType elevationType = ElevationType::Elevated); +WSLCInteractiveSession RunWslcInteractive( + const std::wstring& commandLine, + ElevationType elevationType = ElevationType::Elevated, + std::optional pseudoConsole = std::nullopt); } // namespace WSLCE2ETests From 4365d000e0eed82896026943b68675d1cc749cf8 Mon Sep 17 00:00:00 2001 From: Blue Date: Fri, 5 Jun 2026 14:26:33 -0700 Subject: [PATCH 3/5] Prepare for PR --- src/windows/common/ConsoleState.cpp | 1 - src/windows/wslc/services/ConsoleService.cpp | 6 +-- .../wslc/services/ContainerService.cpp | 2 +- src/windows/wslcsession/WSLCContainer.cpp | 5 +- test/windows/WSLCTests.cpp | 52 ++++++++----------- .../wslc/e2e/WSLCE2EContainerExecTests.cpp | 16 ++++++ .../wslc/e2e/WSLCE2EContainerRunTests.cpp | 15 ++++++ test/windows/wslc/e2e/WSLCE2EHelpers.cpp | 43 +++++++++++++++ test/windows/wslc/e2e/WSLCE2EHelpers.h | 2 + test/windows/wslc/e2e/WSLCExecutor.cpp | 13 ++--- test/windows/wslc/e2e/WSLCExecutor.h | 14 ++--- 11 files changed, 108 insertions(+), 61 deletions(-) diff --git a/src/windows/common/ConsoleState.cpp b/src/windows/common/ConsoleState.cpp index 4bf7b655f..5f3e6144c 100644 --- a/src/windows/common/ConsoleState.cpp +++ b/src/windows/common/ConsoleState.cpp @@ -83,7 +83,6 @@ ConsoleState::ConsoleState() void ConsoleState::SetInteractiveMode() { - // Configuring twice would overwrite the saved original state with the already-modified state. if (m_interactiveModeConfigured) { return; diff --git a/src/windows/wslc/services/ConsoleService.cpp b/src/windows/wslc/services/ConsoleService.cpp index e8e3e2023..c10673b3a 100644 --- a/src/windows/wslc/services/ConsoleService.cpp +++ b/src/windows/wslc/services/ConsoleService.cpp @@ -21,8 +21,7 @@ using wsl::windows::common::ClientRunningWSLCProcess; using wsl::windows::common::io::ReadHandle; using wsl::windows::common::io::RelayHandle; -bool ConsoleService::RelayInteractiveTty( - wsl::windows::common::ConsoleState& console, ClientRunningWSLCProcess& Process, HANDLE Tty, bool triggerRefresh) +bool ConsoleService::RelayInteractiveTty(wsl::windows::common::ConsoleState& console, ClientRunningWSLCProcess& Process, HANDLE Tty, bool triggerRefresh) { // Configure the console for interactive usage. console.SetInteractiveMode(); @@ -109,8 +108,7 @@ void ConsoleService::RelayNonTtyProcess(wil::unique_handle&& Stdin, wil::unique_ io.Run({}); } -int ConsoleService::AttachToCurrentConsole( - wsl::windows::common::ConsoleState& console, wsl::windows::common::ClientRunningWSLCProcess&& process) +int ConsoleService::AttachToCurrentConsole(wsl::windows::common::ConsoleState& console, wsl::windows::common::ClientRunningWSLCProcess&& process) { if (WI_IsFlagSet(process.Flags(), WSLCProcessFlagsTty)) { diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index f91685238..2f125965c 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -378,7 +378,7 @@ int ContainerService::Run(Session& session, const std::string& image, ContainerO } } - THROW_IF_FAILED(container.Start(startFlags, &startOptions, warningCallback.Get())); //TODO: detach keys + THROW_IF_FAILED(container.Start(startFlags, &startOptions, warningCallback.Get())); // TODO: detach keys // Disable auto-delete only after successful start runningContainer.SetDeleteOnClose(false); diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 12ff92527..d399db68d 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -710,8 +710,6 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt { detachKeys = StartOptions->DetachKeys != nullptr ? std::optional(StartOptions->DetachKeys) : std::nullopt; - // A valid (non-zero) tty size is only required when attaching to a tty init process; detached starts (e.g. - // 'container run -d -t' or 'container start' without attach) legitimately have no host terminal to size from. THROW_HR_IF_MSG( E_INVALIDARG, WI_IsFlagSet(Flags, WSLCContainerStartFlagsAttach) && WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty) && @@ -772,8 +770,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt } CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to start container '%hs'", m_id.c_str()); - if (WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty) && StartOptions != nullptr && StartOptions->TtyRows != 0 && - StartOptions->TtyColumns != 0) + if (WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty) && StartOptions != nullptr) { try { diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 568e3f57a..2de422b3e 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -5304,6 +5304,15 @@ class WSLCTests VERIFY_SUCCEEDED(m_defaultSession->CreateContainer(&options, nullptr, &container)); VERIFY_SUCCEEDED(container->Delete(WSLCDeleteFlagsNone)); } + + // Validate that invalid tty sizes are rejected. + { + WSLCContainerLauncher launcher("debian:latest", "invalid-tty-size-init", {"/bin/sh"}, {}, {}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin); + launcher.SetTtySize(0, 0); + + auto [result, container] = launcher.LaunchNoThrow(*m_defaultSession); + VERIFY_ARE_EQUAL(result, E_INVALIDARG); + } } WSLC_TEST_METHOD(ContainerStartAfterStop) @@ -6804,6 +6813,18 @@ class WSLCTests VERIFY_ARE_EQUAL(result, WSLC_E_CONTAINER_NOT_RUNNING); ValidateCOMErrorMessage(std::format(L"Container '{}' is not running.", id)); } + + // Validate that invalid tty sizes are rejected. + { + WSLCContainerLauncher launcher("debian:latest", "invalid-tty-size-exec", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin); + auto container = launcher.Launch(*m_defaultSession); + + WSLCProcessLauncher execLauncher({}, {"/bin/sh", "-c", "stty size"}, {}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin); + + // N.B. SetTtySize is intentionally not called, so the launcher passes a (0, 0) size. + auto [result, process] = execLauncher.LaunchNoThrow(container.Get()); + VERIFY_ARE_EQUAL(result, E_INVALIDARG); + } } WSLC_TEST_METHOD(ExecContainerDelete) @@ -8147,7 +8168,6 @@ class WSLCTests { WSLCContainerLauncher launcher( "debian:latest", "logs-test-5", {"/bin/bash", "-c", "stat -f /dev/stdin | grep -io 'Type:.*$'"}, {}, {}, WSLCProcessFlagsStdin | WSLCProcessFlagsTty); - launcher.SetTtySize(24, 80); auto container = launcher.Launch(*m_defaultSession); auto initProcess = container.GetInitProcess(); @@ -8544,7 +8564,6 @@ class WSLCTests // Validate behavior for tty containers { WSLCContainerLauncher launcher("debian:latest", "attach-test-3", {"/bin/bash"}, {}, {}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin); - launcher.SetTtySize(24, 80); auto container = launcher.Launch(*m_defaultSession); auto process = container.GetInitProcess(); @@ -8643,33 +8662,6 @@ class WSLCTests } } - WSLC_TEST_METHOD(ProcessInvalidTtySize) - { - // A tty process created with an explicit zero size (0 rows / 0 columns) is invalid and must be rejected with - // E_INVALIDARG, both for the container init process (attach) and for an exec'd process. - - // Container init process: attaching to a tty container with a (0, 0) size is rejected. - { - WSLCContainerLauncher launcher("debian:latest", "invalid-tty-size-init", {"/bin/sh"}, {}, {}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin); - - // N.B. SetTtySize is intentionally not called, so the launcher passes a (0, 0) size. - auto [result, container] = launcher.LaunchNoThrow(*m_defaultSession); - VERIFY_ARE_EQUAL(result, E_INVALIDARG); - } - - // Exec process: exec'ing a tty process with a (0, 0) size is rejected. - { - WSLCContainerLauncher launcher("debian:latest", "invalid-tty-size-exec", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin); - auto container = launcher.Launch(*m_defaultSession); - - WSLCProcessLauncher execLauncher({}, {"/bin/sh", "-c", "stty size"}, {}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin); - - // N.B. SetTtySize is intentionally not called, so the launcher passes a (0, 0) size. - auto [result, process] = execLauncher.LaunchNoThrow(container.Get()); - VERIFY_ARE_EQUAL(result, E_INVALIDARG); - } - } - WSLC_TEST_METHOD(ContainerStats_RunningContainer) { // Start a long-lived detached container on a bridged network so network stats are populated. @@ -9291,7 +9283,6 @@ class WSLCTests // Validate detaching from an exec'd process. { WSLCProcessLauncher processLauncher({}, {"sleep", "9999999"}, {}, WSLCProcessFlagsStdin | WSLCProcessFlagsTty); - processLauncher.SetTtySize(24, 80); if (DetachKeys != nullptr) { @@ -9332,7 +9323,6 @@ class WSLCTests VERIFY_ARE_EQUAL(container.Get().Attach("invalid", &unusedHandle, &unusedHandle, &unusedHandle), E_INVALIDARG); WSLCProcessLauncher processLauncher({}, {"cat"}, {}, WSLCProcessFlagsStdin | WSLCProcessFlagsTty); - processLauncher.SetTtySize(24, 80); processLauncher.SetDetachKeys("invalid"); // N.B. Docker returns HTTP 500 if the detach keys are invalid, but unlike other cases there's a proper error message. diff --git a/test/windows/wslc/e2e/WSLCE2EContainerExecTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerExecTests.cpp index bfd9ec861..12bec3168 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerExecTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerExecTests.cpp @@ -154,6 +154,22 @@ class WSLCE2EContainerExecTests session.VerifyNoErrors(); } + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_PseudoConsole_TerminalSize) + { + VerifyContainerIsNotListed(WslcContainerName); + + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + constexpr SHORT columns = 42; + constexpr SHORT rows = 43; + const auto commandLine = + std::format(L"container exec -it {} /bin/sh -c -- \"while true; do stty size; sleep 1; done\"", WslcContainerName); + + auto session = RunWslcInteractive(commandLine, ElevationType::Elevated, PseudoConsole{columns, rows}); + VerifyPseudoConsoleTtySize(session, columns, rows); + } + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_EnvOption) { auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); diff --git a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp index 83dd35dfd..5bc19c3ef 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp @@ -565,6 +565,21 @@ class WSLCE2EContainerRunTests session.VerifyNoErrors(); } + WSLC_TEST_METHOD(WSLCE2E_Container_Run_PseudoConsole_TerminalSize) + { + VerifyContainerIsNotListed(WslcContainerName); + + constexpr SHORT columns = 42; + constexpr SHORT rows = 43; + const auto commandLine = std::format( + L"container run --rm -it --name {} {} /bin/sh -c \"while true; do stty size; sleep 1; done\"", + WslcContainerName, + DebianImage.NameAndTag()); + + auto session = RunWslcInteractive(commandLine, ElevationType::Elevated, PseudoConsole{columns, rows}); + VerifyPseudoConsoleTtySize(session, columns, rows); + } + WSLC_TEST_METHOD(WSLCE2E_Container_Run_Tmpfs) { auto result = RunWslc(std::format( diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp index ff8012fb6..f0cc1a0c3 100644 --- a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp +++ b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp @@ -553,4 +553,47 @@ std::wstring GetPythonHttpServerScript(uint16_t port) { return std::format(L"python3 -m http.server {}", port); } + +namespace { + + void WaitForTtySize(const WSLCInteractiveSession& session, SHORT columns, SHORT rows) + { + try + { + wsl::shared::retry::RetryWithTimeout( + [&]() { + const std::string data = session.GetStdoutData(); + THROW_HR_IF(E_ABORT, data.find(std::format("{} {}\r\n", rows, columns)) == std::string::npos); + }, + std::chrono::milliseconds(200), + std::chrono::seconds(60)); + } + catch (...) + { + const std::string data = session.GetStdoutData(); + VERIFY_FAIL(std::format( + L"Timed out waiting for tty resize. Captured pseudoconsole output: \"{}\"", + wsl::shared::string::MultiByteToWide(EscapeString(data))) + .c_str()); + } + } + +} // namespace + +void VerifyPseudoConsoleTtySize(WSLCInteractiveSession& session, SHORT columns, SHORT rows) +{ + // Size the pseudoconsole is resized to after the initial size has been observed. The values are + // intentionally unusual so they can't be confused with a transient default tty size the container + // might briefly have before the start-time resize lands. + constexpr SHORT resizedColumns = 100; + constexpr SHORT resizedRows = 37; + VERIFY_IS_TRUE(columns != resizedColumns || rows != resizedRows, L"Resized tty size must differ from the initial size"); + + WaitForTtySize(session, columns, rows); + + session.ResizePseudoConsole(resizedColumns, resizedRows); + WaitForTtySize(session, resizedColumns, resizedRows); + + session.Terminate(); +} } // namespace WSLCE2ETests diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.h b/test/windows/wslc/e2e/WSLCE2EHelpers.h index 74478a20b..d05940c58 100644 --- a/test/windows/wslc/e2e/WSLCE2EHelpers.h +++ b/test/windows/wslc/e2e/WSLCE2EHelpers.h @@ -192,6 +192,8 @@ inline void VerifyContainerIsNotListed(const std::wstring& containerNameOrId) wil::com_ptr OpenDefaultElevatedSession(); +void VerifyPseudoConsoleTtySize(WSLCInteractiveSession& session, SHORT columns, SHORT rows); + // Starts a local registry container with host networking using the COM API. // Returns the running container (holds it alive) and the registry address (e.g. "127.0.0.1:PORT"). std::pair StartLocalRegistry( diff --git a/test/windows/wslc/e2e/WSLCExecutor.cpp b/test/windows/wslc/e2e/WSLCExecutor.cpp index f5e592490..1aeb33847 100644 --- a/test/windows/wslc/e2e/WSLCExecutor.cpp +++ b/test/windows/wslc/e2e/WSLCExecutor.cpp @@ -253,8 +253,6 @@ WSLCInteractiveSession RunWslcInteractive(const std::wstring& commandLine, Eleva if (pseudoConsole.has_value()) { - // Pseudoconsole mode: wslc.exe is attached to a ConPTY, so stdin/stdout/stderr are - // multiplexed onto the conpty's single output stream and there is no separate stderr. process.SetPseudoConsole(pseudoConsole->Handle.get()); parentStdinWrite = std::move(pseudoConsole->InputWrite); parentStdoutRead = std::move(pseudoConsole->OutputRead); @@ -298,12 +296,8 @@ WSLCInteractiveSession RunWslcInteractive(const std::wstring& commandLine, Eleva PseudoConsole::PseudoConsole(SHORT columns, SHORT rows) { - // Read end of the input pipe is handed to the conpty (it reads what would be written to stdin); - // the write end is overlapped so WSLCInteractiveSession::Write (which uses OVERLAPPED I/O) works. auto [inputRead, inputWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(0, false, true); - // Read end of the output pipe must be overlapped because PartialHandleRead / InterruptableRead - // use OVERLAPPED I/O with an event-based cancellation pattern. auto [outputRead, outputWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(0, true, false); HPCON rawPseudoConsole{}; @@ -382,7 +376,7 @@ void WSLCInteractiveSession::ResizePseudoConsole(SHORT columns, SHORT rows) void WSLCInteractiveSession::ExpectStderr(const std::string& expected) { - VERIFY_IS_NOT_NULL(m_stderrReader.get(), L"ExpectStderr is not supported for pseudoconsole-backed sessions (stderr is merged into stdout)"); + WI_ASSERT(m_stderrReader.get() != nullptr); Log::Comment(std::format(L"Expecting stderr: \"{}\"", wsl::shared::string::MultiByteToWide(EscapeString(expected))).c_str()); m_stderrReader->ExpectConsume(expected); } @@ -498,7 +492,8 @@ bool WSLCInteractiveSession::Terminate(UINT exitCode) void WSLCInteractiveSession::VerifyNoErrors() { - VERIFY_IS_NOT_NULL(m_stderrReader.get(), L"VerifyNoErrors is not supported for pseudoconsole-backed sessions (stderr is merged into stdout)"); + VERIFY_IS_NOT_NULL( + m_stderrReader.get(), L"VerifyNoErrors is not supported for pseudoconsole-backed sessions (stderr is merged into stdout)"); m_stderrReader->ExpectClosed(DefaultWaitTimeoutMs); // Verify that stderr was actually empty - not just closed @@ -524,4 +519,4 @@ int WSLCInteractiveSession::ExitAndVerifyNoErrors(DWORD timeoutMs) return exitCode; } -} // namespace WSLCE2ETests \ No newline at end of file +} // namespace WSLCE2ETests diff --git a/test/windows/wslc/e2e/WSLCExecutor.h b/test/windows/wslc/e2e/WSLCExecutor.h index f83e3d231..65bc231c8 100644 --- a/test/windows/wslc/e2e/WSLCExecutor.h +++ b/test/windows/wslc/e2e/WSLCExecutor.h @@ -47,19 +47,13 @@ struct WSLCExecutionResult bool StdoutContainsSubstring(const std::wstring& substring) const; }; -// RAII wrapper around a Windows ConPTY pseudoconsole together with the input-write and -// output-read pipe ends the host uses to drive it. Construct one with the desired initial -// size and hand it to RunWslcInteractive to attach wslc.exe to a real pseudoterminal instead -// of plain pipes. Ownership of the conpty and pipes is transferred into the resulting -// WSLCInteractiveSession, which exposes ResizePseudoConsole() and reads the combined output -// stream via the normal stdout reader. struct PseudoConsole { - PseudoConsole(SHORT columns, SHORT rows); - NON_COPYABLE(PseudoConsole); DEFAULT_MOVABLE(PseudoConsole); + PseudoConsole(SHORT columns, SHORT rows); + wil::unique_hfile InputWrite; wil::unique_hfile OutputRead; wsl::windows::common::helpers::unique_pseudo_console Handle; @@ -132,8 +126,6 @@ void RunWslcAndVerify(const std::wstring& cmd, const WSLCExecutionResult& expect std::wstring GetWslcHeader(); WSLCInteractiveSession RunWslcInteractive( - const std::wstring& commandLine, - ElevationType elevationType = ElevationType::Elevated, - std::optional pseudoConsole = std::nullopt); + const std::wstring& commandLine, ElevationType elevationType = ElevationType::Elevated, std::optional pseudoConsole = std::nullopt); } // namespace WSLCE2ETests From 6d1918bd17a1644a710c7316b9d37a8bc1ff6c3e Mon Sep 17 00:00:00 2001 From: Blue Date: Fri, 5 Jun 2026 14:32:13 -0700 Subject: [PATCH 4/5] Cleanup diff --- test/windows/WSLCTests.cpp | 2 +- test/windows/wslc/e2e/WSLCE2EHelpers.cpp | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 2de422b3e..e426423db 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -6820,8 +6820,8 @@ class WSLCTests auto container = launcher.Launch(*m_defaultSession); WSLCProcessLauncher execLauncher({}, {"/bin/sh", "-c", "stty size"}, {}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin); + execLauncher.SetTtySize(0, 0); - // N.B. SetTtySize is intentionally not called, so the launcher passes a (0, 0) size. auto [result, process] = execLauncher.LaunchNoThrow(container.Get()); VERIFY_ARE_EQUAL(result, E_INVALIDARG); } diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp index f0cc1a0c3..5e7a0503d 100644 --- a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp +++ b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp @@ -582,9 +582,6 @@ namespace { void VerifyPseudoConsoleTtySize(WSLCInteractiveSession& session, SHORT columns, SHORT rows) { - // Size the pseudoconsole is resized to after the initial size has been observed. The values are - // intentionally unusual so they can't be confused with a transient default tty size the container - // might briefly have before the start-time resize lands. constexpr SHORT resizedColumns = 100; constexpr SHORT resizedRows = 37; VERIFY_IS_TRUE(columns != resizedColumns || rows != resizedRows, L"Resized tty size must differ from the initial size"); From d590ff8d58ab5a0219597a5385ad1713e30f6d2b Mon Sep 17 00:00:00 2001 From: Blue Date: Fri, 5 Jun 2026 14:33:44 -0700 Subject: [PATCH 5/5] Cleanup diff --- test/windows/wslc/e2e/WSLCExecutor.cpp | 6 +----- test/windows/wslc/e2e/WSLCExecutor.h | 5 ----- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/test/windows/wslc/e2e/WSLCExecutor.cpp b/test/windows/wslc/e2e/WSLCExecutor.cpp index 1aeb33847..7454e1f87 100644 --- a/test/windows/wslc/e2e/WSLCExecutor.cpp +++ b/test/windows/wslc/e2e/WSLCExecutor.cpp @@ -246,7 +246,6 @@ WSLCInteractiveSession RunWslcInteractive(const std::wstring& commandLine, Eleva wil::unique_hfile parentStderrRead; wsl::windows::common::helpers::unique_pseudo_console console; - // Child handles for the pipe-based path; must stay alive until the process has started. wil::unique_hfile childStdinRead; wil::unique_hfile childStdoutWrite; wil::unique_hfile childStderrWrite; @@ -304,8 +303,6 @@ PseudoConsole::PseudoConsole(SHORT columns, SHORT rows) THROW_IF_FAILED(::CreatePseudoConsole(COORD{columns, rows}, inputRead.get(), outputWrite.get(), 0, &rawPseudoConsole)); Handle.reset(rawPseudoConsole); - // ConPTY duplicates the handles internally; release the local references now so that EOF - // propagates correctly once ClosePseudoConsole runs. InputWrite = std::move(inputWrite); OutputRead = std::move(outputRead); } @@ -492,8 +489,7 @@ bool WSLCInteractiveSession::Terminate(UINT exitCode) void WSLCInteractiveSession::VerifyNoErrors() { - VERIFY_IS_NOT_NULL( - m_stderrReader.get(), L"VerifyNoErrors is not supported for pseudoconsole-backed sessions (stderr is merged into stdout)"); + WI_ASSERT(m_stderrReader.get() != nullptr); m_stderrReader->ExpectClosed(DefaultWaitTimeoutMs); // Verify that stderr was actually empty - not just closed diff --git a/test/windows/wslc/e2e/WSLCExecutor.h b/test/windows/wslc/e2e/WSLCExecutor.h index 65bc231c8..e533cad2e 100644 --- a/test/windows/wslc/e2e/WSLCExecutor.h +++ b/test/windows/wslc/e2e/WSLCExecutor.h @@ -87,11 +87,8 @@ struct WSLCInteractiveSession void ExpectStderr(const std::string& expected); void ExpectCommandEcho(const std::string& command); - // Returns a snapshot of everything read from stdout so far (in pseudoconsole mode this is the - // combined output stream). Non-consuming, unlike ExpectStdout. std::string GetStdoutData() const; - // Resizes the attached pseudoconsole. Only valid for sessions created with a PseudoConsole. void ResizePseudoConsole(SHORT columns, SHORT rows); bool IsRunning() const; @@ -108,8 +105,6 @@ struct WSLCInteractiveSession wil::unique_hfile m_stdinWrite; wil::unique_hfile m_stdoutRead; wil::unique_hfile m_stderrRead; - // Destroyed (ClosePseudoConsole) after the readers are stopped but before the read pipes are - // closed, so the conpty is torn down in the right order. Null for pipe-based sessions. wsl::windows::common::helpers::unique_pseudo_console m_pseudoConsole; wil::unique_handle m_processHandle; wil::unique_handle m_nonElevatedToken; // Keep token alive for the lifetime of the session