From 3b60a32ba7cc402a8e7e354e888e113f69e3c523 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Wed, 11 Nov 2026 04:28:45 +0800 Subject: [PATCH] [fix] avoid blocking Windows shutdown and restore display modes --- .../src/gui/application/gui_application.cpp | 25 ++-- .../assets/localization/localization_data.h | 3 - .../windows/gui/session_end_observer.cpp | 72 +++++++++++ .../windows/gui/session_end_observer.h | 39 ++++++ .../windows/gui/shutdown_session_guard.cpp | 121 ------------------ .../windows/gui/shutdown_session_guard.h | 48 ------- .../screen_capturer/screen_capturer_win.cpp | 38 +++++- .../screen_capturer/screen_capturer_win.h | 5 + .../platform/windows/service/shutdown_order.h | 5 +- .../usbmmidd_virtual_display.cpp | 116 +++++++++++++++-- .../usbmmidd_virtual_display.h | 6 +- apps/desktop/xmake/targets.lua | 2 +- localization/catalog.json | 14 -- 13 files changed, 273 insertions(+), 221 deletions(-) create mode 100644 apps/desktop/src/platform/windows/gui/session_end_observer.cpp create mode 100644 apps/desktop/src/platform/windows/gui/session_end_observer.h delete mode 100644 apps/desktop/src/platform/windows/gui/shutdown_session_guard.cpp delete mode 100644 apps/desktop/src/platform/windows/gui/shutdown_session_guard.h diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index a51d6ad5..adc153df 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -62,7 +62,7 @@ #include #include "platform/windows/gui/slint_backend.h" -#include "platform/windows/gui/shutdown_session_guard.h" +#include "platform/windows/gui/session_end_observer.h" #include "platform/windows/gui/tray/win_tray.h" #include "platform/windows/gui/window_icons.h" #include "platform/windows/gui/window_taskbar.h" @@ -989,8 +989,7 @@ void DragWindow(WindowHandle& component, int phase, float mouse_x, struct GuiApplication::SlintUi { #if _WIN32 - std::unique_ptr shutdown_guard; - std::wstring_view shutdown_reason; + SessionEndObserver session_end; #endif gallery::Workspace gallery; std::filesystem::path gallery_path; @@ -1397,8 +1396,7 @@ void GuiApplication::InitializeUi() { ui_->main->set_lan_mode(config_center_->GetConnectionMode() == ConfigCenter::CONNECTION_MODE::DIRECT_IP); #if _WIN32 - ui_->shutdown_guard = - std::make_unique(windows_rdp_handoff_connected_); + ui_->session_end.Start(); #endif ui_->sponsorship_prompt_pending = config_center_->GetSponsorshipPromptDismissedVersion() != CROSSDESK_VERSION; @@ -2533,6 +2531,12 @@ void GuiApplication::BindServerCallbacks() { } void GuiApplication::Tick() { +#if _WIN32 + if (ui_) { + ui_->session_end.Start(); // Retry a transient native-window creation failure. + if (ui_->session_end.Ending()) exit_ = true; + } +#endif if (exit_) { BeginShutdown(); if (ShutdownDrained()) slint::quit_event_loop(); @@ -2647,11 +2651,6 @@ void GuiApplication::Tick() { ? std::string("ConnectionFailed") : lan_error_; ui_->main->set_lan_message(UiText(lan_message)); -#if _WIN32 - if (ui_->shutdown_guard) { - ui_->shutdown_guard->Update(ui_->shutdown_reason); - } -#endif PollUpdateCheck(); HandleSessionCleanup(); HandlePasswordChangeResult(); @@ -3280,9 +3279,6 @@ void GuiApplication::UpdateLocalization() { return; } #if _WIN32 - static std::vector shutdown_reasons(localization::GetSupportedLanguages().size()); - ui_->shutdown_reason = localization::GetWindowsLocalizedLabel( - language, "shutdown_reason", L"Remote session active.", shutdown_reasons); const bool available = SupportsServiceAutostart(); for (const auto& main : {ui_->main, ui_->wall}) { main->set_service_autostart_platform(true); @@ -7081,9 +7077,6 @@ void GuiApplication::Cleanup() { return; } ui_->file_dialog_lifetime.reset(); -#if _WIN32 - ui_->shutdown_guard.reset(); -#endif #if defined(__APPLE__) CancelNativeFileDialog(); #endif diff --git a/apps/desktop/src/gui/assets/localization/localization_data.h b/apps/desktop/src/gui/assets/localization/localization_data.h index e0962ff8..8a1ad126 100644 --- a/apps/desktop/src/gui/assets/localization/localization_data.h +++ b/apps/desktop/src/gui/assets/localization/localization_data.h @@ -433,7 +433,6 @@ inline constexpr TranslationRow kTranslationRows[] = { {"近期连接", {"近期连接", "Recent Connections", "Недавние подключения", "近期連線", "最近の接続", "최근 연결", "Aktuelle Verbindungen", "Connexions récentes", "Conexiones recientes", "Conexões recentes"}}, {"返回", {"返回", "Back", "Назад", "返回", "戻る", "뒤로", "Zurück", "Retour", "Volver", "Voltar"}}, {"还原窗口", {"还原窗口", "Restore window", "Восстановить окно", "還原視窗", "復元ウィンドウ", "복원 창", "Fenster „Wiederherstellen“.", "Fenêtre de restauration", "Restaurar ventana", "Janela de restauração"}}, - {"远程控制连接中,保留连接以确认或取消关机。", {"远程控制连接中,保留连接以确认或取消关机。", "Remote session active. Keeping the connection available to confirm or cancel shutdown.", "Удалённый сеанс активен. Соединение сохранено для подтверждения или отмены выключения.", "遠端控制連線中,保留連線以確認或取消關機。", "リモート接続中です。シャットダウンの確認または取り消しのため、接続を維持しています。", "원격 세션이 활성화되어 있습니다. 종료 확인 또는 취소를 위해 연결을 유지합니다.", "Remotesitzung aktiv. Die Verbindung bleibt zum Bestätigen oder Abbrechen des Herunterfahrens bestehen.", "Session distante active. La connexion reste disponible pour confirmer ou annuler l’arrêt.", "Sesión remota activa. Se mantiene la conexión para confirmar o cancelar el apagado.", "Sessão remota ativa. A conexão é mantida para confirmar ou cancelar o desligamento."}}, {"远程桌面", {"远程桌面", "Remote Desktop", "Удаленный рабочий стол", "遠端桌面", "リモートデスクトップ", "원격 데스크톱", "Remotedesktop", "Bureau à distance", "Escritorio remoto", "Área de Trabalho Remota"}}, {"远端 IP / 端口", {"远端 IP / 端口", "Remote IP / Port", "Удалённый IP / Порт", "遠端 IP / 埠", "リモートIP/ポート", "원격 IP / 포트", "Remote-IP/Port", "IP/port distant", "IP/puerto remoto", "IP/Porta Remota"}}, {"远端 Windows 服务不可用", {"远端 Windows 服务不可用", "Remote Windows service unavailable", "Служба Windows на удаленной стороне недоступна", "遠端 Windows 服務不可用", "リモート Windows サービスが利用できない", "원격 Windows 서비스를 사용할 수 없습니다.", "Remote-Windows-Dienst nicht verfügbar", "Service Windows distant indisponible", "Servicio remoto de Windows no disponible", "Serviço remoto do Windows indisponível"}}, @@ -840,7 +839,6 @@ inline constexpr NamedTranslation kNamedTranslations[] = { {"privacy_unlock_lock", "锁定"}, {"privacy_screen_unlock_hint", "按下快捷键,解除隐私屏"}, {"exit_program", "退出"}, - {"shutdown_reason", "远程控制连接中,保留连接以确认或取消关机。"}, {"pause_video_when_minimized", "最小化时暂停视频"}, {"pause_video_when_minimized_hint", "控制窗口最小化时暂停接收视频,降低本机解码资源占用。"}, {"system_permissions", "系统权限"}, @@ -1352,7 +1350,6 @@ inline constexpr const char* kSlintLiterals[] = { X(privacy_unlock_lock) \ X(privacy_screen_unlock_hint) \ X(exit_program) \ - X(shutdown_reason) \ X(pause_video_when_minimized) \ X(pause_video_when_minimized_hint) \ X(system_permissions) \ diff --git a/apps/desktop/src/platform/windows/gui/session_end_observer.cpp b/apps/desktop/src/platform/windows/gui/session_end_observer.cpp new file mode 100644 index 00000000..3731f9c0 --- /dev/null +++ b/apps/desktop/src/platform/windows/gui/session_end_observer.cpp @@ -0,0 +1,72 @@ +#include "session_end_observer.h" + +#include "rd_log.h" + +namespace crossdesk { +namespace { +constexpr wchar_t kSessionWindowClass[] = L"CrossDesk.SessionEndObserver"; +} + +SessionEndObserver::~SessionEndObserver() { + if (window_) DestroyWindow(window_); +} + +bool SessionEndObserver::ReportError(const char* operation) { + const DWORD error = GetLastError(); + const ULONGLONG now = GetTickCount64(); + if (last_error_tick_ == 0 || now - last_error_tick_ >= 5000) { + LOG_WARN("Windows session end observer {} failed, error={}", operation, error); + last_error_tick_ = now; + } + SetLastError(error); + return false; +} + +bool SessionEndObserver::Start() { + if (window_) return true; + WNDCLASSW klass{}; + klass.lpfnWndProc = WindowProc; + klass.hInstance = GetModuleHandleW(nullptr); + klass.lpszClassName = kSessionWindowClass; + if (!RegisterClassW(&klass) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) + return ReportError("register window"); + // A hidden top-level window receives session-end broadcasts even when the + // Slint windows are in the tray. HWND_MESSAGE does not receive them. + window_ = CreateWindowExW(WS_EX_TOOLWINDOW, kSessionWindowClass, L"CrossDesk", + WS_OVERLAPPED, 0, 0, 0, 0, nullptr, nullptr, klass.hInstance, this); + if (!window_) return ReportError("create window"); + return true; +} + +LRESULT CALLBACK SessionEndObserver::WindowProc( + HWND window, UINT message, WPARAM wparam, LPARAM lparam) { + if (message == WM_NCCREATE) { + const auto* create = reinterpret_cast(lparam); + auto* observer = static_cast(create->lpCreateParams); + observer->window_ = window; + SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast(observer)); + } + auto* observer = reinterpret_cast( + GetWindowLongPtrW(window, GWLP_USERDATA)); + if (observer && message == WM_QUERYENDSESSION) { + // Never stop work or veto shutdown here, including logoff/Restart Manager. + LOG_INFO("Windows shutdown query accepted: flags=0x{:x}", + static_cast(lparam)); + return TRUE; + } + if (observer && message == WM_ENDSESSION) { + LOG_INFO("Windows session end: confirmed={}, flags=0x{:x}", + wparam != FALSE, static_cast(lparam)); + // Let the GUI timer begin its normal cleanup only after confirmation. + // Do not wait for network/device threads inside a Windows message handler. + if (wparam != FALSE) observer->ending_ = true; + return 0; + } + if (observer && message == WM_NCDESTROY) { + observer->window_ = nullptr; + SetWindowLongPtrW(window, GWLP_USERDATA, 0); + } + return DefWindowProcW(window, message, wparam, lparam); +} + +} // namespace crossdesk diff --git a/apps/desktop/src/platform/windows/gui/session_end_observer.h b/apps/desktop/src/platform/windows/gui/session_end_observer.h new file mode 100644 index 00000000..ffdecb27 --- /dev/null +++ b/apps/desktop/src/platform/windows/gui/session_end_observer.h @@ -0,0 +1,39 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-11-11 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _SESSION_END_OBSERVER_H_ +#define _SESSION_END_OBSERVER_H_ + +#include + +namespace crossdesk { + +// Owned and pumped by the UI thread. A query is permission to end the session, +// not a request to tear down transport/capture: another application may still +// cancel shutdown. This observer never registers a shutdown-blocking reason. +class SessionEndObserver { + public: + SessionEndObserver() = default; + ~SessionEndObserver(); + SessionEndObserver(const SessionEndObserver&) = delete; + SessionEndObserver& operator=(const SessionEndObserver&) = delete; + + bool Start(); + bool Ending() const { return ending_; } + + private: + bool ReportError(const char* operation); + static LRESULT CALLBACK WindowProc(HWND window, UINT message, + WPARAM wparam, LPARAM lparam); + + HWND window_ = nullptr; + bool ending_ = false; + ULONGLONG last_error_tick_ = 0; +}; + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/platform/windows/gui/shutdown_session_guard.cpp b/apps/desktop/src/platform/windows/gui/shutdown_session_guard.cpp deleted file mode 100644 index ce3715b8..00000000 --- a/apps/desktop/src/platform/windows/gui/shutdown_session_guard.cpp +++ /dev/null @@ -1,121 +0,0 @@ -#include "shutdown_session_guard.h" - -#include - -#include "rd_log.h" - -namespace crossdesk { -namespace { -constexpr wchar_t kShutdownWindowClass[] = L"CrossDesk.ShutdownSessionGuard"; -} - -ShutdownSessionGuard::ShutdownSessionGuard( - std::shared_ptr> connected) - : connected_(std::move(connected)) {} - -ShutdownSessionGuard::~ShutdownSessionGuard() { - ClearReason(); - if (window_) DestroyWindow(window_); -} - -bool ShutdownSessionGuard::ReportError(const char* operation) { - const DWORD error = GetLastError(); - const ULONGLONG now = GetTickCount64(); - if (last_error_tick_ == 0 || now - last_error_tick_ >= 5000) { - LOG_WARN("Windows shutdown session guard {} failed, error={}", operation, error); - last_error_tick_ = now; - } - SetLastError(error); - return false; -} - -bool ShutdownSessionGuard::Start() { - if (window_) return true; - WNDCLASSW klass{}; - klass.lpfnWndProc = WindowProc; - klass.hInstance = GetModuleHandleW(nullptr); - klass.lpszClassName = kShutdownWindowClass; - if (!RegisterClassW(&klass) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) - return ReportError("register window"); - // A top-level window receives session-end broadcasts even when the Slint - // windows are hidden in the tray. HWND_MESSAGE does not receive them. - window_ = CreateWindowExW(WS_EX_TOOLWINDOW, kShutdownWindowClass, L"CrossDesk", - WS_OVERLAPPED, 0, 0, 0, 0, nullptr, nullptr, klass.hInstance, this); - if (!window_) return ReportError("create window"); - return true; -} - -bool ShutdownSessionGuard::ClearReason() { - if (!reason_registered_) return true; - if (window_ && !ShutdownBlockReasonDestroy(window_)) - return ReportError("clear reason"); - reason_registered_ = false; - LOG_INFO("Windows shutdown session guard released"); - return true; -} - -bool ShutdownSessionGuard::SyncReason() { - if (shutdown_allowed_ || !connected_ || - !connected_->load(std::memory_order_relaxed)) { - return ClearReason(); - } - if (reason_registered_) return true; - if (!ShutdownBlockReasonCreate(window_, reason_.c_str())) - return ReportError("register reason"); - reason_registered_ = true; - LOG_INFO("Windows shutdown session guard armed for connected controller"); - return true; -} - -bool ShutdownSessionGuard::Update(std::wstring_view reason) { - if (!Start()) return false; - if (std::wstring_view(reason_) != reason) { - // Keep the old registration and text on failure so the next tick retries - // removal before registering the newly selected language. - if (!ClearReason()) return false; - reason_.assign(reason); - } - return SyncReason(); -} - -LRESULT CALLBACK ShutdownSessionGuard::WindowProc( - HWND window, UINT message, WPARAM wparam, LPARAM lparam) { - if (message == WM_NCCREATE) { - const auto* create = reinterpret_cast(lparam); - auto* guard = static_cast(create->lpCreateParams); - guard->window_ = window; - SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast(guard)); - } - auto* guard = reinterpret_cast( - GetWindowLongPtrW(window, GWLP_USERDATA)); - if (guard && message == WM_QUERYENDSESSION) { - const auto flags = static_cast(lparam); - // "Shut down anyway", logoff and Restart Manager must remain available. - if ((flags & (ENDSESSION_CRITICAL | ENDSESSION_LOGOFF | ENDSESSION_CLOSEAPP)) != 0) - guard->shutdown_allowed_ = true; - guard->SyncReason(); - const bool retain = !guard->shutdown_allowed_ && guard->reason_registered_ && - guard->connected_ && guard->connected_->load(std::memory_order_relaxed); - LOG_INFO("Windows shutdown query: flags=0x{:x}, retain_remote_session={}", flags, retain); - return retain ? FALSE : TRUE; - } - if (guard && message == WM_ENDSESSION) { - LOG_INFO("Windows session end: confirmed={}, flags=0x{:x}", - wparam != FALSE, static_cast(lparam)); - guard->shutdown_allowed_ = wparam != FALSE; - guard->SyncReason(); - // A cancelled shutdown must leave transport, capture and input running. - return 0; - } - if (guard && message == WM_NCDESTROY) { - guard->ClearReason(); - // Destruction removes the native registration even if explicit removal - // failed. A later window must not inherit the destroyed window's state. - guard->reason_registered_ = false; - guard->window_ = nullptr; - SetWindowLongPtrW(window, GWLP_USERDATA, 0); - } - return DefWindowProcW(window, message, wparam, lparam); -} - -} // namespace crossdesk diff --git a/apps/desktop/src/platform/windows/gui/shutdown_session_guard.h b/apps/desktop/src/platform/windows/gui/shutdown_session_guard.h deleted file mode 100644 index 6cfdb91b..00000000 --- a/apps/desktop/src/platform/windows/gui/shutdown_session_guard.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - * @Author: DI JUNKUN - * @Date: 2026-10-22 - * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. - */ - -#ifndef _SHUTDOWN_SESSION_GUARD_H_ -#define _SHUTDOWN_SESSION_GUARD_H_ - -#include - -#include -#include -#include -#include - -namespace crossdesk { - -// Owned and pumped by the UI thread. The transport may revoke the shared -// connection flag from another thread, including between UI ticks. -class ShutdownSessionGuard { - public: - explicit ShutdownSessionGuard(std::shared_ptr> connected); - ~ShutdownSessionGuard(); - ShutdownSessionGuard(const ShutdownSessionGuard&) = delete; - ShutdownSessionGuard& operator=(const ShutdownSessionGuard&) = delete; - - bool Update(std::wstring_view reason); - - private: - bool Start(); - bool SyncReason(); - bool ClearReason(); - bool ReportError(const char* operation); - static LRESULT CALLBACK WindowProc(HWND window, UINT message, - WPARAM wparam, LPARAM lparam); - - std::shared_ptr> connected_; - HWND window_ = nullptr; - std::wstring reason_; - bool reason_registered_ = false; - bool shutdown_allowed_ = false; - ULONGLONG last_error_tick_ = 0; -}; - -} // namespace crossdesk - -#endif diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp index b92a62df..88026a9f 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp @@ -425,17 +425,22 @@ void ScreenCapturerWin::UpdateNativeCursor(ScreenCapturer* backend) { } bool ScreenCapturerWin::PrepareUnattendedDisplay() { + if (virtual_display_.active() || DetectHeadlessDesktop()) { + physical_mode_recovery_pending_ = true; + physical_mode_restore_attempts_ = 0; + } if (!ForceHeadlessEnabled() && unattended_host_ && virtual_display_.active() && QueryAttachedPhysicalDisplayMode().width > 0) { // The monitor may have returned between controller connections. virtual_display_.Release(); + RestorePhysicalDisplayMode(); return false; } if (!unattended_host_ || (!virtual_display_.active() && !DetectHeadlessDesktop())) return false; virtual_display_.PrepareAcquire(); std::string error; - if (!virtual_display_.Acquire(ResolveVirtualDisplayMode(), &error)) { + if (!virtual_display_.Acquire(ResolveVirtualDisplayMode(false), &error)) { LOG_WARN("Cannot prepare unattended capture display: {}", error); return false; } @@ -778,7 +783,7 @@ int ScreenCapturerWin::Start(bool show_cursor) { if (running_.load(std::memory_order_relaxed)) { return 0; } - ResolveVirtualDisplayMode(); + ResolveVirtualDisplayMode(!physical_mode_recovery_pending_ && physical_mode_restore_attempts_ == 0); native_cursor_enabled_.store(native_cursor_default_.load()); NotifyPrivacyCapture(false); @@ -870,7 +875,7 @@ void ScreenCapturerWin::BeginVirtualDisplayProvisioning() { // A usbmmidd virtual monitor turns the phantom desktop into a real // display target, so the GPU backends and a chosen resolution apply. std::string error; - if (virtual_display_.Acquire(ResolveVirtualDisplayMode(), &error)) { + if (virtual_display_.Acquire(ResolveVirtualDisplayMode(false), &error)) { virtual_display_ready_.store(true, std::memory_order_release); } else if (error != "cancelled") { LOG_WARN("Windows capturer: no usbmmidd virtual display ({}); " @@ -1169,6 +1174,10 @@ void ScreenCapturerWin::CheckDisplayPresence(ULONGLONG now) { const bool headless = DetectHeadlessDesktop(stalled, topology_changed); const bool owns_display = virtual_display_.active(); + if (headless || owns_display) { + physical_mode_recovery_pending_ = true; + physical_mode_restore_attempts_ = 0; + } const bool provisioning = virtual_display_busy_.load(std::memory_order_acquire); if (!provisioning) { @@ -1181,7 +1190,8 @@ void ScreenCapturerWin::CheckDisplayPresence(ULONGLONG now) { physical_binding_changed = !replacement_display_name_.empty() && replacement_display_name_ != GetDisplayLabel(physical_device); } - if (physical_present && (owns_display || physical_binding_changed)) { + if (physical_present && (owns_display || physical_binding_changed || + physical_mode_recovery_pending_)) { // Debounce a hotplug before removing the fallback that is still carrying // the stream. This runs on the management thread, never a frame callback. if (physical_display_since_ == 0) physical_display_since_ = now; @@ -1194,7 +1204,7 @@ void ScreenCapturerWin::CheckDisplayPresence(ULONGLONG now) { physical_display_since_ = 0; // Track user resolution changes during capture, not only process startup. // Resolve ignores phantom, RDP and virtual-display clone modes. - ResolveVirtualDisplayMode(); + ResolveVirtualDisplayMode(!physical_mode_recovery_pending_ && physical_mode_restore_attempts_ == 0); } bool target_ready = false; if (owns_display) { @@ -1222,6 +1232,23 @@ void ScreenCapturerWin::CheckDisplayPresence(ULONGLONG now) { BeginVirtualDisplayProvisioning(); } +void ScreenCapturerWin::RestorePhysicalDisplayMode() { + if (!physical_mode_recovery_pending_ || ForceHeadlessEnabled()) return; + std::string error; + if (RestoreAttachedPhysicalDisplayModes(&error)) { + physical_mode_recovery_pending_ = false; + physical_mode_restore_attempts_ = 0; + } else { + ++physical_mode_restore_attempts_; + LOG_WARN("Physical display mode recovery attempt {} failed: {}", + physical_mode_restore_attempts_, error); + // Retry transient EDID/topology failures, but do not continually interrupt + // capture or fight a user setting if a driver cannot apply its native mode. + if (physical_mode_restore_attempts_ >= 3) + physical_mode_recovery_pending_ = false; + } +} + void ScreenCapturerWin::RestorePhysicalDisplay() { JoinVirtualDisplayThread(); if (!running_.load(std::memory_order_relaxed)) return; @@ -1231,6 +1258,7 @@ void ScreenCapturerWin::RestorePhysicalDisplay() { StopSecureDesktopSharedCapture(secure_shared_session_id_); virtual_display_ready_.store(false, std::memory_order_release); virtual_display_.Release(); + RestorePhysicalDisplayMode(); // Windows may renumber outputs after the unplug. Requery and bind the real // screen to the same registered wire slot, including multi-monitor hotplugs. std::wstring device; diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h index 561a11f8..820d0e44 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h @@ -117,6 +117,10 @@ class ScreenCapturerWin : public ScreenCapturer { std::atomic virtual_display_busy_{false}; ULONGLONG last_virtual_adoption_tick_ = 0; ULONGLONG physical_display_since_ = 0; + // A phantom desktop can return on the same DISPLAYn without ever acquiring + // usbmmidd. Remember that transition independently of virtual ownership. + bool physical_mode_recovery_pending_ = false; + unsigned physical_mode_restore_attempts_ = 0; // Protected by alias_mutex_. The virtual screen and then the returning // physical screen replace the same active wire slot explicitly. std::string replacement_display_name_; @@ -154,6 +158,7 @@ class ScreenCapturerWin : public ScreenCapturer { void CheckCaptureProgress(ULONGLONG now); void CheckDisplayPresence(ULONGLONG now); void RestorePhysicalDisplay(); + void RestorePhysicalDisplayMode(); // Virtual display provisioning runs off the caller's thread. void JoinVirtualDisplayThread(); void BeginVirtualDisplayProvisioning(); diff --git a/apps/desktop/src/platform/windows/service/shutdown_order.h b/apps/desktop/src/platform/windows/service/shutdown_order.h index f13cfddc..c607e3af 100644 --- a/apps/desktop/src/platform/windows/service/shutdown_order.h +++ b/apps/desktop/src/platform/windows/service/shutdown_order.h @@ -22,8 +22,9 @@ inline constexpr DWORD WindowsShutdownLevel(WindowsShutdownRole role) { } inline DWORD ConfigureWindowsShutdownOrder(WindowsShutdownRole role) { - // Change only the order. The GUI's session guard separately defers shutdown - // while a controller is connected; a confirmed shutdown still ends CrossDesk. + // Change only the order, without vetoing shutdown or extending its timeout. + // SCM shutdown notifications still govern the service-owned host/helpers; + // these levels do not override an explicit stop from their owner. if (SetProcessShutdownParameters(WindowsShutdownLevel(role), 0)) return ERROR_SUCCESS; const DWORD error = GetLastError(); diff --git a/apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp b/apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp index 8755950c..1f0f4c79 100644 --- a/apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp +++ b/apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp @@ -237,7 +237,14 @@ void SaveRememberedMode(HKEY root, const RememberedDisplayMode& record) { RegCloseKey(key); } -std::vector ActivePhysicalDisplaySources(bool* queried) { +struct PhysicalDisplayTarget { + std::wstring device; + std::wstring monitor_path; + DISPLAYCONFIG_PATH_TARGET_INFO target; + bool cloned = false; +}; + +std::vector ActivePhysicalDisplays(bool* queried) { *queried = false; for (int attempt = 0; attempt < 4; ++attempt) { UINT32 path_count = 0, mode_count = 0; @@ -252,7 +259,7 @@ std::vector ActivePhysicalDisplaySources(bool* queried) { paths.data(), &mode_count, modes.data(), nullptr); if (result == ERROR_INSUFFICIENT_BUFFER) continue; if (result != ERROR_SUCCESS) break; - std::vector sources; + std::vector displays; for (UINT32 i = 0; i < path_count; ++i) { const auto& path = paths[i]; if (!(path.flags & DISPLAYCONFIG_PATH_ACTIVE) || @@ -268,10 +275,18 @@ std::vector ActivePhysicalDisplaySources(bool* queried) { DISPLAYCONFIG_SOURCE_DEVICE_NAME source{}; source.header = {DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, sizeof(source), path.sourceInfo.adapterId, path.sourceInfo.id}; - if (DisplayConfigGetDeviceInfo(&source.header) == ERROR_SUCCESS) - sources.emplace_back(source.viewGdiDeviceName); + if (DisplayConfigGetDeviceInfo(&source.header) != ERROR_SUCCESS) continue; + const bool cloned = std::count_if(paths.begin(), paths.begin() + path_count, + [&](const DISPLAYCONFIG_PATH_INFO& other) { + return (other.flags & DISPLAYCONFIG_PATH_ACTIVE) && + other.sourceInfo.id == path.sourceInfo.id && + other.sourceInfo.adapterId.LowPart == path.sourceInfo.adapterId.LowPart && + other.sourceInfo.adapterId.HighPart == path.sourceInfo.adapterId.HighPart; + }) > 1; + displays.push_back({source.viewGdiDeviceName, target.monitorDevicePath, + path.targetInfo, cloned}); } - return sources; + return displays; } return {}; } @@ -284,7 +299,7 @@ VirtualDisplayMode QueryAttachedPhysicalDisplayMode(std::wstring* device_name) { // RDP creates a display mode for that client, not the console's next boot. if (!GetSystemMetrics(SM_REMOTESESSION)) { bool queried = false; - const auto sources = ActivePhysicalDisplaySources(&queried); + const auto sources = ActivePhysicalDisplays(&queried); for (DWORD index = 0;; ++index) { DISPLAY_DEVICEW adapter{sizeof(adapter)}; if (!EnumDisplayDevicesW(nullptr, index, &adapter, 0)) break; @@ -296,8 +311,8 @@ VirtualDisplayMode QueryAttachedPhysicalDisplayMode(std::wstring* device_name) { // real target disappears. Require the corresponding available CCD path // when CCD is accessible; an IDD path must not validate a phantom GPU. if (queried && std::none_of(sources.begin(), sources.end(), - [&](const std::wstring& source) { - return _wcsicmp(source.c_str(), adapter.DeviceName) == 0; + [&](const PhysicalDisplayTarget& source) { + return _wcsicmp(source.device.c_str(), adapter.DeviceName) == 0; })) continue; bool present = false; for (DWORD child = 0; !present; ++child) { @@ -325,7 +340,88 @@ VirtualDisplayMode QueryAttachedPhysicalDisplayMode(std::wstring* device_name) { return attached; } -VirtualDisplayMode ResolveVirtualDisplayMode() { +bool RestoreAttachedPhysicalDisplayModes(std::string* error) { + if (GetSystemMetrics(SM_REMOTESESSION) || CountUsbmmiddMonitors() != 0) { + SetError(error, "physical_display_topology_not_ready"); + return false; + } + bool queried = false; + const auto displays = ActivePhysicalDisplays(&queried); + if (displays.empty()) { + SetError(error, "physical_display_not_ready"); + return false; + } + bool restored = true; + for (const auto& display : displays) { + // One source mode affects every cloned target. Do not force a native 4K + // mode onto another real/third-party screen sharing a 1080p clone source. + if (display.cloned) { + LOG_INFO("Physical display '{}': keeping the shared clone mode", Narrow(display.device)); + continue; + } + DISPLAYCONFIG_TARGET_PREFERRED_MODE preferred{}; + preferred.header = {DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_PREFERRED_MODE, + sizeof(preferred), display.target.adapterId, display.target.id}; + const LONG preferred_error = DisplayConfigGetDeviceInfo(&preferred.header); + DEVMODEW mode{}; + mode.dmSize = sizeof(mode); + if (preferred_error != ERROR_SUCCESS || + !ValidDisplayMode({static_cast(preferred.width), static_cast(preferred.height)}) || + !EnumDisplaySettingsExW(display.device.c_str(), ENUM_CURRENT_SETTINGS, &mode, 0)) { + SetError(error, "physical_display_preferred_mode_unavailable:" + std::to_string(preferred_error)); + restored = false; + continue; + } + DWORD width = preferred.width, height = preferred.height; + if (mode.dmDisplayOrientation == DMDO_90 || mode.dmDisplayOrientation == DMDO_270) + std::swap(width, height); + if (mode.dmPelsWidth == width && mode.dmPelsHeight == height) continue; + const DWORD old_width = mode.dmPelsWidth, old_height = mode.dmPelsHeight; + mode.dmPelsWidth = width; + mode.dmPelsHeight = height; + // Do not reuse the phantom's timing, change the primary screen/position, + // or write this temporary recovery into the user's persistent settings. + mode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL | DM_DISPLAYORIENTATION; + LONG result = ChangeDisplaySettingsExW(display.device.c_str(), &mode, nullptr, CDS_TEST, nullptr); + if (result == DISP_CHANGE_SUCCESSFUL) { + // Display numbers may be recycled during hotplug. Validate the target + // identity again after CDS_TEST before changing this source's mode. + const auto current = ActivePhysicalDisplays(&queried); + const bool same_target = std::any_of(current.begin(), current.end(), + [&](const PhysicalDisplayTarget& target) { + return !target.cloned && target.device == display.device && + target.monitor_path == display.monitor_path && + target.target.id == display.target.id && + target.target.adapterId.LowPart == display.target.adapterId.LowPart && + target.target.adapterId.HighPart == display.target.adapterId.HighPart; + }); + if (!same_target || CountUsbmmiddMonitors() != 0) { + SetError(error, "physical_display_changed_during_restore"); + restored = false; + continue; + } + result = ChangeDisplaySettingsExW(display.device.c_str(), &mode, nullptr, 0, nullptr); + } + if (result != DISP_CHANGE_SUCCESSFUL) { + SetError(error, "physical_display_mode_change_failed:" + std::to_string(result)); + restored = false; + continue; + } + DEVMODEW applied{}; + applied.dmSize = sizeof(applied); + if (!EnumDisplaySettingsExW(display.device.c_str(), ENUM_CURRENT_SETTINGS, &applied, 0) || + applied.dmPelsWidth != width || applied.dmPelsHeight != height) { + SetError(error, "physical_display_mode_not_applied"); + restored = false; + continue; + } + LOG_INFO("Physical display '{}' restored from {}x{} to preferred {}x{}", + Narrow(display.device), old_width, old_height, width, height); + } + return restored; +} + +VirtualDisplayMode ResolveVirtualDisplayMode(bool remember_attached) { std::lock_guard lock(remembered_mode_mutex); RememberedDisplayMode saved = last_observed_mode; for (HKEY root : {HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER}) { @@ -336,7 +432,7 @@ VirtualDisplayMode ResolveVirtualDisplayMode() { } // A reattached physical screen may temporarily clone the fallback monitor. // Do not let that temporary desktop mode replace the original observation. - const auto attached = CountUsbmmiddMonitors() == 0 + const auto attached = remember_attached && CountUsbmmiddMonitors() == 0 ? QueryAttachedPhysicalDisplayMode() : VirtualDisplayMode{}; if (ValidDisplayMode(attached)) { diff --git a/apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.h b/apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.h index e24cd868..19c31433 100644 --- a/apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.h +++ b/apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.h @@ -41,11 +41,15 @@ UsbmmiddStatus QueryUsbmmiddStatus(); // Remember an attached display's current pixel mode for GUI/SYSTEM startup. // Phantom targets and our own fallback monitor must not overwrite that mode. -VirtualDisplayMode ResolveVirtualDisplayMode(); +VirtualDisplayMode ResolveVirtualDisplayMode(bool remember_attached = true); // A real active monitor's current mode, or {0, 0} for headless/RDP desktops. // This probe does not update the remembered mode or change display settings. VirtualDisplayMode QueryAttachedPhysicalDisplayMode( std::wstring* device_name = nullptr); +// After a headless/virtual-display handoff, recover each real screen's native +// preferred pixel mode. Keep rotation and layout; never persist fallback modes. +// Returns false while topology/EDID is settling, so the owner can retry. +bool RestoreAttachedPhysicalDisplayModes(std::string* error = nullptr); // Adapters attached to the desktop, i.e. monitors currently plugged in. int CountUsbmmiddMonitors(); diff --git a/apps/desktop/xmake/targets.lua b/apps/desktop/xmake/targets.lua index 354b9611..40866c16 100644 --- a/apps/desktop/xmake/targets.lua +++ b/apps/desktop/xmake/targets.lua @@ -688,7 +688,7 @@ function setup_targets() "apps/desktop/src/platform/windows/gui/slint_backend.cpp", "apps/desktop/src/platform/windows/gui/window_icons.cpp", "apps/desktop/src/platform/windows/gui/window_taskbar.cpp", - "apps/desktop/src/platform/windows/gui/shutdown_session_guard.cpp", + "apps/desktop/src/platform/windows/gui/session_end_observer.cpp", "apps/desktop/src/platform/windows/gui/slint_renderer_probe.cpp", "apps/desktop/src/platform/common/gui/opengl_video_renderer.cpp", "apps/desktop/src/platform/common/gui/cuda_driver.cpp", diff --git a/localization/catalog.json b/localization/catalog.json index bc26157e..4224cc0a 100644 --- a/localization/catalog.json +++ b/localization/catalog.json @@ -10248,18 +10248,6 @@ "pt-BR": "{0}, recebendo {1}, enviando {2}, taxa de perda de pacotes {3}", "ru": "{0}, прием {1}, отправка {2}, скорость потери пакетов {3}" }, - "远程控制连接中,保留连接以确认或取消关机。": { - "zh-Hans": "远程控制连接中,保留连接以确认或取消关机。", - "en": "Remote session active. Keeping the connection available to confirm or cancel shutdown.", - "ru": "Удалённый сеанс активен. Соединение сохранено для подтверждения или отмены выключения.", - "zh-Hant": "遠端控制連線中,保留連線以確認或取消關機。", - "ja": "リモート接続中です。シャットダウンの確認または取り消しのため、接続を維持しています。", - "ko": "원격 세션이 활성화되어 있습니다. 종료 확인 또는 취소를 위해 연결을 유지합니다.", - "de": "Remotesitzung aktiv. Die Verbindung bleibt zum Bestätigen oder Abbrechen des Herunterfahrens bestehen.", - "fr": "Session distante active. La connexion reste disponible pour confirmer ou annuler l’arrêt.", - "es": "Sesión remota activa. Se mantiene la conexión para confirmar o cancelar el apagado.", - "pt-BR": "Sessão remota ativa. A conexão é mantida para confirmar ou cancelar o desligamento." - }, "监听所有 IP 接口": { "zh-Hans": "监听所有 IP 接口", "en": "Listen on all IP interfaces", @@ -11335,7 +11323,6 @@ "privacy_unlock_lock": "锁定", "privacy_screen_unlock_hint": "按下快捷键,解除隐私屏", "exit_program": "退出", - "shutdown_reason": "远程控制连接中,保留连接以确认或取消关机。", "pause_video_when_minimized": "最小化时暂停视频", "pause_video_when_minimized_hint": "控制窗口最小化时暂停接收视频,降低本机解码资源占用。", "system_permissions": "系统权限", @@ -11985,7 +11972,6 @@ "近期连接", "返回", "还原窗口", - "远程控制连接中,保留连接以确认或取消关机。", "远程桌面", "远端 IP / 端口", "远端 Windows 服务不可用",