diff --git a/src/windows/common/ConsoleState.cpp b/src/windows/common/ConsoleState.cpp index 777c218b8..5f3e6144c 100644 --- a/src/windows/common/ConsoleState.cpp +++ b/src/windows/common/ConsoleState.cpp @@ -64,12 +64,33 @@ 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() +{ + 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 +106,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 +121,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 +138,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 +153,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..3c2815789 100644 --- a/src/windows/wslc/services/ConsoleService.cpp +++ b/src/windows/wslc/services/ConsoleService.cpp @@ -21,10 +21,10 @@ 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 +108,11 @@ 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, bool triggerRefresh) { if (WI_IsFlagSet(process.Flags(), WSLCProcessFlagsTty)) { - if (!RelayInteractiveTty(process, process.GetStdHandle(WSLCFDTty).get())) + if (!RelayInteractiveTty(console, process, process.GetStdHandle(WSLCFDTty).get(), triggerRefresh)) { 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..446cf6384 100644 --- a/src/windows/wslc/services/ConsoleService.h +++ b/src/windows/wslc/services/ConsoleService.h @@ -15,13 +15,16 @@ 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, bool triggerRefresh = false); + 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..ea4595634 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,29 @@ 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); + + wsl::windows::common::ConsoleState console; + WSLCProcessStartOptions startOptions{}; + 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 +410,14 @@ 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); + + wsl::windows::common::ConsoleState console; + WSLCProcessStartOptions startOptions{}; + 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 +431,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), true); } void ContainerService::Stop(Session& session, const std::string& id, StopContainerOptions options) @@ -492,6 +512,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 +530,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..c9f58b13d 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,20 @@ 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; + + THROW_HR_IF_MSG( + E_INVALIDARG, + 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 +725,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 +765,19 @@ 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) + { + 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 +1093,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 +1101,16 @@ 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); + if (StartOptions != nullptr) + { + THROW_HR_IF_MSG( + E_INVALIDARG, + WI_IsFlagSet(Options->Flags, WSLCProcessFlagsTty) && (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 +1131,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 +1143,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 +2150,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 +2172,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..e426423db 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); + execLauncher.SetTtySize(0, 0); + + auto [result, process] = execLauncher.LaunchNoThrow(container.Get()); + VERIFY_ARE_EQUAL(result, E_INVALIDARG); + } } WSLC_TEST_METHOD(ExecContainerDelete) @@ -8603,6 +8624,44 @@ 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, {{WSLCFDTty, expectedSize + "\r\n"}}); + } + } + WSLC_TEST_METHOD(ContainerStats_RunningContainer) { // Start a long-lived detached container on a bridged network so network stats are populated. @@ -9196,7 +9255,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(); @@ -9247,7 +9311,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)); diff --git a/test/windows/wslc/e2e/WSLCE2EContainerAttachTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerAttachTests.cpp index 460c15f73..ce46bf449 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerAttachTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerAttachTests.cpp @@ -57,15 +57,13 @@ class WSLCE2EContainerAttachTests result.Verify({.Stderr = L"", .ExitCode = 0}); auto containerId = result.GetStdoutOneLine(); - const auto& expectedAttachPrompt = VT::BuildContainerAttachPrompt(prompt); const auto& expectedPrompt = VT::BuildContainerPrompt(prompt); auto session = RunWslcInteractive(std::format(L"container attach {}", containerId)); VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running"); - // The container attach prompt appears twice. - session.ExpectStdout(expectedAttachPrompt); - session.ExpectStdout(expectedAttachPrompt); + // 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. + session.IgnoreSequence(VT::BuildContainerAttachPrompt(prompt)); session.WriteLine("echo hello"); session.ExpectCommandEcho("echo hello"); @@ -164,4 +162,4 @@ class WSLCE2EContainerAttachTests return options.str(); } }; -} // namespace WSLCE2ETests \ No newline at end of file +} // namespace WSLCE2ETests diff --git a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp index 90f65edcd..7b85b5ba9 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp @@ -467,11 +467,14 @@ class WSLCE2EContainerCreateTests result.Verify({.Stderr = L"", .ExitCode = 0}); auto containerId = result.GetStdoutOneLine(); - const auto& expectedPrompt = VT::BuildContainerPrompt(prompt); + const auto& expectedPrompt = VT::BuildContainerPrompt(prompt, true); auto session = RunWslcInteractive(std::format(L"container start --attach {}", containerId)); VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running"); + // 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. + session.IgnoreSequence(VT::BuildContainerAttachPrompt(prompt)); + session.ExpectStdout(expectedPrompt); session.WriteLine("echo hello"); 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..3b7ab8899 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp @@ -527,7 +527,10 @@ class WSLCE2EContainerRunTests std::format(L"container run -it -e PS1={} --name {} {} bash --norc", prompt, WslcContainerName, DebianImage.NameAndTag())); VERIFY_IS_TRUE(session.IsRunning(), L"Container session should be running"); - const auto& expectedPrompt = VT::BuildContainerPrompt(prompt); + // 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. + session.IgnoreSequence(VT::BuildContainerAttachPrompt(prompt)); + + const auto& expectedPrompt = VT::BuildContainerPrompt(prompt, true); session.ExpectStdout(expectedPrompt); session.WriteLine("echo hello"); @@ -565,6 +568,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..5e7a0503d 100644 --- a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp +++ b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp @@ -553,4 +553,44 @@ 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) +{ + 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 4a2166b21..721497efd 100644 --- a/test/windows/wslc/e2e/WSLCExecutor.cpp +++ b/test/windows/wslc/e2e/WSLCExecutor.cpp @@ -235,20 +235,40 @@ 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()); + wil::unique_hfile childStdinRead; + wil::unique_hfile childStdoutWrite; + wil::unique_hfile childStderrWrite; + + if (pseudoConsole.has_value()) + { + 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 +289,22 @@ 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) +{ + auto [inputRead, inputWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(0, false, true); + + 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); + + InputWrite = std::move(inputWrite); + OutputRead = std::move(outputRead); } // WSLCInteractiveSession implementation @@ -280,16 +315,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() @@ -313,12 +356,34 @@ WSLCInteractiveSession::~WSLCInteractiveSession() void WSLCInteractiveSession::ExpectStdout(const std::string& expected) { + if (m_ignoreSequence.has_value()) + { + while (m_stdoutReader->ReadBytes(m_ignoreSequence->size()) == *m_ignoreSequence) + { + Log::Comment(std::format(L"Consuming ignored sequence: \"{}\"", wsl::shared::string::MultiByteToWide(EscapeString(*m_ignoreSequence))) + .c_str()); + m_stdoutReader->ConsumeBytes(m_ignoreSequence->size()); + } + } + Log::Comment(std::format(L"Expecting stdout: \"{}\"", wsl::shared::string::MultiByteToWide(EscapeString(expected))).c_str()); 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) { + 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); } @@ -329,6 +394,12 @@ void WSLCInteractiveSession::ExpectCommandEcho(const std::string& command) ExpectStdout(std::format("{}\r\n{}\r", command, VT::B_END)); } +void WSLCInteractiveSession::IgnoreSequence(const std::string& sequence) +{ + VERIFY_IS_FALSE(m_ignoreSequence.has_value()); + m_ignoreSequence = sequence; +} + void WSLCInteractiveSession::Write(const std::string& data) { 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) void WSLCInteractiveSession::VerifyNoErrors() { + WI_ASSERT(m_stderrReader.get() != nullptr); m_stderrReader->ExpectClosed(DefaultWaitTimeoutMs); // Verify that stderr was actually empty - not just closed @@ -459,4 +531,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 98cc45de9..a75cfdb61 100644 --- a/test/windows/wslc/e2e/WSLCExecutor.h +++ b/test/windows/wslc/e2e/WSLCExecutor.h @@ -47,6 +47,18 @@ struct WSLCExecutionResult bool StdoutContainsSubstring(const std::wstring& substring) const; }; +struct PseudoConsole +{ + 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; +}; + // Interactive session for testing wslc commands that require stdin/stdout interaction. // Uses PartialHandleRead for race-free output validation struct WSLCInteractiveSession @@ -57,7 +69,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 +87,12 @@ struct WSLCInteractiveSession void ExpectStderr(const std::string& expected); void ExpectCommandEcho(const std::string& command); + void IgnoreSequence(const std::string& sequence); + + std::string GetStdoutData() const; + + void ResizePseudoConsole(SHORT columns, SHORT rows); + bool IsRunning() const; void CloseStdin(); std::optional GetExitCode() const; @@ -88,10 +107,12 @@ struct WSLCInteractiveSession wil::unique_hfile m_stdinWrite; wil::unique_hfile m_stdoutRead; wil::unique_hfile m_stderrRead; + 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; std::unique_ptr m_stderrReader; + std::optional m_ignoreSequence; }; WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType elevationType = ElevationType::Elevated); @@ -102,6 +123,7 @@ 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