diff --git a/src/windows/common/ExecutionContext.cpp b/src/windows/common/ExecutionContext.cpp index f329d84b5..f5ec2d7d4 100644 --- a/src/windows/common/ExecutionContext.cpp +++ b/src/windows/common/ExecutionContext.cpp @@ -422,6 +422,29 @@ bool COMServiceExecutionContext::CanCollectUserErrorMessage() return true; } +std::atomic COMServiceExecutionContext::s_warningsPipe{nullptr}; +wil::srwlock COMServiceExecutionContext::s_warningsPipeLock; + +void COMServiceExecutionContext::SetWarningsPipe(HANDLE pipe) +{ + s_warningsPipe.store(pipe, std::memory_order_release); +} + +bool COMServiceExecutionContext::CollectUserWarning(const std::wstring& warning) +{ + auto pipe = s_warningsPipe.load(std::memory_order_acquire); + if (pipe != nullptr) + { + // Serialize writes so concurrent warnings don't interleave. + auto lock = s_warningsPipeLock.lock_exclusive(); + LOG_IF_WIN32_BOOL_FALSE(WriteFile( + pipe, warning.c_str(), gsl::narrow_cast(warning.size() * sizeof(wchar_t)), nullptr, nullptr)); + return true; + } + + return ExecutionContext::CollectUserWarning(warning); +} + LXSS_ERROR_INFO* ClientExecutionContext::OutError() noexcept { return &m_outError; diff --git a/src/windows/common/ExecutionContext.h b/src/windows/common/ExecutionContext.h index b4d46532a..129a14c89 100644 --- a/src/windows/common/ExecutionContext.h +++ b/src/windows/common/ExecutionContext.h @@ -215,6 +215,18 @@ class COMServiceExecutionContext : public ExecutionContext ~COMServiceExecutionContext() override; bool CanCollectUserErrorMessage() override; + + // Set a process-wide warnings pipe for streaming warnings to the CLI. + // The pipe is owned by the caller and must outlive all COMServiceExecutionContext instances. + // Must be called before any COM methods are serviced (e.g., during Initialize). + static void SetWarningsPipe(HANDLE pipe); + +protected: + bool CollectUserWarning(const std::wstring& warning) override; + +private: + static std::atomic s_warningsPipe; + static wil::srwlock s_warningsPipeLock; }; void EnableContextualizedErrors(bool service, bool useComErrors = false); diff --git a/src/windows/service/exe/WSLCSessionManager.cpp b/src/windows/service/exe/WSLCSessionManager.cpp index 318626cbe..d7f8f0713 100644 --- a/src/windows/service/exe/WSLCSessionManager.cpp +++ b/src/windows/service/exe/WSLCSessionManager.cpp @@ -132,17 +132,25 @@ void WSLCSessionManagerImpl::CreateSession(const WSLCSessionSettings* Settings, auto tokenInfo = GetCallingProcessTokenInfo(); const auto callerToken = wsl::windows::common::security::GetUserToken(TokenImpersonation); + // Save the warnings pipe before resolving settings (it may be present even with default sessions). + WSLCHandle warningsPipe{}; + if (Settings != nullptr) + { + warningsPipe = Settings->WarningsPipe; + } + // Resolve display name upfront (for both default and custom sessions). std::wstring resolvedDisplayName; - if (Settings == nullptr) + const bool isDefaultSession = (Settings == nullptr || Settings->DisplayName == nullptr || wcslen(Settings->DisplayName) == 0); + if (isDefaultSession) { // Default session: name determined from token, qualified with username. resolvedDisplayName = ResolveDefaultSessionName(tokenInfo); Flags = WSLCSessionFlagsOpenExisting | WSLCSessionFlagsPersistent; + Settings = nullptr; // Use server-side defaults for all other settings. } else { - THROW_HR_IF(WSLC_E_INVALID_SESSION_NAME, Settings->DisplayName == nullptr || wcslen(Settings->DisplayName) == 0); THROW_HR_IF(E_INVALIDARG, Settings->StoragePath != nullptr && wcslen(Settings->StoragePath) == 0); THROW_HR_IF(WSLC_E_INVALID_SESSION_NAME, wcslen(Settings->DisplayName) >= std::size(WSLCSessionInformation{}.DisplayName)); THROW_HR_IF_MSG( @@ -216,7 +224,8 @@ void WSLCSessionManagerImpl::CreateSession(const WSLCSessionSettings* Settings, AddSessionProcessToJobObject(factory.get()); // Create the session via the factory. - const auto sessionSettings = CreateSessionSettings(sessionId, creatorPid, Settings, resolvedDisplayName.c_str()); + auto sessionSettings = CreateSessionSettings(sessionId, creatorPid, Settings, resolvedDisplayName.c_str()); + sessionSettings.WarningsPipe = warningsPipe; wil::com_ptr session; wil::com_ptr serviceRef; THROW_IF_FAILED(factory->CreateSession(&sessionSettings, vm.Get(), &session, &serviceRef)); diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 2ad3216f8..c70da5036 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -485,6 +485,9 @@ typedef struct _WSLCSessionSettings { WSLCHandle DmesgOutput; WSLCSessionStorageFlags StorageFlags; + // Optional pipe handle for streaming user warnings to the CLI in real-time. + WSLCHandle WarningsPipe; + // Below options are used for debugging purposes only. [unique] LPCWSTR RootVhdOverride; [unique] LPCSTR RootVhdTypeOverride; @@ -719,6 +722,9 @@ typedef struct _WSLCSessionInitSettings WSLCNetworkingMode NetworkingMode; WSLCFeatureFlags FeatureFlags; [unique] LPCSTR RootVhdTypeOverride; + + // Optional pipe handle for streaming user warnings to the CLI in real-time. + WSLCHandle WarningsPipe; } WSLCSessionInitSettings; [ diff --git a/src/windows/wslc/core/CLIExecutionContext.h b/src/windows/wslc/core/CLIExecutionContext.h index ab68a618a..08d08df55 100644 --- a/src/windows/wslc/core/CLIExecutionContext.h +++ b/src/windows/wslc/core/CLIExecutionContext.h @@ -21,7 +21,7 @@ namespace wsl::windows::wslc::execution { // Contains arguments via Args. struct CLIExecutionContext : public wsl::windows::common::ExecutionContext { - CLIExecutionContext() : wsl::windows::common::ExecutionContext(wsl::windows::common::Context::WslC) + CLIExecutionContext() : wsl::windows::common::ExecutionContext(wsl::windows::common::Context::WslC, stderr) { } ~CLIExecutionContext() override = default; diff --git a/src/windows/wslc/services/SessionModel.h b/src/windows/wslc/services/SessionModel.h index 7424a8c04..ad5b4e693 100644 --- a/src/windows/wslc/services/SessionModel.h +++ b/src/windows/wslc/services/SessionModel.h @@ -22,13 +22,55 @@ struct Session explicit Session(wil::com_ptr session) : m_session(std::move(session)) { } + + Session(wil::com_ptr session, wil::unique_handle warningsPipeRead, wil::unique_handle warningsPipeWrite) : + m_session(std::move(session)), m_warningsPipeWrite(std::move(warningsPipeWrite)) + { + // Spawn a background thread that reads warnings from the pipe and prints them to stderr in real-time. + m_warningsThread = std::thread([read = std::move(warningsPipeRead)]() { + try + { + wchar_t buffer[1024] = {0}; + DWORD bytesRead{}; + while (ReadFile(read.get(), buffer, sizeof(buffer) - sizeof(wchar_t), &bytesRead, nullptr) && bytesRead > 0) + { + const auto endIndex = bytesRead / sizeof(wchar_t); + buffer[endIndex] = UNICODE_NULL; + fwprintf(stderr, L"%ls", buffer); + } + } + CATCH_LOG(); + }); + } + + ~Session() + { + // Close the write end of the pipe so the reader thread exits. + m_warningsPipeWrite.reset(); + if (m_warningsThread.joinable()) + { + m_warningsThread.join(); + } + } + + NON_COPYABLE(Session); + Session(Session&&) = default; + Session& operator=(Session&&) = default; + IWSLCSession* Get() const noexcept { return m_session.get(); } + HANDLE WarningsPipeWrite() const noexcept + { + return m_warningsPipeWrite.get(); + } + private: wil::com_ptr m_session; + wil::unique_handle m_warningsPipeWrite; + std::thread m_warningsThread; }; -} // namespace wsl::windows::wslc::models \ No newline at end of file +} // namespace wsl::windows::wslc::models diff --git a/src/windows/wslc/services/SessionService.cpp b/src/windows/wslc/services/SessionService.cpp index a38b5c5b5..038e5e93d 100644 --- a/src/windows/wslc/services/SessionService.cpp +++ b/src/windows/wslc/services/SessionService.cpp @@ -111,11 +111,18 @@ Session SessionService::CreateDefaultSession() THROW_IF_FAILED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager))); wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get()); - // Null Settings = default session with server-determined name and settings. + // Create a pipe for streaming warnings from the service to the CLI in real-time. + wil::unique_handle pipeRead; + wil::unique_handle pipeWrite; + THROW_IF_WIN32_BOOL_FALSE(CreatePipe(&pipeRead, &pipeWrite, nullptr, 0)); + + WSLCSessionSettings settings{}; + settings.WarningsPipe = wslutil::ToCOMInputHandle(pipeWrite.get()); + wil::com_ptr session; - THROW_IF_FAILED(sessionManager->CreateSession(nullptr, WSLCSessionFlagsNone, &session)); + THROW_IF_FAILED(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session)); wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); - return Session(std::move(session)); + return Session(std::move(session), std::move(pipeRead), std::move(pipeWrite)); } int SessionService::Enter(const std::wstring& storagePath, const std::wstring& displayName) diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index bc40f2925..41d4521e8 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -268,6 +268,15 @@ try m_displayName = Settings->DisplayName ? Settings->DisplayName : L""; m_featureFlags = Settings->FeatureFlags; + // Set up the warnings pipe for streaming warnings to the CLI. + // The handle is duplicated because COM closes the original when the call completes. + if (Settings->WarningsPipe.Type != WSLCHandleTypeUnknown) + { + const auto warningsPipeHandle = wslutil::FromCOMInputHandle(Settings->WarningsPipe); + m_warningsPipe.reset(wslutil::DuplicateHandle(warningsPipeHandle, GENERIC_WRITE | SYNCHRONIZE)); + wsl::windows::common::COMServiceExecutionContext::SetWarningsPipe(m_warningsPipe.get()); + } + // Get user token for the current process const auto tokenInfo = wil::get_token_information(GetCurrentProcessToken()); diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index 572e721c8..86cfd6252 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -222,6 +222,9 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession std::atomic m_terminating{false}; std::atomic m_terminated{false}; + // Pipe handle for streaming user warnings to the CLI in real-time. + wil::unique_handle m_warningsPipe; + // User-provided handles that the session is currently doing IO on. std::mutex m_userHandlesLock; __guarded_by(m_userHandlesLock) std::vector m_userHandles;